MIDI 合成与播放
本文档介绍杰理 AD1x 系列 MCU SDK 中的 MIDI 合成与播放能力:从音色库(tone bank)加载、SMF 乐谱解析、音符事件驱动合成,到 PCM 输出与 DAC 通道注册的完整端到端机制。
Purpose and Scope
本页面覆盖 MIDI 子系统在 SDK 中的完整实现,包括:
- 解码器框架接入层(
decoder/list/midi_api.c、midi_ctrl_api.c) - 2 字节 MIDI 合成引擎(
midi_open/midi_2byte/下的midi_dec.c、midi_play.c、midi_event.c、MIDIDefs.h、midi_dec.h) - 音色库文件(
/midi_cfg/00_MIDI.mda或midi_cfg.bin)的加载方式 - 后构建脚本(
post_build/*/dir_midi、midi_cfg)对音色文件的打包约定
以下内容属于其他页面,不在本文展开:通用解码器框架(decoder_api)、DAC 音频输出通路、文件系统(VFS)实现、以及 doc/ 目录下的 MIDI 应用/工具使用说明(PDF)。
概述
MIDI(Musical Instrument Digital Interface)本身只描述"音符事件"而非声音。要在无专用音频 DSP 的 MCU 上播放 MIDI,必须把事件流实时合成为 PCM 采样。本 SDK 的 MIDI 子系统正是完成这一任务的软件合成器:
- 音色库(tone bank):预先生成、烧录在 SPI Flash 中的采样表(
.mda/.bin文件),每种乐器/按键对应一段压缩采样数据(支持 PCM/ALAW/ADPCM/ADPCM4 多种压缩格式)。 - 乐谱解码:读取 MIDI 文件(SMF 事件流),解析 delta-time、note on/off、控制变更(CC)、弯音(pitch bend)等事件。
- 实时合成:按采样率逐帧渲染,把每个活跃音符(voice/player)的采样混合输出,同时处理音量、声像、延音踏板、表情等控制参数。
- 解码器框架集成:作为
D_TYPE_MIDI解码器注册进通用 decoder 框架,通过if_decoder_io的mp_input/mp_output与文件系统和 DAC 缓冲对接。
设计上最关键的权衡点是资源受限:合成器必须在 RAM 占用(解码 buffer)与同时发声音符数(MAX_DEC_PLAYER_CNT,8~32 可配)之间取得平衡。midi_api.c 中的注释明确说明"该值影响音符的叠加,值越大需要的解码 buffer 越大"。
架构
flowchart TD
subgraph sg_App["应用/框架层"]
DecoderFramework["通用解码器框架<br/>(decoder_api / dac)"]
UserHook["midi_init_info()<br/>(weak, 用户可覆写)"]
end
subgraph sg_API["接入层 decoder/list"]
MidiApi["midi_api.c<br/>midi_decode_api / midi_decode_init"]
MidiCtrlApi["midi_ctrl_api.c<br/>控制接口"]
end
subgraph sg_Engine["合成引擎 midi_open/midi_2byte"]
MidiDec["midi_dec.c<br/>SMF 事件解析 + 主循环"]
MidiPlay["midi_play.c<br/>voice 管理 / 混音"]
MidiEvent["midi_event.c<br/>事件处理"]
MidiDefs["MIDIDefs.h / midi_dec.h<br/>类型与常量"]
end
subgraph sg_Data["数据与硬件"]
Flash["SPI Flash 音色库<br/>/midi_cfg/00_MIDI.mda"]
Cbuf["cbuf_midi 环形缓冲<br/>(.midi_buf 段)"]
Dac["DAC 通道"]
end
DecoderFramework -->|"D_TYPE_MIDI"| MidiApi
MidiApi --> MidiCtrlApi
MidiApi -->|"get_midi_ops()"| MidiDec
MidiApi --> UserHook
MidiDec --> MidiPlay
MidiDec --> MidiEvent
MidiPlay --> MidiDefs
MidiDec --> Flash
MidiApi -->|"mp_output"| Cbuf
Cbuf --> Dac
架构要点说明:
- 接入层(
midi_api.c)是解码器框架与合成引擎之间的桥梁:它把框架传入的文件句柄、缓冲区组织成dec_obj,并通过get_midi_ops()获得引擎的decoder_ops_t操作表(open、format_check、dec_confing、need_dcbuf_size等)。 - 合成引擎(
midi_2byte目录)是无框架依赖的纯算法模块,输入 MIDI 事件流、输出 PCM。midi_play.c负责"发声器(player)"分配与混音,midi_dec.c负责乐谱事件解析与主解码循环,midi_event.c处理具体事件语义(note、CC、pitch bend、tempo)。 - 数据通路:合成结果写入
.midi_buf段中的cbuf_midi环形缓冲,解码器对象通过regist_dac_channel()把该缓冲注册给 DAC,由 DAC 中断持续消费。
classDiagram
class MIDI_CONFIG_PARM {
+int player_t
+int sample_rate
+u16* spi_pos
}
class MIDI_INIT_STRUCT {
+MIDI_CONFIG_PARM init_info
+MIDI_PLAY_CTRL_MODE mode_info
+u32 switch_info
}
class dec_obj {
+u32 type
+void* p_file
+sound_obj sound
+decoder_ops_t* dec_ops
+u32 sr
}
class MIDI_DECODE_VAR {
+u8* midi_spi_pos
+int compressIN
+u16 instr_spi
+u16* instr_map
+MIDI_PLAYER* midi_players
+u8* play_key
+u32 srTicks
+channel_mixer_t channel_mixer[16]
}
MIDI_INIT_STRUCT --> MIDI_CONFIG_PARM : init_info
dec_obj --> MIDI_DECODE_VAR : work_buf
MIDI_DECODE_VAR --> MIDI_PLAYER : midi_players
说明:
dec_obj是框架的通用解码器句柄,其p_dbuf指向的 work buffer 在MIDI_CTRL_OPEN()中被强转为MIDI_DECODE_VAR使用(见下文MIDI_CTRL_OPEN源码)。
音色库加载与初始化
midi_decode_init():挂载文件系统并定位音色库
MIDI 播放的起点是加载音色库。midi_decode_init() 在系统启动阶段被调用(由上层初始化代码触发),其逻辑如下:
void midi_decode_init(void)
{
void *pvfs = 0;
void *pvfile = 0;
u32 err = 0;
err = vfs_mount(&pvfs, (void *)NULL, (void *)NULL);
if (err != 0) {
return;
}
err = vfs_openbypath(pvfs, &pvfile, "/midi_cfg/00_MIDI.mda");
if (err != 0) {
log_info("midi dec mda open fail, try old midi_cfg.bin!\n");
err = vfs_openbypath(pvfs, &pvfile, "/midi_cfg/midi_cfg.bin");
if (err != 0) {
vfs_fs_close(&pvfs);
return;
}
}
///获取midi音色库的cache地址
struct vfs_attr attr;
vfs_get_attrs(pvfile, &attr);
midi_tone_tab = boot_info.sfc.app_addr + attr.sclust;
vfs_file_close(&pvfile);
vfs_fs_close(&pvfs);
}
Source: midi_api.c
关键设计意图:
- 音色库作为文件存在,而非链接进固件。
midi_tone_tab由boot_info.sfc.app_addr + attr.sclust计算得到,即音色文件在 SPI Flash 中的绝对地址。这样音色库可以独立于固件更新,也避免了 RAM 放不下大音色表的问题——合成引擎直接就地读取 Flash 中的采样。 - 文件名双保险:新格式
00_MIDI.mda优先,失败则回退到旧的midi_cfg.bin,保证向后兼容。这两个文件由后构建脚本(post_build/*/dir_midi、midi_cfg)生成并放入文件系统镜像。 - 若挂载失败或文件不存在,函数静默返回,
midi_tone_tab保持为 0,后续midi_decode_api()会以E_MIDI_NO_CFG拒绝播放。
midi_decode_api():解码器打开与参数下发
每次播放 MIDI 文件时,解码器框架调用 midi_decode_api()。这是 MIDI 播放的第二个关键入口:
u32 midi_decode_api(void *p_file, void **ppdec, void *p_dp_buf)
{
dec_obj **p_dec = (dec_obj **)ppdec;
u32 buff_len, sr;
decoder_ops_t *ops;
log_info("midi_decode_api\n");
if (!midi_tone_tab) {
return E_MIDI_NO_CFG;
}
local_irq_disable();
memset(&dec_midi_hld, 0, sizeof(dec_obj));
local_irq_enable();
u32 cal_buf_len;
void *p_cal_buf;
cal_buf_len = sizeof(midi_decode_buff_nomark);
p_cal_buf = midi_decode_buff_nomark;
memset(p_cal_buf, 0, cal_buf_len);
dec_midi_hld.type = D_TYPE_MIDI;
ops = get_midi_ops();
buff_len = ops->need_dcbuf_size();
log_info("MIDI_DEC Need Buff Len:%d\n", buff_len);//buff大小会随MAX_DEC_PLAYER_CNT改变
if (buff_len > cal_buf_len) {
return E_MIDI_DBUF;
}
/******************************************/
cbuf_init(&cbuf_midi, &obuf_midi[0], sizeof(obuf_midi));
...
dec_midi_hld.p_file = p_file;
dec_midi_hld.sound.p_obuf = &cbuf_midi;
dec_midi_hld.p_dbuf = p_cal_buf;
dec_midi_hld.dec_ops = ops;
dec_midi_hld.event_tab = (u8 *)&midi_evt[0];
...
ops->open(p_cal_buf, &midi_dec_io0, NULL); //传入io接口
if (ops->format_check(p_cal_buf)) { //格式检查
return E_MIDIFORMAT;
}
sr = dac_sr_read(); //获取采样率
dec_midi_hld.sr = sr;
...
/**************相对MP3的调用流程多了这个,其他一致。这个一定要配置***************/
midi_t_parm.player_t = MAX_DEC_PLAYER_CNT; //设置需要合成的最多按键个数,8到32可配
midi_t_parm.sample_rate = midi_musicsr_to_cfgsr(sr); //采样率设为16k
midi_t_parm.spi_pos = (u16 *)midi_tone_tab; //spi_memory为音色文件数据起始地址
memset((u8 *)&init_info, 0x00, sizeof(init_info));
init_info.init_info = midi_t_parm;
midi_init_info(&init_info);
ops->dec_confing(p_cal_buf, CMD_INIT_CONFIG, &init_info);
/**输出dec handle*/
*p_dec = &dec_midi_hld;
regist_dac_channel(&dec_midi_hld.sound, kick_decoder); //注册到DAC;
return 0;
}
Source: midi_api.c
该函数的执行序列可以概括为:
- 校验音色库已加载(
midi_tone_tab != 0),否则返回E_MIDI_NO_CFG。 - 在关中断下清零全局解码器句柄
dec_midi_hld,避免多线程/中断竞争导致脏状态。 - 按需检查工作缓冲区:
ops->need_dcbuf_size()返回合成引擎所需的 RAM 大小(随MAX_DEC_PLAYER_CNT变化),若超过静态分配的midi_decode_buff_nomark(位于.midi_buf段)则返回E_MIDI_DBUF。这是 RAM 预算的第一道闸门。 - 初始化输出环形缓冲
cbuf_midi,把文件句柄、输出缓冲、操作表、事件表装配进dec_obj。 - 格式检查:
ops->format_check()校验文件是否为支持的 MIDI 格式,失败返回E_MIDIFORMAT。 - 读取 DAC 采样率并映射为引擎内部采样率索引(
midi_musicsr_to_cfgsr,支持 48k/44.1k/32k/24k/22.05k/16k/12k/11.025k/8k)。 - 下发配置:
MIDI_CONFIG_PARM携带三个关键参数——player_t(最大同时发声数,即复音数)、sample_rate、spi_pos(音色库 Flash 地址),通过CMD_INIT_CONFIG传给引擎。注意midi_init_info()是 weak 函数,用户可覆写它来注入额外的模式/开关配置(mode_info、switch_info)。 - 注册 DAC 通道:
regist_dac_channel(&dec_midi_hld.sound, kick_decoder),把合成输出接入 DAC 消费。
静态内存布局
解码器使用专用内存段 .midi_buf 放置全部关键缓冲:
| 符号 | 位置 | 用途 |
|---|---|---|
midi_t_parm / init_info | .midi_buf | 合成参数与初始化结构 |
cbuf_midi | .midi_buf | 输出环形缓冲控制块 |
obuf_midi[MIDI_DEC_OBUF_SIZE/2] | .midi_buf | 输出 PCM 数据缓冲 |
midi_decode_buff_nomark[MIDI_DEC_BUF_SIZE/4] | .midi_buf | 引擎工作区(player、key 映射、混合状态) |
Source: midi_api.c
这种"静态分配 + 专用段"的设计是嵌入式音频的常见做法:避免运行时堆分配带来的碎片化与不确定性,同时把大块缓冲统一管理在可预测的地址空间。
合成引擎核心机制
MIDI_CTRL_OPEN():发声器与通道状态初始化
引擎收到 CMD_INIT_CONFIG 后,最终落到 midi_play.c 的 MIDI_CTRL_OPEN(),把 work buffer 解析为 MIDI_DECODE_VAR 并建立全部运行状态:
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;
}
mid_dec_obj->midi_tempo_v = 1024;
for (int i = 0; i < CTRL_CHANNEL_NUM; i++) {
mid_dec_obj->decay_speed[i] = 32768;
}
mid_dec_obj->mute_threshold = 1L << 29;
...
for (chn = 0; chn < MAX_CHANNEL_NUM; chn++) {
mid_dec_obj->channel_mixer[chn].cc[MIDI_CTRL_VOL_CC] = 127;
mid_dec_obj->channel_mixer[chn].cc[MIDI_CTRL_EXPR_CC] = 127;
mid_dec_obj->channel_mixer[chn].cc[MIDI_CTRL_PAN_CC] = 64;
mid_dec_obj->pitchBend_v[chn] = 256;
}
mid_dec_obj->srTicks = (midi_dec_param->tempo / 4) * (smpl_rate_tab[mid_dec_obj->sample_rate] * (1 << (MIDI_RESAMPLE_SHIFT - 6)) / 3094);
mid_dec_obj->numTrk = 1;
...
int *ptr = mid_dec_obj->mempool;
mid_dec_obj->midi_players = (MIDI_PLAYER *)ptr;
memset(mid_dec_obj->midi_players, 0, needMidiPlayersBuf());
ptr = ptr + needMidiPlayersBuf() / sizeof(int);
mid_dec_obj->play_key = (u8 *)ptr;
for (int i = 0; i < MAX_CTR_PLAYER_CNT * MAX_CHANNEL_NUM; i++) {
mid_dec_obj->play_key[i] = 255;
}
return 0;
}
Source: midi_play.c
这段初始化代码揭示了几个重要设计:
- 音色库头部的魔数分支:音色库第一个 u16 为
0xABCD时表示新式压缩音色库(compressIN=1),乐器索引与映射表的位置不同;否则为旧式。引擎通过这一魔数兼容两代音色文件格式。 - 16 通道默认控制参数:每个 MIDI 通道的 CC 初始化——音量(
MIDI_CTRL_VOL_CC)与表情(MIDI_CTRL_EXPR_CC)为 127(最大),声像(MIDI_CTRL_PAN_CC)为 64(居中),弯音(pitchBend_v)为 256(无弯音)。这保证了通道在收到明确 CC 之前按中性值发声。 - 复音数上限钳制:应用配置的
player_t超过引擎硬上限MAX_CTR_PLAYER_CNT时被截断,防止越界访问midi_players/play_key数组。 - 内存池划分:
mempool内部先划出MIDI_PLAYER数组(每个 player 是一个独立发声器状态机),随后划出play_key(MAX_CTR_PLAYER_CNT × MAX_CHANNEL_NUM字节,记录每个通道当前占用哪些键位,255表示空闲)。player 与通道的二维对应关系正是"一个通道最多同时占用全部复音"的模型。 - 时基换算:
srTicks由 tempo 与采样率表换算得出,是 delta-time(MIDI 文件中的 tick)到采样数(synthesis 世界的时钟)的桥梁;midi_tempo_v初始 1024 用于 tempo 变速,now_srTicks通过MULSI定点宏实时更新。
主循环与事件分发
MIDI_CTRL_MAIN()(midi_play.c)是每帧/每块解码被反复调用的主函数:它从输入缓冲读取 delta-time 与事件字节,按 2 字节压缩格式解析 note on/off、CC、program change、pitch bend、tempo 等,把状态写入 channel_mixer,并驱动 MIDI_PLAYER 状态机(SEQ_AE_OFF/ON/REL 表示发声器的空闲、持续、释放三个阶段,见 midi_dec.h)。
midi_event.c 承载具体的事件语义处理,例如延音踏板(sustain)、表情(expression)、滑音(soft pedal)等 CC 的控制。midi_dec.h 中定义的 CC 枚举顺序(MIDI_CTRL_SOS_ON_CC 起始)即 channel_mixer[chn].cc[] 数组的下标布局。
采样率与重采样
合成引擎内部以 13bit 定点重采样(MIDI_RESAMPLE_SHIFT 13)工作,smpl_rate_tab 把 midi_musicsr_to_cfgsr() 的索引映射为实际采样率。音色采样通过 spi_key_start/spi_zone_start 定位(spi_key_start = midi_spi_pos + 258 + instr_spi*2,spi_zone_start = spi_key_start + 128*instr_spi),即音色库按"乐器 → 128 个键位 → zone(采样映射区域)"的三级结构组织。每个 zone(my_Zones 结构,含 sampleMap、vol_rel、loopStart、initAtten 等字段)描述一段采样如何被循环播放、音量与衰减如何调整,从而实现不同音高、力度下的真实乐器听感。
核心播放流程
sequenceDiagram
participant App as 应用层
participant Dec as 解码器框架
participant Mapi as midi_api.c
participant Eng as 合成引擎(midi_2byte)
participant VFS as VFS/Flash
participant DAC as DAC
Note over App,VFS: 初始化阶段
App->>Mapi: midi_decode_init()
Mapi->>VFS: 挂载文件系统
Mapi->>VFS: 打开 /midi_cfg/00_MIDI.mda
VFS-->>Mapi: 文件属性 sclust
Mapi->>Mapi: midi_tone_tab = app_addr + sclust
Note over App,DAC: 播放阶段
App->>Dec: 打开 MIDI 文件 (D_TYPE_MIDI)
Dec->>Mapi: midi_decode_api(p_file)
Mapi->>Mapi: 检查 midi_tone_tab / 清零 dec_midi_hld
Mapi->>Eng: get_midi_ops() → need_dcbuf_size()
Eng-->>Mapi: buffer 大小
Mapi->>Eng: ops->open(work_buf, midi_dec_io0)
Mapi->>Eng: ops->format_check() → FORMAT_OK
Mapi->>DAC: dac_sr_read() → 采样率
Mapi->>Eng: dec_confing(CMD_INIT_CONFIG, &init_info)
Eng->>Eng: MIDI_CTRL_OPEN(): 解析音色库头/初始化通道与 player
Mapi->>DAC: regist_dac_channel(sound, kick_decoder)
loop 每块解码
Dec->>Mapi: mp_input() 读取文件数据
Mapi->>Eng: MIDI_CTRL_MAIN(): 解析 delta-time 与事件
Eng->>Eng: 更新 channel_mixer / 驱动 MIDI_PLAYER
Eng->>VFS: 读取音色采样(SPI Flash)
Eng->>Mapi: mp_output(): 混合渲染 PCM
Mapi->>DAC: 写 cbuf_midi → DAC 消费
end
逐步说明:
- 初始化:
midi_decode_init()一次性完成音色库定位;失败不阻塞系统,后续播放返回E_MIDI_NO_CFG。 - 打开:框架以
D_TYPE_MIDI分发到midi_decode_api(),先做 RAM 预算校验(need_dcbuf_size与静态 buffer 比较),再open+format_check确认文件合法。 - 配置:读 DAC 采样率 → 组装
MIDI_CONFIG_PARM→ 调用 weak 钩子midi_init_info()允许应用覆盖模式/开关 →CMD_INIT_CONFIG下发,引擎完成通道 CC 默认值、tempo 换算、player 池分配。 - 输出挂接:解码器句柄通过
regist_dac_channel进入 DAC 的 kick 驱动链,此后 DAC 每消费一帧,就触发mp_output从环形缓冲取新 PCM,形成"推拉结合"的播放节拍。 - 解码循环:
mp_input把文件流喂给引擎,MIDI_CTRL_MAIN把 delta-time 换算为采样数,逐事件更新 16 通道控制状态并驱动 player 状态机,最终把各活跃 player 的采样混合(叠加)写入输出缓冲。多音符叠加即在此完成,叠加数由MAX_DEC_PLAYER_CNT决定。
配置选项
| 配置项 | 类型 | 默认/典型值 | 说明 |
|---|---|---|---|
MAX_DEC_PLAYER_CNT | int | 在 app_config.c 中定义 | 解码时最大同时发声音符数(复音数),8~32 可配;越大音符叠加越多、所需解码 buffer 越大 |
MAX_CTR_PLAYER_CNT | int | 引擎常量 | 引擎硬上限,player_t 超过时被钳制,防止越界 |
MIDI_CONFIG_PARM.player_t | int | MAX_DEC_PLAYER_CNT | 下发到引擎的复音数 |
MIDI_CONFIG_PARM.sample_rate | int | 由 midi_musicsr_to_cfgsr() 映射 | 引擎采样率索引(48k/44.1k/32k/24k/22.05k/16k/12k/11.025k/8k) |
MIDI_CONFIG_PARM.spi_pos | u16* | midi_tone_tab | 音色库在 SPI Flash 中的起始地址 |
midi_init_info() | weak 函数 | 空实现 | 用户覆写点,可注入 mode_info(播放模式)与 switch_info(开关) |
| 音色库文件名 | string | /midi_cfg/00_MIDI.mda | 回退文件:/midi_cfg/midi_cfg.bin |
MIDI_BIT_N(N) / MIDI_OBUF_BLOCK | 宏 | 32 | 输出块粒度与位掩码,影响缓冲划分 |
MIDI_RESAMPLE_SHIFT | 宏 | 13 | 内部重采样定点精度 |
VOL_Norm_Bit | 宏 | 12 | 音量归一化定点位数 |
MAX_GO_BACK | 宏 | 8 | 回退搜索深度(MAX_GO_VAL = (1<<8)-1) |
INBUF_SIZE | 宏 | 700 | 引擎输入缓冲字节数 |
MAX_CHANNEL_NUM / MAX_TRACK_NUM | 宏 | 16 | 最大 MIDI 通道数(即轨数) |
API 参考
void midi_decode_init(void)
挂载文件系统并定位音色库(/midi_cfg/00_MIDI.mda,回退 midi_cfg.bin),把 Flash 地址存入全局 midi_tone_tab。系统启动时调用一次。
u32 midi_decode_api(void *p_file, void **ppdec, void *p_dp_buf)
解码器框架入口,装配 dec_obj 并下发合成配置。
参数:
p_file:VFS 文件句柄ppdec:输出解码器句柄dec_obj *p_dp_buf:预留数据缓冲指针(当前未使用)
返回:
0:成功E_MIDI_NO_CFG:音色库未加载(midi_tone_tab == 0)E_MIDI_DBUF:need_dcbuf_size()超过静态 bufferE_MIDIFORMAT:format_check()失败
u32 midi_buff_api(dec_buf *p_dec_buf)
返回解码器内存区间(midi_buf_start~midi_buf_end,.midi_buf 段),供框架做内存统计/保护。
MIDI_PLAY_CTRL_MODE *get_midi_mode(void) / u32 *get_midi_switch_info(void)
返回 init_info.mode_info / init_info.switch_info 的指针,供运行时查询/修改播放模式与开关(例如在 midi_init_info() 覆写或播放中调整)。
__attribute__((weak)) void midi_init_info(MIDI_INIT_STRUCT *init_info)
用户扩展点。默认空实现;覆写后可在 CMD_INIT_CONFIG 下发前设置 mode_info 与 switch_info。
u32 MIDI_CTRL_OPEN(void *work_buf, void *dec_parm, void *parm)(引擎内部)
引擎初始化函数:解析音色库头(0xABCD 压缩魔数分支)、钳制复音数、初始化 16 通道 CC/弯音默认值、换算 srTicks、划分 player 池与 play_key 表。
返回: 0 成功。
static u32 MIDI_CTRL_MAIN(void *ptr)(引擎内部)
每块解码主循环:读取 delta-time 与事件,更新 channel_mixer,驱动 MIDI_PLAYER 状态机(SEQ_AE_OFF→SEQ_AE_ON→SEQ_AE_REL),渲染 PCM 至输出缓冲。通过 ops 表中的回调与 midi_api.c 对接。
失败模式与边界情况
音色库缺失
midi_decode_init()中 VFS 挂载失败或00_MIDI.mda/midi_cfg.bin均打不开时静默返回,midi_tone_tab == 0。- 后续任何播放尝试在
midi_decode_api()第一步即返回E_MIDI_NO_CFG。设计上"宁可不出声也不崩机"——播放失败以错误码上报,不进入解码循环。
工作缓冲区不足
midi_decode_buff_nomark 是静态数组,若 MAX_DEC_PLAYER_CNT 调大导致 need_dcbuf_size() 超过其容量,返回 E_MIDI_DBUF。这要求应用在增大复音数时同步核对 MIDI_DEC_BUF_SIZE 与 .midi_buf 段的空间。源码注释明确提示"buff 大小会随 MAX_DEC_PLAYER_CNT 改变"。
格式错误
ops->format_check() 返回非 0 时播放中止,返回 E_MIDIFORMAT。midi_dec.h 定义了三级格式检查结果:FORMAT_OK、FORMAT_OK_BUT_NO_SUPPORT(可识别但不支持的扩展)、FORMAT_ERR。后者才真正拒绝播放,前两者允许继续但行为可能降级。
错误码体系
midi_dec.h 中的 MAD_ERROR_* 枚举是引擎向框架上报的运行时错误:MAD_ERROR_MIDI=0x05、MAD_ERROR_FILE_END=0x40、MAD_ERROR_FILESYSTEM_ERR=0x41、MAD_ERROR_DISK_ERR=0x42、MAD_ERROR_SYNC_LIMIT=0x43、MAD_ERROR_FF_FR_FILE_END/FF_FR_END/FF_FR_FILE_START=0x44~0x46(快进/快退越界)、MAD_ERROR_LIMIT=0x47、MAD_ERROR_NODATA=0x48、MAD_ERROR_PAUSE=0x50。这些错误码与通用解码框架的 dec_err 上报机制衔接,用于通知应用层播放异常。
并发与一致性
- 句柄清零的原子性:
midi_decode_api()在local_irq_disable()/enable()之间memset(&dec_midi_hld),防止与 DAC 中断/其他核并发访问解码器句柄造成撕裂。 - 生产-消费模型:引擎(生产者)写
cbuf_midi,DAC(消费者)通过kick_decoder取数。环形缓冲天然解耦两侧节奏:引擎按文件解析速度产数据,DAC 按采样率定时消费,mp_output在缓冲空间不足时阻塞/等待,形成自然背压。 - player 状态机:
SEQ_AE_OFF/ON/REL(空闲/持续/释放)保证 note off 后仍能完成包络释放(decay),decay_speed(每通道 32768 初始)与mute_threshold(1<<29)控制静音判定,避免发声器泄漏。
性能与运行注意
- 复音数 = 内存 × 性能的杠杆:每个
MIDI_PLAYER状态机与play_key表占用固定内存,同时每帧渲染复杂度随活跃 player 数线性增长。典型场景建议 8~16 复音,复杂乐曲才上调至 32,且必须同步核对.midi_buf段容量。 - Flash 直读:音色采样从 SPI Flash 原位读取(
midi_tone_tab = boot_info.sfc.app_addr + attr.sclust),不拷贝到 RAM。采样密集时会频繁访问 Flash,与同时进行的文件读取(乐谱流)共享 SPI 总线,存在总线竞争;引擎通过输入缓冲(INBUF_SIZE=700)与输出块(MIDI_OBUF_BLOCK=32)的流水线设计缓解抖动。 - 重采样定点化:
MIDI_RESAMPLE_SHIFT=13与smpl_rate_tab让变速播放(tempo 变化)在整数运算下完成,避免浮点开销;MULSI宏做 64 位中间量定点乘,保证 16 位输出精度。 - 音量归一化:
VOL_Norm_Bit=12与channel_mixer的 CC 音量/表情相乘叠加,输出前统一归一化,防止多音符叠加削波。
扩展点
midi_init_info()(weak):应用可覆写,注入MIDI_PLAY_CTRL_MODE mode_info与switch_info,例如指定播放模式(单曲/循环)、静音开关、变调等。MAX_DEC_PLAYER_CNT(app_config.c):编译期调整复音数,是"音质 ↔ 内存"的主要旋钮。- 音色库格式:引擎同时兼容
0xABCD压缩头与旧式非压缩头两代.mda/.bin,后构建脚本(post_build/*/dir_midi、midi_cfg)负责把音色打包进文件系统镜像,更换音色只需替换文件而不改固件。 - 采样率适配:
midi_musicsr_to_cfgsr()的白名单决定了引擎可工作的采样率集合;若需新增采样率(如 96k),需同步扩展该表与smpl_rate_tab。 - 控制事件集:
MIDI_CTRL_*_CC枚举(sustain/expression/volume/soft/sustain/mod/pan)是通道混音器支持的控制面;新增 CC 类型需要扩展枚举、channel_mixer.cc[]布局与midi_event.c的处理分支。
相关链接
- midi_api.c — 解码器接入层
- midi_ctrl_api.c — MIDI 控制接口
- midi_dec.h — 引擎头文件与常量定义
- midi_dec.c — SMF 解析与主循环
- midi_play.c — 发声器与混音
- midi_event.c — 事件语义处理
- MIDIDefs.h — MIDI 类型定义
- 后构建配置示例(voice_toy)
- 杰理 AD1x-45678 MIDI 应用说明文档(PDF)
相关页面指引:通用解码器框架与 DAC 输出通路见对应目录文档;MIDI 工具链(音色制作/转换)参考
doc/JLmidi工具使用说明.pdf。