使用 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. 读写XML文档时,去掉新增加节点的“空命名空间”(xmlns=””)

    在做对ReprotViewer编程时,想做一个用户可以更改显示/打印列的功能,大致看了下,只需要通过对rdlc文件中改变其<Hidden>节点值为false/true,即可实现对应某列的显 ...

  2. 调用webserver时出现:请求因 HTTP 状态 401 失败: Unauthorized。

    请求因 HTTP 状态 401 失败: Unauthorized 今天在调用webserver时出现了上述标题的错误,开始认为是由于端口的问题,我把端口恢复80默认端口后,但是问题并没有解决!后来我自 ...

  3. vue-router 结合源码分析原理

    路由响应过程: 浏览器发出请求 服务器监听到num端口(或443)有请求过来,并解析url路径 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等) 浏览器根 ...

  4. RegExp实例

    ECMAScript通过RegExp类型来支持正则表达式,常见的正则表达式为:var expression = /pattern / flags;其中的模式(pattern)部分可以使任何简单或复杂的 ...

  5. Appscan_web安全测试工具 (含修改启动浏览器的方法)

    安全测试应该是测试中非常重要的一部分,但他常常最容易被忽视掉. 尽管国内经常出现各种安全事件,但没有真正的引起人们的注意.不管是开发还是测试都不太关注产品的安全.当然,这也不能怪我们苦B的“民工兄弟” ...

  6. 2018-2019 Exp2 后门原理与实践

    2018-2019 Exp2 后门原理与实践 目录 一.实验内容说明及基础问题回答 二.工具准备 查看WindowsIP和LinuxIP Windows获得Linuxshell Linux 获得Win ...

  7. 杂项:Juice UI

    ylbtech-杂项:Juice UI Juice UI是开源的 WebForms 控件集,是一个功能强大的框架,它可以给ASP .NET开发人员带来丰富的.可以作为易于使用的控件的jQuery UI ...

  8. 阿里云EC2+QEMU虚拟机+ROS完全教程!

    ---恢复内容开始--- 1.安装centos6.5 x64 同时记录,当前centos分配得到的IP,子网掩码,网关,以及MAC!!! 查看IP.mac命令ip add 查看网关命令cat /etc ...

  9. Vue引用其他组件,但组件某些部分不需要时的简单处理

    项目开发时,我们会把多个地方重复使用的模块抽象成组件,提供给大家一起使用,但是使用组件的时候偶尔会遇见一些问题,比如说组件里只有某些东西自己并不需要,这个时候我们可以对组件进行简单的修改,而不影响其他 ...

  10. javascript继承之借用构造函数(二)

    //简单的函数调用 function Father() { this.nums= [1,2]; } function Son() { Father.call(this);//调用超类型,完成son继承 ...