音频解码器与 MIDI 播放
本文档介绍 AD24N SDK 中 MIDI(乐谱解码 + 电子琴)子系统的完整实现:解码框架接入、MIDI 文件解析与音频合成引擎、音色表(Tone Table)加载、应用层播放/弹奏流程、配置项与 API,以及错误处理与扩展点。
Purpose and Scope
本页覆盖以下内容:
- MIDI 解码器在解码框架(decoder framework)中的注册与调用流程(
MIDI_LST/MIDI_CTRL_LST) - MIDI 乐谱解码(
midi_dec)与合成播放引擎(midi_play)的内部机制 - 音色表(SPI Flash 存放的乐器采样数据)的格式与加载方式
- MIDI 消息定义(
MIDIDefs.h)、事件处理与应用层控制(toy_midi/toy_midi_ctrl) - 相关配置项(
app_config.c)与公开 API 参考
不属于本页范围、由其他页面覆盖的内容:通用解码器框架(decoder_api、DAC 注册)的通用机制、USB 设备层协议细节(usb_midi.c 仅作提及)、语音玩具(voice_toy)业务逻辑、以及音频前端(DAC/混音)本身。如需了解这些,请参见对应目录页。
Overview
AD24N 的 MIDI 子系统由两层组成:
- 解码器层(decoder):位于
sdk/app/bsp/common/decoder/list/,通过midi_api.c(乐谱解码)与midi_ctrl_api.c(电子琴/控制器模式)把 MIDI 能力封装成标准解码器接口(decoder_ops_t),从而复用整个 SDK 的decoder_api框架——包括文件读取、DAC 注册、播放控制(暂停/快进/停止)等通用机制。 - 合成模块层(module):位于
sdk/app/bsp/modules/midi/,包括midi_dec.c(MIDI SMF 文件解析与事件调度)、midi_play.c(多声道采样合成器,基于 BasicSynth 引擎)、midi_event.c(事件回调)与MIDIDefs.h(MIDI 消息常量定义)。
MIDI 乐谱解码器的特殊之处在于:它不是把编码音频流解码成 PCM,而是解析音符事件、按时间轴调度,并用内置合成器实时合成多音符混音。因此其核心参数是"最大同时发声的 key 数"(MAX_DEC_PLAYER_CNT / MAX_CTR_PLAYER_CNT),该值直接决定合成缓冲区大小与 CPU 开销。
系统支持两种运行模式(见 toy_main.c):
TOY_MIDI:播放 MIDI 乐谱文件(toy_midi_app())TOY_MIDI_KEYBOARD:电子琴模式,实时响应按键/外部事件(toy_midi_keyboard_app())
Architecture
flowchart TD
subgraph sg_App["应用层 (voice_func)"]
ToyMain["toy_main.c<br/>work_mode 分发"]
ToyMidi["toy_midi<br/>乐谱播放 App"]
ToyMidiCtrl["toy_midi_ctrl<br/>电子琴 App"]
AppCfg["app_config.c<br/>玩家数/通道配置"]
end
subgraph sg_Decoder["解码框架层 (decoder)"]
DecoderApi["decoder_api.c<br/>MIDI_LST / MIDI_CTRL_LST"]
MidiApi["midi_api.c<br/>midi_decode_api"]
MidiCtrlApi["midi_ctrl_api.c<br/>键盘控制 API"]
end
subgraph sg_Module["MIDI 模块层 (modules/midi)"]
MidiDec["midi_dec.c<br/>SMF 解析与事件调度"]
MidiPlay["midi_play.c<br/>采样合成引擎"]
MidiEvent["midi_event.c<br/>事件回调"]
MidiDefs["MIDIDefs.h<br/>消息常量定义"]
end
subgraph sg_IO["硬件/IO 层"]
ToneTab["SPI Flash 音色表<br/>(spi_pos)"]
DAC["DAC 输出"]
USB["usb_midi.c<br/>USB MIDI 设备"]
end
ToyMain -->|"TOY_MIDI"| ToyMidi
ToyMain -->|"TOY_MIDI_KEYBOARD"| ToyMidiCtrl
ToyMidi --> DecoderApi
ToyMidiCtrl --> DecoderApi
DecoderApi --> MidiApi
DecoderApi --> MidiCtrlApi
MidiApi --> MidiDec
MidiCtrlApi --> MidiPlay
MidiDec --> MidiPlay
MidiPlay --> MidiEvent
MidiPlay -->|"读取采样"| ToneTab
MidiDec -->|"注册到 DAC"| DAC
MidiCtrlApi --> USB
AppCfg -.->|"全局配置"| MidiApi
AppCfg -.->|"全局配置"| MidiPlay
架构说明:
- 应用层通过
work_mode选择 MIDI 工作模式,app_config.c中导出的const int(MAX_DEC_PLAYER_CNT等)是贯穿解码器与合成器的全局配置。 - 解码框架层是标准接入点:
decoder_api.c的解码器列表中注册了MIDI_LST与MIDI_CTRL_LST(见 decoder_api.c),使 MIDI 与其他格式(MP3、AAC 等)共用同一套播放控制管线。 - 合成模块层是核心:
midi_play.c的MIDI_CTRL_OPEN/MIDI_CTRL_MAIN负责把音符事件合成为 PCM 采样,并从 SPI 音色表读取乐器波形;MIDIDefs.h提供标准的 MIDI 消息字节常量(源自 BasicSynth 库)。
解码器框架集成
SDK 的解码框架通过"解码器操作表"(decoder_ops_t)抽象各种格式。MIDI 解码器在 decoder_api.c 中被引入:
#include "midi_api.h"
#include "list/midi_ctrl_api.h"
并在解码器列表中占据 MIDI_LST 与 MIDI_CTRL_LST 两个槽位(见 decoder_api.c):
A_LST
MIDI_LST
MIDI_CTRL_LST
Sources:
MIDI_LST 对应乐谱解码(midi_api.c),MIDI_CTRL_LST 对应电子琴/控制器模式(midi_ctrl_api.c)。两者共用模块层的合成引擎,但事件来源不同:前者来自 MIDI 文件解析,后者来自实时控制事件(按键、USB MIDI 等)。
乐谱解码器初始化流程
解码入口是 midi_decode_api()(见 midi_api.c),它完成以下关键步骤:
- 检查音色表配置:静态变量
midi_tone_tab为空时直接返回E_MIDI_NO_CFG——MIDI 解码强依赖音色表,没有音色数据无法发声。 - 初始化解码句柄:关中断清空
dec_midi_hld(dec_obj),设置type = D_TYPE_MIDI。 - 分配工作缓冲区:使用放置段
AT(.midi_buf)的静态数组midi_decode_buff_nomark[4436/4],并调用ops->need_dcbuf_size()校验缓冲区是否足够;不足则返回E_MIDI_DBUF。缓冲区大小会随MAX_DEC_PLAYER_CNT变化。 - 初始化环形输出缓冲:
cbuf_init(&cbuf_midi, &obuf_midi[0], sizeof(obuf_midi)),合成出的 PCM 通过该 cbuffer 送给 DAC。 - 获取文件名与 IO 接口:
midi_dec_io0把mp_input/mp_output作为解码器 IO 回调。 - 打开与格式检查:
ops->open(...)后调用ops->format_check(...),格式错误返回E_MIDIFORMAT。 - 配置采样率:读取 DAC 采样率
dac_sr_read(),通过midi_musicsr_to_cfgsr()映射为合成器支持的索引值(48000/44100/32000/24000/22050/16000/12000/11025/8000),不支持的值会触发ASSERT(0, "midi error sr !!!:%d\n", sr)。 - 填写合成参数并下发:
midi_t_parm.player_t = MAX_DEC_PLAYER_CNT(同时发声 key 数)、sample_rate、spi_pos = (u16 *)midi_tone_tab(音色表起始地址),随后调用弱函数midi_init_info(&init_info)供应用定制,最后ops->dec_confing(p_cal_buf, CMD_INIT_CONFIG, &init_info)。 - 注册到 DAC:
regist_dac_channel(&dec_midi_hld.sound, kick_decoder),此后解码输出由 DAC 任务驱动。
该流程揭示了设计意图:MIDI 解码器复用通用解码框架的生命周期(open → format_check → dec_confing → DAC 注册),但用一段专门的 .midi_buf 静态内存承载合成器工作区,避免大块动态分配;音色表被当作"必须的外部资源"在入口处强制校验。
合成引擎(midi_play.c)
midi_play.c 实现了基于采样回放的波表合成器。核心是 MIDI_CTRL_OPEN(见 midi_play.c):
static u32 MIDI_CTRL_OPEN(void *work_buf, void *dec_parm, void *parm)
{
unsigned int chn;
long long tmp64;
MIDI_DECODE_VAR *mid_dec_obj = (MIDI_DECODE_VAR *)work_buf;
MIDI_CONFIG_PARM *midi_param = (MIDI_CONFIG_PARM *)parm;
MIDI_CTRL_PARM *midi_dec_param = (MIDI_CTRL_PARM *)dec_parm;
EVENT_FIFO_CONTEXT *midi_fifo_t = (EVENT_FIFO_CONTEXT *)(&mid_dec_obj->smf_data);
memset(work_buf, 0, sizeof(MIDI_DECODE_VAR));
memcpy(midi_fifo_t, midi_dec_param, sizeof(MIDI_CTRL_PARM));
u16 *cmporkind;
cmporkind = (u16 *)midi_param->spi_pos;
if (cmporkind[0] == 0xABCD) {
mid_dec_obj->midi_spi_pos = (u8 *)&cmporkind[1];
mid_dec_obj->compressIN = 1;
mid_dec_obj->instr_spi = cmporkind[1];
mid_dec_obj->instr_map = (unsigned short *)&cmporkind[130];
} else {
mid_dec_obj->midi_spi_pos = (u8 *)midi_param->spi_pos;
mid_dec_obj->compressIN = 0;
mid_dec_obj->instr_spi = cmporkind[0];
mid_dec_obj->instr_map = (unsigned short *)&cmporkind[129];
}
mid_dec_obj->sample_rate = midi_param->sample_rate;
mid_dec_obj->MAX_PLAYER_CNTt = midi_param->player_t;
if (mid_dec_obj->MAX_PLAYER_CNTt > MAX_CTR_PLAYER_CNT) {
mid_dec_obj->MAX_PLAYER_CNTt = MAX_CTR_PLAYER_CNT;
}
...
Source: midi_play.c
关键设计点:
- 音色表头部约定:音色数据首字为
0xABCD时表示压缩格式,instr_spi(乐器数)与instr_map(乐器映射表)分别位于偏移 1 与 130;否则为普通格式,乐器数与映射表位于偏移 0 与 129。这把"数据格式版本"编码进音色文件头部,兼容新旧音色文件。 - 玩家数上限:
MAX_PLAYER_CNTt取自配置,但被硬上限MAX_CTR_PLAYER_CNT钳制,防止越界分配MIDI_PLAYER数组。 - 通道初始化:16 个通道的 CC 默认值(音量 127、表情 127、声像 64)与
pitchBend_v = 256(无弯音)在此建立,确保初始状态符合 GM 规范。 - 时间基准:
srTicks由tempo / 4与采样率表计算得出,MULSI宏完成定点乘除,体现嵌入式环境避免浮点运算的优化策略。 - 内存布局:
mempool中先分配MIDI_PLAYER数组(needMidiPlayersBuf()),随后是play_key[MAX_CTR_PLAYER_CNT * MAX_CHANNEL_NUM](初值 255 表示空槽),即"玩家池 + 键位映射表"。
MIDI_CTRL_MAIN 是实时合成主循环:它按 srTicks 推进时间轴、解析排队的 MIDI 事件、为每个活跃音符调用采样插值(MIDI_RESAMPLE_SHIFT 13 位定点重采样)、叠加各通道混音并做音量/衰减处理,最终输出到 cbuffer。合成器的多发声能力(polyphony)完全由 MAX_DEC_PLAYER_CNT / MAX_CTR_PLAYER_CNT 决定,这也是 need_dcbuf_size() 随该参数变化的原因。
MIDI 消息定义(MIDIDefs.h)
MIDIDefs.h 源自开源 BasicSynth 库(Daniel R. Mitchell,GPL),定义了标准 MIDI 消息字节常量(见 MIDIDefs.h):
#define MAX_MIDI_CHNL 16
#define MIDI_EVTMSK 0xF0
#define MIDI_CHNMSK 0x0F
#define MIDI_MSGBIT 0x80
#define MIDI_NOTEOFF 0x80
#define MIDI_NOTEON 0x90
#define MIDI_KEYAT 0xA0
#define MIDI_CTLCHG 0xB0
#define MIDI_PRGCHG 0xC0
#define MIDI_CHNAT 0xD0
#define MIDI_PWCHG 0xE0
#define MIDI_SYSEX 0xF0
#define MIDI_META 0xFF
Source: MIDIDefs.h
这些常量被 midi_dec.c(SMF 文件解析)、midi_ctrl_api.c(电子琴事件构造)与 usb_midi.c(USB MIDI 端点数据)共同使用,保证全链路消息语义一致。控制器编号(CC)同样在此定义,如 MIDI_CTRL_VOL 0x07、MIDI_CTRL_PAN 0x0A、MIDI_CTRL_SUS_ON 0x40,合成引擎内部 MIDI_CTRL_VOL_CC 等枚举与之对应(见 midi_dec.h)。
Core Flow:乐谱播放端到端时序
sequenceDiagram
participant App as toy_midi_app
participant DA as decoder_api (MIDI_LST)
participant MA as midi_api.c
participant MD as midi_dec (解析/调度)
participant MP as midi_play (合成器)
participant Tone as SPI 音色表
participant DAC as DAC
App->>DA: 打开文件, 选择 MIDI_LST 解码器
DA->>MA: midi_decode_api(p_file, &pdec, p_dp_buf)
MA->>MA: 校验 midi_tone_tab / 分配 .midi_buf 工作区
MA->>MD: ops->open + ops->format_check (SMF 头校验)
MA->>MP: dec_confing(CMD_INIT_CONFIG, init_info)
MP->>Tone: 读取音色表头部 (0xABCD 压缩格式判断)
MA->>DAC: regist_dac_channel(&sound, kick_decoder)
DAC-->>MP: kick 请求输出
loop 每个采样块
MP->>MD: 推进 srTicks / 取事件
MD-->>MP: NoteOn/NoteOff/CC 事件
MP->>Tone: 读取乐器采样 (定点重采样)
MP->>MP: 多玩家混音 + 音量/衰减
MP-->>DAC: PCM 输出到 cbuf_midi
end
App->>MA: 播放控制 (暂停/快进/停止)
MA-->>App: 事件/错误码
时序要点:初始化阶段只做"配置下发"(CMD_INIT_CONFIG),真正的合成由 DAC 的 kick 驱动按块执行;midi_play 每块从事件 FIFO 取事件、从音色表取采样、混音输出。播放控制(暂停等)通过 MAD_ERROR_PAUSE(0x50)等错误码返回上层(见 midi_dec.h),与通用解码框架的暂停机制一致。
Usage Examples
示例 1:解码器注册与配置读取
MIDI 解码器通过标准 decoder_ops_t 接口注册,同时暴露两个供应用读取配置的访问器(见 midi_api.c):
const struct if_decoder_io midi_dec_io0 = {
&dec_midi_hld, //input跟output函数的第一个参数,解码器不做处理,直接回传,可以为NULL
mp_input,
mp_output,
};
MIDI_PLAY_CTRL_MODE *get_midi_mode(void)
{
return &init_info.mode_info;
}
u32 *get_midi_switch_info(void)
{
return &init_info.switch_info;
}
Source: midi_api.c
示例 2:应用定制初始化钩子
midi_init_info 被声明为 __attribute__((weak)) 的弱函数,默认空实现;应用层可以重定义它,在 CMD_INIT_CONFIG 下发前修改 init_info(如切换主旋律通道等)(见 midi_api.c):
__attribute__((weak))
void midi_init_info(MIDI_INIT_STRUCT *init_info)
{
}
Source: midi_api.c
示例 3:全局行为配置(app_config.c)
应用通过全局 const int 控制 MIDI 行为,这些变量被解码器模块以 extern 引用(见 app_config.c):
#if (DECODER_MIDI_EN | DECODER_MIDI_KEYBOARD_EN)
const int MAINTRACK_USE_CHN = 0; //0:用track号来区分 1:用channel号来区分。
const int MAX_DEC_PLAYER_CNT = 8; //midi乐谱解码最大同时发声的key数,范围[1,31]
const int MAX_CTR_PLAYER_CNT = 8; //midi琴最大同时发声的key数,范围[1,31]
const int NOTE_OFF_TRIGGER = 0; //midi琴note_off回调 1:time传0时,不会回调 0:time传0时,回调
Source: app_config.c
示例 4:工作模式分发(toy_main.c)
toy_main.c 根据 work_mode 进入不同的 MIDI 应用(见 toy_main.c):
#if defined(DECODER_MIDI_EN) && (DECODER_MIDI_EN)
case TOY_MIDI:
toy_midi_app();
break;
#endif
#if defined(DECODER_MIDI_KEYBOARD_EN) && (DECODER_MIDI_KEYBOARD_EN)
case TOY_MIDI_KEYBOARD:
toy_midi_keyboard_app();
break;
#endif
Source: toy_main.c
Configuration Options
MIDI 子系统的配置集中在两个层面:编译期宏开关(决定是否编译 MIDI 解码器/键盘功能)与运行期全局常量(在 app_config.c 中定义、被解码模块 extern 引用)。
编译期宏
| 宏 | 作用域 | 说明 |
|---|---|---|
DECODER_MIDI_EN | 全局 | 使能 MIDI 乐谱解码器;为 0 时 midi_api.c 整体不编译(#if defined(DECODER_MIDI_EN) && (DECODER_MIDI_EN)) |
DECODER_MIDI_KEYBOARD_EN | 全局 | 使能 MIDI 电子琴(键盘)模式 |
运行期全局常量(app_config.c)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MAINTRACK_USE_CHN | const int | 0 | 主旋律识别方式:0 用 track 号区分,1 用 channel 号区分 |
MAX_DEC_PLAYER_CNT | const int | 8 | 乐谱解码最大同时发声 key 数,范围 [1,31];决定合成缓冲区大小与 CPU 占用 |
MAX_CTR_PLAYER_CNT | const int | 8 | 电子琴模式最大同时发声 key 数,范围 [1,31];同时是 MIDI_CTRL_OPEN 的硬上限 |
NOTE_OFF_TRIGGER | const int | 0 | note_off 回调行为:1 表示 time 传 0 时不回调;0 表示 time 传 0 时也回调 |
模块内常量(midi_dec.h)
| 常量 | 值 | 说明 |
|---|---|---|
INBUF_SIZE | 700 | MIDI 输入缓冲字节数 |
MAX_CHANNEL_NUM / MAX_TRACK_NUM | 16 | 通道/音轨上限 |
MIDI_RESAMPLE_SHIFT | 13 | 定点重采样精度(1<<13) |
MIDI_SwitchLevel_CC | 64 | 开关级 CC 阈值 |
MAX_GO_BACK | 8 | 快退最大跨度(1<<8) |
VOL_Norm_Bit | 12 | 音量归一化位宽 |
API Reference
u32 midi_decode_api(void *p_file, void **ppdec, void *p_dp_buf)
MIDI 乐谱解码器标准入口,被解码框架经 MIDI_LST 调用。
参数:
p_file(void *):VFS 文件句柄ppdec(void **):输出解码句柄(dec_obj *)p_dp_buf(void *):解码参数缓冲(可为 NULL)
返回:
0:成功E_MIDI_NO_CFG:音色表未配置(midi_tone_tab == 0)E_MIDI_DBUF:need_dcbuf_size()超过静态工作区大小E_MIDIFORMAT:format_check失败
来源: midi_api.c
MIDI_PLAY_CTRL_MODE *get_midi_mode(void)
返回 init_info.mode_info 指针,供应用读取/修改播放控制模式(主旋律、开关等)。
来源: midi_api.c
u32 *get_midi_switch_info(void)
返回 init_info.switch_info 指针,供应用读取/修改开关信息。
来源: midi_api.c
void midi_init_info(MIDI_INIT_STRUCT *init_info)(弱函数)
默认为空实现;应用可重定义以在 CMD_INIT_CONFIG 下发前定制初始化参数。
来源: midi_api.c
static u32 MIDI_CTRL_OPEN(void *work_buf, void *dec_parm, void *parm)
合成器打开接口:解析音色表头(含 0xABCD 压缩格式判断)、初始化 16 通道 CC/弯音状态、分配玩家池与键位表、计算 srTicks 时间基准。
参数:
work_buf:MIDI_DECODE_VAR工作区dec_parm:MIDI_CTRL_PARM(含 tempo 等)parm:MIDI_CONFIG_PARM(含spi_pos音色表地址、sample_rate、player_t)
返回: 0(成功)
来源: midi_play.c
Failure Modes、边界与并发
错误码与失败路径
| 场景 | 返回/表现 | 处理位置 |
|---|---|---|
| 音色表未配置 | E_MIDI_NO_CFG | midi_decode_api 入口,midi_tone_tab == 0 |
| 工作缓冲区不足 | E_MIDI_DBUF | need_dcbuf_size() > cal_buf_len,缓冲区随 MAX_DEC_PLAYER_CNT 变化 |
| 文件格式错误 | E_MIDIFORMAT | ops->format_check 失败 |
| 采样率不支持 | ASSERT(0, "midi error sr !!!") | midi_musicsr_to_cfgsr 未命中 9 档采样率表 |
| 解码/播放运行时错误 | MAD_ERROR_* 系列(0x05、0x40–0x48) | MAD_ERROR_PAUSE = 0x50 表示暂停态;文件结束 MAD_ERROR_FILE_END = 0x40 等 |
| 格式部分支持 | FORMAT_OK_BUT_NO_SUPPORT | midi_dec.h 中格式检查三态之一 |
并发与中断安全
midi_decode_api在清理dec_midi_hld时使用local_irq_disable()/local_irq_enable()保护,避免与 DAC 中断路径(kick_decoder)竞争写句柄(见 midi_api.c)。- 合成输出走环形缓冲
cbuf_midi(cbuffer_t),由 DAC 端按块消费,天然解耦实时合成与音频输出节奏。 - 玩家池
play_key初值 255 表示空槽,多通道键位互不冲突;MAX_PLAYER_CNTt被MAX_CTR_PLAYER_CNT钳制,防止玩家数越界导致内存越写。
边界情况
- 同时发声 key 数超过
MAX_DEC_PLAYER_CNT时,音符叠加被丢弃(polyphony 饱和),这是"音质 vs 资源"的显式取舍。 MIDI_CTRL_OPEN中若音色文件头既非0xABCD也无合法instr_spi,instr_key_map仍按默认偏移建立——音色文件损坏时合成器可能输出静音/异常音,但不崩溃。NOTE_OFF_TRIGGER控制 note_off 回调在time == 0时是否触发,影响电子琴 App 的事件驱动逻辑(见 app_config.c)。
Performance 与运维注意
- CPU 开销与玩家数成正比:每个活跃音符都需要独立的采样插值(13 位定点重采样)与混音累加。
MAX_DEC_PLAYER_CNT/MAX_CTR_PLAYER_CNT建议按实际复音需求设置,默认 8 是性能与音质的平衡点;范围上限 31。 - 内存全部静态分配:解码句柄、cbuffer、输出缓冲与合成工作区均放在
AT(.midi_buf)放置段(见 midi_api.c),无运行时 malloc,可预测性好,但需在链接脚本中为该段预留足够空间。 - 音色表读取为 SPI 访问:乐器采样从 SPI Flash 顺序读取,压缩格式(头部
0xABCD)可减少存储占用,代价是解压开销;选择音色文件格式时需权衡 Flash 容量与 CPU。 - 采样率跟随 DAC:解码器按
dac_sr_read()配置合成采样率,改变 DAC 采样率会触发midi_musicsr_to_cfgsr映射,务必使用表中 9 档标准采样率。
Extension Points
midi_init_info(弱函数重定义):在CMD_INIT_CONFIG前定制MIDI_INIT_STRUCT,是实现自定义主旋律通道、特殊开关逻辑的首选钩子。get_midi_mode()/get_midi_switch_info():应用可在解码过程中动态读取/修改播放控制模式与开关信息。midi_event.c事件回调:解码/合成过程中的事件(如 note_on/note_off、mark 等)通过事件表midi_evt[]注册到dec_midi_hld.event_tab,应用可挂接自定义回调实现歌词/按键灯等联动。- 音色表格式:
MIDI_CTRL_OPEN对普通/压缩(0xABCD)两种音色文件头都支持,新增音色格式只需扩展头部解析分支。 - USB MIDI 输入:
usb_midi.c提供 USB 设备端 MIDI 端点,可将外部 MIDI 键盘事件桥接到midi_ctrl_api(电子琴模式),实现"外部设备 + 内置合成器"的组合。
Related Links
- 解码框架通用机制(decoder_api、
MIDI_LST/MIDI_CTRL_LST注册):decoder_api.c - 电子琴控制 API:
sdk/app/bsp/common/decoder/list/midi_ctrl_api.c - 应用层乐谱播放/电子琴控制:
sdk/app/src/voice_func/toy_midi/(toy_midi_ctrl.c、midi_config.c) - USB MIDI 设备实现:
sdk/app/bsp/common/usb/device/usb_midi.c - 官方文档:《杰理 AD1x-45678 MIDI 应用说明文档》(
doc/杰理AD1x-45678_MIDI应用说明文档.pdf)