使用 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 = { };
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 + , suffix.size() - dot_location); /* 添加 MP3 文件到列表 */
if ( suffix.compare("mp3") == ) {
vMusicFiles.push_back({ ToUTF8(fd.cFileName), root_path + fd.cFileName });
}
if ( !FindNextFileA(handle, &fd) ) break;
} sListTitle = "文件列表";
char buf[];
sprintf_s(buf, , " ( %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, , str.c_str(), -, NULL, ); wchar_t* pw_buf = new wchar_t[nw_len + ];
memset(pw_buf, , nw_len * + ); ::MultiByteToWideChar(CP_ACP, , str.c_str(), str.length(), pw_buf, nw_len); int len = WideCharToMultiByte(CP_UTF8, , pw_buf, -, NULL, NULL, NULL, NULL); char* utf8_buf = ( char* ) malloc(len + );
memset(utf8_buf, , len + ); ::WideCharToMultiByte(CP_UTF8, , 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(, ));
ImGui::SetNextWindowSize(ImVec2(, ));
ImGui::Begin("Music File", false, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
// TODO:
ImGui::End();

  窗口的属性设置为无标题,不能改变大小,不能移动。

  使用鼠标右键点击功能,弹出菜单,用于选择保存音频文件的文件夹:

            if ( ImGui::IsMouseClicked() ) {
ImGui::OpenPopup("contex menu");
}
if ( ImGui::BeginPopupContextItem("contex menu") ) {
if ( ImGui::MenuItem("Selected Directory") ) {
this->OpenSelectedDirectory();
}
ImGui::EndPopup();
}

  最后遍历 vMusicFiles 列表,显示音频文件名:

            ImVec2 size = ImVec2(ImGui::GetWindowWidth(), );

            if ( ImGui::CollapsingHeader(sListTitle.c_str(), ImGuiTreeNodeFlags_DefaultOpen) ) {
for ( int i = ; 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() ) {
nSelectedIndex = i;
this->ChangedMusicFile();
}
}
}

  显示窗口

        ImGui::SetNextWindowPos(ImVec2(, ));
ImGui::SetNextWindowSize(ImVec2(, ));
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(, ), ImVec2(, )); 

  其它内容参考源码。

  频谱显示窗口

  频谱显示是播放器的一个特色,由于没有相应的控件显示频谱,只能直接在窗口上绘制。获取窗口的绘制列表,然后绘制频谱:

ImDrawList* draw_list = ImGui::GetWindowDrawList();

  下图是频谱的显示效果:

  分为三个部分:绿色的内圈,放射状的中圈,白色的外圈。

  获取频谱数据:

float* fft = sound_manager->GetFFTData();

  默认为 128 个 float 数据(0-1.0),先绘制绿色的圈。由于图形是对称的,所以绘制一个圈需要 256 个点:

static ImVec2 pos_in[], pos_out[];

  这些点通过画圆的方式计算出来:

            int radius = ;

            for ( int i = ; i < ; 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 = ;

            for ( int i = ; i < ; i++ ) {
float radian = i / 255.0f * 6.28; int fft_index = (i >= ) ? - i : i; float delta_radius = radius - - fft[fft_index] * ; pos_in[i].x = cosf(radian) * delta_radius;
pos_in[i].y = sinf(radian) * delta_radius;
}

  放射状的中圈和白色的外圈也是通过 cos 和 sin 函数计算出来,最后绘制到窗口:

            draw_list->AddPolyline(pos_in, , ImColor(ImVec4(, , , )), true, , true);
draw_list->AddPolyline(pos_out, , ImColor(ImVec4(, , , )), true, , true);

  有一个注意的地方是坐标点的偏移,上面的圆默认绘制在窗口(不是指频谱窗口)的左上角,所以要把那些点变换到频谱窗口中间。

        /* 频谱窗口 */
ImVec2 size = ImVec2(, );
ImGui::SetNextWindowPos(ImVec2(, ));
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[], pos_out[]; float radius = ;
float height = ;
float offset = PI_2 / 256.0;
float radian = ; 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 + ; for ( int i = ; i < ; i++ ) {
radian = offset * i; int fft_index = (i >= ) ? - i : i; c = -cosf(radian);
s = sinf(radian); p1.x = s * radius + offsetx;
p1.y = c * radius + offsety; float delta_radius = radius + + fmaxf(sqrtf(fft[fft_index]) * * height, );
p2.x = s * delta_radius + offsetx;
p2.y = c * delta_radius + offsety; draw_list->AddLine(p1, p2, ImColor(ImVec4(, , , )), ); delta_radius = radius - - fft[fft_index] * ;
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, , ImColor(ImVec4(, , , )), true, , true);
draw_list->AddPolyline(pos_out, , ImColor(ImVec4(, , , )), true, , true);
}
ImGui::End();

  音乐播放器的运行结果:

  音乐播放器设计到此结束了。

  源码下载:Simple2D-14.rar

