Linux音乐播放器项目详细解析
一、项目整体架构
1.1 文件结构
复制代码
项目包含三个文件:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ main.c │ │ func.c │ │ func.h │
│ │ │ │ │ │
│ - main() │ │ - 功能实现 │ │ - 结构体定义│
│ - 菜单循环 │◄───┤ - 底层操作 │◄───┤ - 函数声明 │
│ - 信号处理 │ │ - mplayer │ │ - 枚举定义 │
│ │ │ 通信 │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
二、头文件分析(func.h)
cpp
复制代码
#ifndef _FUNC_H_ // 防止头文件重复包含
#define _FUNC_H_
// 播放模式枚举
typedef enum {
MODE_CYCLE, // 顺序循环播放
MODE_RANDOM, // 随机播放(拼写正确)
MODE_SINGAL // 单曲循环(拼写错误,应为SINGLE)
} MODE;
// 播放列表结构体
typedef struct {
char name[10][512]; // 二维数组:10个文件名,每个最长512字符
int total; // 当前列表中的歌曲总数
int current; // 当前正在播放的歌曲索引
MODE mode; // 当前的播放模式
} LIST;
// 所有功能函数的声明
extern int do_init(); // 初始化播放列表和管道
extern int do_play(char* name);// 播放指定文件
extern int do_list(); // 显示列表并选择播放
extern int do_pause(); // 暂停/继续
extern int do_stop(); // 停止播放
extern int do_prev(); // 上一首(未实现)
extern int do_next(); // 下一首
extern int do_seek(); // 跳转(未实现)
extern int do_speed(); // 速度控制(未实现)
extern int do_mode(); // 切换模式(未实现)
#endif
三、主程序详细分析(main.c)
cpp
复制代码
#include <dirent.h> // 目录操作
#include <errno.h> // 错误号定义
#include <fcntl.h> // 文件控制
#include <signal.h> // 信号处理
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数
#include <string.h> // 字符串操作
#include <sys/stat.h> // 文件状态
#include <sys/types.h> // 数据类型
#include <unistd.h> // POSIX系统调用
#include "func.h" // 自定义头文件
// 全局播放列表变量
LIST list;
// 显示菜单函数
void show_menu()
{
printf("1.list song\n"); // 列出并选择歌曲
printf("2.continue/pause\n"); // 暂停/继续切换
printf("3.stop\n"); // 停止播放
printf("4.prev song\n"); // 上一首
printf("5.next song\n"); // 下一首
printf("6.speed\n"); // 调整播放速度
printf("7.seek\n"); // 跳转到指定位置
printf("8.play mode\n"); // 切换播放模式
printf("9.end\n"); // 退出程序
}
// SIGPIPE信号处理函数
void handle(int num)
{
fprintf(stderr, "管道破裂,mplayer 未启动\n");
// 当向没有读者的管道写入数据时触发
// 通常表示mplayer进程异常退出或未启动
}
int main(int argc, char** argv)
{
// 注册SIGPIPE信号处理函数
signal(SIGPIPE, handle); // 捕获管道破裂信号
// 初始化:扫描媒体文件,创建管道
do_init(); // fill list song
// 主循环:显示菜单,处理用户选择
while (1)
{
char choose[10] = {0}; // 存储用户输入
int num = 0; // 转换后的数字选择
show_menu(); // 显示菜单
printf("input choose:");
// 获取用户输入
fgets(choose, sizeof(choose), stdin);
// 转换为整数
num = atoi(choose);
// 根据选择执行相应操作
switch (num)
{
case 1:
do_list(); // 显示列表并播放
break;
case 2:
do_pause(); // 暂停/继续
break;
case 3:
do_stop(); // 停止播放
break;
case 4:
do_prev(); // 上一首(未实现)
break;
case 5:
do_next(); // 下一首
break;
case 6:
do_speed(); // 调速(未实现)
break;
case 7:
do_seek(); // 跳转(未实现)
break;
case 8:
do_mode(); // 切换模式(未实现)
break;
case 9: // 退出程序
do_stop(); // 先停止播放
exit(0); // 退出程序
break;
}
}
return 0;
}
四、功能函数详细分析(func.c)
4.1 初始化函数(do_init
cpp
复制代码
#include "func.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define MEDIA_PATH "/home/linux" // 媒体文件存放路径
extern LIST list; // 声明外部全局变量
int do_init()
{
// 1. 打开媒体目录
DIR* dir = opendir(MEDIA_PATH);
if (NULL == dir)
{
perror("opendir");
exit(1);
}
// 2. 遍历目录,查找媒体文件
while (1)
{
struct dirent* info = readdir(dir);
if (NULL == info) // 读取完毕
{
break;
}
// 跳过文件名过短的条目(可能不是媒体文件)
if (strlen(info->d_name) < 5)
{
continue;
}
// 3. 检查文件扩展名
// 获取文件名最后3个字符(扩展名)
if (0 == strcmp(&info->d_name[strlen(info->d_name) - 3], "mp4") || // 视频文件
0 == strcmp(&info->d_name[strlen(info->d_name) - 3], "mp3") || // 音频文件
0 == strcmp(&info->d_name[strlen(info->d_name) - 3], "flv")) // 视频文件
{
// 构建完整路径并存入列表
sprintf(list.name[list.total++], "%s/%s", MEDIA_PATH, info->d_name);
// list.total++ 先使用后递增
}
}
// 4. 初始化播放器状态
list.current = 0; // 从第一首开始
list.mode = MODE_CYCLE; // 默认循环模式
closedir(dir); // 关闭目录
// 5. 创建命名管道(FIFO)用于与mplayer通信
int ret = mkfifo("/home/linux/fifo", 0666);
if (-1 == ret)
{
// 如果管道已存在,继续运行
if (EEXIST == errno)
{
// 文件已存在,不视为错误
}
else // 其他错误,退出程序
{
perror("mkfifo");
return 1;
}
}
return 0;
}
4.2 播放函数(do_play)
cpp
复制代码
int do_play(char* name)
{
// mplayer命令格式:
// mplayer -slave -input file=/home/linux/fifo 文件名 -quiet
pid_t pid = fork(); // 创建子进程
if (0 == pid) // 子进程
{
// 替换为mplayer进程
execlp("mplayer", // 程序名
"mplayer", // argv[0]
"-slave", // 启用从模式
"-input", // 指定输入方式
"file=/home/linux/fifo", // 从管道读取命令
name, // 要播放的文件
"-quiet", // 减少输出
NULL); // 参数结束
// 如果execlp失败
exit(1);
}
else if (pid < 0) // fork失败
{
perror("fork");
exit(1);
}
return 0; // 父进程继续执行
}
4.3 列表显示与选择(do_list)
cpp
复制代码
int do_list()
{
int i = 0;
// 1. 显示所有歌曲
for (i = 0; i < list.total; i++)
{
printf("%d %s\n", i, list.name[i]);
}
// 2. 获取用户选择
printf("input num to play:");
char choose[10] = {0};
int num = 0;
fgets(choose, sizeof(choose), stdin);
num = atoi(choose); // 转换为整数
// 3. 更新当前播放索引并播放
list.current = num;
do_play(list.name[num]);
return 0;
}
4.4 暂停/继续控制(do_pause)
cpp
复制代码
int do_pause()
{
// 打开命名管道(以读写方式,非阻塞)
int fd = open("/home/linux/fifo", O_RDWR);
// 向管道写入"pause"命令
// mplayer收到该命令会切换暂停/播放状态
write(fd, "pause\n", 6);
close(fd); // 关闭管道
return 0;
}
4.5 停止播放(do_stop)
cpp
复制代码
int do_stop()
{
// 打开命名管道
int fd = open("/home/linux/fifo", O_RDWR);
// 写入"stop"命令,mplayer停止播放
write(fd, "stop\n", 5);
close(fd);
return 0;
}
4.6 下一首功能(do_next)
cpp
复制代码
int do_next()
{
int num = 0;
// 根据当前播放模式处理
switch (list.mode)
{
case MODE_CYCLE: // 循环模式
num = list.current;
num += 1; // 下一首索引
// 循环处理:如果超出范围,回到第一首
list.current = num % list.total;
// 先停止当前播放
do_stop();
// 播放下一首
do_play(list.name[list.current]);
break;
case MODE_RANDOM: // 随机模式(未实现)
break;
case MODE_SINGAL: // 单曲循环模式(未实现)
break;
}
return 0;
}
五、关键技术与原理
5.1 进程间通信(IPC)机制
复制代码
// 通信架构:
// 用户程序 → FIFO管道 → mplayer进程
//
// 1. 创建管道:
// mkfifo("/home/linux/fifo", 0666)
// 创建了一个特殊的文件,作为进程间的通道
//
// 2. 发送命令:
// write(fd, "pause\n", 6)
// 将命令写入管道,mplayer从另一端读取
//
// 3. mplayer从模式:
// mplayer -slave -input file=/path/to/fifo
// -slave: 启用从模式,接受命令输入
// -input: 指定命令输入源
5.2 信号处理机制
复制代码
// SIGPIPE信号处理
// 当向没有读者的管道写入数据时,内核会发送SIGPIPE信号
// 默认行为是终止进程,这里改为打印错误信息
signal(SIGPIPE, handle);
void handle(int num)
{
// num是信号编号,SIGPIPE通常是13
fprintf(stderr, "管道破裂,mplayer 未启动\n");
// 可能的错误场景:
// 1. mplayer进程异常退出
// 2. 未启动mplayer就发送命令
// 3. 管道被意外删除
}
5.3 文件扩展名判断技巧
复制代码
// 获取字符串最后3个字符的方法
&info->d_name[strlen(info->d_name) - 3]
// 示例:
// 文件名:"song1.mp3"
// 长度:10
// &info->d_name[10-3] = &info->d_name[7] → 指向"mp3"
// 潜在问题:
// 1. 文件名太短会越界访问
// 2. 无法识别".mpeg"等4字符扩展名
// 3. 可能误判"backup.mp3.txt"等文件
六、运行流程图
七、调试和测试建议
7.1 编译和运行问题
复制代码
# 1. 编译错误解决
# 如果提示找不到头文件,检查当前目录
ls -la func.h
# 2. 运行权限问题
chmod +x music_player
# 3. mplayer未安装
sudo apt-get install mplayer # Ubuntu/Debian
sudo yum install mplayer # CentOS/RHEL
# 4. 测试mplayer是否正常工作
mplayer -slave -input file=/tmp/test.fifo test.mp3
7.2 调试技巧
复制代码
// 添加调试信息
printf("调试:当前播放索引=%d,总歌曲数=%d\n", list.current, list.total);
// 检查管道状态
system("ls -l /home/linux/fifo");
// 查看mplayer进程
system("ps aux | grep mplayer");
八、扩展功能实现示例
8.1 实现上一首功能
cpp
复制代码
int do_prev()
{
switch (list.mode)
{
case MODE_CYCLE:
list.current -= 1;
// 处理负索引:循环到最后
if (list.current < 0)
list.current = list.total - 1;
do_stop();
do_play(list.name[list.current]);
break;
case MODE_RANDOM:
// 随机选择
list.current = rand() % list.total;
do_stop();
do_play(list.name[list.current]);
break;
case MODE_SINGAL:
// 重新播放当前歌曲
do_stop();
do_play(list.name[list.current]);
break;
}
return 0;
}
8.2 实现播放模式切换
cpp
复制代码
int do_mode()
{
printf("当前模式:");
switch(list.mode)
{
case MODE_CYCLE: printf("循环播放\n"); break;
case MODE_RANDOM: printf("随机播放\n"); break;
case MODE_SINGAL: printf("单曲循环\n"); break;
}
printf("选择新模式:\n");
printf("1. 循环播放\n");
printf("2. 随机播放\n");
printf("3. 单曲循环\n");
char choose[10] = {0};
fgets(choose, sizeof(choose), stdin);
int mode = atoi(choose);
switch(mode)
{
case 1: list.mode = MODE_CYCLE; break;
case 2: list.mode = MODE_RANDOM; break;
case 3: list.mode = MODE_SINGAL; break;
default: printf("无效选择\n");
}
return 0;
}