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

时间: 2024-12-15 07:20:50

Simple2D-19(音乐播放器)播放器的源码实现的相关文章

SpringMVC拦截器详解[附带源码分析]

目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html 拦截器是每个Web框架必备的功能,也是个老生常谈的主题了. 本文将分析SpringMVC的拦截器功能是如何设计的,让读者了解该功能设计的原理. 重要接口及类介绍 1. Hand

SpringMVC核心分发器DispatcherServlet分析[附带源码分析]

SpringMVC核心分发器DispatcherServlet分析[附带源码分析] 目录 前言 DispatcherServlet初始化过程 DispatcherServlet处理请求过程 总结 参考资料 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html 本文将分析SpringMVC的核心分发器Dispa

[转]SpringMVC拦截器详解[附带源码分析]

目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html 拦截器是每个Web框架必备的功能,也是个老生常谈的主题了. 本文将分析SpringMVC的拦截器功能是如何设计的,让读者了解该功能设计的原理. 重要接口及类介绍 1. Hand

Android 音视频深入 十四 FFmpeg与OpenSL ES 播放mp3音乐,能暂停(附源码

项目地址https://github.com/979451341/FFmpegOpenslES 这次说的是FFmpeg解码mp3,数据给OpenSL ES播放,并且能够暂停.1.创建引擎 slCreateEngine(&engineObject,0,NULL,0,NULL,NULL);//创建引擎 (*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);//实现engineObject接口对象 (*engineObject)->G

struts2内置拦截器和自定义拦截器详解(附源码)

一.Struts2内置拦截器 Struts2中内置类许多的拦截器,它们提供了许多Struts2的核心功能和可选的高级特 性.这些内置的拦截器在struts-default.xml中配置.只有配置了拦截器,拦截器才可以正常的工作和运行.Struts 2已经为您提供丰富多样的,功能齐全的拦截器实现.大家可以至struts2的jar包内的struts-default.xml查看关于默认的拦截器与 拦截器链的配置.内置拦截器虽然在struts2中都定义了,但是并不是都起作用的.因为并不是所有拦截器都被加

SpringMVC拦截器详解[附带源码分析]作奏抓诅做准

http://www.ebay.com/cln/7vt_ldrz/2015.01.29/166953650015 http://www.ebay.com/cln/dtr_rfzf/2015.01.29/166836224013 http://www.ebay.com/cln/ldx_nxtv/2015.01.29/166688651010 http://www.ebay.com/cln/b1t_xbzd/2015.01.29/166688653010 http://www.ebay.com/cl

SpringMVC拦截器详解[附带源码分析]拽柞奏租卓咨

http://www.ebay.com/cln/tjp_btlh/2015.01.29/166584777017 http://www.ebay.com/cln/951_thpr/2015.01.29/166964286015 http://www.ebay.com/cln/r5d_nzjp/2015.01.29/166698555010 http://www.ebay.com/cln/lp5_bpxd/2015.01.29/166964288015 http://www.ebay.com/cl

7个播放器效果展示(附源码)(一,二,三,四)

1.  HTML5+CSS3自定义视频播放器实现物理效果 源码下载/  在线演示 2.  html5触发式音频播放 这个插件集成了一些非常好的 JavaScript 库,提供一个方便使用的文本动画插件. 源码下载 /  在线演示 3. html5+css3酷炫音频播放器 源码下载/  在线演示 4.  css3迷你播放器面板 能在支持 FireFox.Chrome.Safari.傲游.搜狗.360浏览器. 源码下载/  在线演示 7个播放器效果展示(附源码)(一,二,三,四)

Android学习笔记(十八)——使用意图筛选器和实现浏览网页(附源码)

使用意图筛选器 点击下载源码 1.创建一个Intents项目,给该项目添加一个新类,命名为MyBrowserActivity,在res/layout文件夹下新增一个browser.xml: 2.在AndroidManifest.xml文件中添加如下代码: 添加权限: <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="a

关于阅读Struts2部分拦截器源码的记录

Struts2中的拦截器在ActionInvocation对象的invoke()方法中执行. ActionInvocation对象从配置文件中读取Interceptor对象,加入到自己的存取拦截器的容器中. 在invoke()方法中,当容器中还有Interceptor对象时,就执行对应Interceptor的intercept方法;在intercept方法中,除了加入拦截器自己的部分语句,还会调用同一个ActionInvocation对象的invoke方法,然后invoke方法再执行下一个Int