C++大作业-简易音乐播放器的开发
Music Player
1.所需的工具以及库
ImGui+Glfw进行图形界面的开发
miniaudio完成播放器类的开发
2.功能
-
显示歌曲文件的基本信息
-
正常播放.mp3,.wav音乐文件
-
拥有扩展功能(下一首/音量控制/进度条调整)
-
通过用户的交互来进行资源设置
3.思路
写了四个重要的类:
1.UI类
通过ImGui来显示图形窗口以供用户输入
同时改变资源设置
2.Song类以及SongList类
存储歌曲名称以及歌单的数据结构
3.AudioPlayer类
基于miniaudio的一个小型播放器,通过其函数可以控制歌曲的播放,暂停等功能
4.源代码
main.cpp
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>
#include <vector>
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
#include <windows.h>
#include <commdlg.h>
#include "UI.hpp"
// 初始化字体函数
ImFont* InitFonts() {
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
ImFont* font = io.Fonts->AddFontFromFileTTF("./Yugong.ttf", 35.0f, NULL, io.Fonts->GetGlyphRangesChineseFull());
return font;
}
void ApplyCustomStyle() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
// === 配色方案 ===
colors[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.15f, 0.95f);
colors[ImGuiCol_FrameBg] = ImVec4(0.25f, 0.25f, 0.30f, 0.80f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.35f, 0.35f, 0.40f, 0.70f);
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.50f, 0.20f, 0.70f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.60f, 0.30f, 0.80f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.20f, 0.70f, 0.20f, 1.00f);
colors[ImGuiCol_Header] = ImVec4(0.30f, 0.30f, 0.45f, 0.70f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.10f, 0.30f, 0.95f);
// === 圆角设置 ===
style.FrameRounding = 10.0f;
style.GrabRounding = 5.0f;
style.WindowRounding = 8.0f;
// === 阴影和间距 ===
style.WindowPadding = ImVec2(10, 10);
style.ItemSpacing = ImVec2(8, 6);
style.ScrollbarRounding = 5.0f;
// === 字体设置 ===
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
// === 透明度设置 ===
style.Alpha = 0.9f;
}
int main() {
Ui ui;
AudioPlayer player;
SongList list;
// 初始化 GLFW
if (!glfwInit()) {
return -1;
}
// OpenGL 设置:确保使用现代 OpenGL
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(2000, 2000, "Music Player With ImGui (中文测试)", NULL, NULL);
if (window == NULL) {
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // 启用垂直同步
// 初始化 ImGui
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
// 初始化字体(只调用一次)
ImFont*font=InitFonts();
ImGui_ImplOpenGL3_CreateFontsTexture(); // 创建字体纹理
// 渲染清除颜色
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
// 应用美化样式
ApplyCustomStyle();
// 主循环
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// 启动 ImGui 帧
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();;
// 绘制播放器界面
ui.DrawPlayerUI(list, player,font);
// 渲染
ImGui::Render();
int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// 清理资源
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
AudioPlayer.hpp
#pragma once
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
#include <string>
#include <iostream>
using namespace std;
class AudioPlayer {
public:
AudioPlayer() : m_initialized(false), m_volume(1.0f), m_isPaused(false), m_currentSoundDuration(0.0f) {}
~AudioPlayer() { stop(); }
bool load(const string& filepath, bool loop = true) {
stop();
if (m_initialized) stop();
// 初始化解码器
ma_result result = ma_decoder_init_file(filepath.c_str(), NULL, &m_decoder);
if (result != MA_SUCCESS) {
cerr << "初始化文件解码器失败" << endl;
m_initialized = false;
return false;
}
// 获取音频总时长(秒)
ma_uint64 totalFrames = 0;
if (ma_decoder_get_length_in_pcm_frames(&m_decoder, &totalFrames) == MA_SUCCESS) {
m_currentSoundDuration = (float)totalFrames / m_decoder.outputSampleRate;
}
else {
cerr << "无法获取音频长度" << endl;
m_currentSoundDuration = 0.0f;
}
//// 循环设置
//ma_data_source_set_looping(&m_decoder, loop ? MA_TRUE : MA_FALSE);
// 设备设置
m_deviceConfig = ma_device_config_init(ma_device_type_playback);
m_deviceConfig.playback.format = m_decoder.outputFormat;
m_deviceConfig.playback.channels = m_decoder.outputChannels;
m_deviceConfig.sampleRate = m_decoder.outputSampleRate;
m_deviceConfig.dataCallback = data_callback;
m_deviceConfig.pUserData = this; // 修改为当前对象
if (ma_device_init(NULL, &m_deviceConfig, &m_device) != MA_SUCCESS) {
cerr << "初始化设备失败" << endl;
ma_decoder_uninit(&m_decoder);
m_initialized = false;
return false;
}
ma_device_set_master_volume(&m_device, m_volume);
m_initialized = true;
m_isPaused = false;
return true;
}
bool play() {
if (!m_initialized) {
cerr << "设备未初始化" << endl;
return false;
}
if (ma_device_start(&m_device) != MA_SUCCESS) {
cerr << "播放音频失败" << endl;
return false;
}
m_isPaused = false;
return true;
}
void stop() {
if (m_initialized) {
ma_device_uninit(&m_device);
ma_decoder_uninit(&m_decoder);
m_initialized = false;
m_isPaused = false;
}
}
void pause() {
if (m_initialized && !m_isPaused) {
ma_device_stop(&m_device);
m_isPaused = true;
}
}
void resume() {
if (m_initialized && m_isPaused) {
ma_device_start(&m_device);
m_isPaused = false;
}
}
void setVolume(float volume) {
if (!m_initialized) return;
m_volume = (volume < 0.0f) ? 0.0f : (volume > 1.0f) ? 1.0f : volume;
ma_device_set_master_volume(&m_device, m_volume);
}
float getVolume() const { return m_volume; }
bool isPlaying() const { return m_initialized && !m_isPaused; }
bool isPaused() const { return m_initialized && m_isPaused; }
// 新增:获取当前播放进度(秒)
float getCurrentTime() {
ma_uint64 currentFrame = 0;
if (ma_decoder_get_cursor_in_pcm_frames(&m_decoder, ¤tFrame) == MA_SUCCESS) {
return (float)currentFrame / m_decoder.outputSampleRate;
}
return 0.0f;
}
// 新增:跳转到指定时间
void seek(float seconds) {
if (!m_initialized) return;
ma_uint64 targetFrame = (ma_uint64)(seconds * m_decoder.outputSampleRate);
ma_data_source_seek_to_pcm_frame(&m_decoder, targetFrame);
}
float m_currentSoundDuration; // 音频总时长(秒)
private:
static void data_callback(ma_device* pDevice, void* pOutput, const void* /*pInput*/, ma_uint32 frameCount) {
AudioPlayer* player = (AudioPlayer*)pDevice->pUserData;
if (player && player->m_initialized) {
ma_uint64 framesRead = 0;
ma_result result = ma_data_source_read_pcm_frames(&player->m_decoder, pOutput, frameCount, &framesRead);
if (result != MA_SUCCESS || framesRead == 0) {
memset(pOutput, 0, frameCount * player->m_deviceConfig.playback.channels * ma_get_bytes_per_sample(player->m_deviceConfig.playback.format));
player->stop();
}
}
}
ma_decoder m_decoder;
ma_device_config m_deviceConfig;
ma_device m_device;
bool m_initialized;
bool m_isPaused;
float m_volume;
};
Song类以及SongList类
#pragma once
#include<filesystem>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
namespace fs = std::filesystem;
class Song{
public:
string filename, filepath;
Song() {};
Song(const string&filename, const string& filepath) :filename(filename),filepath(filepath){}
string getTitle() const{ return filename; }
string getPath() const{ return filepath; }
void show() { cerr << "正在播放:" << filename << endl; }
};
#pragma once
#include"Song.hpp"
#include<iostream>
#include<string>
#include<vector>
class SongList {
public:
vector<Song>body;
void add_song(const string &filename,const string&filepath);
void del_song(int index);
int now = 0;
void loadSongsFromFolder(const string& folderPath) {//从文件夹中读取音频文件
if (!fs::exists(folderPath) || !fs::is_directory(folderPath)) {
cerr << "目录不存在: " << folderPath << endl;
return;
}
body.clear();
for (const auto& entry : fs::directory_iterator(folderPath)) {
if (entry.is_regular_file()) {
string path = entry.path().string();
string ext = entry.path().extension().string();
// 支持的音频格式
if (ext == ".mp3" || ext == ".wav" || ext == ".flac" || ext == ".ogg") {
string filename = entry.path().stem().string(); // 文件名(无扩展名)
body.emplace_back(filename, path);
}
}
}
return;
}
};
void SongList::add_song(const string &filename,const string &filepath) {
for (const auto& song : body) {
if (song.filepath == filepath) { cerr << "The music is already existed." << endl; return; }
}
body.emplace_back(filename, filepath);
cout << "add the song:" << filename << "(" << filepath << ")" << endl;
}
void SongList::del_song(int index) {
if (index<=0 || index>body.size()) {
cerr << "invalid index" << endl; return;
}
cout << "delete the song:" << body[index - 1].filename << endl;
body.erase(body.begin() + index - 1);
//如果删除的是当前播放歌曲
if (now == index)now = 0; else if (now > index)now--;
}
UI类
#pragma once
#include"ImGuiAll.h"
#include"AudioPlayer.hpp"
#include"SongList.hpp"
#include<iostream>
using namespace std;
class Ui {
public:
// 创建播放器的图形界面
void DrawPlayerUI(SongList&list,AudioPlayer& player,ImFont*font);
float volume=1.0f;
float songProgress = 0.0f;
int lastShownSong = -1; // 用于记录上次显示的歌曲索引
};
// 创建播放器的图形界面
void Ui::DrawPlayerUI(SongList&list,AudioPlayer&player,ImFont*font) {
ImGui::PushFont(font); // 设置字体
ImGui::Begin("This is a Music Player!");
//list的初始化设置
ImGui::Text("Choose the music filefolder");//选择初始歌单文件夹
IGFD::FileDialogConfig config;
config.path = ".";//初始目录
if (ImGui::Button("Choose Folder")) {
ImGuiFileDialog::Instance()->OpenDialog("ChooseFolder", "Choose Folder", nullptr, config);
}
// 处理文件夹选择
if (ImGuiFileDialog::Instance()->Display("ChooseFolder")) {
if (ImGuiFileDialog::Instance()->IsOk()) {
std::string folderPath = ImGuiFileDialog::Instance()->GetCurrentPath(); // 获取选中的文件夹路径
list.loadSongsFromFolder(folderPath);
cerr << "Loaded songs from folder: " << folderPath << endl;
}
ImGuiFileDialog::Instance()->Close();
}
//收藏歌曲
ImGui::Text("Tip: If you want to add a song to Myfav,please drag your music files to 'Myfav' under the dictionary.");
ImGui::Text("Push the Button to switch to Myfav songs");
if (ImGui::Button("The Song you like")) {
list.loadSongsFromFolder("./Myfav");
player.stop();
lastShownSong = -1;
}
// 显示当前播放的歌曲
if (list.now > 0 && list.now <= list.body.size()) {
ImGui::Text("Now Playing is : %s", list.body[list.now - 1].filename);
if (lastShownSong != list.now) {
list.body[list.now - 1].show(); // 仅显示一次
lastShownSong = list.now;
}
}
else {
ImGui::Text("There is No Song playing now.");
lastShownSong = -1; // 重置,表示没有歌曲播放
}
if (list.now>0&&list.now<=list.body.size()) {
//加载音频
if (!player.isPlaying() && !player.isPaused()) {
player.load(list.body[list.now - 1].filepath, true);
}
//播放/暂停按钮
if (ImGui::Button(player.isPlaying() ? "Pause" : "Play")) {
if (player.isPlaying())player.pause();
else player.play();
}
ImGui::SameLine();
//重播按钮
if (ImGui::Button("Resume")) {
player.stop();
player.resume();
}
//歌曲进度条控制
float current_time = player.getCurrentTime();
float total_time = player.m_currentSoundDuration;
songProgress = (total_time > 0.0f) ? current_time / total_time : 0.0f;
ImGui::Text("Progress: %.2f / %.2f sec", current_time, total_time);
if (ImGui::SliderFloat("##Progress", &songProgress, 0.0f, 1.0f)) {
// 拖动进度条 -> 手动跳转
player.seek(songProgress * total_time);
}
// 音量控制
ImGui::Text("Volume:");
if (ImGui::SliderFloat("##Volume", &volume, 0.0f, 1.0f))player.setVolume(volume);
//下一首
if (ImGui::Button("Next")) {
player.stop();
list.now++;
if (list.now > list.body.size())list.now = 1;
lastShownSong = list.now;
}
ImGui::SameLine();
//上一首
if (ImGui:: Button("Last")) {
player.stop();
list.now--;
if (list.now < 1)list.now = list.body.size();
lastShownSong = list.now;
}
}
// 歌曲列表选择框
ImGui::Text("Song List:");
if (ImGui::BeginCombo("##SongList", list.now>0&&list.now<=list.body.size()?list.body[list.now-1].filename.c_str():"Select a Song")) {
for (int i = 0; i < list.body.size(); i++) {//选择歌曲
bool isSelected = (list.now == i+1);
if (ImGui::Selectable(list.body[i].filename.c_str(), isSelected)) {
list.now = i + 1; // 切换歌曲
player.stop(); // 停止当前播放
player.load(list.body[list.now - 1].filepath, true); // 加载新歌曲
lastShownSong = -1;
}
if (isSelected)ImGui::SetItemDefaultFocus();//显示高亮
}
ImGui::EndCombo();
}
// 添加歌曲
ImGui::Text("Add a Song:");
config.path = "."; // 初始目录
if (ImGui::Button("Choose a file")) {
ImGuiFileDialog::Instance()->OpenDialog("ChooseFile", "Choose a file", ".mp3,.wav", config);
}
if (ImGuiFileDialog::Instance()->Display("ChooseFile")) {
if (ImGuiFileDialog::Instance()->IsOk()) {
string path = ImGuiFileDialog::Instance()->GetFilePathName();
string name = ImGuiFileDialog::Instance()->GetCurrentFileName();
list.add_song(name,path);
}
ImGuiFileDialog::Instance()->Close();
}
//删除歌曲
ImGui::Text("Delete a Song:");
if (ImGui::BeginCombo("##Song_to_del", "Choose a Song to Delete")) {
for (int i = 0; i < list.body.size(); i++) {//选择歌曲
bool isSelected = (list.now == i + 1);
if (ImGui::Selectable(list.body[i].filename.c_str(), isSelected)) {
if(list.now==i+1)player.stop(); // 停止当前播放
list.del_song(i+1);
}
if (isSelected)ImGui::SetItemDefaultFocus();//显示高亮
}
ImGui::EndCombo();
}
ImGui::End();
ImGui::PopFont(); // 恢复默认字体
}
5.展示效果




6.记录
这是大一下的实践周作业,由本人独立完成(当然少不了ChatGpt的帮助)
程序目前测试似乎没有什么明显的bug
不过程序运行起来还是有些弊端的,比如ImGui不支持中文(尽管已经我换了字体),比如不能在程序内直接操作音乐文件
耗时大概一天半
在这之前用了一天写了个字符界面的播放器版本
不过字符界面版本弊端更大:只能手动输入文件路径,由于用playsound函数只能播放wav文件,播放暂停都不够灵活等等问题
我发现ImGui还是挺好用的,写一个小窗口之类的还是比较轻松的
如果你看出来程序有bug,请留言

浙公网安备 33010602011771号