MIDI 合成与播放
本文档介绍 fw-AD16N_GP-MCU_SDK 中 MIDI 乐谱解码与音色合成播放子系统的完整实现,涵盖解码器框架接入层(midi_api.c / midi_ctrl_api.c)、核心解码与合成引擎(midi_2byte / midi_4byte)、音色库加载、播放控制命令以及内存布局与配置方式。
Purpose and Scope
本页面覆盖 MIDI 子系统的端到端机制:
- MIDI 文件播放解码器如何接入 SDK 的统一解码器框架(decoder list);
- 乐谱解码(SMF 解析)与多音色合成引擎(2 字节 / 4 字节两个版本)的实现要点;
- 音色库(tone bank)的挂载、寻址与格式识别;
- 播放控制命令集(速度、Mark、主旋律、变调、限幅、OKON 模式等)以及各类功能开关;
- 内存分段布局与中断保护策略。
与本页相关的兄弟主题请参见各自页面:
- 解码器通用框架与解码对象生命周期 → 见「音频解码框架」相关页面;
- DAC 输出通道与混音注册 → 见「DAC 音频输出」相关页面;
- 应用层 MIDI 模式(
src/mbox_flash/midi_dec/、midi_keyboard/)属于具体产品逻辑,本页只做交叉引用,不展开。
概述
MIDI(Musical Instrument Digital Interface)播放与 MP3/WAV 等音频解码的本质区别在于:解码器不直接输出波形,而是输出音符事件(note on/off、音色、音量、弯音等),由合成引擎按音色库(采样表)实时合成 PCM 波形。因此 MIDI 播放链路包含「乐谱解析 → 事件分发 → 多音源(player)合成 → 混音 → DAC 输出」五个阶段。
SDK 中该能力被实现为解码器框架中的一个标准解码器(D_TYPE_MIDI),并额外提供一个实时键盘控制模式(D_TYPE_MIDI_CTRL),用于电子琴 / 按键演奏类产品:midi_ctrl_api.c 不读取文件,而是直接响应实时按键事件进行合成。两个模式共用 midi_open/ 目录下的合成引擎,只是入口与参数装配不同。
MIDI 解码所需的内存较大且对实时性敏感,因此所有关键缓冲都通过 #pragma 段声明放入专用内存段(.midi_dec.data、.midi_buf 等),并采用关中断方式保护解码对象初始化,避免与 DAC 中断回调竞争。
架构
flowchart TD
subgraph sg_App["应用层 (App)"]
MboxDec["mbox_flash/midi_dec<br/>文件播放模式"]
MboxKey["mbox_flash/midi_keyboard<br/>键盘演奏模式"]
end
subgraph sg_DecFrame["解码器框架 (decoder list)"]
Api["midi_api.c<br/>midi_decode_api / midi_decode_cfg_init"]
CtrlApi["midi_ctrl_api.c<br/>midi_ctrl_decode_api / midi_ctrl_cfg_init"]
end
subgraph sg_Engine["合成引擎 (midi_open)"]
Dec2["midi_2byte<br/>midi_dec.c / midi_event.c / midi_play.c"]
Dec4["midi_4byte<br/>midi_dec.c / midi_event.c / midi_play.c"]
Defs["MIDIDefs.h<br/>命令/开关定义"]
end
subgraph sg_Tone["音色库 (Tone Bank)"]
Mda["post_build/uc03/midi_cfg<br/>00_MIDI.mda / midi_cfg.bin"]
Mdb["post_build/uc03/midi_cfg_ster1<br/>00_MIDI.mdb (4byte)"]
end
subgraph sg_Out["输出"]
Cbuf["circular_buf 输出缓冲"]
Dac["audio_dac / DAC 通道"]
end
MboxDec --> Api
MboxKey --> CtrlApi
Api -->|"ops->open / dec_confing"| Dec2
Api -->|"MIDI_VER_SELECT"| Dec4
CtrlApi --> Dec2
CtrlApi --> Dec4
Api -->|"vfs 挂载 + SPI 地址映射"| Mda
CtrlApi -->|"vfs 挂载 + SPI 地址映射"| Mda
Mdb -.->|"4byte 版本"| Dec4
Dec2 -->|"PCM 输出"| Cbuf
Dec4 -->|"PCM 输出"| Cbuf
Cbuf --> Dac
架构说明:
- 入口分流:
midi_api.c处理"文件播放"(strm_source为文件流),midi_ctrl_api.c处理"实时键盘"(要求strm_source == NULL,否则返回E_MIDI_FILEHDL)。两者都遵循解码器框架的decoder_ops_t协议(open/format_check/dec_confing/need_dcbuf_size)。 - 引擎双版本:
midi_2byte与midi_4byte是同一合成算法的两种指令/数据宽度实现,通过MIDI_VER_4BYTE && (MIDI_VER_SELECT == MIDI_VER_4BYTE)编译开关选择;4byte 版本使用.mdb音色库并支持多输出声道(MIDI_CTRL_OUT_CHANNEL)。 - 音色库直读:音色库文件通过 VFS 定位,但其内容不做读取拷贝,而是计算其在 SPI Flash 中的直接映射地址(
boot_info.sfc.app_addr + attr.sclust)交给合成引擎按需读取,实现低延迟随机访问。 - 输出:合成结果写入
cbuffer_t环形缓冲,与普通解码器一致地注册到 DAC 通道,因此混音、采样率转换等复用系统公共链路。
解码器接入与初始化流程
文件播放模式入口:midi_decode_api
midi_decode_api() 是文件播放模式的统一入口,由解码器框架在播放开始时调用,其执行序列完整反映了 MIDI 解码器的初始化协议:
u32 midi_decode_api(void *strm, void **ppdec, void *p_dp_buf)
{
dec_data_stream *p_strm = strm;
dec_obj **p_dec = (dec_obj **)ppdec;
u32 buff_len, sr;
decoder_ops_t *ops;
log_info("midi_decode_api file:0x%x\n", (u32)p_strm->strm_source);
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;
if (MIDI_MAX_MARK_CNT) {
cal_buf_len = sizeof(midi_decode_buff_full);
p_cal_buf = midi_decode_buff_full;
} else {
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();
if (buff_len > cal_buf_len) {
log_info("MIDI_DEC Need Buff Len:%d > %d\n", buff_len, cal_buf_len);
return E_MIDI_DBUF;
}
/******************************************/
cbuf_init(&cbuf_midi, &obuf_midi[0], sizeof(obuf_midi));
memcpy(&midi_dec_io0, p_strm->io, sizeof(struct if_decoder_io));
midi_dec_io0.priv = &dec_midi_hld;
sound_stream_obj *psound_strm = p_strm->strm_source;
dec_midi_hld.p_file = (void *)psound_strm;
dec_midi_hld.sound.p_obuf = &cbuf_midi;
dec_midi_hld.sound.info |= MIDI_DEC_TRACK;
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);
if (!(B_DEC_NO_CHECK & p_strm->strm_ctl)) {
if (ops->format_check(p_cal_buf)) {
return E_MIDIFORMAT;
}
}
sr = dac_sr_read();
memset((u8 *)&init_info, 0x00, sizeof(init_info));
midi_init_info(&init_info, midi_musicsr_to_cfgsr(sr), midi_tone_tab, MAX_DEC_PLAYER_CNT);
ops->dec_confing(p_cal_buf, CMD_INIT_CONFIG, &init_info);
*p_dec = &dec_midi_hld;
return 0;
}
Source: midi_api.c
设计要点(WHY):
- 前置音色库检查:
midi_tone_tab为空立即返回E_MIDI_NO_CFG,因为合成引擎没有音色库根本无法工作;这个错误在midi_decode_cfg_init()阶段就应该被规避。 - 关中断保护:
dec_midi_hld是全局静态解码对象,DAC 中断可能随时调用输出回调读取它;清零操作必须在关中断区间完成,避免撕裂读写。 - 缓冲区分级:支持 Mark(标记/段落)功能的构建使用
midi_decode_buff_full,否则使用更小的midi_decode_buff_nomark,因为 Mark 循环需要额外的位置回溯缓冲。need_dcbuf_size()返回所需大小,超出即返回E_MIDI_DBUF,避免静默越界。 - 采样率跟随 DAC:
dac_sr_read()读取当前 DAC 采样率,再经midi_musicsr_to_cfgsr()映射为合成引擎的采样率索引(0~8,对应 48k/44.1k/32k/24k/22.05k/16k/12k/11.025k/8k)。合成器必须在与 DAC 相同的采样率下工作,否则会产生音高偏差。 midi_init_info是 weak 函数:默认空实现,应用层可覆盖它来注入产品自定义的初始化参数(如主轨道、默认音色、变调等),这是主要的扩展点。
音色库配置:midi_decode_cfg_init
音色库以文件形式存放于文件系统(实际是 SPI Flash 上的镜像分区),启动时挂载 VFS 并解析文件地址:
int midi_decode_cfg_init(void)
{
void *pvfs = 0;
void *pvfile = 0;
u32 err = 0;
err = vfs_mount(&pvfs, (void *)NULL, (void *)NULL);
if (err != 0) {
return E_MOUNT;
}
#if defined(MIDI_VER_4BYTE) && (MIDI_VER_SELECT == MIDI_VER_4BYTE)
/* 4byte的音色库文件后缀是mdb */
err = vfs_openbypath(pvfs, &pvfile, "/midi_cfg_ster1/00_MIDI.mdb");
#else
err = vfs_openbypath(pvfs, &pvfile, "/midi_cfg/00_MIDI.mda");
#endif
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 E_OPENBYPATH;
}
}
///获取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);
return 0;
}
Source: midi_api.c
要点:优先加载 /midi_cfg/00_MIDI.mda(4byte 版本为 /midi_cfg_ster1/00_MIDI.mdb),失败回退到旧版 /midi_cfg/midi_cfg.bin;最终得到的是 SPI Flash 线性地址(boot_info.sfc.app_addr + attr.sclust),此后合成引擎直接按地址读取采样数据,全程零拷贝。
键盘控制模式入口:midi_ctrl_decode_api
键盘模式与文件模式最大的差异是不接文件流,改接实时参数结构 MIDI_CTRL_PARM(输出函数指针、tempo、轨道数、priv):
u32 midi_ctrl_decode_api(void *strm, void **ppdec, void *p_dp_buf)
{
dec_data_stream *p_strm = strm;
if (p_strm->strm_source != NULL) {
return E_MIDI_FILEHDL;
}
...
midi_ctrl_t_parm.player_t = MAX_CTR_PLAYER_CNT;
midi_ctrl_t_parm.sample_rate = midi_musicsr_to_cfgsr(sr);
midi_ctrl_t_parm.spi_pos = (MIDI_CTRL_POS_TYPE)midi_ctrl_tone_tab;
#if defined(MIDI_VER_4BYTE) && (MIDI_VER_SELECT == MIDI_VER_4BYTE)
midi_ctrl_t_parm.bitwidth = 16;
midi_ctrl_t_parm.OutdataBit = 0;
midi_ctrl_t_parm.out_channel = MIDI_CTRL_OUT_CHANNEL;
#endif
midi_ctrl_parmt.output = (int (*)(void *, void *, int))p_strm->io->output;
midi_ctrl_parmt.tempo = 1000;
midi_ctrl_parmt.track_num = 1;
midi_ctrl_parmt.priv = &dec_midi_ctrl_hld;
memset(&midi_ctrl_dec_inf, 0, sizeof(midi_ctrl_dec_inf));
midi_ctrl_dec_inf.sr = sr;
midi_ctrl_dec_inf.nch = MIDI_CTRL_CHANNEL;
ops->open(MIDI_CTRL_CAL_BUF, (const struct if_decoder_io *)&midi_ctrl_parmt, (u8 *)&midi_ctrl_t_parm);
*p_dec = &dec_midi_ctrl_hld;
midi_keyboard_function_switch = 0;
return 0;
}
Source: midi_ctrl_api.c
要点:strm_source != NULL 直接报 E_MIDI_FILEHDL 防止误用;4byte 版本在此额外配置位宽(16bit)与输出声道数;midi_keyboard_function_switch 作为应用层与引擎共享的功能开关标志,初始化为 0。
合成引擎与命令分发
音色与通道初始化:midi_tone_init
合成引擎在 CMD_INIT_CONFIG 时初始化音色库指针与 16 通道(MAX_CHANNEL_NUM)状态。音色库头部有压缩标志与乐器映射表:
static u32 midi_tone_init(MIDI_DECODE_VAR *mid_dec_obj, MIDI_CONFIG_PARM *itt_info)
{
int chn;
u16 *cmporkind;
cmporkind = itt_info->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 = &cmporkind[130];
} else {
mid_dec_obj->midi_spi_pos = (u8 *)itt_info->spi_pos;
mid_dec_obj->compressIN = 0;
mid_dec_obj->instr_spi = cmporkind[0];
mid_dec_obj->instr_map = &cmporkind[129];
}
mid_dec_obj->sample_rate = itt_info->sample_rate;
mid_dec_obj->MAX_PLAYER_CNTt = itt_info->player_t;
if (mid_dec_obj->MAX_PLAYER_CNTt > MAX_DEC_PLAYER_CNT) {
mid_dec_obj->MAX_PLAYER_CNTt = MAX_DEC_PLAYER_CNT;
}
mid_dec_obj->spi_key_start = (unsigned char *)(&mid_dec_obj->midi_spi_pos[258 + mid_dec_obj->instr_spi * 2]);
mid_dec_obj->spi_zone_start = mid_dec_obj->spi_key_start + 128 * mid_dec_obj->instr_spi;
...
for (chn = 0; chn < MAX_CHANNEL_NUM; chn++) {
mid_dec_obj->channel_mixer[chn].cc[MIDI_CTRL_VOL_CC] = 100;
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->dec_info.sr = smpl_rate_tab[mid_dec_obj->sample_rate];
mid_dec_obj->dec_info.nch = 1;
mid_dec_obj->dec_info.br = 1;
return 0;
}
Source: midi_dec.c
要点:0xABCD 魔数区分压缩音色库与普通格式;instr_map 是音色号到采样区的映射表;所有通道默认 CC 值(音量 100、表情 127、声像 64)与 pitchBend 初始 256 保证"未配置即正确"的合成行为;MAX_PLAYER_CNTt(最大同时发音数)被裁剪到编译期上限 MAX_DEC_PLAYER_CNT,防止越界。
播放控制命令集:midi_dec_confing
所有运行时控制都通过 midi_dec_confing() 的命令分发实现(应用层经 midi_dec_confing_api() 转发),命令覆盖了产品级 MIDI 播放的全部需求:
u32 midi_dec_confing(void *work_buf, u32 cmd, void *parm)
{
MIDI_DECODE_VAR *mid_dec_obj = (MIDI_DECODE_VAR *)work_buf;
if (cmd == CMD_MIDI_SEEK_BACK_N) {
if (mid_dec_obj->save_ptr_enable) {
MIDI_SEEK_BACK_STRUCT *t_obj = (MIDI_SEEK_BACK_STRUCT *)parm;
...
int seek_back_n = t_obj->seek_back_n;
if (seek_back_n > MAX_GO_BACK) {
seek_back_n = MAX_GO_BACK;
}
int rd_cnt = mid_dec_obj->bk_wr_cnt - seek_back_n;
if (rd_cnt < 0) {
rd_cnt = rd_cnt + MAX_GO_BACK;
}
...
tmp_obj = &mid_dec_obj->bk_array_ptr[rd_cnt];
memcpy(&mid_dec_obj->fpos_cnt, tmp_obj, sizeof(MIDI_SAVE_POS_STRUCT) - 8);
mid_dec_obj->fpos_cnt -= tmp_obj->remain;
mid_dec_obj->smf_data.rd_pos = 0;
mid_dec_obj->smf_data.remain = 0;
...
}
} else if (cmd == CMD_MIDI_SET_CHN_PROG) {
int zone_key_v, tbank;
MIDI_PROG_CTRL_STRUCT *t_obj = (MIDI_PROG_CTRL_STRUCT *)parm;
mid_dec_obj->ins_trk = t_obj->prog;
mid_dec_obj->replace_mode = t_obj->replace_mode;
mid_dec_obj->set_chvol_in = t_obj->ex_vol;
tbank = mid_dec_obj->ins_trk;
zone_key_v = mid_dec_obj->midi_spi_pos[2 + tbank];
mid_dec_obj->instr_start_index_set = mid_dec_obj->instr_map[zone_key_v];
mid_dec_obj->instr_key_map_set = (unsigned char *)(&mid_dec_obj->spi_key_start[zone_key_v * 128]);
} else if (cmd == CMD_MIDI_CTRL_TEMPO) {
midi_play_set_tempo(work_buf, (MIDI_PLAY_CTRL_TEMPO *)parm);
} else if (cmd == CMD_MIDI_GOON) {
midi_play_goon(work_buf);
} else if (cmd == CMD_MIDI_CTRL_MODE) {
...
midi_play_ctrl_on(work_buf, tparm);
} else if (cmd == CMD_MIDI_SET_SWITCH) {
u32 v_switch = *(u32 *)parm;
midi_switch_control(work_buf, (u32)v_switch);
} else if (cmd == CMD_MIDI_SET_EX_VOL) {
EX_CH_VOL_PARM *tparm = (EX_CH_VOL_PARM *)parm;
memcpy((u8 *)(&mid_dec_obj->ex_vol), tparm, sizeof(EX_CH_VOL_PARM));
} else if (cmd == CMD_MIDI_MELODY_TRIGGER) {
EX_MELODY_STRUCT *tparm = (EX_MELODY_STRUCT *)parm;
memcpy((u8 *)(&mid_dec_obj->melody_trig), tparm, sizeof(EX_MELODY_STRUCT));
} else if (cmd == CMD_INIT_CONFIG) {
...
midi_tone_init(mid_dec_obj, &itt_info->init_info);
midi_play_ctrl_on(work_buf, &itt_info->mode_info);
midi_play_set_tempo(work_buf, (MIDI_PLAY_CTRL_TEMPO *)&itt_info->tempo_info);
midi_switch_control(work_buf, itt_info->switch_info);
...
} else if (cmd == CMD_MIDI_OKON_MODE) {
MIDI_OKON_MODE *okon = (MIDI_OKON_MODE *)parm;
mid_dec_obj->okon_mode = okon->OKON_Mode;
mid_dec_obj->key_mode = okon->Melody_Key_Mode;
...
} else if (cmd == CMD_MIDI_SET_SEMITONE) {
MIDI_SEMITONE_CTRL_STRUCT *semitone_ctrl = (MIDI_SEMITONE_CTRL_STRUCT *)parm;
memcpy(&mid_dec_obj->semitone_ctrl, semitone_ctrl, sizeof(MIDI_SEMITONE_CTRL_STRUCT));
} else if (cmd == CMD_MIDI_LIMITER_TRIGGER) {
MIDI_Limiter *limiter_trig = (MIDI_Limiter *)parm;
memcpy(&mid_dec_obj->limiter_trig, limiter_trig, sizeof(MIDI_Limiter));
} else if (cmd == CMD_MIDI_SET_MARK) {
MIDI_MARK_PARAM *mark_info = (MIDI_MARK_PARAM *)parm;
...
if (mark_start <= 0) {
mark_enable = 0;
}
if (mark_start >= mark_end) {
mark_enable = 0;
}
}
...
}
Source: midi_dec.c
设计意图:所有控制都以"结构体参数 + 命令字"的形式下发,而不是开放内部变量,从而把引擎状态机封装在黑盒内。例如 CMD_MIDI_SEEK_BACK_N 实现了"回退 N 个音符位置"的跟唱/纠错功能——引擎周期性调用 midi_save_bk_fun() 把解析位置快照(MIDI_SAVE_POS_STRUCT)写入环形回溯数组 bk_array_ptr,回退时直接恢复快照并把剩余字节流清零重解析;CMD_MIDI_SET_MARK 对 mark_start >= mark_end 等非法区间自动禁用,防御性处理输入。
核心播放流程
sequenceDiagram
participant App as 应用 (midi_dec 模式)
participant Api as midi_api.c
participant Ops as 合成引擎 (midi_2byte/4byte)
participant Tone as SPI 音色库
participant Cbuf as 环形缓冲
participant DAC as DAC 通道
App->>Api: midi_decode_cfg_init()
Api->>Api: vfs 挂载 → 打开 00_MIDI.mda/.mdb
Api->>Tone: 计算 SPI 直接地址 (app_addr + sclust)
Api-->>App: 0 (midi_tone_tab 就绪)
App->>Api: midi_decode_api(strm, &dec)
Api->>Api: 关中断清零 dec_midi_hld + 选择 Mark/无Mark 缓冲
Api->>Ops: ops->open(cal_buf, io, NULL)
Ops->>Ops: 初始化 SMF 解析器
Api->>Ops: format_check(cal_buf)
Ops-->>Api: 格式合法
Api->>Api: dac_sr_read() → midi_musicsr_to_cfgsr()
Api->>Ops: dec_confing(CMD_INIT_CONFIG, &init_info)
Ops->>Tone: midi_tone_init 解析音色头/乐器映射
Ops->>Ops: midi_play_set_tempo / midi_switch_control
Api-->>App: &dec_midi_hld
loop 播放循环
App->>Ops: kick_decoder / 读数据
Ops->>Ops: 解析 SMF 事件 (note on/off, tempo, program)
Ops->>Tone: 按音色号读取采样区
Ops->>Ops: 多 player 合成 + 通道混音 (CC/pan/pitchBend)
Ops->>Cbuf: 写入 PCM
Cbuf->>DAC: 中断输出
App->>Ops: dec_confing(CMD_MIDI_CTRL_TEMPO / CMD_MIDI_SET_MARK ...)
Ops-->>App: 状态更新
end
播放循环与合成过程
- 打开与校验:
midi_decode_api依次完成缓冲选择、ops->open、format_check(若B_DEC_NO_CHECK未置位则跳过校验)。 - 初始化配置:
CMD_INIT_CONFIG一次性下发采样率索引、SPI 音色地址、最大并发音数、播放模式、速度、默认音色、功能开关(见midi_dec_confing中CMD_INIT_CONFIG分支对itt_info各子结构的批量 memcpy 与midi_play_ctrl_on/midi_switch_control的调用)。 - 实时合成:DAC 通过 kick 机制拉取数据;引擎从
smf_data字节流解析 delta-time 事件,维护note_on_cnt、midi_players[]等状态,按MAX_PLAYER_CNTt个 player 叠加采样,经 16 通道 mixer(CC 音量/表情/声像、pitchBend)混音后写入cbuf_midi。 - 播放控制:应用层在播放中随时调用
midi_dec_confing_api(obj, cmd, parm)调整速度、音色、变调、Mark、主旋律开关等,全部走命令分发,不直接触碰引擎内部状态。
功能开关映射
CMD_MIDI_SET_SWITCH 通过位掩码一次性控制多项合成行为(midi_switch_control 逐位解析):
| 开关位 | 内部标志 | 作用 |
|---|---|---|
MARK_ENABLE | triggered | 使能 Mark(段落标记)触发 |
MELODY_ENABLE | melody_on | 使能主旋律通道 |
TIM_DIV_ENABLE | tmDiv_enable | 使能时间划分(跟唱判定) |
MUTE_ENABLE | mute_enable | 静音 |
SAVE_DIV_ENBALE | save_ptr_enable | 使能位置保存(MAX_DEC_PLAYER_CNT < 18 时强制关闭,因为回溯缓冲占用 player 数组空间) |
EX_VOL_ENABLE | ex_flag | 使能扩展音量控制 |
SET_PROG_ENABLE | ins_set | 使能运行时换音色 |
MELODY_PLAY_ENABLE | melody_enable | 使能主旋律播放 |
BEAT_TRIG_ENABLE | beat_enable | 使能节拍触发 |
MELODY_STOP_ENABLE | melody_stop | 使能主旋律停止 |
MARK_LOOP_ENABLE | loop_on | 使能 Mark 循环 |
SEMITONE_ENABLE | pitch_enable | 使能变调(半音) |
LIMITER_ENABLE | limiter_enable | 使能输出限幅器 |
Source: midi_dec.c
速度控制算法
midi_play_set_tempo 展示了合成引擎如何用定点运算把速度值折算为采样时钟,并对每个通道的包络衰减速度做查表修正:
void midi_play_set_tempo(void *work_buf, MIDI_PLAY_CTRL_TEMPO *tempo_obj)
{
long long tmp64;
MIDI_DECODE_VAR *mid_dec_obj = (MIDI_DECODE_VAR *)work_buf;
u16 decayval0, decayval1;
mid_dec_obj->midi_tempo_v = tempo_obj->tempo_val;
MULSI(mid_dec_obj->now_srTicks, tmp64, mid_dec_obj->srTicks, mid_dec_obj->midi_tempo_v, 10);
for (int i = 0; i < MAX_CHANNEL_NUM; i++) {
decayval0 = tempo_obj->decay_val[i] & 0x7ff;
decayval1 = tempo_obj->decay_val[i] >> 11;
mid_dec_obj->decay_speed[i] = 32600 + ((168 * decayval0) >> 10);
if (mid_dec_obj->decay_speed[i] > 32768) {
mid_dec_obj->decay_speed[i] = 32768;
}
}
mid_dec_obj->mute_threshold = tempo_obj->mute_threshold;
}
Source: midi_dec.c
说明:MULSI 宏完成 64 位中间量的乘加,避免 16 位 MCU 上溢出;每个通道独立 decay_val(低 11 位衰减斜率 + 高 5 位扩展)修正包络,使变速时音符的听感衰减保持一致;decay_speed 上限 32768 防止除法/查表越界。
配置选项
| 配置项 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
DECODER_MIDI_EN | 宏 | 0/1 | 编译期使能文件播放 MIDI 解码器(midi_api.c) |
DECODER_MIDI_KEYBOARD_EN | 宏 | 0/1 | 编译期使能键盘演奏 MIDI 解码器(midi_ctrl_api.c) |
MIDI_VER_SELECT / MIDI_VER_4BYTE | 宏 | 2byte / 4byte | 选择合成引擎版本,同时决定音色库后缀(.mda / .mdb) |
MAX_DEC_PLAYER_CNT | const int | app_config.c 定义 | 文件模式最大同时发音数(8~32),直接决定解码缓冲大小 |
MAX_CTR_PLAYER_CNT | const int | app_config.c 定义 | 键盘模式最大同时发音数 |
MIDI_CTRL_OUT_CHANNEL | const int | app_config.c 定义 | 4byte 键盘模式输出声道数 |
MIDI_MAX_MARK_CNT | extern const int | — | Mark 最大数量;非 0 时使用 midi_decode_buff_full 大缓冲 |
MIDI_DEC_DBUF_SIZE | 宏 | — | 文件模式解码缓冲大小(随 MAX_DEC_PLAYER_CNT 变化) |
MIDI_DEC_NOMARK_NEED_BUF_SIZE | 宏 | — | 无 Mark 时的最小解码缓冲大小 |
MIDI_CTRL_DBUF_SIZE | 宏 | — | 键盘模式解码缓冲大小 |
midi_init_info() | weak 函数 | 空实现 | 应用层覆盖点:注入播放模式/默认音色/变调等初始化参数 |
| 音色库路径 | 文件 | /midi_cfg/00_MIDI.mda → /midi_cfg/midi_cfg.bin;4byte:/midi_cfg_ster1/00_MIDI.mdb | 启动时由 midi_decode_cfg_init / midi_ctrl_cfg_init 解析 |
API 参考
u32 midi_decode_api(void *strm, void **ppdec, void *p_dp_buf)
打开文件播放模式 MIDI 解码器。
- 参数:
strm(dec_data_stream*,输入流,含io接口与strm_source文件对象);ppdec(输出dec_obj*句柄);p_dp_buf(预留数据缓冲,当前未使用)。 - 返回:0 成功;
E_MIDI_NO_CFG(音色库未初始化)、E_MIDI_DBUF(缓冲不足)、E_MIDIFORMAT(格式校验失败)。
int midi_decode_cfg_init(void)
挂载 VFS、定位并解析音色库地址到 midi_tone_tab。
- 返回:0 成功;
E_MOUNT(VFS 挂载失败)、E_OPENBYPATH(音色库文件均未找到)。
u32 midi_dec_confing_api(dec_obj *obj, u32 cmd, void *parm)
运行时播放控制入口,转发到引擎的 midi_dec_confing 命令分发。
- 参数:
obj解码句柄;cmd命令字(CMD_INIT_CONFIG、CMD_MIDI_CTRL_TEMPO、CMD_MIDI_SET_MARK、CMD_MIDI_SEEK_BACK_N、CMD_MIDI_SET_CHN_PROG、CMD_MIDI_SET_SWITCH、CMD_MIDI_OKON_MODE、CMD_MIDI_SET_SEMITONE、CMD_MIDI_LIMITER_TRIGGER、CMD_MIDI_MELODY_TRIGGER、CMD_MIDI_STOP_MELODY_TRIGGER等);parm对应结构体指针。 - 返回:0 成功;
-1(obj或ops为空)。 - 错误:非法命令/参数由引擎内部防御(如
CMD_MIDI_SET_MARK自动禁用非法区间)。
u32 midi_ctrl_decode_api(void *strm, void **ppdec, void *p_dp_buf)
打开键盘演奏模式合成器(无文件输入)。
- 返回:0 成功;
E_MIDI_FILEHDL(strm_source非空,误用文件模式)、E_MIDI_NO_CFG、E_MIDI_DBUF。
u32 midi_buff_api(dec_buf *p_dec_buf)
返回链接段 midi_buf_start ~ midi_buf_end 之间的专用内存区间,供框架了解 MIDI 解码器的内存占用。
MIDI_PLAY_CTRL_MODE *get_midi_mode(void) / u32 *get_midi_switch_info(void)
获取当前播放模式与功能开关的实时快照(读 init_info),供应用层查询引擎状态。
void midi_error_play_end_cb(dec_obj *obj, u32 ret)
播放结束回调:收到 MAD_ERROR_PLAY_END 时置 B_DEC_PAUSE 停止 DAC 拉取,用于文件播放到末尾的自动收尾。
内存布局与链接段
MIDI 合成对内存带宽与确定性要求极高,工程上把所有关键对象静态放置到专用链接段,而不是堆分配:
| 段名 | 内容 |
|---|---|
.midi_dec.data.bss / .midi_dec.data | 文件模式引擎全局变量与初始数据 |
.midi_dec.text.const / .midi_dec.text | 引擎代码与常量(配合 str_literal_override 收拢字符串) |
.midi_buf | cbuf_midi、obuf_midi、midi_decode_buff_full/nomark、midi_tone_tab、init_info、midi_dec_io0 等大缓冲 |
.midi_keyboard.data.bss / .midi_keyboard.data / .midi_keyboard.text* | 键盘模式引擎段 |
.midi_ctrl_buf | cbuf_midi_ctrl、midi_ctrl_decode_buff、midi_ctrl_tone_tab、midi_ctrl_parmt 等 |
Source: midi_api.c、midi_ctrl_api.c
这种"段内静态放置 + midi_buff_api 上报区间"的做法,使链接器在编译期就能确定内存总量,配合 need_dcbuf_size() 的运行时校验(buff_len > cal_buf_len 即报 E_MIDI_DBUF),在资源受限的 MCU 上避免了动态内存碎片与静默越界。
失败模式、边界情况与并发
错误码与失败路径
| 错误码 | 触发场景 | 处理 |
|---|---|---|
E_MIDI_NO_CFG | midi_tone_tab 为空(音色库未初始化) | 解码打开直接失败,应用应确保先调 midi_decode_cfg_init |
E_MIDI_DBUF | need_dcbuf_size() 超出编译期缓冲 | 提示需增大 MAX_DEC_PLAYER_CNT 对应缓冲;音符叠加数越大所需缓冲越大 |
E_MIDIFORMAT | format_check 失败(非 MIDI 文件) | 解码打开失败;B_DEC_NO_CHECK 置位时跳过校验 |
E_MOUNT / E_OPENBYPATH | VFS 挂载失败 / 音色库文件缺失 | 启动配置失败,需检查镜像中 midi_cfg 分区 |
E_MIDI_FILEHDL | 键盘模式收到非空文件流 | 拒绝打开,防止错误使用 |
MAD_ERROR_PLAY_END | 播放到结尾 | midi_error_play_end_cb 置 B_DEC_PAUSE 停止输出 |
边界情况
- 非法 Mark 区间:
mark_start <= 0或mark_start >= mark_end时CMD_MIDI_SET_MARK自动置mark_enable = 0,避免死循环。 - 回退越界:
CMD_MIDI_SEEK_BACK_N的seek_back_n被裁剪到MAX_GO_BACK,rd_cnt < 0时环形取模,bk_check_flag位图记录有效快照,防止回退到未保存位置。 - 并发音数上限:
player_t被裁剪到编译期MAX_DEC_PLAYER_CNT;SAVE_DIV_ENBALE在MAX_DEC_PLAYER_CNT < 18时被强制关闭,因为回溯快照数组复用了midi_players[10]之后的空间。 - 非法采样率:DAC 采样率不在 9 档表内时
midi_musicsr_to_cfgsr直接ASSERT(0),因为合成器无法在该采样率下工作。 - 变速极限:
decay_speed计算后钳位到 32768,防止包络衰减查表越界。
并发与中断安全
- 关中断保护:
dec_midi_hld/dec_midi_ctrl_hld在local_irq_disable()/local_irq_enable()区间内清零,避免与 DAC 中断输出回调(读取同一dec_obj与环形缓冲)竞争。 - 环形缓冲:输出使用
cbuffer_t(生产者=合成引擎,消费者=DAC 中断),由框架的环形缓冲实现保证单生产者/单消费者的安全读写。 - 状态查询:
get_midi_mode()/get_midi_switch_info()直接返回init_info内部字段地址,属只读快照,调用方不应长期持有指针跨并发修改。
性能与运维
- 零拷贝音色访问:音色库经 SPI Flash 直接映射地址访问,无文件系统读取拷贝,随机访问采样区延迟低;代价是要求音色库位于支持线性寻址的存储(
boot_info.sfc.app_addr + attr.sclust)。 - 缓冲双轨:无 Mark 场景使用更小的
midi_decode_buff_nomark,节省 RAM;需要 Mark/跟唱/回退功能的产品必须启用大缓冲。 - 定点运算:速度换算用
MULSI64 位中间量、衰减用查表+移位,避免浮点开销,适合无 FPU 的 MCU 实时合成。 - 运行时开销与并发音数强相关:
MAX_DEC_PLAYER_CNT越大,每采样周期需叠加的 player 越多,CPU 占用越高;同时它直接决定解码缓冲大小,是"音质/功能"与"内存/功耗"的主要权衡旋钮。 - 日志观测:
log_info("midi_decode_api file:0x%x")、MIDI_DEC Need Buff Len、midi_dec mda open fail等日志可用于定位打开失败、缓冲不足、音色库缺失三类高频问题。
扩展点
midi_init_info()(weak 函数):应用层覆盖此函数即可在产品启动时注入MIDI_INIT_STRUCT(播放模式、OKON/按键模式、默认音色prog_info、主轨道mainTrack_info、变调semitone_info、Markmark_info、主旋律moledy_info、节拍beat_info、看门狗wdt_clear、变速录制w2s_info等),而无需改动引擎代码。这是产品差异化的主要入口。- 编译期开关族:
DECODER_MIDI_EN、DECODER_MIDI_KEYBOARD_EN、MIDI_VER_SELECT、MIDI_MAX_MARK_CNT组合出"文件播放 / 键盘演奏 / 2byte / 4byte / 带 Mark / 无 Mark"等产品形态。 - 命令集扩展:在
midi_dec_confing中新增CMD_*分支即可扩展控制能力,命令字与结构体定义集中在MIDI_CTRL_API.h/MIDI_DEC_API.h/MIDIDefs.h,上层经midi_dec_confing_api统一访问。 - 应用层模式:
src/mbox_flash/midi_dec/(midi_dec_mode.c/midi_dec_mode_key.c)与src/mbox_flash/midi_keyboard/(midi_keyboard_mode_key.c)展示了如何将本解码器接入具体产品 UI 与按键逻辑,可作为新产品的移植参考模板。
相关链接
- 解码器框架与解码对象生命周期(decoder list /
decoder_api.h):音频解码框架相关页面 - DAC 通道注册与混音输出(
audio_dac.h/audio_dac_api.h):DAC 音频输出页面 - 音色库镜像制作(
post_build/uc03/midi_cfg、dir_midi、midi_cfg_ster1):资源镜像/打包相关页面 - 应用层 MIDI 播放模式:
src/mbox_flash/midi_dec/midi_dec_mode.c - 键盘演奏模式:
src/mbox_flash/midi_keyboard/midi_keyboard_mode_key.c