音乐播放与外部音源
本文档介绍 AD15N voice-toy 应用中「音乐播放」(TOY_MUSIC / toy_music_app)与「外部音源」(TOY_LINEIN / toy_linein_app)两个工作模式:它们如何通过统一的工作模式状态机被调度、如何接入系统级的音频解码与 Line-in 检测服务,以及相关的编译开关、音量持久化和错误处理机制。
Purpose and Scope
本页面聚焦 voice-toy 应用中与「音乐播放与外部音源」直接相关的能力:
- 工作模式状态机:
toy_main.c中的work_mode切换与主循环调度; - 音乐播放模式:
TOY_MUSIC(由SIMPLE_DEC_EN控制)及其依赖的music/music_play.h解码播放服务; - 外部音源模式:
TOY_LINEIN(由LINEIN_MODE_EN控制)及其依赖的line_in/line_in_mode.h音源检测/切换服务; - 系统级集成:
mbox_main.c中的 50ms Line-in 插入检测轮询、SDX_SUSPEND_EVENT_LINEIN挂起事件,以及fm_radio.c中与收音机模块共用的 Line-in 初始化。
以下主题属于同目录下的兄弟页面,不在本页展开:MIDI 播放(TOY_MIDI / TOY_MIDI_KEYBOARD)、扬声器(TOY_SPEAKER / toy_speaker_app)、录音(TOY_RECORD / toy_record_app)、USB 从机(TOY_USB_SLAVE)与待机(TOY_IDLE / TOY_SOFTOFF)。
Overview
在 voice-toy(语音玩具)应用中,固件启动后进入一个永不退出的主循环:每个循环周期先清空消息队列、预擦除 VM 存储,然后根据全局变量 work_mode 分发到对应的模式应用函数。TOY_MUSIC 与 TOY_LINEIN 就是其中两个可编译选择的工作模式:
- TOY_MUSIC(音乐播放):玩具以本地解码方式播放音频,依赖
music/music_play.h提供的播放服务,是默认工作模式(work_mode = TOY_MUSIC;)。 - TOY_LINEIN(外部音源):玩具将 AUX/Line-in 输入作为音源,依赖
line_in/line_in_mode.h进行音源检测与模式切换;在系统层(mbox)还由line_in_det()以约 50ms 周期轮询外部音源插入状态。
这两个模式并非孤立存在,而是通过编译宏(SIMPLE_DEC_EN、LINEIN_MODE_EN、LINEIN_EN)条件编译,与系统层音频服务(music_play、line_in_mode)以及硬件外设(DAC 输出、ADC 采样)协同工作。设计意图是:用一个可扩展的模式状态机把"玩具的多种玩法"(音乐、外音、录音、MIDI 等)解耦为独立的应用函数,每个模式由编译宏按产品配置裁剪,主循环只负责调度,从而兼顾代码复用与固件体积控制。
Architecture
下图展示了「音乐播放与外部音源」在 voice-toy 应用中的分层架构与依赖关系:
flowchart TD
subgraph sg_App["应用层 voice_toy"]
ToyMain["toy_main.c 主循环<br/>work_mode 状态机"]
ToyMusic["toy_music_app() 音乐播放"]
ToyLinein["toy_linein_app() 外部音源"]
end
subgraph sg_Svc["系统服务层"]
MusicPlay["music/music_play.h<br/>解码播放服务"]
LineInMode["line_in/line_in_mode.h<br/>音源模式切换"]
LineDet["mbox_main.c<br/>line_in_det() 50ms 轮询"]
end
subgraph sg_HW["硬件层"]
DAC["DAC 音频输出"]
ADC["AUX/Line-in 采样输入"]
end
ToyMain -->|"case TOY_MUSIC<br/>SIMPLE_DEC_EN"| ToyMusic
ToyMain -->|"case TOY_LINEIN<br/>LINEIN_MODE_EN"| ToyLinein
ToyMusic --> MusicPlay
ToyLinein --> LineInMode
LineDet -->|"LINEIN_EN 保护"| LineInMode
MusicPlay --> DAC
LineInMode --> ADC
架构说明:
- 应用层:
toy_main.c是唯一入口,通过switch (work_mode)把控制权交给toy_music_app()或toy_linein_app()。模式函数返回后主循环重新调度,因此每个模式函数内部通常是"事件循环 + 消息处理"结构。 - 系统服务层:
music/music_play.h是跨应用复用的播放服务接口(mbox_main.c、fm_radio.c也包含它);line_in/line_in_mode.h提供外部音源模式支持,且受LINEIN_EN宏保护。mbox_main.c的系统任务中周期性调用line_in_det()完成音源插入检测,并通过挂起事件SDX_SUSPEND_EVENT_LINEIN与低功耗流程联动。 - 硬件层:播放路径最终驱动 DAC 输出,外部音源路径通过 ADC 采样 AUX 输入;
fm_radio.c的fm_linein_init()表明收音机与 Line-in 共享部分初始化资源。
说明:
toy_music_app()/toy_linein_app()的具体函数体(消息处理细节)位于 voice_toy 目录下的对应实现文件中;在本次源码调研范围内未读取到其函数体实现,下文将以已读到的调度代码、编译宏与系统级集成点为准进行说明。
主内容:工作模式状态机与调度机制
工作模式定义与编译裁剪
toy_main.c 通过一组编译宏决定哪些模式被编译进固件。模式常量(TOY_MUSIC、TOY_LINEIN 等)与模式应用函数一一对应,work_mode 变量在启动时被显式赋值为 TOY_MUSIC,其余模式以注释形式保留作为可选配置:
work_mode = TOY_MUSIC;
/* work_mode = TOY_MIDI; */
/* work_mode = TOY_MIDI_KEYBOARD; */
/* work_mode = TOY_SPEAKER; */
/* work_mode = TOY_LINEIN; */
/* work_mode = TOY_RECORD; */
/* work_mode = TOY_IDLE; */
Source: toy_main.c
设计意图:把"产品玩法"声明为编译期配置而不是运行时配置。玩具厂商可以在不修改调度逻辑的前提下,通过注释/宏选择固件内置的玩法组合,同时借助条件编译让未启用的模式完全不占用代码空间。
主循环与模式分发
主循环是 while (1) 形式的超级循环(super-loop),每个周期做三件事:清空消息、预擦除 VM、分发模式。分发逻辑如下(节选):
while (1) {
clear_all_message();
vm_pre_erase();
switch (work_mode) {
#if SIMPLE_DEC_EN
case TOY_MUSIC:
toy_music_app();
break;
#endif
...
#if defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN)
case TOY_LINEIN:
toy_linein_app();
break;
#endif
...
case TOY_IDLE:
toy_idle_app();
break;
case TOY_SOFTOFF:
toy_softoff();
break;
default:
work_mode++;
if (work_mode >= MAX_MODE) {
work_mode = TOY_MUSIC;
}
break;
}
}
Source: toy_main.c
这段代码揭示了三个关键机制:
- 条件编译保护:
TOY_MUSIC分支由SIMPLE_DEC_EN宏保护,TOY_LINEIN分支由defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN)双条件保护(先检查宏是否定义,再检查其值是否为真)。 - 默认回退:
default分支执行work_mode++,超出MAX_MODE后回绕到TOY_MUSIC。这意味着即使某个模式宏被裁剪掉,状态机也能自动跳到下一个合法模式,不会死循环在无效值上——这是嵌入式状态机常见的"兜底"设计。 - 模式函数阻塞语义:
toy_music_app()/toy_linein_app()返回后才会进入下一轮循环,因此每个模式函数内部必须自行处理事件驱动(消息循环),主循环本身不提供抢占式调度。
音量持久化与启动恢复
启动阶段(主循环之前)从 VM(虚拟存储/掉电保存存储)读取用户音量并写入 DAC:
u8 vol = 0;
u32 res = vm_read(VM_INDEX_VOL, &vol, sizeof(vol));
if ((vol <= 31) && (res == sizeof(vol))) {
dac_vol(0, vol);
log_info("powerup set vol : %d\n", vol);
}
Source: toy_main.c
要点:
VM_INDEX_VOL是 VM 存储中的音量索引;读取结果res必须等于sizeof(vol)才认为有效,这是 VM 读取的常规校验方式。- 校验
vol <= 31限制了音量合法范围(0~31),防止越界值写入 DAC。 dac_vol(0, vol)将音量应用到 DAC 通道 0,log_info输出上电音量日志便于调试。
外部音源:系统级检测与集成
音乐/外音模式在系统层(mbox)还有独立的集成点。mbox_main.c 的系统任务以 50ms 为周期调用 line_in_det() 检测外部音源:
if (0 == (cnt % 25)) { //50ms
#ifdef LINEIN_EN
line_in_det();
#endif
Source: mbox_main.c
这里的 cnt % 25 配合系统节拍(注释标注 50ms,即每 25 个 2ms 节拍一次)形成软件定时器;LINEIN_EN 宏决定是否编译 Line-in 检测逻辑。另外,mbox_main.c 还处理与 Line-in 相关的低功耗挂起事件:
if (event & BIT(SDX_SUSPEND_EVENT_LINEIN)) {
void line_in_det1(void);
Source: mbox_main.c
这表示 Line-in 检测还参与 SDX(掉电/挂起)事件仲裁:当 SDX_SUSPEND_EVENT_LINEIN 位置位时,会调用 line_in_det1() 做挂起前的补充检测,避免在外部音源仍在播放时误入低功耗。此外 mbox_main.c 在 LINEIN_EN 保护下支持 AUX_MODE 模式分支(见 mbox_main.c),说明系统消息层对 AUX/Line-in 音源也有模式化处理。
与收音机模块的共用初始化
fm_radio.c 同时包含 music/music_play.h 与 line_in/line_in_mode.h,并提供 fm_linein_init():
#include "music/music_play.h"
...
#ifdef LINEIN_EN
#include "line_in/line_in_mode.h"
...
int fm_linein_init(void)
Source: fm_radio.c
设计意图:收音机(FM)与外部音源(Line-in)在硬件上共享模拟音频通路(输入选择、增益配置),因此 fm_linein_init() 把两者的初始化合并,避免重复配置 ADC/增益寄存器;LINEIN_EN 宏再次作为裁剪开关,未启用外音的产品不会引入 line_in_mode.h 的依赖。
Core Flow:从启动到外部音源播放的完整流程
sequenceDiagram
participant Boot as 启动代码
participant Main as toy_main.c 主循环
participant App as 模式应用函数
participant Svc as 系统服务层
participant HW as 硬件外设
Boot->>Boot: 读取 VM_INDEX_VOL 恢复音量
Boot->>Main: work_mode = TOY_MUSIC (默认)
loop 主循环 while(1)
Main->>Main: clear_all_message() / vm_pre_erase()
Main->>App: switch(work_mode) 分发
alt TOY_MUSIC (SIMPLE_DEC_EN)
App->>Svc: toy_music_app() → music_play 服务
Svc->>HW: 解码数据 → DAC 输出
else TOY_LINEIN (LINEIN_MODE_EN)
App->>Svc: toy_linein_app() → line_in_mode
Svc->>HW: 采样 AUX 输入 → 播放
Svc-->>App: 音源插入状态变化
end
App-->>Main: 模式函数返回,进入下一轮
end
执行步骤分解:
- 启动音量恢复:
toy_main.c从 VM 读取VM_INDEX_VOL,校验后调用dac_vol(0, vol)设置 DAC 音量(toy_main.c#L80-L86)。 - 设置初始模式:
work_mode = TOY_MUSIC,默认进入音乐播放。 - 主循环调度:每轮先
clear_all_message()清空遗留消息、vm_pre_erase()预擦除 VM 页,然后switch(work_mode)分发(toy_main.c#L96-L152)。 - 模式执行:
toy_music_app()(受SIMPLE_DEC_EN保护)走本地解码路径;toy_linein_app()(受LINEIN_MODE_EN保护)走外部音源路径。两者都依赖系统服务层,但硬件通路不同:DAC 输出 vs. ADC 采样 AUX。 - 外部音源并行检测:无论当前处于哪个模式,mbox 系统任务每 50ms 调用一次
line_in_det()(受LINEIN_EN保护)感知音源插入(mbox_main.c#L66-L68)。 - 低功耗联动:挂起事件
SDX_SUSPEND_EVENT_LINEIN置位时调用line_in_det1()做挂起前检测,避免外音播放中被误挂起(mbox_main.c#L93-L95)。 - 模式回绕:若
work_mode指向未编译的模式,default分支自增并回绕到TOY_MUSIC,保证状态机始终收敛到合法模式。
Configuration Options
「音乐播放与外部音源」的行为完全由编译期宏控制,没有运行时配置文件。产品工程师通过修改 toy_main.c 顶部的模式赋值与工程配置中的宏定义来裁剪功能:
| 宏 | 类型 | 生效范围 | 说明 |
|---|---|---|---|
SIMPLE_DEC_EN | 编译宏 | TOY_MUSIC 分支 | 启用简单解码播放(音乐模式);未定义时 case TOY_MUSIC 不编译 |
LINEIN_MODE_EN | 编译宏(值判断) | TOY_LINEIN 分支 | 启用外部音源模式;需 defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN) 双条件满足 |
LINEIN_EN | 编译宏 | 系统层 Line-in 功能 | 启用 line_in_det() 检测、line_in_mode.h 依赖与 AUX_MODE 分支 |
MAX_MODE | 常量 | 主循环回绕边界 | 模式枚举上限;work_mode 超过后回绕到 TOY_MUSIC |
VM_INDEX_VOL | VM 索引 | 音量持久化 | 音量存储索引;有效值范围校验为 vol <= 31 |
SDX_SUSPEND_EVENT_LINEIN | 事件位 | 低功耗仲裁 | Line-in 挂起事件位,置位时执行挂起前音源检测 |
配置要点:
- 三个宏的关系是"应用层模式开关 + 系统层功能开关":即使
LINEIN_MODE_EN打开,若LINEIN_EN未开,toy_linein_app()虽然编译,但系统层不会执行line_in_det()检测,音源切换能力受限。 TOY_MUSIC是默认且兜底模式:work_mode初始化与回绕目标都指向它,产品若只需外音功能,也应保留SIMPLE_DEC_EN或确保回绕路径可用,否则default分支可能反复自增。
API Reference
以下接口/函数均从已读取的源码中提取,属于本页主题的直接相关入口。toy_music_app() 与 toy_linein_app() 的声明位于 toy_linein.h(被 toy_main.c 包含),其函数体实现在本次调研范围外,故仅列出由调用点确认的签名。
toy_music_app(void)
音乐播放模式应用入口。由主循环在 case TOY_MUSIC 下调用,仅在 SIMPLE_DEC_EN 编译时存在。
- 返回:
void;返回后主循环进入下一轮调度。 - 调用点:toy_main.c#L101-L103
toy_linein_app(void)
外部音源模式应用入口。由主循环在 case TOY_LINEIN 下调用,仅在 defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN) 编译时存在。
- 返回:
void;返回后主循环进入下一轮调度。 - 调用点:toy_main.c#L120-L123
line_in_det(void)
外部音源插入检测函数(mbox 系统层)。由系统任务以 50ms 周期调用,仅在 LINEIN_EN 编译时存在。
- 返回:
void - 调用点:mbox_main.c#L67-L68
line_in_det1(void)
挂起前的 Line-in 补充检测函数。在 SDX_SUSPEND_EVENT_LINEIN 事件置位时调用,用于低功耗仲裁。
- 返回:
void(前向声明void line_in_det1(void);) - 调用点:mbox_main.c#L94-L95
fm_linein_init(void)
收音机模块的 Line-in 共用初始化函数,合并 FM 与外部音源的模拟通路初始化。
- 返回:
int - 定义位置:fm_radio.c#L81
dac_vol(u8 ch, u8 vol)
DAC 音量设置函数(硬件驱动层)。启动时用于恢复用户音量。
- 参数:
ch— DAC 通道(启动流程使用 0);vol— 音量值(合法范围 0~31)。 - 调用点:toy_main.c#L84
vm_read(u16 index, void *buf, u32 len)
VM 存储读取函数,用于读取 VM_INDEX_VOL 持久化的音量。
- 返回:实际读取字节数;调用方用返回值与
sizeof(vol)比较判断有效性。 - 调用点:toy_main.c#L82-L83
失败模式、边界情况与并发
音量数据损坏
若 vm_read(VM_INDEX_VOL, ...) 返回值不等于 sizeof(vol)(首次上电、VM 页损坏、长度不匹配),启动代码跳过 dac_vol 调用,音量保持硬件默认值。校验 vol <= 31 进一步防止非法值进入 DAC。失败路径为静默降级而非报错,符合嵌入式产品的容错预期(toy_main.c#L82-L86)。
模式宏不一致导致的状态机空转
若 work_mode 指向被裁剪的模式(例如 LINEIN_MODE_EN 关闭但运行时仍赋值 TOY_LINEIN),default 分支执行 work_mode++ 并回绕到 TOY_MUSIC。风险在于:若所有模式宏均关闭,状态机会在 default 分支空转并反复擦写 VM(每轮 vm_pre_erase()),因此产品配置必须至少保留一个可用模式。
事件/检测的并发时序
line_in_det()由系统任务周期性调用,而toy_linein_app()运行在应用主循环中,两者属于不同执行上下文。音源插入状态通过共享标志/事件传递,存在经典的生产者-消费者时序:检测到插入到应用层响应之间可能有最多 50ms 延迟。SDX_SUSPEND_EVENT_LINEIN事件位在挂起仲裁中被检查,若外音正在播放,挂起流程会先执行line_in_det1()确认状态,避免"边播放边挂起"导致音频通路异常。
低功耗与外音冲突
外部音源通路(ADC 采样)与系统低功耗(SDX)天然冲突:进入挂起会切断模拟通路。因此 SDX_SUSPEND_EVENT_LINEIN 仲裁是保证"外音播放不被静默打断"的关键边界,产品若支持外音必须保留该事件处理。
性能与运维
- 检测周期:
line_in_det()每 50ms 一次(cnt % 25),是固定的软件定时器节拍;检测开销小(多为 GPIO/ADC 状态读取),不会显著占用 CPU。若产品需要更快的音源响应,可调整节拍计数,但需同步评估 ADC 采样与低功耗时序。 - 启动路径:音量恢复(一次
vm_read+ 一次dac_vol)在进入主循环前完成,开销可忽略;主循环每轮vm_pre_erase()是 VM 磨损均衡的一部分,频繁模式切换会加速 VM 页擦写,应避免在循环内高频切换work_mode。 - 日志:
log_info("powerup set vol : %d\n", vol)提供上电音量日志,便于产测与现场排查音量异常问题(toy_main.c#L85)。
扩展点
- 新增玩法模式:在
toy_main.c的switch (work_mode)中仿照TOY_MUSIC/TOY_LINEIN添加case TOY_XXX,配套一个#if defined(XXX_MODE_EN)编译宏与一个toy_xxx_app()模式函数,并确保MAX_MODE覆盖新枚举值。该模式即自动获得主循环调度、消息清空与 VM 预擦写服务。 - 外音源检测策略:替换/扩展
line_in_det()的检测算法(电平阈值、消抖窗口)时,保持其"50ms 周期 + 事件置位"的对外契约,应用层无需改动。 - 共用初始化:新增与 Line-in 共用模拟通路的模块(如录音输入),可参照
fm_linein_init()的模式把初始化集中到一处,用宏裁剪避免重复配置 ADC/增益寄存器。
Related Links
- toy_main.c(工作模式状态机与主循环)
- mbox_main.c(Line-in 50ms 检测与挂起事件)
- fm_radio.c(fm_linein_init 共用初始化)
- 兄弟页面:MIDI 播放(TOY_MIDI)、扬声器模式(TOY_SPEAKER)、录音模式(TOY_RECORD)、USB 从机(TOY_USB_SLAVE)、待机与软关机(TOY_IDLE / TOY_SOFTOFF)
Usage Examples
以下示例均直接提取自仓库源码,展示如何启用与集成「音乐播放与外部音源」能力。
示例 1:选择工作模式(产品玩法配置)
产品工程师通过修改 toy_main.c 启动处的赋值选择默认玩法。取消 TOY_LINEIN 注释即把玩具默认切到外部音源模式:
work_mode = TOY_MUSIC;
/* work_mode = TOY_MIDI; */
/* work_mode = TOY_MIDI_KEYBOARD; */
/* work_mode = TOY_SPEAKER; */
/* work_mode = TOY_LINEIN; */
/* work_mode = TOY_RECORD; */
/* work_mode = TOY_IDLE; */
Source: toy_main.c
示例 2:主循环内模式分发(调度契约)
所有模式应用函数都必须遵循"返回后由主循环重新调度"的契约;default 分支保证模式回绕不会死锁:
switch (work_mode) {
#if SIMPLE_DEC_EN
case TOY_MUSIC:
toy_music_app();
break;
#endif
...
#if defined(LINEIN_MODE_EN) && (LINEIN_MODE_EN)
case TOY_LINEIN:
toy_linein_app();
break;
#endif
default:
work_mode++;
if (work_mode >= MAX_MODE) {
work_mode = TOY_MUSIC;
}
break;
}
Source: toy_main.c
示例 3:系统层周期检测外部音源
在 mbox 系统任务中加入 50ms 周期检测,并受 LINEIN_EN 宏保护——这是外部音源插入感知的底层入口:
if (0 == (cnt % 25)) { //50ms
#ifdef LINEIN_EN
line_in_det();
#endif
Source: mbox_main.c
示例 4:挂起前音源检测(低功耗联动)
在 SDX 挂起仲裁中响应 Line-in 事件,防止外音播放期间进入低功耗:
if (event & BIT(SDX_SUSPEND_EVENT_LINEIN)) {
void line_in_det1(void);
Source: mbox_main.c
示例 5:收音机与 Line-in 共用初始化
新增与外部音源共用模拟通路的模块时,参照 fm_linein_init() 集中初始化:
#include "music/music_play.h"
...
#ifdef LINEIN_EN
#include "line_in/line_in_mode.h"
...
int fm_linein_init(void)
Source: fm_radio.c
文档小结:本文档基于 toy_main.c、mbox_main.c、fm_radio.c 的已读源码,系统梳理了 voice-toy 应用中「音乐播放」(TOY_MUSIC)与「外部音源」(TOY_LINEIN)的模式状态机调度、系统级 Line-in 检测与低功耗联动、编译宏配置、关键 API、失败模式与扩展方式。toy_music_app() / toy_linein_app() 的函数体实现位于 voice_toy 目录对应文件中,本次调研未读取到,已如实标注。