杰理 SDK 文档中心
首页
首页
  • 项目概览

    • AD23N SDK 概述与芯片平台
    • 工程结构与模块划分
  • 快速开始

    • 开发环境搭建与工具链
    • 编译构建指南
    • 烧录与固件升级工具
  • 应用框架与产品工作流

    • 应用入口与模式调度
    • 音乐播放应用
    • MIDI 解码与键盘演奏
    • 录音应用
    • LINEIN 与扩音应用
    • USB 从设备应用
    • 待机、软关机与空闲检测
    • 公共 UI 与 LED 显示
  • 音频子系统

    • 音频解码器框架
    • 音频编码器框架
    • 音效算法库
    • 音频管理与输出通路
  • 存储与文件系统

    • 文件系统层
    • NOR Flash 与虚拟机存储
    • 设备与设备管理
  • 系统服务与运行时

    • 消息机制与事件分发
    • 按键扫描与输入处理
    • 电源管理与低功耗控制
    • 定时器与系统任务
  • 外设驱动与平台

    • CPU 平台与启动流程
    • USB 协议栈与主机/设备驱动
    • SPI 与通用外设接口
  • 固件升级与构建工具

    • 固件升级机制
    • 编译后处理与镜像打包
    • 构建系统与命令行工具

音乐播放应用

音乐播放应用(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

设计意图

嵌入式音频设备的播放功能具有强烈的实时性、低资源占用与可重入性要求。音乐播放应用被设计为一个单线程消息泵:

  1. 消息驱动:所有外部事件(按键、设备热插拔、解码结束、500ms 心跳)都由系统统一转换为 MSG_* 消息,通过 get_msg() 在 music_app() 主循环中串行消费,天然避免了并发竞态。
  2. 分层解耦:应用层只持有 play_control pctl[1] 抽象句柄,通过 music_play_control() 下发 MBOX_MUSIC_CMD 命令;真正的设备枚举、文件索引、解码调度由下层 play_file / decoder_api 完成,应用层不直接接触文件系统细节。
  3. 段式链接与内存复用:源码通过 #pragma bss_seg/data_seg/const_seg/code_seg 将音乐应用代码与数据放入专用段(.music_play.*),并将 pctl、err_device、breakpoint 等大结构放入 .mode_music_overlay_data 覆盖段,与其它模式共享 RAM,是典型的多模式(FM/TWS/录音等)内存复用设计。
  4. 面向资源受限环境:FOLDER_PLAY_EN 宏注释明确标注"代码消耗约 1.1k",说明开发者以字节级成本考量功能裁剪;上电 1.5s 内忽略设备插入消息(jiffies < 150)则是对启动时序的保护。

关键概念与术语

术语含义
pctlplay_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)

启动时依次完成:

  1. vm_write(VM_INDEX_SYSMODE, ...) 把当前工作模式写入 VM,保证断电重启后仍进入音乐模式。
  2. 读取并暂存 DAC 采样率(dac_sr_read()),切到默认采样率 SR_DEFAULT——因为解码器会按文件实际采样率重配 DAC,进入模式前先统一基准,退出时再恢复(见 music_play.c 的 __out_music_mode)。
  3. music_info_init(&used_device) 初始化播放上下文,并从 VM 恢复上次使用的设备。
  4. 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_EQdecoder_eq_mode_switch() 切换 EQ 并刷新 MENU_DEC_EQ均衡器模式循环切换
MSG_PPdecoder_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_DEVICEDEV_CMD_SEL_NEW_DEV 优先选指定设备,失败则 DEV_CMD_NEXT 顺延设备选择回退策略
MSG_PREV_DEVICE / MSG_NEXT_DEVICEDEV_CMD_PREV / DEV_CMD_NEXT(NO_WAIT)跨设备切换
MSG_PREV_FILE / MSG_NEXT_FILEFILE_CMD_PREV / FILE_CMD_NEXT(NO_WAIT)上下曲
MSG_INPUT_TIMEOUT(仅 KEY_IR_EN)红外输入超时后按 Input_Number 执行 FILE_CMD_PLAY_BY_INDEX,越界则回到主界面红外选曲
MSG_NEXT_PLAYMODEplay_mode++,到达 MAX_PLAY_MODE 回绕 REPEAT_ALL,刷新 MENU_PLAYMODE播放模式轮换
MSG_MUSIC_FF / MSG_MUSIC_FRdecoder_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_MODEgoto __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_NULL0-空命令
DEV_CMD_SEL_NEW_DEV1应用→底层选择指定新设备(配合 used_device 参数)
DEV_CMD_PREV2应用→底层切到上一个设备
DEV_CMD_NEXT3应用→底层切到下一个设备
DEV_CMD_AUTO_PREV4底层内部设备内部自动回退(外部不可调用)
DEV_CMD_AUTO_NEXT5底层内部设备内部自动前进(外部不可调用)
FILE_CMD_PLAY_BY_INDEX0x80应用→底层按文件索引播放(红外选曲等)
FILE_CMD_PREV0x81应用→底层上一曲
FILE_CMD_NEXT0x82应用→底层下一曲
FILE_CMD_AUTO_PREV0x83底层内部自动上一曲(列表头回退)
FILE_CMD_AUTO_NEXT0x84底层内部自动下一曲(文件结束/出错)

设计要点:

  • 命令空间分段: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_DEVVM 索引-存储"最后使用的设备号"的 VM 键,用于断电续播
VM_INDEX_SYSMODEVM 索引-存储当前工作模式,进入音乐模式时写入
上电设备消息窗口时间常量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_deviceu8出错设备标记,music_info_init() 清零
dec_eq_modeu8当前 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

  1. 新增音频格式:在 music_info_init() 的 pctl[0].dec_type 增加对应 BIT_*,同时在解码器子系统注册解码器,并在消息循环补充对应的 *_FILE_END / *_FILE_ERR 分支(参考 WAV/MP3/F1A1 的三组 case)。
  2. 新增播放模式:扩展 FILE_PLAY_MODE 枚举(music_play.h),MAX_PLAY_MODE 自动生效;底层 play_file 需同步实现新模式的文件索引策略。
  3. 自定义按键布局:修改 music_key_table.c 中按键表与 music_key_msg_filter() 的映射,消息层与主循环无需改动。
  4. 红外选曲:KEY_IR_EN 宏 + MSG_INPUT_TIMEOUT 分支已实现按索引选曲,接入新遥控器只需调整按键表。
  5. 多目录文件夹播放:扩展 dir_inr_tab 数组(当前仅 "/dir_song",music_play.c)即可加入更多扫描目录,pctl[0].pdir/dir_index 负责索引。
  6. 退出钩子:__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
Prev
应用入口与模式调度
Next
MIDI 解码与键盘演奏