# 一款使用 C 语言编写的音乐播放器
**Repository Path**: zeng-pinpeng/C_MusicPlayer
## Basic Information
- **Project Name**: 一款使用 C 语言编写的音乐播放器
- **Description**: 这是一款用纯 C 语言编写的音乐播放器,有命令行界面,可能会有图形界面.
- **Primary Language**: C
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 2
- **Created**: 2023-07-25
- **Last Updated**: 2023-07-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 一款使用 C 语言编写的音乐播放器
> 作者:小荣
💡 如果编译失败请查看下文已知问题
## 简介
这是一款用纯 C 语言编写的音乐播放器,有命令行界面,~~可能会有图形界面~~(不会有了,因为使用 Win32 API 来写图形界面实在是太难了)。
这个程序是作为我的 C 语言课程设计作业,虽然这个作业除了要求至少100行代码之外并没有很严格的要求,也没有明确说明是否允许使用其他语言(如 C++),是否允许使用他人的库文件等等。
但是我认为,既然这是作业,而且是 C 语言作业。我并不是在做商业产品。
所以我打算以
1. 仅使用 C 语言作开发语言。
2. 仅使用 Dev-C++ 软件和 Windows 系统自带的库文件。
3. 不使用他人的代码。
4. 尽量不使用现成的轮子。
为原则来完成这个作业。
### 演示视频
视频链接
### 环境要求
该程序使用了 Windows API ,所以只能在 Windows 下编译和运行。
本人在 Dev-C++ 5.11 下编译通过,在 Windows 10 2004 下测试通过。
## 已经实现的功能
* 播放 WAV 文件
* 扫描本地音乐
* 显示歌词
* 显示播放进度条
* 歌单排序
* 歌单滚动显示
* 音乐可视化
* 多线程,在播放音乐的同时能显示进度条,响应用户操作。
* 较为先进的交互功能,用户可以通过键盘直接操作,无需输入特殊指令。
## 中途放弃的功能
* 扫描电脑所有音乐
在 file.h 下有一个 findAllMusic 函数,这个函数一开始就是被设计用来遍历电脑所有文件夹来查找音乐文件。有由个人水平有限,该程序在功能上有很强的局限性。如只能解码 WAV 格式的音乐文件、歌词文件只能读取 GB2312 编码的。所以我干脆放弃了扫描电脑所有音乐的功能。findAllMusic 仅仅被用来扫描程序所在文件夹下的音乐。
* 欢迎页面和帮助页面
这是该程序结构上的问题,由于设计缺陷。添加页面变得十分困难。所以我放弃了欢迎页面,
而原计划帮助页面的一些功能则添加到了主页面。
* 播放 MP3 格式和 FLAC 格式的音乐
由于 MP3 和 FLAC 的文件结构十分复杂,我在不使用他人的库文件下难以完成对 MP3 和 FLAC 文件的解码,所以我也放弃了这个功能。
## 已知问题
* 关于编译失败,提示 `undefined reference to '__imp_waveOutWrite'` 。
该程序需要使用到一个 winmm.lib 库文件。
Dev-C++ 只需要在编译器 -> 编译器选项加入 `-lwinmm` 即可。如图。

* 关于编译失败,提示 `'for' loop initial declarations are only allowed in C99 or C11 mode`。为要 C99 或者 C11 才能支持在 for 循环里面定义 i,Dev-C++ 只需要在编译器选项 -> 代码生成/优化 -> 语言标准选择 ISO C99 即可。如图。

* 在 
Windows Terminal 中运行该程序时,程序会在 getThisPath() 函数处崩溃,原因不明。
## 操作指南
程序界面如下


