C++大作业-简易音乐播放器的开发

Music Player

1.所需的工具以及库

ImGui+Glfw进行图形界面的开发

miniaudio完成播放器类的开发

2.功能

  1. 显示歌曲文件的基本信息

  2. 正常播放.mp3,.wav音乐文件

  3. 拥有扩展功能(下一首/音量控制/进度条调整)

  4. 通过用户的交互来进行资源设置

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, &currentFrame) == 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,请留言

posted @ 2025-05-08 10:49  Marinaco  阅读(160)  评论(0)    收藏  举报
//雪花飘落效果