• Simple2D-19(音乐播放器)播放器的源码实现


      使用 BASS 和 ImGui 实现音乐播放器 MusicPlayer。

      将播放器和一个文件夹关联起来,程序刚开始运行的时候就从该文件夹加载所有音频文件。而文件夹的路径则保存在配置文件中,所以程序的第一步就是读取配置文件。

      1、读取配置文件


       配置文件以 XML 格式进行储存,使用 TinyXml 库解析:

            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();

      第一次启动程序的时候,没有配置文件,所以要创建配置文件:

        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,弹出对话框,选择文件夹后获取文件夹的路径。然后将文件夹的路径更新到配置文件:

        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 更新配置文件,下一次打开程序时就会加载该文件夹下的所有音频文件。

      2、搜索文件夹下的音频文件


       调用系统 API,搜索文件夹中的文件:

        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 格式的字符串。

      3、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;
        }

      整个播放器的设计有四个窗口:

        1、文件列表窗口

        2、当前播放文件显示窗口

        3、频谱显示窗口

        4、播放控件窗口

      文件列表窗口

      创建一个空白窗口(显示窗口前先设置窗口位置和大小):

            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;};

  • 相关阅读:
    php无法连接mongodb 3.0问题解决
    mongodb安全配置
    RedHat6/Centos6.5安装mongodb php driver
    RedHat6/Centos6.5安装mongodb
    ASP.NET Identity 2集成到MVC5项目--笔记02
    ASP.NET Identity 2集成到MVC5项目--笔记01
    C#实体类序列化为XML
    MVC4学习笔记之--身份认证过滤器
    【WPF】学习笔记(三)——这个家伙跟电子签名板有个约定
    【WPF】学习笔记(二)——依旧是一个电子签名板
  • 原文地址:https://www.cnblogs.com/ForEmail5/p/7231868.html
Copyright © 2020-2023  润新知