音乐播放应用
音乐播放应用(Music Playback Application)是 AD23N 蓝牙音频 SoC 固件中的应用层模块,运行于 sdk/app/src/mbox_flash/music/ 目录下。它以消息驱动状态机的形式管理 U 盘 / SD 卡 / 内置 Flash 等存储设备上的音频文件解码播放,负责设备扫描与切换、曲目上下曲、播放模式(顺序/文件夹/单曲/随机)、EQ 切换、快进快退、断电续播与 UI 联动等完整播放能力。
Purpose and Scope
本文档深入讲解 MUSIC_MODE_EN 使能下的音乐播放应用:包括其消息循环架构、play_control 播放控制结构、设备热插拔处理、解码错误恢复、播放模式状态机、EQ 切换、按键过滤与持久化(VM)机制,以及退出/关机的完整流程。
本页边界:本页聚焦"音乐播放应用"自身(music_play.c / music_play.h / music_device.c / music_key_table.c)的行为与实现。以下相关主题由兄弟页面覆盖,此处不做展开:
- 解码器底层实现(WAV/MP3/F1A1 解码、
decoder_api)——属于解码器子系统 play_file/simple_play_file曲目索引与顺序控制的具体算法——属于播放框架层- 设备扫描挂载(
device_mge、dev_scan_info)——属于设备管理子系统 - UI 菜单渲染(
UI_menu、MENU_*常量)——属于 UI 子系统
Overview
设计意图
嵌入式音频设备的播放功能具有强烈的实时性、低资源占用与可重入性要求。音乐播放应用被设计为一个单线程消息泵:
- 消息驱动:所有外部事件(按键、设备热插拔、解码结束、500ms 心跳)都由系统统一转换为
MSG_*消息,通过get_msg()在music_app()主循环中串行消费,天然避免了并发竞态。 - 分层解耦:应用层只持有
play_control pctl[1]抽象句柄,通过music_play_control()下发MBOX_MUSIC_CMD命令;真正的设备枚举、文件索引、解码调度由下层play_file/decoder_api完成,应用层不直接接触文件系统细节。 - 段式链接与内存复用:源码通过
#pragma bss_seg/data_seg/const_seg/code_seg将音乐应用代码与数据放入专用段(.music_play.*),并将pctl、err_device、breakpoint等大结构放入.mode_music_overlay_data覆盖段,与其它模式共享 RAM,是典型的多模式(FM/TWS/录音等)内存复用设计。 - 面向资源受限环境:
FOLDER_PLAY_EN宏注释明确标注"代码消耗约 1.1k",说明开发者以字节级成本考量功能裁剪;上电 1.5s 内忽略设备插入消息(jiffies < 150)则是对启动时序的保护。
关键概念与术语
| 术语 | 含义 |
|---|---|
pctl | play_control 播放控制结构(位于 play_file.h / simple_play_file.h),应用层操作播放的核心句柄,含 dev_index、dec_type、play_mode、pdp(断点)、pdir(目录表)、p_dec_obj(解码对象)等字段 |
MBOX_MUSIC_CMD | 设备级/文件级播放命令枚举,分为 DEV_CMD_*(设备切换)与 FILE_CMD_*(文件操作) |
FILE_PLAY_MODE | 播放模式:顺序(REPEAT_ALL)、文件夹(REPEAT_FOLDER)、单曲(REPEAT_ONE)、随机(REPEAT_RANDOM) |
MSG_* | 系统消息(按键、设备、解码结束、定时器等),经 post_msg / get_msg 传递 |
VM | 虚拟机(virtual memory)Flash 存储接口,vm_read / vm_write 用于持久化工作模式与最后使用的设备 |
decoder_disk_out | 设备拔出时通知解码器释放资源的接口,返回是否有文件被中断 |
Architecture
分层架构
flowchart TD
subgraph sg_Event["事件源层"]
Key["按键/红外 KEY_IR_EN<br/>(music_key_table.c)"]
Device["设备热插拔<br/>U盘/SD卡"]
Decoder["解码器消息<br/>FILE_END/FILE_ERR"]
Timer["系统定时器<br/>MSG_500MS"]
end
subgraph sg_Msg["消息层"]
PostMsg["post_msg / get_msg"]
end
subgraph sg_App["音乐应用层 (music_play.c)"]
MusicApp["music_app() 主循环<br/>switch(msg[0]) 状态机"]
InfoInit["music_info_init()<br/>pctl 初始化/恢复设备"]
EqSwitch["decoder_eq_mode_switch()<br/>EQ 切换"]
end
subgraph sg_Ctrl["播放控制层"]
PlayCtrl["music_play_control()<br/>MBOX_MUSIC_CMD 分发"]
Pctl["play_control pctl[1]<br/>(.mode_music_overlay_data)"]
end
subgraph sg_Lower["底层能力"]
DecoderApi["decoder_api<br/>解码/暂停/快进快退/断点"]
DevMge["device_mge / dev_scan_info<br/>设备扫描管理"]
UI["UI_menu / SET_UI_MAIN<br/>界面联动"]
Vm["vm_read / vm_write<br/>断电续播持久化"]
Dac["dac_api<br/>采样率/淡入淡出"]
end
Key --> PostMsg
Device --> PostMsg
Decoder --> PostMsg
Timer --> PostMsg
PostMsg --> MusicApp
MusicApp --> InfoInit
MusicApp --> EqSwitch
MusicApp --> PlayCtrl
PlayCtrl --> Pctl
Pctl --> DecoderApi
Pctl --> DevMge
MusicApp --> UI
MusicApp --> Vm
MusicApp --> Dac
架构说明:
- 事件源层把物理/逻辑事件统一转为消息,
music_app()只面对msg[0]一个字段做分发,决策逻辑集中且可测试。 - 播放控制层是应用与底层之间的"命令总线":
music_play_control(&pctl[0], cmd, param, wait)的四个参数分别指定句柄、命令、参数与是否需要阻塞等待(NEED_WAIT/NO_WAIT),例如解码错误后的自动切歌用NO_WAIT,手动切换设备用NEED_WAIT以保证界面反馈。 - 底层能力通过
decoder_api、device_mge、vm_api、dac_api、ui_api五个接口面被应用调用;应用本身不直接访问寄存器或文件系统。
数据段与内存布局
music_play.c 开头通过段指令把整个模块放入独立链接段,并让大对象进入模式覆盖段:
#pragma bss_seg(".music_play.data.bss")
#pragma data_seg(".music_play.data")
#pragma const_seg(".music_play.text.const")
#pragma code_seg(".music_play.text")
#pragma str_literal_override(".music_play.text.const")
(来源:music_play.c)
设计意图:音乐模式与其它工作模式(FM、录音等)互斥运行,play_control pctl[1]、err_device、breakpoint[1] 声明为 AT(.mode_music_overlay_data) 覆盖变量,多个模式共享同一块 RAM,从而把峰值 RAM 占用压到最低;代码段独立(.music_play.text)则便于按模式做链接裁剪与热区管理。
主消息循环:music_app()
music_app() 是整个音乐播放应用的中枢,定义于 music_play.c。它由应用框架(app.c)在切换工作模式为音乐时调用,运行一个永不返回的 while(1) 循环,直到收到 MSG_CHANGE_WORK_MODE 才跳出。
启动序列
void music_app(void)
{
log_info("run music_app\n");
vm_write(VM_INDEX_SYSMODE, &work_mode, sizeof(work_mode));
u32 dac_sr = dac_sr_read();
dac_sr_api(SR_DEFAULT);
u8 music_vol, dindex, used_device;
int msg[2], err;
music_info_init(&used_device);
post_msg(1, MSG_MUSIC_SELECT_NEW_DEVICE);
while (1) {
err = get_msg(2, &msg[0]);
bsp_loop();
...
(来源:music_play.c)
启动时依次完成:
vm_write(VM_INDEX_SYSMODE, ...)把当前工作模式写入 VM,保证断电重启后仍进入音乐模式。- 读取并暂存 DAC 采样率(
dac_sr_read()),切到默认采样率SR_DEFAULT——因为解码器会按文件实际采样率重配 DAC,进入模式前先统一基准,退出时再恢复(见 music_play.c 的__out_music_mode)。 music_info_init(&used_device)初始化播放上下文,并从 VM 恢复上次使用的设备。post_msg(1, MSG_MUSIC_SELECT_NEW_DEVICE)自我投递一条"选择新设备"消息,让主循环在第一个迭代就触发设备扫描与自动播放——这样启动逻辑与事件处理共用同一路径,代码更简洁。
music_info_init() 初始化细节
static void music_info_init(u8 *p_dev)
{
#if KEY_IR_EN
Sys_IRInput = 1;
#endif
key_table_sel(music_key_msg_filter);
decoder_init();
dac_fade_out_api();
err_device = 0;
fsn_music = 0;
err_device = 0;
memset(&pctl[0], 0, sizeof(pctl));
memset(&dev_scan_info[0], 0, sizeof(dev_scan_info));
memset(&breakpoint[0], 0, sizeof(breakpoint));
pctl[0].pdp = &breakpoint[0];
pctl[0].dev_index = NO_DEVICE;
pctl[0].dec_type = BIT_WAV | BIT_MP3_ST | BIT_F1A1 | BIT_A | BIT_UMP3; //播放需要使用的解码器
pctl[0].pdir = (void *)&dir_inr_tab[0];
pctl[0].dir_index = 0;
u8 device;
if (sizeof(u8) == vm_read(VM_INDEX_ACTIVE_DEV, &device, sizeof(u8))) {
*p_dev = device;
} else {
*p_dev = 0;
}
}
(来源:music_play.c)
关键点:
- 按键接管:
key_table_sel(music_key_msg_filter)将按键表切换到音乐模式专用过滤器(实现在music_key_table.c),退出时以key_table_sel(NULL)释放(music_play.c)。 - 解码能力位掩码:
dec_type = BIT_WAV | BIT_MP3_ST | BIT_F1A1 | BIT_A | BIT_UMP3声明本应用支持 WAV、MP3(标准)、F1A1、A(ADPCM)、UMP3 五种格式,底层解码器据此筛选文件。 - 断点续播:
pctl[0].pdp = &breakpoint[0]挂接dp_buff断点缓冲,配合vm_pre_erase()(见下文 MSG_500MS)实现断电续播。 - 设备恢复:从
VM_INDEX_ACTIVE_DEV读取上次使用的设备号存入used_device,供后续MSG_MUSIC_SELECT_NEW_DEVICE处理时优先选择该设备。 - 目录表:
pdir = dir_inr_tab(含/dir_song),文件夹播放模式下按此目录列表索引文件。
消息分发主循环
while(1) 中每次迭代先 get_msg(2, &msg[0]) 拉取消息,再无条件 bsp_loop() 保活系统服务,然后按 msg[0] 分发。各类消息的处理策略如下表:
| 消息 | 处理动作 | 设计意图 |
|---|---|---|
MSG_MUSIC_NEXT_EQ | decoder_eq_mode_switch() 切换 EQ 并刷新 MENU_DEC_EQ | 均衡器模式循环切换 |
MSG_PP | decoder_pause() 暂停/恢复,UI 在 MENU_MUSIC_MAIN 与 MENU_PAUSE 间切换 | 播放/暂停单键控制 |
MSG_USB_DISK_OUT | 置 dev_scan_info[UDISK_INDEX].active = 0;若 decoder_disk_out() 返回中断,则 DEV_CMD_NEXT(NEED_WAIT)切到下一设备 | 拔出正在播放的设备立即换源 |
MSG_USB_DISK_IN / MSG_SDMMCA_IN | 上电 1.5s 内忽略;否则计算 used_device = msg[0] - MSG_USB_DISK_IN 并投递 MSG_MUSIC_SELECT_NEW_DEVICE | 启动时序保护 + 按消息序号推导设备号 |
MSG_SDMMCA_OUT | 同 MSG_USB_DISK_OUT,索引为 SD0_INDEX | 同上 |
MSG_MUSIC_SELECT_NEW_DEVICE | DEV_CMD_SEL_NEW_DEV 优先选指定设备,失败则 DEV_CMD_NEXT 顺延 | 设备选择回退策略 |
MSG_PREV_DEVICE / MSG_NEXT_DEVICE | DEV_CMD_PREV / DEV_CMD_NEXT(NO_WAIT) | 跨设备切换 |
MSG_PREV_FILE / MSG_NEXT_FILE | FILE_CMD_PREV / FILE_CMD_NEXT(NO_WAIT) | 上下曲 |
MSG_INPUT_TIMEOUT(仅 KEY_IR_EN) | 红外输入超时后按 Input_Number 执行 FILE_CMD_PLAY_BY_INDEX,越界则回到主界面 | 红外选曲 |
MSG_NEXT_PLAYMODE | play_mode++,到达 MAX_PLAY_MODE 回绕 REPEAT_ALL,刷新 MENU_PLAYMODE | 播放模式轮换 |
MSG_MUSIC_FF / MSG_MUSIC_FR | decoder_ff / decoder_fr(步长 2)并刷新 UI | 快进/快退 |
MSG_WAV/MP3/F1A1_FILE_END | 若解码器带 B_DEC_ERR 标志,FILE_CMD_AUTO_NEXT(NEED_WAIT) | 文件正常结束但解码异常时自动切歌 |
MSG_WAV/MP3/F1A1_FILE_ERR | 同上但用 NO_WAIT | 解码出错快速跳过错文件 |
MSG_CHANGE_WORK_MODE | goto __out_music_mode 退出模式 | 模式切换出口 |
MSG_500MS | 刷新 MENU_MAIN/MENU_HALF_SEC_REFRESH;按解码状态决定 app_powerdown_deal(0/1) | 半秒心跳:UI 刷新 + 关机前状态判断 |
| 其它 | ap_handle_hotkey(msg[0]) | 兜底热键处理 |
上表对应的实际代码见 music_play.c。
解码结束/出错的双路径处理
注意 FILE_END 与 FILE_ERR 都只在 B_DEC_ERR 置位时才自动切歌,但等待策略不同:
FILE_END+B_DEC_ERR→FILE_CMD_AUTO_NEXT, NEED_WAIT:文件播完了但解码层标记异常,需要同步等待新文件就绪再继续,避免界面/解码对象状态错乱。FILE_ERR+B_DEC_ERR→FILE_CMD_AUTO_NEXT, NO_WAIT:文件中途出错,异步快速跳过,用户几乎无感。
(来源:music_play.c)
半秒心跳与断电续播
case MSG_500MS:
UI_menu(MENU_MAIN, (int)&pctl[0]);
UI_menu(MENU_HALF_SEC_REFRESH, (int)&pctl[0]);
if (MUSIC_PLAY != get_decoder_status(pctl[0].p_dec_obj)) {
vm_pre_erase();
app_powerdown_deal(0);
} else {
app_powerdown_deal(1);
}
(来源:music_play.c)
- 每 500ms 刷新主界面与半秒刷新菜单(播放时间进度等)。
- 若解码器当前不在
MUSIC_PLAY状态(例如暂停或停止),则执行vm_pre_erase()预擦除 VM 断点区,为写入断点做准备,并以参数 0 通知电源管理"未在播放";否则参数 1。这保证只有真正处于播放态时才可能保存断点,同时让电源管理在播放中知道保持音频时钟/防休眠。
播放命令体系:MBOX_MUSIC_CMD
所有播放控制都收敛到 music_play_control(&pctl[0], cmd, param, wait) 命令接口,命令码定义于 music_play.h:
| 命令 | 值 | 方向 | 说明 |
|---|---|---|---|
DEV_CMD_NULL | 0 | - | 空命令 |
DEV_CMD_SEL_NEW_DEV | 1 | 应用→底层 | 选择指定新设备(配合 used_device 参数) |
DEV_CMD_PREV | 2 | 应用→底层 | 切到上一个设备 |
DEV_CMD_NEXT | 3 | 应用→底层 | 切到下一个设备 |
DEV_CMD_AUTO_PREV | 4 | 底层内部 | 设备内部自动回退(外部不可调用) |
DEV_CMD_AUTO_NEXT | 5 | 底层内部 | 设备内部自动前进(外部不可调用) |
FILE_CMD_PLAY_BY_INDEX | 0x80 | 应用→底层 | 按文件索引播放(红外选曲等) |
FILE_CMD_PREV | 0x81 | 应用→底层 | 上一曲 |
FILE_CMD_NEXT | 0x82 | 应用→底层 | 下一曲 |
FILE_CMD_AUTO_PREV | 0x83 | 底层内部 | 自动上一曲(列表头回退) |
FILE_CMD_AUTO_NEXT | 0x84 | 底层内部 | 自动下一曲(文件结束/出错) |
设计要点:
- 命令空间分段:
DEV_CMD_*(0~5)与FILE_CMD_*(0x80~0x84)分离,值域留空用于扩展;FILE_CMD_PLAY_BY_INDEX = 0x80作为文件命令段的起始基准。 - 外部/内部命令隔离:
DEV_CMD_AUTO_*与FILE_CMD_AUTO_*注释明确"用于设备内部切换,外部不可调用"——这是播放框架(play_file)在设备内部(如目录切换、文件列表遍历)使用的命令,应用层只用带AUTO后缀的命令做错误恢复,保证状态机边界清晰。
播放模式状态机:FILE_PLAY_MODE
stateDiagram-v2
[*] --> REPEAT_ALL
REPEAT_ALL --> REPEAT_FOLDER : MSG_NEXT_PLAYMODE<br/>(FOLDER_PLAY_EN)
REPEAT_FOLDER --> REPEAT_ONE : MSG_NEXT_PLAYMODE
REPEAT_ONE --> REPEAT_RANDOM : MSG_NEXT_PLAYMODE<br/>(RANDOM_PLAY_EN)
REPEAT_RANDOM --> REPEAT_ALL : MSG_NEXT_PLAYMODE<br/>(到达 MAX_PLAY_MODE 回绕)
REPEAT_ALL --> REPEAT_ONE : MSG_NEXT_PLAYMODE<br/>(未编译 FOLDER/RANDOM 宏)
模式定义见 music_play.h,切换逻辑在 music_play.c:
case MSG_NEXT_PLAYMODE:
pctl[0].play_mode++;
if (pctl[0].play_mode >= MAX_PLAY_MODE) {
pctl[0].play_mode = REPEAT_ALL;
}
UI_menu(MENU_PLAYMODE, (int)&pctl[0]);
log_info("MSG_NEXT_PLAYMODE : %d\n", pctl[0].play_mode);
break;
MAX_PLAY_MODE 是编译期由宏推导的"哨兵值":编译 FOLDER_PLAY_EN 与 RANDOM_PLAY_EN 与否,模式数量随之变化,play_mode 永远在合法区间内回绕,无需运行时判断哪个模式被裁剪。这是以预编译宏做功能裁剪 + 枚举自动扩展的典型嵌入式手法。
设备热插拔核心时序
sequenceDiagram
participant HW as 硬件(USB/SD)
participant MSG as 消息系统
participant App as music_app()
participant PC as music_play_control()
participant DEC as decoder_api
participant DEV as device_mge
Note over HW,MSG: 设备插入
HW->>MSG: MSG_USB_DISK_IN / MSG_SDMMCA_IN
MSG->>App: get_msg()
App->>App: jiffies < 150 ? 忽略 : 计算 used_device
App->>MSG: post_msg(MSG_MUSIC_SELECT_NEW_DEVICE)
MSG->>App: get_msg()
App->>PC: DEV_CMD_SEL_NEW_DEV (used_device, NO_WAIT)
PC->>DEV: 扫描/挂载目标设备
DEV-->>PC: 结果
PC-->>App: 成功/失败
alt 指定设备不可用
App->>PC: DEV_CMD_NEXT (NEED_WAIT) 顺延
end
PC->>DEC: 按 pctl.dec_type 筛选并解码首个文件
DEC-->>App: 解码对象就绪
Note over HW,MSG: 设备拔出(正在播放)
HW->>MSG: MSG_USB_DISK_OUT / MSG_SDMMCA_OUT
MSG->>App: get_msg()
App->>App: dev_scan_info[idx].active = 0
App->>DEC: decoder_disk_out(&pctl, idx)
alt 有文件被中断
App->>PC: DEV_CMD_NEXT (NEED_WAIT) 自动换源
end
对应代码路径:
- 插入:music_play.c(
MSG_USB_DISK_IN/MSG_SDMMCA_IN合并分支)→ music_play.c(MSG_MUSIC_SELECT_NEW_DEVICE) - 拔出:music_play.c(U 盘)、music_play.c(SD 卡)
注释中特别说明"插 U 盘和插卡消息分支写在一起,中间不可插入其他消息,否则影响设备升级"——合并分支是为了代码省空间,且两个插入事件必须连续处理,其间不得穿插其它消息处理,保证设备升级流程的时序完整性。
EQ 切换:decoder_eq_mode_switch()
int decoder_eq_mode_switch(dec_obj *obj)
{
if ((obj == NULL) || (obj->eq == NULL)) {
return -1;
}
dec_eq_mode++;
u32 ret = obj->eq(obj, dec_eq_mode);
if (ret != -1) {
dec_eq_mode = ret;
} else {
dec_eq_mode = 0;
ret = obj->eq(obj, dec_eq_mode);
}
log_info("switch dec_eq mode:%d \n", ret);
return ret;
}
(来源:music_play.c)
- 通过解码对象虚接口
obj->eq()切换均衡器档位:先尝试递增档位,若解码器返回-1(越界)则回绕到 0 档重新设置。 - 返回值同时写回全局
dec_eq_mode,供MSG_MUSIC_NEXT_EQ分支更新MENU_DEC_EQ界面(music_play.c)。 - 空指针防御(
obj == NULL || obj->eq == NULL)返回 -1,避免在解码对象未就绪时被按键消息触发崩溃。
模式退出与资源释放
__out_music_mode:
music_vol_update();
for (u8 i = 0; i < MAX_DEVICE; i++) {
decoder_disk_out(&pctl[0], i);
}
#if KEY_IR_EN
Sys_IRInput = 0;
#endif
SET_UI_MAIN(MENU_POWER_UP);
UI_menu(MENU_POWER_UP, 0);
key_table_sel(NULL);
dac_sr_api(dac_sr);
dac_fade_in_api();
(来源:music_play.c)
退出路径严格逆序执行启动动作:保存音量 → 对所有设备执行 decoder_disk_out 释放解码资源 → 恢复红外输入 → 界面切回开机画面 → 释放按键表 → 恢复 DAC 采样率 → DAC 淡入。这种"对称初始化/清理"保证模式切换后其它模式不会受到残留状态污染。
Usage Examples
以下示例均直接取自本模块源码,展示如何接入与扩展音乐播放应用。
示例 1:功能裁剪宏开关
在编译期决定是否支持随机播放、文件夹播放:
#define RANDOM_PLAY_EN //是否支持随机播放功能
#define FOLDER_PLAY_EN //是否支持文件夹播放功能,代码消耗约1.1k
(来源:music_play.h)
这两个宏同时影响枚举 FILE_PLAY_MODE 的成员数量(REPEAT_FOLDER/REPEAT_RANDOM 是否编译)与 MAX_PLAY_MODE 的值,从而自动决定 MSG_NEXT_PLAYMODE 的轮换集合。产品定制时注释掉宏即可裁剪功能并节省约 1.1KB 代码。
示例 2:声明应用可解码的文件类型
music_info_init() 中通过 dec_type 位掩码声明播放器支持的文件格式,底层据此过滤设备上的文件:
pctl[0].dev_index = NO_DEVICE;
pctl[0].dec_type = BIT_WAV | BIT_MP3_ST | BIT_F1A1 | BIT_A | BIT_UMP3; //播放需要使用的解码器
pctl[0].pdir = (void *)&dir_inr_tab[0];
pctl[0].dir_index = 0;
(来源:music_play.c)
新增一种音频格式时,需要在此处加入对应的 BIT_* 位,并在解码器子系统注册对应解码器。
示例 3:应用层发送播放命令的标准范式
应用层不直接调用解码函数,而是通过命令接口统一驱动:
case MSG_PREV_FILE:
log_info("MSG_PREV_FILE\n");
music_play_control(&pctl[0], FILE_CMD_PREV, 0, NO_WAIT);
break;
(来源:music_play.c)
对比 FILE_CMD_AUTO_NEXT 的两种用法(错误恢复):
case MSG_MP3_FILE_END:
log_info("FILE_END:0x%x\n", msg[0]);
if (pctl[0].p_dec_obj->sound.enable & B_DEC_ERR) {
music_play_control(&pctl[0], FILE_CMD_AUTO_NEXT, 0, NEED_WAIT);
}
break;
case MSG_MP3_FILE_ERR:
log_info("FILE_ERR:0x%x\n", msg[0]);
if (pctl[0].p_dec_obj->sound.enable & B_DEC_ERR) {
music_play_control(&pctl[0], FILE_CMD_AUTO_NEXT, 0, NO_WAIT);
}
break;
(来源:music_play.c)
示例 4:按键过滤器接入
音乐模式通过 key_table_sel() 注册专属按键过滤器(实现在 music_key_table.c),头文件暴露其签名:
void music_app(void);
extern u16 music_key_msg_filter(u8 key_status, u8 key_num, u8 key_type);
(来源:music_play.h)
注册点见 music_info_init()(music_play.c):key_table_sel(music_key_msg_filter);,退出时 key_table_sel(NULL);(music_play.c)。按键事件经过滤器映射为 MSG_PP、MSG_PREV_FILE、MSG_NEXT_PLAYMODE 等消息,再进入主循环分发。
Configuration Options
| 配置宏/常量 | 类型 | 默认 | 说明 |
|---|---|---|---|
MUSIC_MODE_EN | 编译宏 | 依赖工程配置 | 整个音乐播放应用的总开关,未定义时 music_play.c 全部代码不编译(#if MUSIC_MODE_EN) |
RANDOM_PLAY_EN | 编译宏 | 开启 | 支持随机播放模式 REPEAT_RANDOM |
FOLDER_PLAY_EN | 编译宏 | 开启 | 支持文件夹播放模式 REPEAT_FOLDER,代码消耗约 1.1KB |
KEY_IR_EN | 编译宏 | 依赖工程 | 红外遥控支持;开启时初始化 Sys_IRInput、注册 MSG_INPUT_TIMEOUT 选曲分支、退出时复位 |
dir_inr_tab | 静态数组 | {"/dir_song"} | 文件夹播放模式的目录列表,由 pctl[0].pdir 引用 |
pctl[0].dec_type | 位掩码 | BIT_WAV|BIT_MP3_ST|BIT_F1A1|BIT_A|BIT_UMP3 | 播放器支持的解码格式集合 |
VM_INDEX_ACTIVE_DEV | VM 索引 | - | 存储"最后使用的设备号"的 VM 键,用于断电续播 |
VM_INDEX_SYSMODE | VM 索引 | - | 存储当前工作模式,进入音乐模式时写入 |
| 上电设备消息窗口 | 时间常量 | jiffies < 150(1.5s) | 上电 1.5 秒内忽略 MSG_*_IN 设备插入消息,避免启动竞态 |
MSG_500MS 心跳 | 系统周期 | 500ms | 驱动 UI 刷新与断电续播状态判断 |
API Reference
void music_app(void)
音乐播放应用主入口,由应用框架在切换到音乐工作模式时调用。内部为 while(1) 消息循环,直到收到 MSG_CHANGE_WORK_MODE 退出。
- 调用时机:应用层模式切换(参考
app.c的工作模式分发) - 不返回(直到模式退出)
- 源码:music_play.c
u16 music_key_msg_filter(u8 key_status, u8 key_num, u8 key_type)
音乐模式按键过滤器,由 key_table_sel() 注册。将按键事件转换为音乐消息(如 MSG_PP、MSG_NEXT_FILE 等)。
- 参数:
key_status按键状态;key_num键号;key_type键类型 - 返回:转换后的消息码
- 声明:music_play.h,实现在
music_key_table.c
int decoder_eq_mode_switch(dec_obj *obj)
切换解码器 EQ 档位(全局函数,非 static)。
- 参数:
obj(dec_obj *)当前解码对象 - 返回:当前 EQ 档位;
obj为空或obj->eq为空时返回-1 - 行为:档位递增,越界回绕到 0 档
- 源码:music_play.c
全局变量(供其它模块/UI 引用)
| 变量 | 类型 | 说明 |
|---|---|---|
pctl[0] | play_control | 播放控制结构(.mode_music_overlay_data 覆盖段),UI 经 (int)&pctl[0] 传递其地址 |
err_device | u8 | 出错设备标记,music_info_init() 清零 |
dec_eq_mode | u8 | 当前 EQ 档位,EQ 切换后更新,UI 读取显示 |
fsn_music | 外部变量 | 音乐模式文件序号(music_info_init() 中清零) |
声明见 music_play.h,定义见 music_play.c。
Failure Modes、Edge Cases & Concurrency
设备拔出导致播放中断
正在播放的设备被拔出时,应用先标记 dev_scan_info[idx].active = 0,再调用 decoder_disk_out(&pctl[0], idx)。仅当该接口返回"有文件被中断"时,才执行 DEV_CMD_NEXT (NEED_WAIT) 自动换源;若被拔设备不是当前播放源,则不做任何动作。这避免了拔出无关设备(如第二个 U 盘)时干扰当前播放。
(来源:music_play.c、music_play.c)
上电 1.5 秒内的设备插入
case MSG_USB_DISK_IN://应用为了节省代码将插U盘和插卡消息分支写在一起,中间不可插入其他消息,否则影响设备升级
case MSG_SDMMCA_IN:
if (time_before(jiffies, 150)) {
break;//上电1.5s内不响应设备上线消息
}
(来源:music_play.c)
设计意图:系统上电初期 USB/SD 控制器尚未稳定,若立即响应插入消息可能导致误扫描或设备升级流程被中断。1.5s 窗口内直接丢弃插入事件,窗口过后由后续消息或用户操作重新触发。同时注释强调两个插入分支必须连续处理,中间不得穿插其它消息,保证设备升级时序。
解码错误恢复(跳过坏文件)
B_DEC_ERR 标志是"是否自动切歌"的判定依据:
- 文件正常结束(
*_FILE_END):仅当解码对象带B_DEC_ERR时切下一曲(NEED_WAIT),正常播完的文件不会意外跳曲。 - 文件中途出错(
*_FILE_ERR):带B_DEC_ERR时以NO_WAIT快速跳过,最大限度减少用户可感知的中断。
(来源:music_play.c)
EQ 切换的边界防御
decoder_eq_mode_switch() 对空解码对象/空 eq 函数指针返回 -1(music_play.c);档位越界时回绕到 0(music_play.c)。调用侧 MSG_MUSIC_NEXT_EQ 仅在 dec_eq_mode != (u8)(-1) 时才刷新 EQ 界面,避免无效档位污染 UI(music_play.c)。
红外选曲越界
MSG_INPUT_TIMEOUT 分支中,红外输入序号大于 pctl[0].ftotal(总文件数)或为 0 时,不执行 FILE_CMD_PLAY_BY_INDEX,而是回到音乐主界面,并把 Input_Number 清零,防止残留输入影响下一次选曲(music_play.c)。
并发与重入性
- 单线程消息泵:
music_app()是唯一消费者,所有事件先入消息队列再串行处理,pctl、dev_scan_info、breakpoint等共享结构不存在多线程竞争。 - 中断上下文隔离:硬件中断只负责
post_msg,不直接操作播放状态,因此decoder_*接口都只在主循环上下文被调用。 - 消息获取失败:
get_msg返回非MSG_NO_ERROR时把msg[0]置为NO_MSG,落入default分支由ap_handle_hotkey兜底,循环继续,不因单条消息异常退出(music_play.c)。
覆盖段内存复用(模式间竞争)
pctl、err_device、breakpoint 位于 .mode_music_overlay_data 覆盖段,与其它工作模式(FM、录音等)共享 RAM。因此模式切换前必须完成所有清理:music_app() 退出时对 MAX_DEVICE 个设备逐一 decoder_disk_out,再恢复 DAC/按键/UI 状态;若清理不完整,下一个模式将读到残留的 pctl 内容。
Performance & Operational Considerations
- RAM 优化:大结构入覆盖段、
dir_inr_tab用const段存放、函数代码独立段编译——整套段布局把音乐模式的静态 RAM 与 Flash 占用最小化,代价是模式互斥(同一时刻只有一个模式在 RAM 中活跃)。 - 代码裁剪粒度:
FOLDER_PLAY_EN明示约 1.1KB 成本,RANDOM_PLAY_EN、KEY_IR_EN、MUSIC_MODE_EN均为编译期裁剪点,产品化时按 ROM 预算取舍。 - 心跳刷新:
MSG_500MS每 500ms 触发MENU_MAIN+MENU_HALF_SEC_REFRESH两次 UI 刷新;UI 渲染层需保证该频率下的开销可接受(播放进度刷新粒度即 0.5s)。 - 开机时序:
dac_sr_api(SR_DEFAULT)统一采样率基准后再解码,避免不同采样率文件之间 DAC 配置残留;退出时dac_sr_api(dac_sr)+dac_fade_in_api()恢复并淡入。 - 断电续播:
MSG_500MS中非播放态执行vm_pre_erase()预擦除 VM 断点区,播放态才允许保存断点;vm_read(VM_INDEX_ACTIVE_DEV)恢复上次设备。这要求 VM 操作发生在主循环(非中断)上下文,保证 Flash 擦写安全。
Extension Points
- 新增音频格式:在
music_info_init()的pctl[0].dec_type增加对应BIT_*,同时在解码器子系统注册解码器,并在消息循环补充对应的*_FILE_END/*_FILE_ERR分支(参考 WAV/MP3/F1A1 的三组 case)。 - 新增播放模式:扩展
FILE_PLAY_MODE枚举(music_play.h),MAX_PLAY_MODE自动生效;底层play_file需同步实现新模式的文件索引策略。 - 自定义按键布局:修改
music_key_table.c中按键表与music_key_msg_filter()的映射,消息层与主循环无需改动。 - 红外选曲:
KEY_IR_EN宏 +MSG_INPUT_TIMEOUT分支已实现按索引选曲,接入新遥控器只需调整按键表。 - 多目录文件夹播放:扩展
dir_inr_tab数组(当前仅"/dir_song",music_play.c)即可加入更多扫描目录,pctl[0].pdir/dir_index负责索引。 - 退出钩子:
__out_music_mode退出路径(music_play.c)是模式切换前的清理钩子,需要额外资源回收时在此追加对称的逆序清理。
Related Links
- music_play.c — 主实现
- music_play.h — 命令与模式定义
- music_device.c / music_device.h — 设备管理
- music_key_table.c — 按键映射表
- app.c / app.h — 应用框架与模式分发
- app_config.c / app_config.h — 应用配置
- 相关子系统(由对应目录页覆盖):解码器
decoder_api、播放框架play_file/simple_play_file、设备管理device_mge、UI 层ui_api