使用 BASS 和 ImGui 實現音樂播放器 MusicPlayer。ide
將播放器和一個文件夾關聯起來,程序剛開始運行的時候就從該文件夾加載全部音頻文件。而文件夾的路徑則保存在配置文件中,因此程序的第一步就是讀取配置文件。函數
一、讀取配置文件字體
配置文件以 XML 格式進行儲存,使用 TinyXml 庫解析:ui
tinyxml2::XMLDocument doc; if ( doc.LoadFile(path.c_str()) != tinyxml2::XML_NO_ERROR ) { this->CreateConfiFile(); /* 從新加載 */ doc.LoadFile(path.c_str()); } sMusicFilePath = doc.FirstChildElement("Path")->GetText();
第一次啓動程序的時候,沒有配置文件,因此要建立配置文件:this
void MusicPlayer::CreateConfiFile() { const char* declaration = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"; tinyxml2::XMLDocument doc; doc.Parse(declaration); sMusicFilePath = "C:"; tinyxml2::XMLElement* path = doc.NewElement("Path"); path->SetText(sMusicFilePath.c_str()); doc.InsertEndChild(path); doc.SaveFile(this->GetSavePath().c_str()); }
默認使用 C 盤路徑做爲保存音頻文件,雖然開始的時候使用 C 盤路徑,但保存音頻文件的文件夾由用戶來選擇。用戶能夠打開文件夾選擇對話框,選擇保存音頻文件的文件夾:編碼
std::string Dialog::OpenSelectedDirDialog(const std::string& title) { char file[MAX_PATH] = ""; BROWSEINFOA bif = { 0 }; bif.lpszTitle = title.c_str(); bif.pszDisplayName = file; bif.ulFlags = BIF_BROWSEINCLUDEFILES; if ( LPITEMIDLIST pil = SHBrowseForFolderA(&bif) ) { SHGetPathFromIDListA(pil, file); return file; } return ""; }
調用系統 API,彈出對話框,選擇文件夾後獲取文件夾的路徑。而後將文件夾的路徑更新到配置文件:spa
void MusicPlayer::SaveConfigFile() { std::string path = this->GetSavePath(); tinyxml2::XMLDocument doc; doc.LoadFile(path.c_str()); tinyxml2::XMLElement* ele = doc.FirstChildElement("Path"); ele->SetText(sMusicFilePath.c_str()); doc.SaveFile(path.c_str()); }
主要使用 TinyXml 更新配置文件,下一次打開程序時就會加載該文件夾下的全部音頻文件。設計
二、搜索文件夾下的音頻文件code
調用系統 API,搜索文件夾中的文件:xml
void MusicPlayer::SearchMusicFile(const std::string path) { vMusicFiles.clear(); std::string root_path = path + "\\"; WIN32_FIND_DATAA fd; HANDLE handle = FindFirstFileA((root_path + "*").c_str(), &fd); if ( handle == INVALID_HANDLE_VALUE ) { throw std::exception(""); } std::string suffix; while ( true ) { if ( fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) { if ( !FindNextFileA(handle, &fd) ) break; continue; } /* 截取文件後綴 */ suffix = fd.cFileName; auto dot_location = suffix.find_last_of("."); suffix = suffix.substr(dot_location + 1, suffix.size() - dot_location); /* 添加 MP3 文件到列表 */ if ( suffix.compare("mp3") == 0 ) { vMusicFiles.push_back({ ToUTF8(fd.cFileName), root_path + fd.cFileName }); } if ( !FindNextFileA(handle, &fd) ) break; } sListTitle = "文件列表"; char buf[64]; sprintf_s(buf, 64, " ( %d )", vMusicFiles.size()); sListTitle = sListTitle + buf; /* 轉換爲 utf8,以便 ImGui 正確顯示中文 */ sListTitle = ToUTF8(sListTitle); }
搜索文件的同時篩選文件,經過文件的後綴判斷該文件是否爲音頻文件,這裏只獲取 .mp3 後綴的文件(BASS 支持其餘格式的音頻文件)。最終將符合條件的文件路徑添加到一個列表:
struct MusicFile { std::string filename_utf8; std::string filename; };
std::vector<MusicFile> vMusicFiles;
這裏文件路徑的儲存使用了兩個 std::string,由於 ImGui 要顯示中文的話,要傳入 utf-8 格式的字符串。
三、ImGui 界面繪製
中文顯示
ImGui 是支持中文顯示的,首先是添加支持中文的 TTF 字體:
ImGuiIO& io = ImGui::GetIO(); /* 使用微軟雅黑字體 */ io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\msyh.ttc", 18.0f, NULL, io.Fonts->GetGlyphRangesChinese());
程序使用了微軟雅黑字體,而後傳入 ImGui 的字符串必須是 utf-8 編碼的。根據 ImGui 的介紹,使用字面值 u8 便可:
ImGui::Text(u8"顯示中文");
可是筆者使用的 vs2013 不支持字面值 u8,全部將字符串傳入 ImGui 前要轉換爲 utf-8 編碼的字符串。
inline std::string ToUTF8(const std::string str) { int nw_len = ::MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0); wchar_t* pw_buf = new wchar_t[nw_len + 1]; memset(pw_buf, 0, nw_len * 2 + 2); ::MultiByteToWideChar(CP_ACP, 0, str.c_str(), str.length(), pw_buf, nw_len); int len = WideCharToMultiByte(CP_UTF8, 0, pw_buf, -1, NULL, NULL, NULL, NULL); char* utf8_buf = ( char* ) malloc(len + 1); memset(utf8_buf, 0, len + 1); ::WideCharToMultiByte(CP_UTF8, 0, pw_buf, nw_len, utf8_buf, len, NULL, NULL); std::string outstr(utf8_buf); delete[] pw_buf; delete[] utf8_buf; return outstr; }
整個播放器的設計有四個窗口:
一、文件列表窗口
二、當前播放文件顯示窗口
三、頻譜顯示窗口
四、播放控件窗口
文件列表窗口
建立一個空白窗口(顯示窗口前先設置窗口位置和大小):
ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(310, 650)); ImGui::Begin("Music File", false, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); // TODO: ImGui::End();
窗口的屬性設置爲無標題,不能改變大小,不能移動。
使用鼠標右鍵點擊功能,彈出菜單,用於選擇保存音頻文件的文件夾:
if ( ImGui::IsMouseClicked(1) ) { ImGui::OpenPopup("contex menu"); } if ( ImGui::BeginPopupContextItem("contex menu") ) { if ( ImGui::MenuItem("Selected Directory") ) { this->OpenSelectedDirectory(); } ImGui::EndPopup(); }
最後遍歷 vMusicFiles 列表,顯示音頻文件名:
ImVec2 size = ImVec2(ImGui::GetWindowWidth(), 15); if ( ImGui::CollapsingHeader(sListTitle.c_str(), ImGuiTreeNodeFlags_DefaultOpen) ) { for ( int i = 0; i < vMusicFiles.size(); i++ ) { bool click = ImGui::Selectable(vMusicFiles[i].filename_utf8.c_str(), nSelectedIndex == i, ImGuiSelectableFlags_AllowDoubleClick, size); if ( ImGui::IsItemHovered() ) { ImGui::SetTooltip(vMusicFiles[i].filename_utf8.c_str()); } if ( click && ImGui::IsMouseDoubleClicked(0) ) { nSelectedIndex = i; this->ChangedMusicFile(); } } }
顯示窗口
ImGui::SetNextWindowPos(ImVec2(310, 0)); ImGui::SetNextWindowSize(ImVec2(600, 32)); ImGui::Begin("Display", false, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); { ImGui::Text("PLAY: "); ImGui::SameLine(); ImGui::Text(displayInfo.title.c_str()); } ImGui::End();
播放控件窗口
主要使用了圖片按鈕 ImGui::ImageButton(),圖片顯示接受一個紋理 ID,這個紋理 ID 能夠經過前面的 TextureManager 對象加載圖像文件獲取
Texture* texture = nullptr; texture = TextureManager::instance()->getTexture("prev.png");
而後進行簡單的封裝:
struct Image { unsigned int id; ImVec2 size; };
btnPrev.id = texture->texture;
btnPrev.size = ImVec2(texture->size.w, texture->size.h);
ImGui::ImageButton(( void* ) btnPrev.id, btnPrev.size, ImVec2(0, 1), ImVec2(1, 0));
其它內容參考源碼。
頻譜顯示窗口
頻譜顯示是播放器的一個特點,因爲沒有相應的控件顯示頻譜,只能直接在窗口上繪製。獲取窗口的繪製列表,而後繪製頻譜:
ImDrawList* draw_list = ImGui::GetWindowDrawList();
下圖是頻譜的顯示效果:
分爲三個部分:綠色的內圈,放射狀的中圈,白色的外圈。
獲取頻譜數據:
float* fft = sound_manager->GetFFTData();
默認爲 128 個 float 數據(0-1.0),先繪製綠色的圈。因爲圖形是對稱的,因此繪製一個圈須要 256 個點:
static ImVec2 pos_in[256], pos_out[256];
這些點經過畫圓的方式計算出來:
int radius = 150; for ( int i = 0; i < 256; i++ ) { float radian = i / 255.0f * 6.28; pos_in[i].x = cosf(radian) * radius; pos_in[i].y = sinf(radian) * radius; }
主要是使用三角函數 cos 和 sin,上面計算出了半徑爲 150 的圓上的 256 個點,若是要半徑的大小隨頻譜變化:
int radius = 150; for ( int i = 0; i < 256; i++ ) { float radian = i / 255.0f * 6.28; int fft_index = (i >= 128) ? 255 - i : i; float delta_radius = radius - 5 - fft[fft_index] * 100; pos_in[i].x = cosf(radian) * delta_radius; pos_in[i].y = sinf(radian) * delta_radius; }
放射狀的中圈和白色的外圈也是經過 cos 和 sin 函數計算出來,最後繪製到窗口:
draw_list->AddPolyline(pos_in, 256, ImColor(ImVec4(0, 1, 0, 1)), true, 2, true); draw_list->AddPolyline(pos_out, 256, ImColor(ImVec4(1, 1, 1, 1)), true, 2, true);
有一個注意的地方是座標點的偏移,上面的圓默認繪製在窗口(不是指頻譜窗口)的左上角,因此要把那些點變換到頻譜窗口中間。
/* 頻譜窗口 */ ImVec2 size = ImVec2(600, 572); ImGui::SetNextWindowPos(ImVec2(310, 32)); ImGui::SetNextWindowSize(size); ImGui::Begin("FFT", false, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); { ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); float* fft = sound_manager->GetFFTData(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); static ImVec2 pos_in[256], pos_out[256]; float radius = 100; float height = 120; float offset = PI_2 / 256.0; float radian = 0; ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 p1, p2; static float c, s; int offsetx = p.x + size.x * 0.5f; int offsety = p.y + size.y * 0.5f + 50; for ( int i = 0; i < 256; i++ ) { radian = offset * i; int fft_index = (i >= 128) ? 255 - i : i; c = -cosf(radian); s = sinf(radian); p1.x = s * radius + offsetx; p1.y = c * radius + offsety; float delta_radius = radius + 5 + fmaxf(sqrtf(fft[fft_index]) * 3 * height, 0); p2.x = s * delta_radius + offsetx; p2.y = c * delta_radius + offsety; draw_list->AddLine(p1, p2, ImColor(ImVec4(0, 1, 1, 1)), 1); delta_radius = radius - 5 - fft[fft_index] * 100; pos_in[i].x = s * delta_radius + offsetx; pos_in[i].y = c * delta_radius + offsety; pos_out[i] = p2; } draw_list->AddPolyline(pos_in, 256, ImColor(ImVec4(0, 1, 0, 1)), true, 2, true); draw_list->AddPolyline(pos_out, 256, ImColor(ImVec4(1, 1, 1, 1)), true, 2, true); } ImGui::End();
音樂播放器的運行結果:
音樂播放器設計到此結束了。
源碼下載:Simple2D-14.rar
struct MusicFile{std::string filename_utf8;std::string filename;};