欢迎光临
我们一直在努力

简单的音乐播放列表管理器 数据结构C++后端+html前端

AtomGit | GitCode – 全球开发者的开源社区,开源代码托管平台}https://gitcode.com/2401_85824583/MusicPlayer项目链接在上面!

简单的音乐播放列表管理器

  • 项目内容描述

本项目旨在开发一个功能完备、交互现代化的“简易音乐播放列表管理器”。项目基于 C++ 语言开发,深入应用了双向链表与栈这两种核心数据结构,实现了对音乐播放顺序的高效管理及播放历史的精确回溯。

项目不仅完成了传统的控制台交互功能,还创新性地引入了 B/S(浏览器/服务器)架构。通过内嵌轻量级 HTTP 服务器,实现了 Web 端图形化界面 与 C++ 后端 的实时双向通信。用户既可以通过键盘在控制台操作,也可以通过浏览器享受玻璃拟态风格的现代化 UI,实现切歌、暂停、列表管理及数据持久化等操作。

  • 需求分析

  • 项目背景与目标

  • 1.1 项目背景

    在数字多媒体时代,音乐播放器是用户接触最频繁的软件之一。虽然市面上的播放器功能繁多,但其核心本质是对“有序数据集合”的管理与操作。传统的课程设计往往局限于黑底白字的控制台交互,缺乏直观性和现代感。

    本项目旨在回归数据结构的本源,通过开发一个功能精简但架构完整的音乐播放列表管理系统,深入剖析双向链表在数据动态增删中的优势,以及栈在处理历史回溯问题上的独特作用。同时,项目尝试引入 B/S(浏览器/服务器)架构,实现计算逻辑与交互界面的分离,提升系统的实用价值。

    1.2 项目目标

    数据结构应用目标:构建高效的数据模型。利用双向链表实现歌曲的灵活管理(O(1)复杂度的节点插入与删除),利用栈实现“上一首”功能的精确历史回溯。

    系统设计目标:实现模块化设计。将核心数据逻辑层与用户界面层解耦,确保核心算法的独立性与可复用性。

    创新拓展目标:打破传统交互限制。通过引入轻量级 Web 服务技术,实现图形化界面(GUI),使用户能通过浏览器直观地控制播放流程,模拟真实软件的交互体验。

  • 功能需求
  • 2.1 详细功能描述

    模块

    功能名称

    详细描述

    优先级

    列表

    管理

    添加歌曲

    系统需支持动态向播放列表中添加新歌曲。支持在列表末尾追加,确保操作的时间复杂度最优。

    P0 (核心)

    删除歌曲

    用户指定歌曲名称或序号后,系统需从链表中移除对应节点,并正确重连前后节点,防止断链。

    P0 (核心)

    浏览列表

    能够完整遍历当前播放列表,输出所有歌曲的名称及歌手信息,供用户查看。

    P0 (核心)

    播放

    控制

    播放

    下一首

    控制当前播放指针向后移动。若处于列表末尾,根据设定模式(如循环播放)跳转至列表头部。

    P0 (核心)

    播放

    上一首

    需记录用户的每一次切歌行为。当点击上一首时,利用后进先出的特性,准确回到上一次播放的歌曲,而非简单的链表前驱。

    P0 (核心)

    数据

    持久化

    存取档案

    系统关闭时,自动将内存中的链表数据序列化保存到本地文件;系统启动时,自动读取文件重建链表结构。

    P1 (主要)

    Web

    拓展

    远程控制

    启动 HTTP 服务端口,允许用户通过浏览器访问操作界面,实时显示当前歌曲信息,并发送控制指令。

    P2 (拓展)

  • 非功能需求
  • 3.1 响应时间

    • C++版:在100首歌曲的数据量下,添加/删除操作应在 0.01秒内完成;搜索操作不超过0.5秒。
    • Web版:点击“播放”后,音频缓冲延迟不超过1秒;界面无卡顿,FPS 保持60。

    3.2 易用性

    • 控制台界面:菜单层级不超过 2 层,提示语清晰(例如:"输入 1 添加歌曲")。
    • Web UI:图标直观(使用通用的播放/暂停图标),支持键盘快捷键(空格暂停,方向键切歌)。

    3.3 可靠性

    • 边界条件处理:当列表为空时,点击“下一首”程序不应崩溃。
    • 异常处理:读取损坏的文件或无效的音频链接时,应提示错误而非退出程序。

    3.4 可扩展性

    • 代码应采用模块化设计(例如 C++ 中将 Playlist 类与 main 函数分离),方便未来添加功能。
  • 验收标准
  • 4.1 功能验收

    • 添加与删除:

    能成功添加一首新歌到列表末尾。

    能指定删除列表中的某一首歌,且链表结构不断裂(前后节点自动连接)。

    • 播放逻辑:

    在第一首歌点击“上一首”,应跳转到最后一首歌(循环测试)。

    在最后一首歌点击“下一首”,应跳转到第一首歌。

    4.2 技术验收

    • 数据结构正确性:

    析构函数中是否正确释放了所有节点的内存(无内存泄漏)。

    指针操作逻辑清晰,没有悬空指针。

    • 代码规范:

    变量命名规范,有必要的注释。

    图3-1 系统用例图

    • 系统总体设计

    1. 系统架构设计

    本系统采用分层架构设计,将系统自上而下划分为表现层、服务层和核心逻辑层。这种设计有效地实现了用户交互与业务逻辑的解耦,便于功能的扩展与维护。

    1.1表现层:

    • 控制台界面:基于 C++ 标准输入输出流,结合 <conio.h> 实现无阻塞按键检测,提供轻量级的命令行交互。
    • Web 图形界面:基于 HTML5/CSS3/JavaScript 构建,提供玻璃拟态风格的现代化操作界面,支持左右分栏布局与动态交互。

    1.2服务层:

    • HTTP 服务器:利用httplib库搭建嵌入式服务器,监听8080端口,负责接收前端请求并分发至逻辑层。
    • API 接口与适配:处理JSON数据的序列化与反序列化,并利用Windows API实现GBK与UTF-8的字符集双向转换,解决跨平台乱码问题。

    1.3逻辑与数据层:

    • 播放列表管理器:系统的核心引擎,维护双向链表结构,实现增删改查、时间计算及播放模式控制。
    • 历史记录栈:维护播放历史,提供后进先出的回溯能力。
    • 数据持久化:负责与本地文件系统交互,实现数据的存取。

    图4-1 系统部署图

    1.4 代码组织架构与模块化设计

    为了提高代码的可维护性、可读性以及团队协作的效率,本系统严格遵循软件工程中的“高内聚、低耦合”原则,采用了头文件(.h)与源文件(.cpp)分离的工程化代码组织方式。

    • 接口声明 (.h): DataStructures.h、Playlist.h 等文件仅包含类、结构体的定义及函数原型的声明。它们充当了模块间的接口,使得其他模块(如 main.cpp 或 WebServer.h)只需关注“能做什么”,而无需关心“怎么做”。
    • 逻辑实现 (.cpp): Playlist.cpp、HistoryStack.cpp 等文件包含了具体的算法逻辑和成员函数实现。这种分离确保了核心业务逻辑被封装在模块内部,外部无法直接干涉,同时也便于团队成员并行开发不同模块,避免了代码冲突。
    • 依赖管理: 通过 #pragma once 预处理指令防止头文件重复包含,构建了清晰的依赖引用关系树。

    图4-2 系统代码文件组织依赖图

    2. 系统模块结构图

    图4-3 系统类图

    根据功能划分,系统包含以下核心模块及其输入输出定义:

    2.1 输入/交互模块

    • 功能:接收用户的键盘指令(如空格暂停、ESC退出)或 HTTP 请求(如点击切歌)。
    • 输入:键盘扫描码、HTTP POST请求。
    • 输出:调用逻辑层的对应函数。

    2.2 核心控制模块

    • 功能:协调数据流动,控制播放状态。
    • 子功能:
    • 时间控制:计算歌曲播放进度,处理暂停/继续的时间累积。
    • 模式控制:在顺序播放与随机播放(含防撞逻辑)间切换。
    • 自动流转:监测歌曲是否结束,触发自动切歌。

    2.3 数据结构管理模块

    • 双向链表模块:
    • 功能:存储歌曲数据 (Song)。
    • 操作:尾插法添加 (addSong)、指针重连删除 (deleteSong)、正向/反向遍历。
    • 历史栈模块:
    • 功能:记录播放足迹。
    • 操作:切歌时压栈 (push)、回退时弹栈 (pop)。

    2.4 文件存储模块

    同时,Playlist 类维护了head(头指针)、tail(尾指针)和current(当前播放指针)。

    // 1. [基础数据对象] 歌曲信息

    // 作用:系统的最小数据单元,封装歌曲元数据

    struct Song {

        string title;   // 歌名

        string artist;  // 歌手

        int duration;   // 时长(秒)

        // 构造函数:方便快速初始化

        Song(string t = "", string a = "", int d = 0)

            : title(t), artist(a), duration(d) {}

    };

    // 2. [链表节点] 双向节点 DNode

    // 作用:双向链表的基本组成单位

    struct DNode {

        Song data;      // 数据域:存储歌曲对象

        DNode* prev;    // 指针域:指向前驱节点 (上一首)

        DNode* next;    // 指针域:指向后继节点 (下一首)

        // 构造函数:初始化指针为空

        DNode(Song s) : data(s), prev(nullptr), next(nullptr) {}

    };

    // 3. [核心 ADT] 播放列表管理类 Playlist

    // 作用:封装双向链表的操作,对外提供高层接口

    class Playlist {

    private:

        // — 核心指针 —

        DNode* head;    // 头指针:指向列表第一首歌

        DNode* tail;    // 尾指针:指向列表最后一首歌 (实现O(1)尾插的关键)

        DNode* current; // 当前指针:指向当前正在播放的歌曲

        // — 状态属性 —

        int songCount;          // 歌曲总数

        bool isPaused;          // 播放状态标记

        int playMode;           // 播放模式 (顺序/随机/单曲)

    public:

        Playlist();     // 构造函数:初始化空链表

        ~Playlist();    // 析构函数:释放所有节点内存

        // — 核心操作接口 —

        

        // 增加:在列表末尾添加歌曲 [时间复杂度 O(1)]

        void addSong(string title, string artist, int duration);

        // 删除:根据歌名查找并删除节点,自动重连前后指针 [时间复杂度 O(N)]

        void deleteSong(string title);

        // 遍历:切换到下一首 (包含随机防撞/循环逻辑)

        void playNext();

        // 回溯:切换到上一首 (调用栈的 Pop 操作)

        void playPrevious();

        // 控制:暂停/继续播放

        void togglePause();

        // 查询:获取当前播放进度字符串 (MM:SS)

        string getProgressString();

        

        // 持久化:清空列表与文件存取

        void clearAll();

        void saveToFile(string filename);

    };

    数据结构

    任意位置插入/删除

    索引访问

    双向遍历

    内存空间

    结论

    数组 (Vector)

    O(N) (需移动元素)

    O(1)

    支持

    连续且紧凑

    不适合频繁增删

    单链表

    O(1) (需已知前驱)

    O(N)

    不支持

    节点分散

    无法实现上一首

    双向链表

    O(1)

    O(N)

    支持

    节点分散,额外指针开销

    最适合本系统

    表5-1 数据结构比较

    1.2 栈 (用于历史回溯)

    自定义 HistoryStack 类,底层采用链式存储(防止栈溢出)。每个 StackNode 存储一个指向 DNode 的指针,代表用户曾经听过的歌曲节点。

    // 1. [栈节点] StackNode

    // 作用:链式栈的节点,仅存储引用,不复制数据

    struct StackNode {

        DNode* playlistNode; // 数据域:存储指向 DNode 的指针 (轻量级引用)

        StackNode* next;     // 指针域:指向栈中下一个元素

        StackNode(DNode* node) : playlistNode(node), next(nullptr) {}

    };

    // 2. [核心 ADT] 历史记录栈 HistoryStack

    // 作用:利用 LIFO 特性管理播放历史,支持非线性回溯

    class HistoryStack {

    private:

        StackNode* topNode; // 栈顶指针:始终指向最新压入的历史记录

    public:

        HistoryStack();     // 构造函数

        ~HistoryStack();    // 析构函数:清理栈空间

        // — 核心操作接口 (Operations) —

        // 入栈 (Push):在切歌前保存当前歌曲指针 [时间复杂度 O(1)]

        void push(DNode* node);

        // 出栈 (Pop):获取并移除最近一次播放的歌曲指针 [时间复杂度 O(1)]

        DNode* pop();

        // 判空 (IsEmpty):检查是否有历史记录

        bool isEmpty();

        // 清空 (Clear):释放栈中所有节点

        void clear();

    };

    算法流程描述

    若为空:说明是列表的第一首歌。将 head、tail 和 current 全部指向新节点。

    若非空:执行尾部挂载。

    将原尾节点的 next 指向新节点 (tail->next = newNode)。

    将新节点的 prev 指向原尾节点 (newNode->prev = tail)。

    更新 tail 指针指向新节点 (tail = newNode)。

    图5-1 歌曲添加算法程序流程图

    void Playlist::addSong(string title, string artist, int duration) {

        Song newSong(title, artist, "", duration);

        DNode* newNode = new DNode(newSong);

        if (head == nullptr) {

            // 如果列表为空,头尾都指向新节点

            head = tail = current = newNode;

            // 初始化状态…

        } else {

            // 如果不为空,挂载到 tail 后面

            tail->next = newNode;   // 原尾指向新节点

            newNode->prev = tail;   // 新节点指向原尾

            tail = newNode;         // 更新尾指针

        }

        songCount++;

    }

    • 功能:数据的序列化与反序列化。
    • 输入:链表内存数据。
    • 输出:music_data.txt 文本文件。
    • 数据结构与算法详细设计

    • 1. 核心数据结构设计

      本系统针对“音乐播放列表管理”的特定场景,精心选择了双向链表和栈作为核心数据结构。

      1.1 双向链表 (用于播放列表管理)

    • 结构定义:
    • 系统定义了 DNode 结构体作为链表节点,每个节点包含:

    • 数据域 Song data:存储歌名、歌手、时长等信息。
    • 指针域 prev:指向前一首歌曲。
    • 指针域 next:指向后一首歌曲。
    • 选择理由:
    • 高频插入与删除:用户经常需要在列表末尾添加歌曲或删除中间某首歌曲。双向链表在已知节点位置的情况下,插入和删除操作的时间复杂度为 O(1),而数组(如vector)在中间删除需要移动大量元素,时间复杂度为 O(n)。
    • 双向遍历需求:播放器核心功能包含“上一首”和“下一首”。双向链表的 prev 和 next 指针支持O(1)时间的双向跳转,非常适合线性播放逻辑。
    • 动态性:播放列表长度不固定,链表支持动态内存分配,无固定容量限制。
    • 结构定义:
    • 选择理由:
    • 非线性回溯需求:在“随机播放”模式下,播放顺序是跳跃的(如 A -> C -> B)。此时点击“上一首”,用户期望回到 C 而不是列表逻辑上的 A。
    • 后进先出:用户的听歌足迹符合“最近听过的最先回溯”的特性,这与栈的操作逻辑完美契合。
    • 核心算法设计与分析

    •  歌曲添加算法

    • 创建节点:根据输入的歌曲信息(歌名、歌手、时长)在堆内存中申请一个新的 DNode 节点。
    • 判空检查:检查链表头指针 head 是否为空。
    • 链接操作:
    • 更新状态:歌曲总数 songCount 加 1。
    • 复杂度分析:
    • 时间复杂度:O(1)。得益于tail尾指针的设计,系统直接定位到队尾进行挂载,避免了线性遍历,确保了大规模数据下的极速响应。
    • 空间复杂度:O(1)。每次添加仅分配单个节点的内存空间。
    • 歌曲删除算法

    算法流程描述

    遍历查找:从链表头指针 head 开始,逐一比对节点的 data.title。 当前指针维护:若待删节点 p 等于当前播放指针 current:将 current 移至后继节点(若无后继则移至前驱)。若链表删空,置 current 为 nullptr。重置播放器状态为“暂停”并归零时间,防止逻辑错误。 断链重连:

    删头:更新 head 为 p->next。若新头存在,断开其 prev 指针。

    删尾:更新 tail 为 p->prev。若新尾存在,断开其 next 指针。

    删中间:将前驱节点的 next 指向后继,将后继节点的 prev 指向前驱。

    图5-2 歌曲删除算法程序流程图

    void Playlist::deleteSong(string title) {

        if (!head) return;

        DNode* p = head;

        // 遍历查找

        while (p != nullptr) {

            if (p->data.title == title) {

                // 1. 如果删除的是当前正在播放的歌,指针后移

                if (p == current) {

                    current = (p->next) ? p->next : p->prev;

                    if (head == tail) current = nullptr; // 删完空了

                    // … 重置播放状态 …

                }

                // 2. 断链核心逻辑

                if (p == head && p == tail) { // 只有一首

                    head = tail = nullptr;

                } else if (p == head) {       // 删头

                    head = p->next;

                    head->prev = nullptr;

                } else if (p == tail) {       // 删尾

                    tail = p->prev;

                    tail->next = nullptr;

                } else {                      // 删中间

                    p->prev->next = p->next;  // 前驱指向后继

                    p->next->prev = p->prev;  // 后继指向前驱

                }

                delete p; // 释放内存

                songCount;

                return;

            }

            p = p->next;

        }

    }

    随机播放防撞算法

    此算法用于在随机模式下决定下一首播放的歌曲,重点在于解决“随机到当前歌曲”的体验问题。

    否:保持 target 不变。

    图5-3 随机播放防撞算法程序流程图

    判断 if (target == current)?

    是 (发生冲突):强制移动 target 指针。若 target->next 存在,则 target = target->next;否则(已是队尾),target = head。

    • 资源释放:执行 delete p 释放堆内存,songCount 减 1。
    • 复杂度分析:
    • 时间复杂度:主要消耗在查找节点的遍历上,平均与最坏时间复杂度均为 O(N)。实际删除动作(指针操作)为 O(1)。
    • 空间复杂度:仅使用了几个指针变量,空间复杂度为O(1)。
    • 算法流程描述:
    • 输入:当前歌曲指针 current,歌曲总数 songCount。
    • 生成步数:计算随机步数 steps = rand() % songCount。
    • 定位目标:从头节点 head 开始,向后移动 steps 次,找到候选节点 target。
    • 防撞检测:
    • 输出:将 current 更新为 target 并开始播放。

    void playNext_Shuffle() {

        // 1. 生成随机步数

        int steps = rand() % songCount;

        DNode* target = head;

        // 2. 移动指针寻找目标

        for (int i = 0; i < steps; i++) {

            if (target->next) target = target->next;

        }

        // 3. 防撞逻辑:如果随机到了自己,强行移一位

        if (target == current) {

            if (target->next) target = target->next; // 往后挪

            else target = head; // 队尾则回到队头

        }

        current = target; // 更新播放指针

    }

    复杂度分析:

    顺序循环播放算法

    此算法是播放器的默认行为,负责按照链表顺序切换歌曲,并在到达列表末尾时自动循环至头部。

    判断 current->next 是否存在?

    是 (非队尾):current = current->next,直接移动到后继节点。

    否 (是队尾):current = head,指针回绕到头节点,实现列表循环。

    图5-4 顺序循环播放算法程序流程图

    void Playlist::playNext_Sequential() {

          // 1. 记录历史

          history.push(current);

          // 2. 指针移动逻辑

          if (current->next != nullptr) {

                // 普通情况:移向下一首

                current = current->next;

          }

          else {

                // 边界情况:列表循环

                current = head;

          }

          // 3. 重置播放时间状态

          playStartTime = time(NULL);

          accumulatedTime = 0;

          isPaused = false;

    }

    单曲循环播放算法

    此算法集成在系统的自动切歌检测机制中。每当主循环检测到当前歌曲播放结束时,根据当前播放模式决定是切换下一首还是重新播放当前曲目。

    (1) 算法流程描述

    是 (单曲模式):保持 current 指针不变。将 playStartTime 更新为当前时间,并将 accumulatedTime 重置为 0。

    否 (其他模式):调用 playNext() 切换到下一首歌曲。

    图5-5 单曲循环播放算法程序流程图

    • 时间复杂度:主要消耗在寻找target的遍历上。最坏情况下步数为 songCount – 1,因此时间复杂度为O(N),其中N为歌曲总数。对于一般的播放列表(N < 10000),该延迟可忽略不计。
    • 空间复杂度:仅使用了几个指针变量,空间复杂度为O(1)。
    • 算法流程描述:
    • 输入:当前播放指针 current,链表头指针 head。
    • 判空:若 current 为空,则不执行任何操作。
    • 入栈:将当前 current 压入历史栈(保存播放足迹)。
    • 指针流转:
    • 状态重置:更新 playStartTime 为当前时间,重置accumulatedTime为0,设置 isPaused = false。
    • 复杂度分析:
    • 时间复杂度:O(1)。该算法仅涉及一次条件判断和一次指针赋值操作,不随歌曲数量N增加而耗时。
    • 空间复杂度:O(1)。仅需常数级辅助空间。
    • 触发条件:主程序轮询检测到 getElapsed() >= current->duration(当前播放进度超过歌曲总时长)。
    • 模式判断:检查当前 playMode 是否为 SINGLE (单曲循环)。
    • 执行分支:
    • 输出:播放器无缝重新开始播放当前歌曲,进度条归零。

    // 自动播放检测逻辑

    bool Playlist::checkAutoNext() {

        // 1. 基础检查

        if (!current || isPaused) return false;

        

        // 2. 检查是否播放结束

        if (getElapsed() >= current->data.duration) {

            

            // 3. 模式判断

            if (playMode == SINGLE) {

                // === 单曲循环核心逻辑 ===

                // 不移动 current 指针

                // 仅重置时间状态,实现原地重播

                playStartTime = time(NULL);

                accumulatedTime = 0;

            } else {

                // 其他模式则切歌

                playNext();

            }

            return true; // 触发了状态变更

        }

    return false;

    }

    复杂度分析:

    历史回溯算法

    本系统利用栈的“后进先出”特性来管理播放指针的历史记录。每当发生非线性的切歌操作(如下一首、随机跳转)时,将当前节点入栈;当用户点击“上一首”时,从栈顶弹出节点以恢复播放状态。

    (1) 入栈操作

    当执行playNext或随机跳转前,系统调用此函数记录当前播放位置。

    图5-6-1 历史回溯入栈操作算法程序流程图

    void HistoryStack::push(DNode* node) {

          // 创建一个新的栈节点,存储指向歌单节点的指针

          StackNode* newNode = new StackNode(node);

          // 头插法:新节点指向原栈顶

          newNode->next = topNode;

          // 更新栈顶指针

          topNode = newNode;          

    }

    (2) 出栈回溯操作

    当用户触发“上一首”指令时,系统调用此函数恢复上一次的播放状态。

    图5-6-2 历史回溯出栈操作算法程序流程图

    void Playlist::playPrevious() {

          // 1. 判空检查 O(1)

          if (history.isEmpty()) return;

          

          // 2. 弹出操作 O(1)

          // pop() 内部执行:获取栈顶元素 -> 移动 topNode -> delete 原栈顶

          DNode* prevNode = history.pop();

          

          // 3. 恢复播放指针 O(1)

          if (prevNode) {

                current = prevNode;

                // 重置播放时间状态 (O(1))

                playStartTime = time(NULL);

                accumulatedTime = 0;

                isPaused = false;

          }

    }

    • 时间复杂度:O(1)。算法仅涉及一次时间获取 time(NULL) 和两次变量赋值,不涉及链表遍历或复杂的计算,执行效率极高,不会阻塞主线程。
    • 空间复杂度:O(1)。无需申请额外内存。
    • 复杂度分析:
    • 时间复杂度:O(1)。该算法仅涉及内存分配、指针赋值(next指针挂载)和栈顶指针更新这三个基本操作,不包含任何循环结构,执行时间不随历史记录长度的变化而改变。
    • 空间复杂度:O(1)。每次调用仅申请一个StackNode的内存空间,用于存储指针地址。
    • 复杂度分析:
    • 时间复杂度:O(1)。isEmpty()判空、pop()出栈(涉及删除栈顶节点并移动指针)以及更新current播放指针均为常数级操作,系统能瞬间完成回溯,无延迟。
    • 空间复杂度:O(1)。该操作仅使用了少量的辅助指针变量(如prevNode)。

    暂停/继续时间计算算法

    为了实现暂停功能,不能简单依赖系统当前时间 – 开始时间,需要引入“累积时间”。

    逻辑说明:

    图5-7 暂停/继续时间计算算法程序流程图

    void Playlist::togglePause() {

          if (!current) return;

          

          if (isPaused) {

                // [继续播放]:重置开始时间为"现在"

                playStartTime = time(NULL);

                isPaused = false;

          } else {

                // [暂停]:结算刚才那段播放时间,存入累积池

                accumulatedTime += (time(NULL) playStartTime);

                isPaused = true;

          }

    }

    // 获取当前播放进度(秒)

    int Playlist::getElapsed() {

          if (!current) return 0;

          if (isPaused) {

                // 暂停态:直接返回存下来的累积时间

                return (int)accumulatedTime;

          } else {

                // 播放态:累积时间 + 本次播放时长

                return (int)(accumulatedTime + (time(NULL) playStartTime));

          }

    }

    停止播放算法

    此算法用于重置播放器的状态。与“暂停”不同,“停止”操作不仅会中断播放,还会将当前歌曲的播放进度强制归零,使用户下次点击播放时从头开始。

    图5-8 停止播放算法程序流程图

    void Playlist::stop() {

        // 1. 强制暂停

        isPaused = true;

        // 2. 核心区别:将累积时间彻底归零

        accumulatedTime = 0;

        // 3. 重置时间锚点 (为下次重新开始做准备)

        playStartTime = time(NULL);

        // (注:current 指针保持不变,仍指向当前歌曲)

    }

    字符编码转换算法

    本系统涉及 Windows 控制台(默认 GBK 编码)与 Web 浏览器(默认 UTF-8 编码)的数据交互。为解决中文乱码问题,设计了双向转码算法。

    以 GBK 转 UTF-8 为例:

    注:调两次 API的作用:

          // 参数7 NULL:       无法转换字符的默认字符 (UTF-8 通常设为 NULL)

          // 参数8 NULL:       是否使用了默认字符的标志 (通常设为 NULL)

          int len_utf8 = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], 1, NULL, 0, NULL, NULL);

          // 如果计算失败,原样返回

          if (len_utf8 == 0) return str;

          // 根据计算出的长度 len_utf8 分配 string 缓冲区

          string str_utf8(len_utf8, 0);

          // 第 2 次调用 WideCharToMultiByte: 执行实际转换

          // 参数5 &str_utf8[0]: 输出缓冲区的首地址

          // 参数6 len_utf8:       缓冲区的大小

          WideCharToMultiByte(CP_UTF8, 0, &wstr[0], 1, &str_utf8[0], len_utf8, NULL, NULL);

          // 移除末尾可能多余的结束符 '\\0' (API 有时会把 null 结束符也算进长度里)

          if (!str_utf8.empty() && str_utf8.back() == '\\0') str_utf8.pop_back();

          return str_utf8;

    空间复杂度: O(N)。为了完成转换,算法需要开辟额外的内存空间来存储:中间缓冲区存储转换过程中的宽字符(wstring / UTF-16),其大小与输入长度成正比。结果缓冲区存储最终转换后的字符串(string / UTF-8),其大小同样与输入长度成正比。因此整体空间复杂度为线性。这意味着内存消耗随文本长度线性增长,对于音乐播放器的文本数据量级,内存开销极小且可控。

    图5-9 字符编码转换算法程序流程图

    // 辅助函数:将 GBK 编码字符串转换为 UTF-8 编码字符串

    // 参数 str: 输入的 GBK 字符串 (通常来自 C++ 控制台或 txt 文件)

    // 返回值: 转换后的 UTF-8 字符串 (用于发送给 Web 浏览器)

    static string GBKToUTF8(const string& str) {

          // 空检查:如果输入为空,直接返回空,避免不必要的 API 调用

          if (str.empty()) return "";

          // 阶段 1: GBK (多字节) -> UTF-16 (宽字符)

          // 第 1 次调用 MultiByteToWideChar: 获取所需的缓冲区长度

          // 参数1 CP_ACP:   Code Page ANSI Code Page,指当前系统的默认 ANSI 编码 (中文 Windows 下即为 GBK)

          // 参数2 0:          标志位,通常为 0

          // 参数3 str.c_str(): 输入字符串的指针

          // 参数4 -1:         输入字符串长度。传 -1 表示字符串以 null 结尾,API 会自动计算长度

          // 参数5 NULL:      输出缓冲区指针。传 NULL 表示不执行转换,只计算长度

          // 参数6 0:          输出缓冲区大小。传 0 配合参数5使用

          int len = MultiByteToWideChar(CP_ACP, 0, str.c_str(), 1, NULL, 0);

          // 如果计算失败或长度为0,原样返回

          if (len == 0) return str;

          // 根据计算出的长度 len 分配宽字符缓冲区 (wstring)

          wstring wstr(len, 0);

          // 第 2 次调用 MultiByteToWideChar: 执行实际转换

          // 参数5 &wstr[0]: 输出缓冲区的首地址

          // 参数6 len:         缓冲区的大小

          MultiByteToWideChar(CP_ACP, 0, str.c_str(), 1, &wstr[0], len);

          // 阶段 2: UTF-16 (宽字符) -> UTF-8 (多字节)

          // 第 1 次调用 WideCharToMultiByte: 获取所需的 UTF-8 缓冲区长度

          // 参数1 CP_UTF8:   目标编码为 UTF-8

          // 参数2 0:            标志位

          // 参数3 &wstr[0]: 输入的宽字符串指针 (来自上一阶段的输出)

          // 参数4 -1:          输入长度,-1 表示自动检测 null 结尾

          // 参数5 NULL:       输出缓冲区。传 NULL 表示只计算长度

          // 参数6 0:            输出缓冲区大小

    • 播放中:实际进度 = 累积时间(accumulatedTime) + (当前系统时间 -本次开始时间)。
    • 暂停动作:当用户点击暂停时,计算 (当前系统时间 – 本次开始时间) 并加到 accumulatedTime 中。
    • 继续动作:更新 playStartTime 为当前系统时间,继续计时。
    • 复杂度分析:
    • 时间复杂度:O(1)。仅包含布尔值的判断、简单的加减法算术运算以及一次系统时间调用。这些操作的执行次数是固定的,不包含任何循环结构,也不依赖于歌曲数量N。
    • 空间复杂度:O(1)。函数执行过程中,仅在寄存器或栈上产生极少量的临时变量用于存储计算中间值,未申请任何新的堆内存或数组空间。
    • 逻辑说明:
    • 输入:用户触发“停止”指令(控制台按 S 键或 Web 端点击停止按钮)。
    • 状态锁定:将播放状态标记 isPaused 设置为 true,立即停止界面的自动刷新和时间递增。
    • 进度归零:将累积播放时间 accumulatedTime 强制重置为 0。
    • 时间锚点重置:更新 playStartTime 为当前系统时间(防止逻辑恢复播放时出现时间跳变)。
    • 输出:界面进度条回退至 00:00,状态显示为“[Stopped]”。
    • 复杂度分析:
    • 时间复杂度:O(1)。该算法仅涉及三个成员变量的赋值操作,不涉及任何循环或递归,执行速度极快,不受歌单长度影响。
    • 空间复杂度:O(1)。不需要申请任何额外的辅助空间。
    • 逻辑说明:
    • 计算长度:调用系统 API 获取 GBK 字符串转换所需的宽字符(Unicode)长度。
    • 第一重转换:将 GBK 字符串转换为 UTF-16 宽字符数组 (wstring)。
    • 计算长度:获取宽字符数组转换为 UTF-8 所需的字节长度。
    • 第二重转换:将 UTF-16 宽字符数组转换为 UTF-8 编码字符串 (string)。
    • 后处理:去除字符串末尾可能多余的结束符 \\0。
    • 第一次(传 NULL):是为了“问路”。问问系统:“转这个字符串需要多大的内存?”
    • 中间:根据问到的结果,用 wstring 或 string 申请内存。
    • 第二次(传缓冲区指针):才是真正的“走路”,把数据写进去。
    • 复杂度分析:
    • 时间复杂度: O(N)。转换过程分为两个阶段:GBK 转 UTF-16:第一次调用 API 计算所需缓冲区大小:需要遍历整个输入字符串,耗时O(N)。第二次调用 API 进行实际转换:再次遍历输入字符串写入缓冲区,耗时 O(N)。由于中间结果长度与输入长度N成正比,因此整体时间复杂度为线性。对于歌名、歌手名等短文本,转换几乎是瞬时的。

    数据持久化算法

    数据持久化是系统在关闭后保留状态的关键机制。本系统采用文本序列化的方式,将内存中的双向链表结构转换为逗号分隔的文本格式,存储于本地music_data.txt文件中。

    数据自动保存算法

    当用户触发“退出”指令或点击“保存”按钮时,系统将遍历链表,将所有歌曲信息写入文件。

    算法流程描述:

    Playlist::saveToFile(string filename) {

        ofstream outFile(filename);

        if (!outFile.is_open()) return;

        DNode* p = head;

        while (p != nullptr) {

            // 格式化写入:歌名,歌手,时长

            outFile << p->data.title << ","

                    << p->data.artist << ","

                    << p->data.duration << std::endl;

            p = p->next;

        }

        outFile.close();

    }

    系统启动时,自动读取本地文件并重建链表结构。

    算法流程描述:

                int d = 180;

                if (getline(ss, d_str)) d = stoi(d_str);

                // 3. 重建节点

                addSong(t, a, d);

            }

        }

        inFile.close();

    }

    图5-10 停止播放算法程序流程图

    void Playlist::loadFromFile(std::string filename) {

        ifstream inFile(filename);

        if (!inFile.is_open()) return;

        // 1. 关键步骤:先清空现有数据

        clearAll();

       string line;

        while (getline(inFile, line)) {

            stringstream ss(line);

            string t, a, d_str;

    • 打开文件:使用 ofstream 以截断模式(trunc)打开文件,确保覆盖旧数据。
    • 遍历链表:从头节点 head 开始,逐个访问节点。
    • 序列化写入:将每个节点的 title、artist 和 duration 按照 歌名,歌手,时长 的特定格式拼接成字符串,并写入文件的一行。
    • 资源释放:遍历结束后关闭文件流。
    • 复杂度分析:
    • 时间复杂度:O(N)。需要遍历链表中所有N个节点进行写入。
    • 空间复杂度:O(1)。仅需常数级的辅助变量。
    • 数据自动读取算法
    • 打开文件:使用 ifstream 打开文件,若文件不存在则跳过。
    • 内存清空:调用 clearAll() 函数,彻底释放当前内存中的所有链表节点和栈数据,防止“重复导入”导致的数据堆叠错误。
    • 逐行解析:利用 getline 读取文件每一行。
    • 反序列化:使用 stringstream 以逗号 , 为分隔符,解析出 title、artist 和 duration。
    • 链表重建:调用 addSong 函数(尾插法),将解析出的数据重新插入链表。
    • 复杂度分析:
    • 时间复杂度:O(N)。取决于文件中的行数,每行解析和插入操作均为常数时间。
    • 空间复杂度:O(1)。读取过程中的缓冲区大小固定,重建链表所需的内存属于必要的数据存储空间。

     // 2. 解析 CSV 格式

            if (getline(ss, t, ',') && getline(ss, a, ',')) {

                int d = 180;

                if (getline(ss, d_str)) d = stoi(d_str);

                // 3. 重建节点

                addSong(t, a, d);

            }

        }

        inFile.close();

    }

    复杂度分析:

    • 时间复杂度:O(N)。取决于文件中的行数,每行解析和插入操作均为常数时间。
    • 空间复杂度:O(1)。读取过程中的缓冲区大小固定,重建链表所需的内存属于必要的数据存储空间。
    • 系统实现

    1. 系统主要功能截图

    1.1 控制台启动与数据加载

    运行程序,显示黑框界面,且显示“[初始化] 成功导入 music_data.txt”的画面。

    图 6-1 控制台启动与数据初始化

    说明:系统启动时自动读取本地 music_data.txt 文件,调用 loadFromFile 函数重建双向链表,并提示成功导入的歌曲数量。

    1.2 Web 端主界面概览

    打开浏览器 localhost:8080,展示左侧黑胶唱片播放器和右侧常驻播放列表的完整界面。

    图 6-2 Web 端主界面

    说明:Web 界面采用玻璃拟态设计风格。左侧为播放控制区,包含旋转的黑胶唱片动画、进度条及大尺寸控制按钮;右侧为常驻播放列表,高亮显示当前曲目,并提供保存/读取入口。

    1.3 双端实时同步演示

    将浏览器窗口和 C++ 黑框窗口并排显示。浏览器显示正在播放某首歌(如

      《七里香》),黑框里也显示正在播放同一首歌,且进度条时间基本一致。

      图 6-3 C++后端与 Web 前端实时同步

      说明:展示了系统的核心特性——双端状态同步。前端通过JS轮询/api/status接口,毫秒级同步后端的播放进度、暂停状态及歌曲信息。

      1.4 随机播放与防撞机制

      控制台或 Web 端显示模式为“随机播放 [R]”,并切歌几次后的状态。

      图 6-4 随机播放模式

        说明:系统切换至随机模式,状态栏显示 [R]。此时点击“下一首”,系统将利用随机算法跳转至非当前歌曲的任意节点,避免了“切歌切到自己”的逻辑冲突。

        1.5 添加歌曲交互

        在控制台按 5 后输入歌名和歌手的界面。在 Web 端点击添加按钮,弹出模态框,输入歌名歌手后的画面。

        图 6-5 系统添加歌曲交互

        说明:展示了系统的动态数据录入能力。用户通过Web端模态框或控制台输入歌曲信息。后端接收数据后,利用Playlist类的addSong方法,采用尾插法创建新的 DNode 节点并挂载至链表尾部。由于维护了 tail 指针,该操作的时间复杂度为O(1),确保了在大量数据下依然能瞬间完成添加。

        1.6 歌曲删除交互

        在控制台按 6 进入删除模式,输入一首歌名(如“七里香”),回车后显示 [系统] 已删除: 七里香 的画面。在 Web 端点击垃圾桶图标后的界面变化。

        图 6-6 系统歌曲删除交互

        说明:展示了删除指定歌曲的流程。用户输入歌名后,系统通过遍历链表定位目标节点,执行断链重连操作,并释放内存。界面实时反馈删除结果,若删除的是当前播放歌曲,系统会自动切换至下一首。

        1.7 退出系统数据保存

        在控制台主界面按下 ESC 键,屏幕显示 正在保存数据… 以及 [系统] 数据已保存 [系统] 内存数据已清空。,随即程序关闭前的最后画面。

        图 6-7 系统自动保存界面

        说明:展示了系统的安全退出机制。当用户按下 ESC 键时,主循环捕获退出信号,触发数据持久化逻辑。系统自动调用 saveToFile 函数,将内存中的双向链表数据序列化写入 music_data.txt 文件,确保数据不丢失,随后安全终止程序。

        2. 系统测试

        为了验证系统的正确性与健壮性,我们设计了覆盖核心数据结构操作与业务逻辑的测试用例。

        2.1 核心功能测试用例表

        测试ID

        测试模块

        输入数据 / 操作步骤

        预期结果

        实际结果

        测试结论

        TC-01

        列表管理

        在空列表中依次添加《A,B》;然后删除《A》。

        链表先增至长度2,删除后长度为1head 指向《B》,tail 指向《B》。

        与预期一致

        通过

        TC-02

        边界删除

        播放列表中仅剩一首歌时,执行删除操作。

        链表清空,head,tail,current 均置空,系统状态转为已停止,程序不崩溃。

        界面显示已停止,未崩溃

        通过

        TC-03

        历史回溯

        顺序播放 A -> B -> C,切歌到 D,点击上一首

        播放指针 current 准确回退到 C,栈顶元素弹出。

        成功回退到 C

        通过

        TC-04

        随机防撞

        列表中仅有 A, B 两首歌。当前播放 A,模式设为随机,连续点击10下一首

        每次切歌的目标必定是 B,不会出现 A -> A 的情况。

        10次均为切换至 B

        通过

        TC-05

        暂停计时

        播放至 00:10 时按暂停(空格键),等待 5 秒后按继续。

        恢复播放时,进度条应从 00:10 继续走,而不是跳变到 00:15

        时间衔接准确

        通过

        TC-06

        数据持久化

        添加新歌后执行保存,重启程序,执行导入

        重启后列表包含了之前添加的新歌,且顺序一致。

        数据完整恢复

        通过

        TC-07

        Web同步

        Web 端点击暂停,观察控制台;在控制台按空格,观察 Web 端。

        Web 点击后控制台显示 [已暂停];控制台恢复后 Web 图标变回播放。

        双向同步延迟<1s

        通过

        2.2 性能与健壮性分析

        • 时间复杂度验证:

        在测试中导入了包含 100 条模拟数据的文本文件。

        • 插入操作:Web 端连续添加歌曲,响应无延迟,验证了尾插法 O(1)的高效性。
        • 删除操作:删除指定歌曲,系统瞬间完成,验证了双向链表在指针重连上的优势。
        • 内存管理验证:

        利用 VS2022 的诊断工具监测,在频繁执行 clearAll()(导入前清空)和添加操作时,内存占用保持稳定,未出现内存泄漏,证明析构函数与delete逻辑正确。

        • 异常处理验证:
          • 导入不存在的文件:系统提示“[错误] 找不到文件”,未崩溃。
          • Web 端输入特殊字符(如 Emoji):后端转码逻辑自动过滤或转换,未导致乱码崩溃。

        2.3 结论

        经过上述系统性测试,本“简易音乐播放列表管理器”的所有核心功能(增删改查、播放控制、持久化、Web交互)均运行正常,逻辑闭环,满足了《数据结构》课程设计对于数据结构正确性、算法有效性及系统稳定性的要求。

        赞(0)
        未经允许不得转载:171主机测评 » 简单的音乐播放列表管理器 数据结构C++后端+html前端
        分享到: 更多 (0)

        评论 抢沙发

        • 昵称 (必填)
        • 邮箱 (必填)
        • 网址