### 界面组件
#### 程序名
位于界面最上方,蓝底白字,显示程序名称及作者名
#### 消息提示
本程序消息提示有两处,黄色字体,较为醒目,用于程序给用户的反馈和提示。
#### 歌曲列表和选择条
歌曲列表在程序中也被称作 列表视图 或 ListView(这个名字是我自己起的,确实有点表达不准确),其中每一首歌由编号和歌名组成,用白色标注的是选择条。用户可以按方向上下键来移动选择条,按回车播放选择条所在的音乐。当选择条到达屏幕区域开头或末尾时,歌曲列表能滚动显示。当选择条到达整个歌曲列表开头或末尾时,程序还会通过消息提示反馈。用户还可以用PgUp、PgDn键来翻页。
#### 播放信息
播放信息第一行左边显示的是音乐播放状态,播放、暂停、或者停止。右边显示的是循环模式,一共有 不循环、单曲循环、列表循环、随机播放 四种模式,用户可以按 W 键切换。第二行显示的当前正在播放歌曲的歌名。如果是刚运行程序,还未开始播放音乐,那么播放信息、歌词、音乐可视化部件均不会显示。
#### 歌词
虽然本程序歌词只显示两行,不过程序歌词能据音乐播放进度自动切换(具体是通过读取歌词时间信息实现的)。其中白色字体的是当前音乐进度的歌词,绿色的是下一句歌词。如果没有歌词则会显示“未找到歌词”。
#### 音乐可视化部件
音乐可视化部件是本程序的特色功能,该部件在程序中也被称作 LevelBar,该部件是通过音乐音量水平计算出来的数据显示的,并且高速刷新。能给用户带来强烈的视觉冲击。
#### 进度条
左边的数字是音乐已播放时间,右边的数字是歌曲总时间,中间则是进度。
#### 帮助信息
显示程序的一些操作提示。
#### 调试信息
显示最近按下按键的键码。
### 其它功能
#### 扫描文件寻找音乐
程序在启动时或者按 F5 刷新后会扫描程序所在目录及其子目录,将 WAV 文件添加到歌单。
#### 从歌单移除
如果用户不喜欢某一首歌,可以按 DEL 键将某一首歌从歌单移除,按 Shift 键 + DEL 键可以将某一首歌从硬盘上删除(目前还未实现)。由于本程序列表视图支持实时更改的特性,用户可以立即看到删除后的歌单。
#### 刷新功能
这个功能被设计用来解决:1、程序自身 BUG 导致界面错乱;2、因调整控制台大小导致界面错乱;3、在程序运行时更改了歌曲文件,但程序歌单未改变等问题。用户按下 F5 键之后,程序会刷新控制台,并重新扫描音乐。不影响播放音乐。
## 代码详细介绍
### 使用的 Windows 接口
#### CompareStringA 函数
文档链接
该函数用于比较字符串(为什么不使用 strcmp 函数,因为大多数中文汉字的编码不是按照拼音排序的,strcmp 函数用来比较会有很强的局限性),声明如下
```C
int CompareStringA(
LCID Locale,
DWORD dwCmpFlags,
PCNZCH lpString1,
int cchCount1,
PCNZCH lpString2,
int cchCount2
);
```
参数 Locale 是区域标识符,LOCALE_CUSTOM_UI_DEFAULT 表示根据用户界面语言确定。
参数 dwCmpFlags 决定如何比较字符串, 0 表示默认。
参数 lpString1,cchCount1 为待比较的字符串和长度。
参数 lpString2,cchCount2 同理。
#### SetConsoleTextAttribute 函数和 GetStdHandle 函数
文档链接
```C
BOOL WINAPI SetConsoleTextAttribute(
_In_ HANDLE hConsoleOutput,
_In_ WORD wAttributes
);
```
参数 hConsoleOutput 为控制台句柄可通过 GetStdHandle(STD_OUTPUT_HANDLE) 获取。
参数 wAttributes 为颜色标识符,微软的使用起来有点麻烦,因此我重新宏定义了一套,enum COLOUR_WIN
#### SetConsoleCursorInfo 函数和 SetConsoleCursorPosition
文档链接
这两个函数可以用来设置光标属性和光标位置,使用较为简单,因此这里不做详细介绍,
#### waveOutOpen 函数
该函数用来打开声卡
文档链接
函数声明如下
```C
MMRESULT waveOutOpen(
LPHWAVEOUT phwo,
UINT uDeviceID,
LPCWAVEFORMATEX pwfx,
DWORD_PTR dwCallback,
DWORD_PTR dwInstance,
DWORD fdwOpen
);
```
参数 phwo,这个参数为一个指针,用来返回声卡的句柄。
参数 uDeviceID 用来选择声卡,WAVE_MAPPER 表示系统默认。
参数 pwfx,指向输入音频格式结构体的指针。
参数 dwInstance,(DWORD_PTR) waveOutProc 表示使用 waveOutProc 函数为回调函数。
参数 fdwOpen,CALLBACK_FUNCTION 表示使用回调函数。
#### waveOutWrite 函数
往声卡填数据的函数
文档链接
原型如下
```C
MMRESULT waveOutWrite(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
```
参数 hwo 表示,声卡句柄。
参数 pwh 为指向包含音频数据结构体(WAVEHDR structure)的指针。
参数 cbwh 为 pwh 指向的结构体的大小。
调用该函数前需要将包含音频数据的结构体用 waveOutPrepareHeader 函数处理一下,使用方法与 waveOutWrite 差不多。
#### waveOutGetPosition 函数
该函数用来获取当前播放位置的信息。
文档链接
#### waveOutClose 函数
关闭声卡
#### FindFirstFile函数 和 FindNextFile 函数
这两个是查找文件的函数。文档链接(FindFirstFile)
(FindNextFile)
### utils.h 头文件
这个头文件主要包含一些我自己写的一些实用函数,如:
fill 函数,用来填充字符串。
findLastChar 函数,用来查找某字符在字符串中最后一次出现的位置。random 函数,可以生成指定范围内的随机数。
这个头文件是这个项目刚开始的时候就开始写的,后续也主要是作补充,所以最终这个项目完成时可能会有很多函数没有被使用。
### test.h 头文件
这个头文件是在前期用来测试程序的,可以单独编译。很多函数我在 test.h 里面写了一个测试函数,这样能提高我的开发效率。不过在后期,程序变得非常复杂,仅靠一个测试函数并不能完成测试。
### my_list.h 头文件和 my_queue.h 头文件
这两个程序里面和包含了本程序所有需要的一个信息表结构和队列结构。
其中 SqList_SONG、LIST_VIEW 等线性表均使用线性结构。其中 SqList_SONG 线性表就带有 添加(末尾)、插入、获取、弹出、删除、修改、查找、排序、反转、清空、自动扩容等功能。
另外,由于C语言没有类的概念,所以也没有构造函数,折构函数。在使用线性表之前必须调用相应的初始化函数(如 SqList_SONG_init),在不再使用时必须调用相应的销毁函数(如 SqList_SONG_destroy)来释放内存。
下面是 SqList_SONG 线性表的一些功能函数,其它线性表、队列大致相同。
#### SqList_SONG_expand 函数
为了解决数组空间有限的问题,我写了 SqList_SONG_expand 函数,用来在数组空间不足时给数组扩容。
函数原型如下
```C
void SqList_SONG_expand(pSqList_SONG list);
```
当调用 SqList_SONG_push 函数或者 SqList_SONG_insert 给线性表添加元素时,程序会检查线性表是否已满,如果已满,则调用 SqList_SONG_expand 扩容。SqList_SONG_expand 函数则会向系统申请原线性表所需内存 2 倍的内存,并将旧内存的数据复制到新的内存,再释放掉旧的内存。
#### SqList_SONG_init 函数
线性表初始化函数,函数原型如下
```C
void SqList_SONG_init(pSqList_SONG list);
```
参数 list 是待初始化的线性表。
#### SqList_LYRIC_destroy 函数
线性表销毁函数,函数原型如下
```C
void SqList_SONG_destroy(pSqList_SONG list);
```
参数 list 是待销毁的线性表。
#### SqList_SONG_push 函数
这个函数用与在线性表末尾加入新元素,函数原型如下
```C
void SqList_SONG_push(pSqList_SONG list, const SONG *elem);
```
参数 list 是目标线性表,elem 是待插入的元素(程序会将 elem 复制到线性表,因此,你可以在添加到线性表之后释放 elem)。如果线性表已满,该函数会自动调用 SqList_SONG_expand 函数扩容。
#### SqList_SONG_insert 函数
这个函数用于在线性表中间插入元素,函数原型如下
```C
void SqList_SONG_insert(pSqList_SONG list, int position, const SONG *elem);
```
参数 list 是目标线性表,参数 position 是准备插入的位置(下标),elem 是待插入的元素(程序会将 elem 复制到线性表,因此,你可以在添加到线性表之后释放 elem)。原本位置的元素及后面的元素会自动后移。如果线性表已满,该函数会自动调用 SqList_SONG_expand 函数扩容。
#### SqList_SONG_get 函数
这个函数用于获取指定位置的元素,函数原型如下
```C
SONG SqList_SONG_get(pSqList_SONG list, int position, SONG *elem);
```
参数 list 是目标线性表,参数 position 是待获取的元素的位置(下标)。elem 指向用来存储获取到的元素的地址,返回值为获取到的元素,如果不需要从 elem 传出可将参数 elem 置为 NULL。
#### SqList_SONG_remove 函数
这个函数用于移除指定位置的元素,函数原型如下
```C
void SqList_SONG_remove(pSqList_SONG list, int position);
```
参数 list 是目标线性表,参数 position 是待移除的元素的位置(下标)。原本位置后面的元素会自动后移。
#### SqList_SONG_pop 函数
这个函数用于弹出指定位置元素(相当于获取再移除),函数原型如下
```C
SONG SqList_SONG_pop(pSqList_SONG list, int position, SONG *elem);
```
参数 list 是目标线性表,参数 position 是待弹出的元素的位置(下标)。elem 指向用来存储弹出的元素的地址,返回值为弹出的元素,如果不需要从 elem 传出可将参数 elem 置为 NULL。原本位置后面的元素会自动后移。
#### SqList_SONG_modify 函数
这个函数用于修改线性表中某一元素(替换式修改),函数原型如下
```C
SONG SqList_SONG_modify(pSqList_SONG list, int position, SONG *elem);
```
参数 list 是目标线性表,参数 position 是待修改的元素的位置(下标)。elem 是新元素的地址(程序会将 elem 复制到线性表,因此,你可以在添加到线性表之后释放 elem),返回值为修改前的元素。
#### SqList_SONG_findByName 函数
这个函数用于在线性表中根据歌名来查找元素,函数原型如下
```C
SONG* SqList_SONG_findByName(pSqList_SONG list, char name[]);
```
参数 list 是目标线性表,参数 name 是查找的歌名。返回查找到的第一个元素的地址,如果没有找到,则返回 NULL。
#### SqList_SONG_sortByName 函数
这个函数用于将线性表按歌名排序(拼音序),函数原型如下
```C
void SqList_SONG_sortByName(pSqList_SONG list);
```
参数 list 是目标线性表。
#### SqList_SONG_reverse 函数
这个函数可以将线性表反转,函数原型如下
```C
void SqList_SONG_reverse(pSqList_SONG list);
```
参数 list 是目标线性表。
#### SqList_SONG_getLength 函数
这个函数用来获取线性表长度,函数原型如下
```C
int SqList_SONG_getLength(pSqList_SONG list);
```
参数 list 是目标线性表,返回值为线性表长度。
#### SqList_SONG_empty 函数
这个函数用来获取线性表是否为空,函数原型如下
```C
bool SqList_SONG_empty(pSqList_SONG list);
```
参数 list 是目标线性表,线性表为空返回真,否则返回假。
#### SqList_SONG_clear 函数
这个函数用于清空线性表,函数原型如下
```C
void SqList_SONG_clear(pSqList_SONG list);
```
参数 list 是目标线性表.
### file.h 头文件
这个文件主要包含一些文件操作的函数。
#### getThisPath 函数
这个函数用于获取当前程序所在的文件夹,函数原型如下
```C
void getThisPath(char *path);
```
参数 path 为指向输出在字符串。
#### findAllMusic 函数
这个函数用于查找文件夹下所有音乐。使用了 Win32 API。函数原型如下
```C
void findAllMusic(const char path[], pSqList_SONG pSongList);
```
参数 path 为目标文件夹。参数 pSongList 指向用来保存音乐的线性表。
#### fileIsExist 函数
这个函数用来检测文件是否存在,函数原型如下
```C
bool fileIsExist(char path[]);
```
参数 path 为目标路径,如果存在返回真,否则返回假。
#### readLyrics 函数
这个函数用于加载歌词,函数原型如下
```C
void readLyrics(pSONG song, pSqList_LYRIC lyrics);
```
参数 song 指向目标音乐,lyrics 指向用于保存歌词的线性表。
### decode.h 头文件
#### decode 函数
音乐文件解码函数,原型如下
```C
void decode(char fileName[], WAVEFORMATEX *format, unsigned char **data, uint32_t *length);
```
参数 format 为输出音频格式,参数 data 为输出音频数据缓存的地址(程序会自动申请内存),length 为输出音频数据长度。
#### AudioBufferClear 函数
因为 decode 会申请一块内存来存放音频数据缓存,如果需要释放内存,调用该函数即可。
### message.h 头文件
#### 消息
我在这个程序中自己造了一套消息机制,下面是宏定义的消息。
```C
// 下面定义了一些消息。
#define EVENT_MY_KEY 1 // 按键消息。
#define EVENT_MY_EXIT 0 // 程序退出消息。
#define EVENT_MY_MUSIC_DONE 2 // 音乐播放完成消息。
#define EVENT_MY_MUSIC_START 3 // 音乐播放开始消息。
#define EVENT_MY_PLAYER_START 4 // 本程序启动消息。
#define EVENT_MY_LIST_END 5 // 到达音乐列表末尾消息。
#define EVENT_MY_LIST_BEGIN 6 // 到达音乐列表开头消息。
// 下面定义了一些按键,右边的数字的键码。
#define KEY_MY_ESC 27
#define KEY_MY_CTRL_C 3
#define KEY_MY_UP 72
#define KEY_MY_RIGHT 77
#define KEY_MY_LEFT 75
#define KEY_MY_DOWN 80
#define KEY_MY_ENTER 13
#define KEY_MY_APACE 32
#define KEY_MY_PAGE_UP 73
#define KEY_MY_PAGE_DOWN 81
#define KEY_MY_S 115
#define KEY_MY_F5 63
#define KEY_MY_W 119
#define KEY_MY_DEL 83
#define KEY_MY_ARROW 224
#define KEY_MY_FUNCTION 0
```
#### msgQueue_My_init 函数
无参数,用于消息队列初始化。
#### msgQueue_My_clear 函数
无参数,用于清空消息队列。
#### msgQueue_My_destroy 函数
无参数,用于销毁消息队列。
#### MSG_MY 结构体
这个结构体用于存储消息。定义如下
```C
typedef struct MSG_MY {
UINT message; // 消息内容。
UINT param; // 消息附加参数。
} MSG_MY, pMSG_MY;
```
#### postMessage_My 函数
这个函数用于发送消息到队列,函数定义如下
```C
void postMessage_My(const MSG_MY *pMsg);
```
参数 pMsg 为消息结构体,与线性表一样,程序会将 pMsg 复制到消息队列。使用了线程锁,确保在多线程下正常工作。
#### getMessage_My 函数
这个函数用于获取消息,函数定义如下
```C
void getMessage_My(const MSG_MY *pMsg);
```
参数 pMsg 为消息结构体。这是个阻塞函数,如果队列为空,程序会一直停止这里,直到队列中有消息。使用了线程锁,确保在多线程下正常工作。
### sond.h 头文件
#### soundPlay 函数
这个函数用于播放音乐,整合了许多功能,函数原型如下
```C
void soundPlay(const WAVEFORMATEX *format, const unsigned char *data, uint32_t length);
```
参数 format 为输入音频数据格式,data 为音频数据,length 为音频数据长度。
该程序会自检测声卡是否打开,若未打开,则打开声卡,已打开则重置声卡。接下来复制音频缓存数据,再计算音频消息和音乐可视化所需的数据。接下来会调用 waveOutWrite(Win32 API) 填数据,waveOutWrite 函数会打开一个新线程播放声音。
#### soundInfo 函数
这个函数用于获取音乐播放信息。函数原型如下
```C
void soundInfo(SOUND_INFO *info);
```
参数 info 指向用于存储获取到的音乐信息结构体的地址。
SOUND_INFO 结构体定义如下
```C
typedef struct SOUND_INFO { // 储存当前播放音乐的信息。
double totalTime; // 音乐总时间,单位:秒。
double currentTime; // 当前已播放的时间,单位:秒。
int totalByteNum; // 音乐总大小,单位:字节。
int BitsPerSample; // 采样位数。
int channel; // 声道。
double level; // 当前音平大小,范围 0 到 1。
}SOUND_INFO;
```
#### waveOutProc 函数
这是回调函数,具体请查看
waveOutOpen 函数