struct MusicFile{std::string filename_utf8;std::string filename;};

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

  1. Springboot拦截器使用及其底层源码剖析

    博主最近看了一下公司刚刚开发的微服务,准备入手从基本的过滤器以及拦截器开始剖析,以及在帮同学们分析一下上次的jetty过滤器源码与本次Springboot中tomcat中过滤器的区别.正题开始,拦截器 ...

  2. 详解Mybatis拦截器(从使用到源码)

    详解Mybatis拦截器(从使用到源码) MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能. 本文从配置到源码进行分析. 一.拦截器介绍 MyBatis 允许你在 ...

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

    目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:h ...

  4. 带你深入理解STL之空间配置器(思维导图+源码)

    前不久把STL细看了一遍,由于看得太"认真",忘了做笔记,归纳和总结这步漏掉了.于是为了加深印象,打算重看一遍,并记录下来里面的一些实现细节.方便以后能较好的复习它. 以前在项目中 ...

  5. 给大家推荐一个C#下文件监听器和资源管理器的示例Demo-含源码

    C#下文件监听器和资源管理器的示例Demo:源码下载地址

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

      目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:ht ...

  7. SpringMVC拦截器详解[附带源码分析](转)

    本文转自http://www.cnblogs.com/fangjian0423/p/springMVC-interceptor.html 感谢作者 目录 前言 重要接口及类介绍 源码分析 拦截器的配置 ...

  8. struts2 paramsPrepareParamsStack拦截器简化代码(源码分析)

    目录 一.在讲 paramsPrepareParamsStack 之前,先看一个增删改查的例子. 1. Dao.java准备数据和提供增删改查 2. Employee.java 为model 3. E ...

  9. Java爬虫系列之实战:爬取酷狗音乐网 TOP500 的歌曲(附源码)

    在前面分享的两篇随笔中分别介绍了HttpClient和Jsoup以及简单的代码案例: Java爬虫系列二:使用HttpClient抓取页面HTML Java爬虫系列三:使用Jsoup解析HTML 今天 ...

  10. EasyPlayer Android安卓流媒体播放器实现播放同步录像功能实现(附源码)

    本文转自EasyDarwin团队John的博客:http://blog.csdn.net/jyt0551,John是EasyPusher安卓直播推流.EasyPlayer直播流媒体播放端的开发和维护者 ...

随机推荐

  1. jaeger 使用ElasticSearch 作为后端存储

    jaeger 支持es 作为后端存储,这样对于查询.以及系统扩展是比较方便的 使用docker-compose 运行 环境准备 参考项目: https://github.com/rongfenglia ...

  2. IEPNGFix 解决IE6支持PNG透明问题

    IE6本身是支持索引色透明度(PNG8)格式,但不支持真彩色透明度(PNG24)格式. 使用IE PNG Fix 2.0可以完美解决IE6支持PNG透明问题,而且支持背景定位和重复属性. IE PNG ...

  3. smarty 学习 ——smarty 开发环境配置

    smarty 对于开发的便利性不用多说了,直接进行开发环境的配置. 1.下载smarty 开发包 直接在官网进行下载即可 2.引用开发核心库 将libs文件中的东西拷贝到工程. smarty.clas ...

  4. JMeter连接数据库(查询出的数据作为参数)

    针对Mysql jdbc:mysql://ip:3306/数据库名?useUnicode=true&characterEncoding=utf8&allowMultiQueries=t ...

  5. ThinkPHP 的一个神秘版本 ThinkPHP 1.2

    ThinkPHP 的一个神秘版本 ThinkPHP 1.2 询问过 ThinkPHP 官网的小伙伴都知道,偶尔 ThinkPHP 故障时会出现 ThinkPHP 1.2(下次看到就截图下来). 但是我 ...

  6. tomcat catalina.out切割脚本

    shell脚本catalina.out 切割脚本...每天23.30切割.删除七天之前的日志这里3个tomcat实例(1)拷贝日志文件(2)清空日志文件*只能清空如果删除tomcat不重启不会生成新的 ...

  7. 使用 ASMCMD 工具管理ASM目录及文件

    ============================== -- 使用ASMCMD 工具管理ASM目录及文件 --============================== 在ASM实例中,所有的 ...

  8. Apache mod_rewrite实现HTTP和HTTPS重定向跳转

    当你的站点使用了HTTPS之后,你可能会想把所有的HTTP请求(即端口80的请求),全部都重定向至HTTPS(即端口443).这时候你可以用以下的方式来做到:(Apache mod_rewrite) ...

  9. Linux系统Centos安装Python3.7

    Linux下默认系统自带python2.7的版本,这个版本被系统很多程序所依赖,所以不建议删除,如果使用最新的Python3那么我们知道编译安装源码包和系统默认包之间是没有任何影响的,所以可以安装py ...

  10. AI(四): 微信与luis结合(下)

    LUIS(Language Understanding Intelligent Services)是微软新近推出了的的语义理解服务,可以方便用户进行API调用,创建自己场景的语义理解服务,网址为 ht ...