MIDI 乐器模式
MIDI 乐器模式是语音玩具(voice-toy)应用中基于杰理 AD1x 系列 MCU 的 MIDI 解码与实时合成能力:应用层通过 midi_api.c 提供的解码器接口打开 MIDI 乐谱文件,由 midi_2byte 引擎完成音色加载、事件解析与多音符 PCM 实时合成,最终经 DAC 输出。
Purpose and Scope
本页说明 voice-toy 应用中 MIDI 乐器模式 的完整实现机制,覆盖:
- 音色库(tone bank)的加载与缓存地址解析(
midi_decode_init) - MIDI 解码器的打开、格式校验与配置流程(
midi_decode_api) - 实时合成引擎的初始化(
MIDI_CTRL_OPEN)与主循环(MIDI_CTRL_MAIN) - 多音符(player)叠加、音量包络(ADSR/ENV)与通道混音机制
- 压缩音色格式与原始音色格式的兼容处理
- 关键配置项、API、失败模式与扩展点
以下内容明确不属于本页范围,请参见对应页面:
- 语音玩具的按键矩阵、触摸/IO 扫描与按键→音符映射:见「按键控制模式」等应用层页面
- 通用音频解码框架(
dec_obj、DAC 注册、cbuffer 机制):见「解码器框架」页面 - MIDI 文件(SMF)在 PC 端的制作与转换:见
doc/杰理AD1x-45678_MIDI应用说明文档.pdf与doc/JLmidi工具使用说明.pdf
说明:本页源代码依据主要来自
sdk/app/bsp/common/decoder/list/midi_api.c与sdk/app/bsp/common/midi_open/midi_2byte/midi_play.c;voice-toy 应用层的按键→MIDI 事件封装逻辑在本次阅读范围内未完全展开,相关 build 配置位于sdk/app/post_build/ch58/voice_toy/midi_cfg等路径。
Overview
在杰理 AD1x 语音玩具方案中,MIDI 乐器模式让设备可以:
- 播放 MIDI 乐谱文件:将
.mid/00_MIDI.mda等格式的乐谱数据交给解码器,解码器逐事件驱动合成引擎发声; - 作为电子琴演奏:应用层把按键事件翻译成 Note On/Off 控制消息,驱动同一个合成引擎实时发声(这也是
midi_ctrl_api.c与MIDI_CTRL_PARM控制参数存在的意义); - 多音符复音合成:同时最多支持
MAX_CTR_PLAYER_CNT个发声"player",每个 player 独立拥有波形、音高、音量包络与通道属性,最终混音成 PCM 送入 DAC。
整个模式的关键设计思想是 解码(decode)与合成(synthesis)分层:
- 解码层(
midi_api.c+midi_2byte/midi_dec.c+midi_event.c):负责从文件读取 SMF 事件流,将其转换成控制消息; - 合成层(
midi_2byte/midi_play.c):负责根据控制消息维护一组MIDI_PLAYER发声体,逐采样点生成波形并混音; - 音色库(tone bank):以
midi_cfg/00_MIDI.mda(旧版midi_cfg.bin)形式放在 SPI Flash 中,通过 VFS 挂载后按sclust计算出 cache 地址,供合成引擎随机读取采样数据。
音色文件头还支持一个可选的 0xABCD 魔数,用于标记"压缩音色格式",引擎据此切换音色索引与映射表的解析方式。
Architecture
flowchart TD
subgraph sg_App["应用层 (voice-toy)"]
App["按键/乐谱播放逻辑"]
end
subgraph sg_Decoder["解码层 (decoder/list)"]
midi_api["midi_api.c<br/>midi_decode_init / midi_decode_api"]
midi_ctrl["midi_ctrl_api.c<br/>控制/演奏 API"]
dec_obj["dec_obj (D_TYPE_MIDI)"]
end
subgraph sg_Engine["MIDI 引擎 (midi_open/midi_2byte)"]
midi_dec["midi_dec.c<br/>SMF 事件解析"]
midi_event["midi_event.c<br/>事件分发 (midi_evt[])"]
midi_play["midi_play.c<br/>MIDI_CTRL_OPEN / MIDI_CTRL_MAIN<br/>多 player 合成"]
MIDIDefs["MIDIDefs.h<br/>常量与数据结构"]
end
subgraph sg_Storage["存储与输出"]
VFS["VFS 挂载 /midi_cfg/00_MIDI.mda"]
SPI["SPI Flash 音色库 cache"]
DAC["DAC 通道 (cbuffer + 混音)"]
end
App -->|"打开乐谱文件"| midi_api
App -->|"控制参数/按键事件"| midi_ctrl
midi_api --> dec_obj
dec_obj --> midi_dec
midi_dec --> midi_event
midi_event -->|"控制消息"| midi_play
midi_api -->|"midi_tone_tab 地址"| VFS
VFS --> SPI
midi_play -->|"读取采样 (spi_pos)"| SPI
midi_play -->|"PCM 输出 (output 回调)"| DAC
架构说明:
midi_api.c是解码器框架(decoder_api.h)与 MIDI 引擎之间的适配层:它实现if_decoder_io接口(mp_input/mp_output),为框架提供need_dcbuf_size、open、format_check、dec_confing等decoder_ops,并负责把音色库地址、采样率、复音数等填入MIDI_CONFIG_PARM。midi_play.c是合成核心:MIDI_CTRL_OPEN初始化MIDI_DECODE_VAR运行态(player 池、按键表、通道 mixer、速度/节拍),MIDI_CTRL_MAIN是逐块(block)执行的实时合成主循环。- 事件表
midi_evt[](midi_api.c中dec_midi_hld.event_tab = (u8 *)&midi_evt[0])把解析出的 MIDI 事件分发到合成引擎对应的控制处理函数。 - 音色数据不拷贝进 RAM,合成时通过
spi_pos指向的 SPI cache 地址直接随机读取,这对小 RAM 的 MCU 方案至关重要。
音色库加载与初始化
MIDI 乐器模式启动时首先要定位音色库在 SPI Flash 中的位置,这一步由 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
设计要点:
- 文件名回退策略:优先打开新格式
00_MIDI.mda,失败后回退到旧的midi_cfg.bin,保证新旧烧录工具产出的镜像都能被识别;两者都失败则静默返回,后续midi_decode_api会因为midi_tone_tab == 0返回E_MIDI_NO_CFG。 - 零拷贝定位:音色库不搬入 RAM,而是通过
vfs_get_attrs拿到 FAT 簇号sclust,再叠加boot_info.sfc.app_addr(SPI Flash 应用分区基址)换算成 CPU 可直接访问的 cache 地址,存入静态全局midi_tone_tab。 - 该地址是整个模式的"音色句柄":
midi_decode_api中midi_t_parm.spi_pos = (u16 *)midi_tone_tab,即把它直接作为MIDI_CONFIG_PARM的音色数据起始地址传入合成引擎。
解码器打开与配置流程
应用层播放一个 MIDI 乐谱文件时,解码框架最终调用 midi_decode_api() 完成打开与配置:
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;
}
...
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;
}
...
ops->open(p_cal_buf, &midi_dec_io0, NULL); //传入io接口,说明如下
if (ops->format_check(p_cal_buf)) { //格式检查
return E_MIDIFORMAT;
}
...
sr = dac_sr_read(); //获取采样率
...
/**************相对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,避免未初始化音色库时进入合成导致跑飞。 - 缓冲区预算校验:
need_dcbuf_size()返回的所需解码缓冲随MAX_DEC_PLAYER_CNT(复音数)变化——复音越多,需要同时驻留的MIDI_PLAYER与play_key表越大;若超出静态分配的midi_decode_buff_nomark[MIDI_DEC_BUF_SIZE/4],返回E_MIDI_DBUF,防止内存越界。 - IO 接口注入:
midi_dec_io0把mp_input/mp_output作为解码器的读写回调,输入来自文件(dec_midi_hld.p_file),输出写入cbuf_midi(obuf_midi)。 - 采样率对齐:
midi_musicsr_to_cfgsr()把 DAC 当前采样率(48000/44100/32000/24000/22050/16000/12000/11025/8000)映射成引擎内部索引,不匹配时ASSERT(0, "midi error sr !!!"),说明合成引擎对采样率是固定表驱动、不支持任意值。 - 应用可定制:
midi_init_info()是__attribute__((weak))弱符号,应用可在自己的代码里实现同名强符号,在打开时注入自定义的mode_info/switch_info(由get_midi_mode()/get_midi_switch_info()暴露给应用层查询当前演奏模式与开关状态)。 - 注册 DAC 通道:
regist_dac_channel(&dec_midi_hld.sound, kick_decoder)与普通解码器一致,合成产生的 PCM 经 cbuffer 由 DAC 任务取走播放。
核心控制流
sequenceDiagram
participant App as 应用层 (voice-toy)
participant Api as midi_api.c
participant Ops as decoder_ops (midi_2byte)
participant Play as midi_play.c 合成引擎
participant DAC as DAC/cbuffer
App->>Api: midi_decode_init()
Api->>Api: VFS 打开 /midi_cfg/00_MIDI.mda
Api->>Api: midi_tone_tab = app_addr + sclust
App->>Api: midi_decode_api(file)
Api->>Api: 校验 midi_tone_tab / buffer 大小
Api->>Ops: open(midi_dec_io0)
Api->>Ops: format_check()
Api->>Ops: dec_confing(CMD_INIT_CONFIG, init_info)
Ops->>Play: MIDI_CTRL_OPEN(work_buf, ctrl_parm, config)
Play->>Play: 解析 0xABCD 压缩头 / 初始化 player 池、通道 mixer、节拍
Api->>DAC: regist_dac_channel()
App->>Api: 播放/按键事件
Api->>Play: MIDI_CTRL_MAIN() 逐块合成
Play->>Play: theTick 累加 → sample_cnt
Play->>Play: 每个活跃 player 生成波形/包络
Play->>DAC: output() 写入 PCM
DAC-->>Play: 消费进度 (o_index 续传)
Source: midi_api.c、midi_play.c
合成引擎初始化:MIDI_CTRL_OPEN
MIDI_CTRL_OPEN 是合成引擎的入口初始化函数,接收解码框架传入的工作缓冲、控制参数(MIDI_CTRL_PARM)与配置(MIDI_CONFIG_PARM):
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;
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;
mid_dec_obj->instr_key_map[0] = (char *)mid_dec_obj->spi_key_start;
mid_dec_obj->instr_start_index[0] = 0;
for (chn = 1; chn < MAX_CHANNEL_NUM; chn++) {
mid_dec_obj->instr_key_map[chn] = mid_dec_obj->instr_key_map[0];
mid_dec_obj->instr_start_index[chn] = mid_dec_obj->instr_start_index[0];
}
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;
MULSI(mid_dec_obj->now_srTicks, tmp64, mid_dec_obj->srTicks, mid_dec_obj->midi_tempo_v, 10);
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
设计意图逐条解读:
- 控制参数复用 smf_data 区域:
EVENT_FIFO_CONTEXT直接把MIDI_CTRL_PARM(内含output回调与priv)塞进MIDI_DECODE_VAR的smf_data字段,实现"控制上下文 + 乐谱数据"两种用途共用同一块内存,省 RAM。 - 压缩/原始音色双格式:音色库头两个字节若为魔数
0xABCD,则为压缩格式——乐器号在cmporkind[1],音色映射表在偏移 130 处;否则为原始格式——乐器号即cmporkind[0],映射表在偏移 129 处。compressIN标志供后续采样读取时决定是否解压。 - 乐器采样定位:
spi_key_start = midi_spi_pos[258 + instr_spi * 2]指向当前乐器的键位(key)数据区起点,spi_zone_start进一步按乐器号偏移 128 字节得到力度分层(zone)起点;所有 16 个通道初始共享同一张键位映射。 - 通道混音器默认值:每个通道的 CC 默认 CC7(音量)=127、CC11(表情)=127、CC64(声像)=64,弯音
pitchBend_v = 256(即 0 号弯音),保证无控制器事件时以最大音量、居中原样发声。 - 节拍与采样率换算:
srTicks把 MIDI 节拍(tempo)换算成每 tick 的采样数,smpl_rate_tab[sample_rate]是采样率查找表,MIDI_RESAMPLE_SHIFT控制定点精度;now_srTicks = srTicks * tempo_v / 1024(MULSI宏做 64 位乘再右移 10)实现实时变速不变调基础。 - 内存池分区:
mempool前部切给MIDI_PLAYER数组(needMidiPlayersBuf()),紧接着是play_key表(needPlayKeyBuf(),初始化为 255 表示"无按键")。player 池与按键表大小均由MAX_CTR_PLAYER_CNT × MAX_CHANNEL_NUM决定,是复音数上限。
实时合成主循环:MIDI_CTRL_MAIN
合成主循环按"输出块"驱动,核心是 theTick 累积器与逐 player 的波形生成:
mid_dec_obj->theTick += mid_dec_obj->now_srTicks;
sample_cnt = mid_dec_obj->theTick >> MIDI_RESAMPLE_SHIFT;
mid_dec_obj->theTick &= MIDI_RESAMPLE_VAL;
int midi_olen;
_MIDI_OUTPUT:
while (sample_cnt) {
int len_out = (sample_cnt > MIDI_OBUF_BLOCK) ? MIDI_OBUF_BLOCK : sample_cnt;
for (j = 0; j < mid_dec_obj->MAX_PLAYER_CNTt; j++) {
long long tmp64;
if (mid_dec_obj->midi_player_on & BIT(j)) {
WaveInfo_t *waveInfo;
int tmp_r;
waveInfo = &(mid_dec_obj->midi_players[j].wave_info);
...
if (waveInfo->ison == SEQ_AE_ON) {
if (midi_ctrl_gen_sample(mid_dec_obj->out_val, waveInfo, modulatep, vol_chn, j, len_out)) {
if ((!waveInfo->pendingStop) && (waveInfo->voice.isend)) {
waveInfo->exclOn = 0;
mid_dec_obj->midi_player_on &= ~BIT(j);
if (mid_dec_obj->melody_stop && waveInfo->active_off_flag) {
mid_dec_obj->melody_stop_trig.melody_stop_trigger(mid_dec_obj->melody_stop_trig.priv, mid_dec_obj->main_key[j], waveInfo->chnl);
}
}
}
}
}
}
...
}
Source: midi_play.c
循环机制:
- 节拍驱动采样数:每次进入主循环把
now_srTicks累加到theTick,右移MIDI_RESAMPLE_SHIFT得到本次应合成的采样数sample_cnt,余数保留在theTick供下次累积——这就是"非整数采样率比"的定点实现,避免长时间播放产生累积误差。 - 分块输出:
sample_cnt超过MIDI_OBUF_BLOCK时按块合成,保证输出缓冲(out_val)尺寸固定、可被output回调逐块消费。 - 按 player 合成:
midi_player_on位图标记当前活跃的发声体;每个活跃 player 的WaveInfo_t独立维护音量包络状态(attack/decay/env),midi_ctrl_gen_sample()生成len_out个采样并累加进混音缓冲out_val。 - 自然结束回收:当
voice.isend置位且无pendingStop时,清exclOn、熄灭 player 位,若注册了melody_stop_trigger回调则通知应用"某键的旋律已播完",应用层可据此点亮/熄灭按键 LED 或触发下一动作。 - 续传机制:
o_len/o_index记录输出回调未消费完的剩余量,output()只消费了部分数据时立即返回,下次进入先补发剩余数据再合成新采样,保证 DAC 不间断。
音量包络合成
MIDI_CTRL_MAIN 内部对每个采样点依次处理三种音量状态:
- 起音(attack):
vol_now += attack_incr,封顶1L << 30,达到后置vol_cnt = 1进入保持/衰减; - 包络(ENV):若音色带包络数据(
env_info.env_use),按变长 7-bit 编码(readval & 0x7f,0x80为继续标志)读取包络点序列,插值出stepval/stepcnt,逐点逼近目标音量——这是电钢/风琴类音色"踩踏板渐弱"效果的关键; - 自然衰减(decay):无包络时用
MULSI(vol_now, vol_now, vol_dec_now, vol_dec_now_bit)做定点指数衰减,并钳位到vol_hold_new保持音量。
最终每点音量再乘以通道 CC 音量(可被 ex_vol 外部音量缩放,VOL_Norm_Bit 定点归一)与 vol_atten 衰减系数,写入 vol_array 供 midi_ctrl_gen_sample 使用。
使用示例
以下示例均提取自实际源码,展示 MIDI 乐器模式在应用/框架中的典型调用形态。
示例 1:获取当前演奏模式与开关信息
应用层需要知道 MIDI 引擎当前处于哪种演奏模式(如乐谱播放/键盘演奏)以及开关状态时,直接读取 init_info 中暴露的句柄:
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))
void midi_init_info(MIDI_INIT_STRUCT *init_info)
{
}
Source: midi_api.c
示例 3:采样率到引擎索引的映射
midi_decode_api 通过 dac_sr_read() 获得当前采样率,再映射为引擎内部索引(引擎的节拍/包络换算依赖该索引查 smpl_rate_tab):
static u8 midi_musicsr_to_cfgsr(u32 sr)
{
const int midi_sr_tab[] = {
48000, 44100, 32000, 24000, 22050,
16000, 12000, 11025, 8000,
};
for (int i = 0; i < sizeof(midi_sr_tab) / sizeof(midi_sr_tab[0]); i++) {
if (sr == midi_sr_tab[i]) {
return i;
}
}
ASSERT(0, "midi error sr !!!:%d \n", sr);
return 0;
}
Source: midi_api.c
示例 4:内存池划分(player 池 + 按键表)
合成引擎运行所需的缓冲区按固定公式从 mempool 划分,复音数与通道数决定其大小:
static u32 needMidiPlayersBuf()
{
return MAX_CTR_PLAYER_CNT * sizeof(MIDI_PLAYER);
}
static u32 needPlayKeyBuf()
{
return MAX_CTR_PLAYER_CNT * MAX_CHANNEL_NUM * sizeof(u8);
}
Source: midi_play.c
配置选项
| 配置项 | 类型 | 默认值/取值 | 说明 |
|---|---|---|---|
DECODER_MIDI_EN | 宏 | 0/1 | 编译开关,为 0 时整个 midi_api.c 不参与编译(#if defined(DECODER_MIDI_EN) && (DECODER_MIDI_EN)) |
MAX_DEC_PLAYER_CNT | int(app_config.c 定义) | 8~32 | 乐谱解码最大同时发声 key 数,决定解码 buffer 大小,由 need_dcbuf_size() 换算 |
MAX_CTR_PLAYER_CNT | 宏 | 引擎上限 | 控制/演奏模式最大 player 数,MIDI_CTRL_OPEN 中对 player_t 做上限钳制 |
MAX_CHANNEL_NUM | 宏 | 16 | MIDI 通道数(channel_mixer[]、play_key[] 维度) |
CTRL_CHANNEL_NUM | 宏 | — | 控制通道数,用于 decay_speed[] 初始化 |
midi_t_parm.player_t | u8 | MAX_DEC_PLAYER_CNT | 注入 MIDI_CONFIG_PARM 的复音数(8~32 可配) |
midi_t_parm.sample_rate | u8 | 由 dac_sr_read() 映射 | 引擎采样率索引(9 档固定表) |
midi_t_parm.spi_pos | u16* | midi_tone_tab | 音色库 SPI cache 起始地址 |
midi_tone_tab | u32 | 0 | 音色库地址,midi_decode_init 成功后由 app_addr + sclust 得到;为 0 时 midi_decode_api 返回 E_MIDI_NO_CFG |
midi_tempo_v | u32 | 1024 | 初始速度倍率(1024 = 100%),now_srTicks 实时变速基准 |
mute_threshold | u32 | 1L << 29 | 静音阈值,低于此振幅的采样判定为可忽略/静音 |
decay_speed[chn] | u32 | 32768 | 各通道自然衰减速度初值 |
| 通道 CC 默认值 | u8 | VOL=127 / EXPR=127 / PAN=64 | channel_mixer[].cc[] 初始控制器值 |
pitchBend_v[chn] | u16 | 256 | 初始弯音值(256 = 不弯音) |
| 音色文件 | — | midi_cfg/00_MIDI.mda → midi_cfg.bin | 音色库文件名,打开失败自动回退旧格式 |
API 参考
void midi_decode_init(void)
- 挂载 VFS、打开音色库文件并计算
midi_tone_tab(SPI cache 地址)。 - 无返回值;失败时静默返回,后续打开解码器会报
E_MIDI_NO_CFG。
u32 midi_decode_api(void *p_file, void **ppdec, void *p_dp_buf)
p_file:MIDI 乐谱文件句柄(VFS file)。ppdec:输出解码器句柄(dec_obj*)。p_dp_buf:解码工作缓冲。- 返回:
0成功;E_MIDI_NO_CFG(音色库未初始化)、E_MIDI_DBUF(解码缓冲不足)、E_MIDIFORMAT(格式检查失败)。 - 完成
dec_midi_hld初始化、cbuf_midi初始化、ops->open/format_check/dec_confing(CMD_INIT_CONFIG)并regist_dac_channel。
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中的演奏模式与开关信息句柄,供应用层查询/修改。
static u32 MIDI_CTRL_OPEN(void *work_buf, void *dec_parm, void *parm)
- 合成引擎打开函数(由
decoder_ops.open间接调用)。 work_buf:MIDI_DECODE_VAR工作区;dec_parm:MIDI_CTRL_PARM(含output回调);parm:MIDI_CONFIG_PARM(含spi_pos/sample_rate/player_t)。- 返回
0;负责双格式音色解析、player 池/按键表/通道 mixer/节拍初始化。
static u32 MIDI_CTRL_MAIN(void *ptr)
- 合成主循环(由解码框架周期调用)。
- 按
theTick累积采样数,逐块调用midi_ctrl_gen_sample合成并交给output回调;处理o_len续传与melody_stop_trigger播完通知。
失败模式、边界情况与并发
失败模式
| 失败场景 | 检测点 | 行为 |
|---|---|---|
| 音色库未加载 | midi_decode_api 首行 if (!midi_tone_tab) | 返回 E_MIDI_NO_CFG,解码器不打开 |
| 音色文件缺失 | midi_decode_init 中 vfs_openbypath | 尝试 midi_cfg.bin 回退;仍失败则静默返回,midi_tone_tab 保持 0 |
| 解码缓冲不足 | buff_len > cal_buf_len | 返回 E_MIDI_DBUF;buff_len 随 MAX_DEC_PLAYER_CNT 增长,调大复音数需同步增大 MIDI_DEC_BUF_SIZE |
| 格式非法 | ops->format_check() 非 0 | 返回 E_MIDIFORMAT |
| 采样率不支持 | midi_musicsr_to_cfgsr 查表失败 | ASSERT(0, "midi error sr !!!") 断言,说明引擎仅支持 9 档固定采样率 |
| 复音数超上限 | MIDI_CTRL_OPEN 中 player_t > MAX_CTR_PLAYER_CNT | 静默钳制为 MAX_CTR_PLAYER_CNT,多余按键事件将被拒绝/静音 |
边界情况
- 播放末尾补发:
output()回调消费少于请求长度时,MIDI_CTRL_MAIN置o_len并立即返回,不生成新采样,避免数据覆盖;这是"DAC 慢于合成"时的背压机制。 - 按键表哨兵值:
play_key[]初始化为255,与任何合法 MIDI 音符号(0~127)区分,防止未占用槽位被误判为按下的键。 - 音量钳位:包络插值把
vol_f钳在[0, 1073741824](1L<<30),起音阶段同样封顶1L<<30,防止定点运算溢出产生爆音。 - 静音检测:低于
mute_threshold(1L<<29)的包络值走"快速归零"路径,避免残余直流导致 DAC 底噪。
并发与实时性
- MIDI 合成是单线程实时任务:
MIDI_CTRL_MAIN在解码器任务中被周期调用,事件(Note On/Off、CC)通过MIDI_CTRL_PARM/事件表在任务边界同步,合成循环内部不主动加锁,因此所有控制消息必须由同一任务上下文写入,不可在中断里直接修改MIDI_DECODE_VAR。 midi_decode_api在memset(&dec_midi_hld, ...)前后用local_irq_disable()/local_irq_enable()保护,防止 DMA/中断回调与初始化竞争。midi_tone_tab是全局静态量,初始化发生在任意解码之前,属"一次写、多次读",无需运行时互斥。
性能与运维注意事项
- 复音数 = 内存 × CPU 的权衡:每个活跃 player 都要在
MIDI_CTRL_MAIN里逐采样点做包络计算与midi_ctrl_gen_sample合成,MAX_DEC_PLAYER_CNT每 +1,解码缓冲按need_dcbuf_size()增长。应用侧应结合乐曲最大同时音符数配置(8~32),避免"配置 32 复音但只弹单音"浪费内存。 - 采样率越低越省 CPU:引擎按 9 档固定采样率表工作,
dac_sr_read()当前值决定合成工作量;16k/8k 档适合语音玩具,48k 档需评估主频余量。 - 定点运算密集:
MULSI(64 位乘后移位)、smpl_rate_tab、MIDI_RESAMPLE_SHIFT等全部为定点实现,无浮点依赖——这是 MCU 上实时合成可行性的前提;修改节拍/包络算法时须保持定点精度(如vol_now1.30 格式),否则会出现音量跳变。 - 音色库放 SPI Flash 不搬 RAM:
spi_pos直读 cache 地址,随机访问时延取决于 SPI Flash 控制器;压缩格式(0xABCD头)可减小音色体积,但读取时要多一步解压,属于"空间换时间"的取舍。
扩展点
midi_init_info(弱符号):应用可定义强符号覆盖默认空实现,在每次CMD_INIT_CONFIG时写入自定义mode_info/switch_info,实现不同产品(电子琴/音乐盒/乐器玩具)的差异化初始状态。get_midi_mode()/get_midi_switch_info():暴露init_info句柄,应用层可随时读写演奏模式与开关(如"单音/多音模式切换")。melody_stop_trigger回调:注册在MIDI_DECODE_VAR上的播完通知(携带main_key[j]与通道号),可用于按键 LED 熄灭、翻谱、连奏逻辑等。decoder_ops接口:open/format_check/dec_confing/need_dcbuf_size由get_midi_ops()提供,若要支持新的 MIDI 事件类型或音色格式,可在midi_2byte内部扩展事件表midi_evt[]与对应处理函数。- 双格式音色头:
0xABCD魔数机制预留了压缩/原始两种音色文件布局,新增格式只需在MIDI_CTRL_OPEN的魔数分支里扩展解析逻辑(注意instr_map偏移随之调整)。
Related Links
- midi_api.c(解码适配层)
- midi_ctrl_api.c(控制/演奏 API)
- midi_play.c(合成引擎核心)
- midi_dec.c / midi_dec.h(SMF 事件解析)
- midi_event.c(事件分发)
- MIDIDefs.h(常量定义)
- 工具与文档:JLmidi工具使用说明.pdf、杰理AD1x-45678_MIDI应用说明文档.pdf
- 构建配置:voice-toy 的 MIDI 资源清单
sdk/app/post_build/ch58/voice_toy/dir_midi与midi_cfg - 相关页面:解码器框架(
dec_obj/DAC 注册)、按键控制模式(按键→音符映射)