MIDI 解码与键盘演奏
本文档介绍 AD23N GP-MCU SDK 中 MIDI 能力的完整实现:基于波表(W2S 采样)的 MIDI 文件解码播放模式(midi_dec)与 MIDI 键盘演奏模式(midi_ctrl),涵盖应用层模式、解码器框架接口、数据结构、按键映射与配置方式。
Purpose and Scope
本页覆盖 AD23N 上 MIDI 子系统的两条端到端链路:
- MIDI 解码播放:
midi_decode_app()从片内 SPI Flash 目录/dir_midi读取 MIDI 文件,通过BIT_MIDI解码器播放,并支持 NORM/OKON 模式切换、暂停、上下曲。 - MIDI 键盘演奏:
midi_keyboard_app()将 GPIO 按键映射为钢琴琴键(Do/Re/Mi/Fa 等),通过BIT_MIDI_CTRL控制通道实时触发音色采样发声,支持弯音(Pitch Bend)与 16 通道乐器切换。
页面边界:按键扫描驱动(key_drv_*)、通用解码器框架(decoder_api)、文件系统与电源管理等通用机制属于各自目录项,本文仅描述它们与 MIDI 模式的衔接点(按键消息、decoder_io、simple_dev_fs_mount)。MIDI 音色库制作工具与格式细节见 doc/杰理AD1x-45678_MIDI应用说明文档.pdf,不在本页展开。
Overview
MIDI(Musical Instrument Digital Interface)本身不携带音频波形,而是携带"音符开/关、力度、弯音、程序号(音色)"等控制事件。AD23N 的实现采用波表合成方式:每个音色由若干采样 Zone(Zone_t,位于 Flash)构成,解码器在收到 note_on 事件后,根据音符号(key)查表找到对应采样并做变调重采样播放,收到 note_off 或释放事件后停止。
系统提供两个独立工作模式(由 work_mode 与 VM_INDEX_SYSMODE 持久化):
- 解码模式(
DECODER_MIDI_EN使能,midi_dec_mode.c):类似普通音乐播放器,但解码对象是.mid文件,目录固定为/dir_midi,解码类型为BIT_MIDI。 - 键盘模式(
DECODER_MIDI_KEYBOARD_EN使能,midi_keyboard_mode.c):把硬件按键变成电子琴琴键,属实时演奏场景,解码类型为BIT_MIDI_CTRL,由midi_ctrl_api直接下发音符事件。
两者共享底层解码器核心(sdk/app/bsp/modules/midi/ 下的 midi_dec.c、midi_event.c、midi_play.c),以及 MIDIDefs.h、MIDI_DEC_API.h、MIDI_CTRL_API.h 三个头文件定义的数据结构与接口。
Architecture
flowchart TD
subgraph sg_HW["硬件层"]
KEY["按键驱动 key_drv_io / key_drv_ad"]
DAC["DAC 音频输出"]
FLASH[("SPI Flash<br/>音色库 + /dir_midi")]
end
subgraph sg_MSG["消息层"]
MSG["消息队列 get_msg / post_msg"]
end
subgraph sg_APP["应用模式层 (mbox_flash)"]
MODE_DEC["midi_dec_mode<br/>midi_decode_app"]
MODE_KEY["midi_keyboard_mode<br/>midi_keyboard_app"]
end
subgraph sg_DEC["解码器框架层"]
DEC_API["decoder_api<br/>decoder_io / decoder_stop"]
MIDI_CORE["midi 模块<br/>midi_dec / midi_event / midi_play"]
CTRL_API["list/midi_ctrl_api<br/>midi_ctrl_note_on 等"]
end
KEY -->|"按键事件"| MSG
MSG --> MODE_DEC
MSG --> MODE_KEY
MODE_DEC -->|"BIT_MIDI"| DEC_API
MODE_KEY -->|"BIT_MIDI_CTRL"| DEC_API
DEC_API --> MIDI_CORE
DEC_API --> CTRL_API
CTRL_API --> FLASH
MIDI_CORE --> DAC
各层职责:
- 应用模式层(
sdk/app/src/mbox_flash/midi_dec/、midi_keyboard/):定义工作模式入口、消息循环、按键表选择(key_table_sel)与电源/空闲策略。 - 解码器框架层(
sdk/app/bsp/common/decoder/):decoder_io()创建解码对象(dec_obj),decoder_stop()停止;decoder_api.h提供audio_decoder_ops,通过dec_confing命令字下发CMD_MIDI_MELODY_TRIGGER回调。 - MIDI 核心模块(
sdk/app/bsp/modules/midi/):真正的波表解码与发声控制。midi_ctrl_api是键盘模式的"演奏入口",midi_dec是文件解码的"播放入口"。 - 数据流方向:按键事件 → 消息 → 应用模式 → 解码器 → 采样 Zone 查表(Flash)→ DAC 输出;音符起止同时通过回调反馈给应用层做空闲计数。
解码器核心数据结构(midi_dec.h)
midi_dec.h 是解码器核心头文件,定义了波表解码所需的全部类型,是理解两种模式底层机制的关键。下面按类别说明(源码见 midi_dec.h)。
采样压缩格式
每个采样 Zone 以固定压缩格式存放于 Flash,KIND 宏即格式编号,后四位为"降采样"版本(_DOWN),括号内为压缩比:
| 宏 | 值 | 含义 |
|---|---|---|
PCM_KIND | 0 | PCM 原始数据 |
ALAW_KIND | 1 | A-Law 压缩(1:2) |
ADPCM_KIND | 2 | ADPCM 压缩(1:3) |
ADPCM4_KIND | 3 | 4-bit ADPCM(1:4) |
PCM_KIND_DOWN | 4 | PCM 降采样(1:2) |
ADPCM4_KIND_DOWN | 7 | ADPCM4 降采样(1:8) |
设计意图:音色库体积直接决定产品 Flash 成本,用 ADPCM/降采样组合可以在音质与容量间折中;CMP_KIND_END(8)作为格式表结束哨兵。
解码错误码
enum {
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 = 0x44,
MAD_ERROR_FF_FR_END = 0x45,
MAD_ERROR_FF_FR_FILE_START = 0x46,
MAD_ERROR_LIMIT = 0x47,
MAD_ERROR_NODATA = 0x48
};
Source: midi_dec.h
错误码与通用解码框架(MAD_ERROR_* 家族)对齐:MAD_ERROR_MIDI 表示 MIDI 格式本身非法;FF_FR_* 系列对应快进/快退(FF/FR)到文件边界;MAD_ERROR_NODATA 表示输入缓冲无数据。
关键类型速览
| 类型 | 作用 |
|---|---|
MIDI_INPUT_CONTROL | 输入环形缓冲控制:input_buf[INBUF_SIZE](700 字节)、remain、rd_pos |
MIDI_TRACK_CONTROL | 轨道控制,仅记录 deltat(delta 时间,用于事件定时) |
Zone_t | 音色采样区描述:sampleMap(Flash 地址位图)、loopStart/loopLen/tableEnd、音量包络 vol_incr/vol_dec/vol_hold、cents(音分微调)、keyNum、pan |
Voice_t | 发声声部:isend、indexIncr(变调增量)、index(采样相位)、wavebuf |
ENV | 音量包络状态机:env_use、env_points、stepcnt/stepval |
WaveInfo_t | 单个音符的完整发声状态:轨道/键号/力度、采样表指针、循环点、声像 panLft/panRgt(2^7 归一化)、衰减 ctrlAtten/initAtten、包络与 vol_array[MIDI_OBUF_BLOCK](32 点音量块) |
MIDI_PLAYER | 播放器实例,内嵌 WaveInfo_t |
MIDI_CHANNEL_CONTROL | 每通道 CC 控制器数组 cc[MIDI_total_CC](sustain、expression、volume、soft、sostenuto、modulation、pan) |
MidiMarkInfo | A-B 复读标记:mark_start/end、loop_enable、mark_enable |
通道控制枚举(midi_dec.h)将常用 CC 控制器映射为固定索引:
enum {
MIDI_CTRL_SOS_ON_CC = 0x00, //sustain
MIDI_CTRL_EXPR_CC,
MIDI_CTRL_VOL_CC,
MIDI_CTRL_SFT_ON_CC, //vol
MIDI_CTRL_SUS_ON_CC,
MIDI_CTRL_MOD_CC,
MIDI_CTRL_PAN_CC,
MIDI_total_CC
};
Source: midi_dec.h
常量约束(midi_dec.h):MAX_CHANNEL_NUM = 16(MIDI 标准通道数)、MAX_TRACK_NUM = 16、MIDI_OBUF_BLOCK = 32、INBUF_SIZE = 700;重采样采用定点移位 MIDI_RESAMPLE_SHIFT = 13;MIDI_SwitchLevel_CC = 64 是延音踏板 CC64;MAX_GO_BACK = 8 是跳变回退位数。
MIDI 解码播放模式(midi_decode_app)
入口位于 midi_dec_mode.c,由宏 DECODER_MIDI_EN 编译开关控制。该模式把 MIDI 文件当作"曲目"顺序播放,是文件解码(BIT_MIDI)而非实时演奏(BIT_MIDI_CTRL)。
初始化与播放控制块
static play_control midi_pctl[2] AT(.midi_buf);
static u8 app_midi_mode AT(.midi_buf);
static const char *const dir_midi_tab[] = {
"/dir_midi",
};
static const char *const dir_tab_a[] = {
"/dir_a",
};
Source: midi_dec_mode.c
midi_pctl[2] 是双播放控制块:midi_pctl[0] 承载 MIDI 曲目,midi_pctl[1] 预留为 A 音(BIT_A,/dir_a,loop = 255 无限循环),但代码中 post_msg(1, MSG_A_PLAY) 被注释,即默认未启用,保留扩展能力。两个控制块均显式置于 .midi_buf 段,保证该模式独占的静态内存落在专用 RAM 段,便于内存规划与低功耗管理。
播放链路装配
decoder_init();
memset(&midi_pctl[0], 0, sizeof(midi_pctl));
midi_pctl[0].dev_index = INNER_FLASH_RO;
midi_pctl[0].findex = 1;
midi_pctl[0].dec_type = BIT_MIDI;
midi_pctl[0].pdir = (void *)&dir_midi_tab[0];
midi_pctl[0].dir_total = sizeof(dir_midi_tab) / 4;
simple_dev_fs_mount(&midi_pctl[0]);
post_msg(1, MSG_PLAY_FILE1);
Source: midi_dec_mode.c
关键点:
dec_type = BIT_MIDI告知框架创建 MIDI 解码器实例;INNER_FLASH_RO表示只读片内 Flash 设备。simple_dev_fs_mount()为控制块挂载文件系统后,post_msg(1, MSG_PLAY_FILE1)以自投消息方式进入播放——这种"消息驱动初始化"保证与主循环的get_msg处理顺序一致,避免在初始化阶段直接阻塞。findex = 1表示从目录内第 1 个文件开始播。
主消息循环
while(1) 循环先 get_msg(2, &msg[0]) 取消息,随后 bsp_loop() 保持系统心跳,再按 msg[0] 分发:
MSG_MIDI_MODE_SWITCH:在CMD_MIDI_CTRL_MODE_0(NORM 普通模式)与CMD_MIDI_CTRL_MODE_1(OKON 模式)之间翻转,并调用midi_decode_set_mode(app_midi_mode)下发到解码器。这是"OKON"(一键开启/一键关闭?)行为切换的开关。MSG_MIDI_OKON_GOON:仅当处于CMD_MIDI_CTRL_MODE_1时调用midi_decode_okon_goon()继续 OKON 播放。MSG_PLAY_FILE1:simple_play_file_byindex(&midi_pctl[0])按索引播放;返回非 0(无文件)则投递MSG_NEXT_FILE走下一曲逻辑。MSG_PP:decoder_pause(midi_pctl[0].p_dec_obj)暂停/继续。MSG_PREV_FILE:findex--,归零时回绕到ftotal,goto __midi_dec_play_file_entry复用播放入口(goto在此是嵌入式消息循环的常见写法,避免重复代码段)。MSG_CHANGE_WORK_MODE:退出当前模式。
模式入口首先 vm_write(VM_INDEX_SYSMODE, &work_mode, ...) 把工作模式持久化到 VM 区,保证掉电重启后恢复;若 midi_decode_init() 失败,则 work_mode++ 直接跳到下一个工作模式,避免设备"卡死"在不可用模式。
MIDI 键盘演奏模式(midi_keyboard_app)
入口位于 midi_keyboard_mode.c,由宏 DECODER_MIDI_KEYBOARD_EN 控制。这是本页的核心:把物理按键变成电子琴琴键。
初始化序列
err = midi_ctrl_decode_init();
if (err) {
log_info("midi_keyboard_init fail!\n");
work_mode++;
return;
}
decoder_init();
midi_keyboard_obj = decoder_io(NULL, BIT_MIDI_CTRL, NULL, 0);
if (NULL == midi_keyboard_obj) {
log_info("midi_keyboard init fail!\n");
work_mode++;
return;
}
midi_on_off_callback_init(midi_keyboard_obj, \
midi_ctrl_melody_trigger, \
midi_ctrl_melody_stop_trigger);
Source: midi_keyboard_mode.c
初始化顺序体现依赖关系:先 midi_ctrl_decode_init() 初始化控制解码器并加载音色库索引,再 decoder_init() 初始化解码框架,最后 decoder_io(NULL, BIT_MIDI_CTRL, NULL, 0) 创建控制型解码对象。注意解码类型是 BIT_MIDI_CTRL(实时控制)而非 BIT_MIDI(文件解码)——控制型对象不读文件,直接接受 midi_ctrl_* 事件。
midi_on_off_callback_init 通过 audio_decoder_ops->dec_confing() 下发 CMD_MIDI_MELODY_TRIGGER 命令,注册音符起止回调,实现"演奏状态反馈":
static void midi_on_off_callback_init(dec_obj *obj, u32(*melody_callback)(void *, u8, u8), u32(*melody_stop_callback)(void *, u8, u8))
{
audio_decoder_ops *ops = (audio_decoder_ops *)obj->dec_ops;
EX_MELODY_STRUCT melody_parm;
melody_parm.priv = obj;
melody_parm.melody_trigger = melody_callback;
ops->dec_confing(obj->p_dbuf, CMD_MIDI_MELODY_TRIGGER, &melody_parm);
...
Source: midi_keyboard_mode.c
通道音色表
const u8 Channal_Prog_Tab[CTRL_CHANNEL_NUM] = {
/* 标准乐器0~8 */
MIDI_PROG_ACOUSTIC_GRAND_PIANO,
MIDI_PROG_MUSIC_BOX_CHROMATIC_PERCUSSION,
MIDI_PROG_HARMONICA_ORGAN,
MIDI_PROG_NYLON_ACOUSTIC_GUITAR,
MIDI_PROG_ACOUSTIC_BASS,
MIDI_PROG_VIOLIN_SOLO_STRINGS,
MIDI_PROG_TRUMPET_BRASS,
MIDI_PROG_SOPRANO_SAX_REED,
MIDI_PROG_TAIKO_DRUM_PERCUSSIVE,
/* 乐器号>=128的打击乐器,只能用通道9 */
MIDI_PROG_PERCUSSION_STANDARD,
/* 标准乐器10~15 */
MIDI_PROG_TUBULAR_BELLS_CHROMATIC_PERCUSSION,
MIDI_PROG_ORCHESTRAL_HARP_SOLO_STRINGS,
MIDI_PROG_FLUTE_PIPE,
MIDI_PROG_BANJO_ETHNIC,
MIDI_PROG_SHANAI_ETHNIC,
MIDI_PROG_TELEPHONE_RING_SOUND_EFFECTS,
};
Source: midi_keyboard_mode.c
设计要点:
- 数组长度即
CTRL_CHANNEL_NUM,对应 MIDI 标准 16 通道(含 0~15)。 - 通道 9 特殊处理:MIDI 规范中通道 10(索引 9)专用于打击乐,音色号需减 128(
Channal_Prog_Tab[9] - 128),因为MIDI_PROG_PERCUSSION_STANDARD的枚举值按 >=128 编码。初始化循环里显式区分了这一点(见 midi_keyboard_mode.c)。 - 源码注释明确:demo 音色库只有钢琴支持全键区,其余乐器 key 范围仅 [60, 71]——这是波表容量限制下的产品化取舍。
- 初始化完成后会先
midi_ctrl_note_on(..., 60, 127, 0)弹一个中央 C 提示音,给用户开机反馈。
按键消息 → 音符事件
主循环将按键消息翻译为 MIDI 事件。音符计算采用 key_offset + scale * 12:scale = 5 时基准音为 60(中央 C,C4),Do/Re/Mi/Fa 的 offset 分别为 0/2/4/5:
switch (msg[0]) {
case MSG_MIDICTRL_NOTE_ON_DO:
log_info("Do 1");
key_offset = 0;
goto __midikey_note_on;
case MSG_MIDICTRL_NOTE_ON_RE:
log_info("Re 2");
key_offset = 2;
goto __midikey_note_on;
case MSG_MIDICTRL_NOTE_ON_MI:
log_info("Mi 3");
key_offset = 4;
goto __midikey_note_on;
case MSG_MIDICTRL_NOTE_ON_FA:
log_info("Fa 4");
key_offset = 5;
__midikey_note_on:
midi_ctrl_note_on(midi_keyboard_obj->p_dbuf, \
key_offset + scale * 12, \
127, \
cur_channal);
Source: midi_keyboard_mode.c
goto合并公共路径:4 个音符消息共享__midikey_note_on标签,避免重复调用代码——嵌入式场景以可读性换代码体积。- 力度固定 127(最大值),简化按键扫描(GPIO 无法表达力度),产品若要力度感应需换 AD 按键。
MSG_MIDICTRL_NOTE_OFF_*对称处理midi_ctrl_note_off(..., 0);源码注释强调"部分乐器需要 NOTE_OFF 才会停止发声"(如管风琴类长音音色),因此按键释放必须映射为显式 NOTE_OFF 事件。
弯音与通道切换
case MSG_MIDICTRL_PITCH_BEND_DOWN:
pitch_val -= 16;
goto __midikeypitch_bend;
case MSG_MIDICTRL_PITCH_BEND_UP:
pitch_val += 16;
__midikeypitch_bend:
midi_ctrl_pitch_bend(midi_keyboard_obj->p_dbuf, pitch_val, cur_channal);
log_info("Pitch Bend : %d\n", pitch_val);
break;
case MSG_MIDICTRL_CHANNAL_PREV:
cur_channal--;
if (cur_channal >= CTRL_CHANNEL_NUM) {
cur_channal = CTRL_CHANNEL_NUM - 1;
}
goto __midikey_channal_sel;
case MSG_MIDICTRL_CHANNAL_NEXT:
cur_channal++;
if (cur_channal >= CTRL_CHANNEL_NUM) {
cur_channal = 0;
}
__midikey_channal_sel:
log_info("Channal:%d, Prog:%d\n", cur_channal, Channal_Prog_Tab[cur_channal]);
break;
Source: midi_keyboard_mode.c
弯音初值 pitch_val = 256(对应无弯音中点),每次按键 ±16,可叠加演奏滑音。通道切换用无符号回绕技巧:cur_channal-- 后若 >= CTRL_CHANNEL_NUM 说明下溢,回绕到最大值;cur_channal++ 越界则归零。
演奏状态回调与空闲处理
/* 音符开始回调 */
static u32 midi_ctrl_melody_trigger(void *priv, u8 key, u8 vel)
{
log_info("ON %d %d\n", key, vel);
midi_keyboard_idle_cnt++;
return 0;
}
/* 音符结束回调 */
static u32 midi_ctrl_melody_stop_trigger(void *priv, u8 key, u8 chn)
{
log_info("OFF %d %d\n", key, chn);
midi_keyboard_idle_cnt--;
if (0 == midi_keyboard_idle_cnt) {
dec_obj *obj = (dec_obj *)priv;
obj->sound.enable |= B_DEC_PAUSE;
}
return 0;
}
Source: midi_keyboard_mode.c
midi_keyboard_idle_cnt 统计正在发声的音符数:所有音符结束时(cnt 归 0),通过 obj->sound.enable |= B_DEC_PAUSE 让解码器进入暂停态,降低无演奏时的功耗;而 MSG_500MS 定时消息里根据该计数决定 app_powerdown_deal(0)(进入掉电)还是 (1)(维持运行),实现"无人弹奏自动关机"的产品行为。
键盘按键映射(midi_keyboard_mode_key.c)
按键表位于 midi_keyboard_mode_key.c,由 KEY_IO_EN 控制,通过 key_table_sel(midi_keyboard_key_msg_filter) 注册到按键驱动。按键行为分五类:
| 按键表宏 | 触发方式 | 默认映射 |
|---|---|---|
IOKEY_MIDI_KEYBOARD_SHORT_UP | 短按抬起 | IO0→MSG_MIDICTRL_PITCH_BEND_DOWN,IO1→MSG_MIDICTRL_PITCH_BEND_UP |
IOKEY_MIDI_KEYBOARD_LONG | 长按 | IO0→MSG_MIDICTRL_CHANNAL_NEXT,IO1→MSG_NEXT_WORKMODE |
IOKEY_MIDI_KEYBOARD_HOLD | 按住保持 | 全部 NO_MSG |
IOKEY_MIDI_KEYBOARD_LONG_UP | 长按抬起 | 全部 NO_MSG |
IOKEY_MIDI_KEYBOARD_SHORT | 短按 | 全部 NO_MSG |
IOKEY_MIDI_KEYBOARD_DOUBLE_KICK / TRIPLE_KICK | 双击/三击(KEY_DOUBLE_CLICK_EN) | 全部 NO_MSG |
示例(短按抬起用于弯音):
#define IOKEY_MIDI_KEYBOARD_SHORT_UP \
/*00*/ MSG_MIDICTRL_PITCH_BEND_DOWN,\
/*01*/ MSG_MIDICTRL_PITCH_BEND_UP,\
/*02*/ NO_MSG,\
/*03*/ NO_MSG,\
/*04*/ NO_MSG,\
/*05*/ NO_MSG,\
/*06*/ NO_MSG,\
/*07*/ NO_MSG,\
/*08*/ NO_MSG,\
/*09*/ NO_MSG,
Source: midi_keyboard_mode_key.c
注意:琴键(Do/Re/Mi/Fa 的 NOTE_ON/OFF)消息不在 IO 按键表中——按键表将 IO 映射为控制消息,而音符消息(MSG_MIDICTRL_NOTE_ON_* 等)由 midi_keyboard_key_msg_filter 消息过滤器按按键事件类型生成,琴键功能与 IO 口解耦,方便复用到矩阵键盘(key_matrix)或触摸按键(key_touch)等不同输入源。midi_dec 侧对应的按键过滤函数为 midi_dec_key_msg_filter(见 midi_dec_mode_key.c)。
Core Flow:键盘演奏端到端时序
下面用序列图展示"按下一个琴键 → 发声 → 松开停止"的完整时序,这是 midi_keyboard_app 的核心路径:
sequenceDiagram
participant K as 按键驱动 (key)
participant A as midi_keyboard_app
participant C as midi_ctrl_api
participant D as MIDI 解码核心 (midi_dec)
participant CB as 回调 melody_trigger
K->>A: MSG_MIDICTRL_NOTE_ON_DO
A->>A: key_offset = 0 → 60 号音 (scale=5)
A->>C: midi_ctrl_note_on(p_dbuf, 60, 127, chn)
C->>D: 查 Zone 表并启动采样变调播放
D->>CB: 音符开始回调
CB->>A: midi_keyboard_idle_cnt++
Note over D: DAC 持续输出波形
K->>A: MSG_MIDICTRL_NOTE_OFF_DO
A->>C: midi_ctrl_note_off(p_dbuf, 60, chn, 0)
C->>D: 停止采样(部分乐器走释放包络)
D->>CB: 音符结束回调
CB->>A: midi_keyboard_idle_cnt--
alt idle_cnt == 0
CB->>A: obj->sound.enable |= B_DEC_PAUSE
end
关键时序语义:
- 按键驱动把按键事件转成
MSG_MIDICTRL_NOTE_ON_*消息,应用模式在消息循环中翻译为"键号 = offset + scale×12"。 midi_ctrl_note_on走BIT_MIDI_CTRL控制链路:解码核心查Instr_s.zoneMap[128]得到音色 Zone 的 Flash 地址,以Voice_t.indexIncr定点重采样推进采样相位,经vol_array音量块与panLft/panRgt声像处理后送 DAC。- 回调机制(
CMD_MIDI_MELODY_TRIGGER)把"实际发声状态"回传应用层——注意回调在解码线程/中断上下文执行,只做计数,不做事关时序的操作,避免阻塞发声。 - 全部音符结束后解码器自动暂停(
B_DEC_PAUSE),为低功耗待机创造条件。
Core Flow:MIDI 文件解码播放流程
flowchart TD
Start([进入 midi_decode_app]) --> VM["vm_write 持久化 work_mode"]
VM --> KT["key_table_sel(midi_dec_key_msg_filter)"]
KT --> INIT["midi_decode_init"]
INIT -->|"失败"| FAIL["work_mode++ 跳到下一模式"]
INIT -->|"成功"| DECINIT["decoder_init"]
DECINIT --> P0["配置 midi_pctl[0]<br/>dev=INNER_FLASH_RO, dec_type=BIT_MIDI, dir=/dir_midi"]
P0 --> MOUNT["simple_dev_fs_mount"]
MOUNT --> POST["post_msg MSG_PLAY_FILE1"]
POST --> LOOP["get_msg 消息循环 + bsp_loop"]
LOOP --> SW{"消息类型"}
SW -->|"MSG_PLAY_FILE1"| PLAY["simple_play_file_byindex"]
PLAY -->|"返回非0 无文件"| NEXT["post_msg MSG_NEXT_FILE"]
SW -->|"MSG_MIDI_MODE_SWITCH"| MODE{"当前模式"}
MODE -->|"NORM"| M1["切 OKON (CMD_MIDI_CTRL_MODE_1)"]
MODE -->|"OKON"| M0["切 NORM (CMD_MIDI_CTRL_MODE_0)"]
M1 --> SETMODE["midi_decode_set_mode"]
M0 --> SETMODE
SW -->|"MSG_MIDI_OKON_GOON"| GOON["OKON 模式下 midi_decode_okon_goon"]
SW -->|"MSG_PP"| PP["decoder_pause(p_dec_obj)"]
SW -->|"MSG_PREV_FILE"| PREV["findex--, 归零回绕 ftotal"]
PREV --> PLAY
SW -->|"MSG_CHANGE_WORK_MODE"| EXIT([退出模式])
SETMODE --> LOOP
GOON --> LOOP
PP --> LOOP
NEXT --> LOOP
流程要点:
- 消息自启动:初始化末尾
post_msg(1, MSG_PLAY_FILE1)使首曲播放与后续按键触发的播放走完全相同的代码路径,消除"首次播放"分支。 - 模式切换:
app_midi_mode全局变量记忆 NORM/OKON 状态,MSG_MIDI_MODE_SWITCH每次翻转后通过midi_decode_set_mode()同步解码器;OKON 的"继续播放"由独立的midi_decode_okon_goon()处理,仅在 OKON 模式生效。 - 错误降级:
midi_decode_init失败即work_mode++切换模式并返回,get_msg出错则按NO_MSG继续循环——任何单点失败都不会挂死整机。
配置选项
MIDI 子系统由编译开关与运行参数两级配置控制:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DECODER_MIDI_EN | 编译宏 | 0(按工程配置) | 使能 MIDI 解码播放模式(midi_decode_app) |
DECODER_MIDI_KEYBOARD_EN | 编译宏 | 0(按工程配置) | 使能 MIDI 键盘演奏模式(midi_keyboard_app) |
MIDI_VIBRATO_ENABLE | 宏 | 0 | 琴键颤音功能开关(开启后 note_on 追加 midi_ctrl_vel_vibrate) |
CTRL_CHANNEL_NUM | 宏 | 16 | 控制通道数,即 Channal_Prog_Tab 长度 |
scale | 运行变量 | 5 | 八度基准:key_offset + scale*12 计算实际 MIDI 键号 |
pitch_val 初值 | 运行变量 | 256 | 弯音中点值,每按一次 ±16 |
Channal_Prog_Tab[] | 常量数组 | 16 乐器 | 各通道音色(Program Change)配置,通道 9 为打击乐 |
| 解码格式 | 宏 | ADPCM_KIND 等 | 音色库采样压缩格式(midi_dec.h *_KIND) |
INBUF_SIZE | 宏 | 700 | 解码器输入缓冲字节数 |
MAX_GO_BACK | 宏 | 8 | 跳变回退位数(MAX_GO_VAL = (1<<8)-1) |
运行参数(scale、pitch_val、cur_channal)均为 midi_keyboard_app 栈上局部变量或 .midi_ctrl_buf 段全局量,掉电不保存;工作模式本身通过 VM_INDEX_SYSMODE 持久化。
API Reference
以下接口形态均依据本页引用的调用点归纳;未读取实现的函数仅标注其调用形态与语义,不臆造参数细节。
MIDI 控制接口(键盘演奏,midi_ctrl_api)
| 函数 | 调用形态(来自源码调用点) | 说明 |
|---|---|---|
midi_ctrl_decode_init() | err = midi_ctrl_decode_init() | 初始化控制型 MIDI 解码器并加载音色库;返回非 0 表示失败 |
midi_ctrl_set_prog(buf, prog, chn) | midi_ctrl_set_prog(obj->p_dbuf, Channal_Prog_Tab[n], n) | 设置通道 chn 的音色(Program Change);打击乐通道传入 prog - 128 |
midi_ctrl_note_on(buf, key, vel, chn) | midi_ctrl_note_on(obj->p_dbuf, key_offset + scale*12, 127, cur_channal) | 触发 key 号音符,vel 力度(0~127),在 chn 通道发声 |
midi_ctrl_note_off(buf, key, chn, arg) | midi_ctrl_note_off(obj->p_dbuf, key, cur_channal, 0) | 停止 key 号音符;部分乐器依赖此事件结束长音 |
midi_ctrl_pitch_bend(buf, val, chn) | midi_ctrl_pitch_bend(obj->p_dbuf, pitch_val, cur_channal) | 弯音;pitch_val=256 为无弯音中点 |
midi_ctrl_vel_vibrate(buf, key, a, b, chn) | midi_ctrl_vel_vibrate(obj->p_dbuf, key, 2, 9, cur_channal) | 力度颤音(MIDI_VIBRATO_ENABLE 时启用),参数 2/9 为调用点常量 |
MIDI 文件解码接口(解码播放,midi_api)
| 函数 | 调用形态 | 说明 |
|---|---|---|
midi_decode_init() | err = midi_decode_init() | 初始化文件解码器;失败返回非 0 |
midi_decode_set_mode(mode) | midi_decode_set_mode(app_midi_mode) | 设置 NORM(CMD_MIDI_CTRL_MODE_0)/ OKON(CMD_MIDI_CTRL_MODE_1)模式 |
midi_decode_okon_goon() | 仅 OKON 模式调用 | 继续 OKON 播放 |
解码器框架接口(decoder_api)
| 函数 | 调用形态 | 说明 |
|---|---|---|
decoder_init() | decoder_init() | 初始化解码框架 |
decoder_io(NULL, BIT_MIDI_CTRL, NULL, 0) | 返回 dec_obj * | 创建解码对象;BIT_MIDI 为文件解码、BIT_MIDI_CTRL 为控制解码;返回 NULL 表示失败 |
decoder_stop(obj, NEED_WAIT) | 模式退出时调用 | 停止并释放解码对象 |
decoder_pause(p_dec_obj) | MSG_PP 处理 | 暂停/继续播放 |
dec_confing(p_dbuf, CMD_MIDI_MELODY_TRIGGER, &melody_parm) | 经 audio_decoder_ops 调用 | 注册音符起止回调 EX_MELODY_STRUCT{priv, melody_trigger, melody_stop_trigger} |
应用层辅助接口
| 函数 | 调用形态 | 说明 |
|---|---|---|
key_table_sel(filter) | key_table_sel(midi_keyboard_key_msg_filter) | 注册按键消息过滤器;NULL 注销 |
vm_write(VM_INDEX_SYSMODE, &work_mode, sizeof(work_mode)) | 模式入口 | 持久化当前工作模式 |
simple_dev_fs_mount(pctl) / simple_play_file_byindex(pctl) | 播放控制块 | 挂载文件系统 / 按索引播放 |
get_msg(2, &msg[0]) + bsp_loop() | 主循环 | 取消息并维持系统心跳 |
app_powerdown_deal(0/1) | MSG_500MS 处理 | 空闲掉电/维持运行决策 |
ap_handle_hotkey(msg[0]) | 默认分支 | 兜底处理未识别热键消息 |
Usage Examples 汇编
示例 1:弹奏一个音符(Do,中央 C)
键盘模式按下 Do 键的完整处理:消息 MSG_MIDICTRL_NOTE_ON_DO → 计算键号 60 → midi_ctrl_note_on。力度固定 127,通道为当前选择通道。
case MSG_MIDICTRL_NOTE_ON_DO:
log_info("Do 1");
key_offset = 0;
goto __midikey_note_on;
...
__midikey_note_on:
midi_ctrl_note_on(midi_keyboard_obj->p_dbuf, \
key_offset + scale * 12, \
127, \
cur_channal);
Source: midi_keyboard_mode.c
示例 2:启动时配置 16 通道音色
初始化循环把 Channal_Prog_Tab 下发到各通道;通道 9(打击乐)音色号减 128,符合 MIDI 规范中"打击乐专用通道"约定。
for (int chn_num = 0; chn_num < CTRL_CHANNEL_NUM; chn_num++) {
if (9 == chn_num) {
midi_ctrl_set_prog(midi_keyboard_obj->p_dbuf, Channal_Prog_Tab[chn_num] - 128, chn_num);
} else {
midi_ctrl_set_prog(midi_keyboard_obj->p_dbuf, Channal_Prog_Tab[chn_num], chn_num);
}
}
midi_ctrl_note_on(midi_keyboard_obj->p_dbuf, 60, 127, 0);
Source: midi_keyboard_mode.c
示例 3:注册音符起止回调
回调把解码器"实际发声状态"反馈给应用层,用于空闲计数与自动掉电判断。
midi_on_off_callback_init(midi_keyboard_obj, \
midi_ctrl_melody_trigger, \
midi_ctrl_melody_stop_trigger);
Source: midi_keyboard_mode.c
示例 4:配置 MIDI 曲目播放控制块
解码模式装配 play_control:指定只读片内 Flash、BIT_MIDI 解码类型与 /dir_midi 目录后挂载并自投播放消息。
midi_pctl[0].dev_index = INNER_FLASH_RO;
midi_pctl[0].findex = 1;
midi_pctl[0].dec_type = BIT_MIDI;
midi_pctl[0].pdir = (void *)&dir_midi_tab[0];
midi_pctl[0].dir_total = sizeof(dir_midi_tab) / 4;
simple_dev_fs_mount(&midi_pctl[0]);
post_msg(1, MSG_PLAY_FILE1);
Source: midi_dec_mode.c
示例 5:按键表注册弯音/切通道
IO 按键短按抬起映射为弯音,长按映射为通道切换与模式退出;琴键音符消息由过滤器单独生成。
#define IOKEY_MIDI_KEYBOARD_SHORT_UP \
/*00*/ MSG_MIDICTRL_PITCH_BEND_DOWN,\
/*01*/ MSG_MIDICTRL_PITCH_BEND_UP,\
...
#define IOKEY_MIDI_KEYBOARD_LONG \
/*00*/ MSG_MIDICTRL_CHANNAL_NEXT,\
/*01*/ MSG_NEXT_WORKMODE,\
...
Source: midi_keyboard_mode_key.c
Failure Modes、边界与并发
初始化失败降级
两个模式入口对初始化失败的处理一致:work_mode++ 并立即返回(midi_keyboard_mode.c、midi_dec_mode.c)。work_mode 是全局工作模式号,自增后由系统切换到下一个可用模式,避免因音色库缺失、Flash 损坏等原因导致设备无法启动。代价是用户可能"莫名"进入其它模式,这是嵌入式场景"宁可换模式不可死机"的取舍。
解码错误码
解码器返回 MAD_ERROR_* 家族错误码(midi_dec.h):MAD_ERROR_MIDI 表示 MIDI 文件格式非法;MAD_ERROR_FILE_END / FF_FR_* 对应文件末尾与快进快退边界;MAD_ERROR_DISK_ERR / FILESYSTEM_ERR 对应存储层故障;MAD_ERROR_NODATA 表示输入缓冲无数据(常见于码流中断)。格式检查另有 FORMAT_OK / FORMAT_OK_BUT_NO_SUPPORT / FORMAT_ERR 三态(midi_dec.h),区分"完全支持/能解但不支持部分特性/无法解析"。
边界条件
- 无曲目文件:
simple_play_file_byindex返回非 0 即投递MSG_NEXT_FILE,循环寻找可播文件。 - 上一曲回绕:
findex == 0时回绕到ftotal,目录是环形列表语义。 - 通道回绕:
cur_channal无符号回绕(--下溢回最大值,++越界归零),始终落在[0, CTRL_CHANNEL_NUM)。 - demo 音色库键区限制:除钢琴外乐器 key 仅 [60, 71],超出范围的
note_on无声——扩展音色库时需同步扩展 Zone 表。 - 打击乐通道:通道 9 必须使用
prog - 128的打击乐音色号,否则按普通乐器解释。 - 长音音色:不发送
note_off时管风琴等音色不会停止,按键释放事件不可省略。
并发模型
系统是单线程消息循环模型:get_msg → 分发 → bsp_loop,所有 midi_ctrl_* 调用均在该上下文执行,天然串行,无锁竞争。唯一跨上下文的是 CMD_MIDI_MELODY_TRIGGER 回调(解码器上下文调用),因此回调内只做计数与标志位操作(midi_keyboard_idle_cnt++/--、enable |= B_DEC_PAUSE),不做阻塞操作,符合"中断/解码上下文最小化"原则。
资源与内存
静态缓冲通过段指令固定放置:midi_keyboard_obj/idle_cnt/function_switch 在 .midi_ctrl_buf 段(midi_keyboard_mode.c),midi_pctl[2]/app_midi_mode 在 .midi_buf 段(midi_dec_mode.c),函数代码段分别落在 .midi_keyboard_mode.text 与 .midi_dec_mode.text。这样做的目的是让两种互斥的工作模式共享同一块 RAM/Flash 段——同一时刻只有一个模式运行,段复用可显著节省内存,代价是两种模式不能同时使能。
Performance 与运维
- 定点重采样:变调播放用定点增量
indexIncr与MIDI_RESAMPLE_SHIFT = 13位精度(midi_dec.h),避免浮点运算,适合无 FPU 的 MCU。 - 音量块批处理:
vol_array[MIDI_OBUF_BLOCK](32 点)按块计算包络,减少逐样本开销;panLft/panRgt以 2^7 定点声像。 - Flash 直接寻址:
Instr_s.zoneMap[128]存 Zone 的 Flash 地址,播放时直接读采样,无加载到 RAM 的整库拷贝,降低启动时间与 RAM 占用。 - 低功耗:音符全部结束后解码器置
B_DEC_PAUSE;MSG_500MS定时检查midi_keyboard_idle_cnt,空闲时vm_pre_erase()后app_powerdown_deal(0)掉电,运行时仅(1)维持。 - 调试手段:全程
log_info输出("Do 1"、"ON %d %d"、"Pitch Bend : %d"、"Channal:%d, Prog:%d"),可在LOG_TAG "[midi_dec]"下过滤跟踪。
Extension Points
- 更换/扩充音色:修改
Channal_Prog_Tab(MIDI_PROG_*枚举定义于 midi_prog.h);如需全键区演奏需扩充音色库 Zone 表并同步midi_config.c。 - 增加琴键:在按键过滤器中新增
MSG_MIDICTRL_NOTE_ON_XX消息及对应 case(仿 Do/Re/Mi/Fa 四键),或接入矩阵键盘(key_matrix)/触摸(key_touch)输入源。 - 八度切换:修改
scale(当前固定 5),可加按键消息实现高低八度演奏。 - 颤音:置
MIDI_VIBRATO_ENABLE = 1启用midi_ctrl_vel_vibrate力度颤音。 - 背景音:
midi_pctl[1]已预留BIT_A无限循环播放块(loop = 255),放开注释的MSG_A_PLAY即可实现"演奏时播放伴奏/背景音"。 - OKON 模式:
MSG_MIDI_MODE_SWITCH/midi_decode_okon_goon提供弹奏跟随(OKON)与普通播放的双态扩展。
Tests
仓库未发现针对 MIDI 模式的自动化测试用例(midi_* 目录下无 test 文件)。验证依赖两类手段:一是运行时 log_info 日志(消息到达、音符 ON/OFF、弯音值、通道切换均有打印);二是 demo 板实测(钢琴全键区、其余乐器 [60,71] 键区行为、掉电后 VM_INDEX_SYSMODE 恢复)。新增按键或音色配置后,建议按"初始化 → 单键弹奏 → 多键连弹 → 弯音 → 切通道 → 空闲掉电 → 唤醒"路径回归。
Related Links
- 源码入口:midi_dec.h、midi_keyboard_mode.c、midi_dec_mode.c、midi_keyboard_mode_key.c
- 接口与配置:midi_ctrl_api.c、midi_api.c、midi_config.c、midi_prog.h
- 解码器核心实现:midi_dec.c、midi_event.c、midi_play.c、MIDIDefs.h
- 官方说明文档:杰理AD1x-45678_MIDI应用说明文档.pdf
- 兄弟目录项:按键驱动(
sdk/app/bsp/common/key/)、解码器框架(decoder_api)、工作模式切换(work_mode与VM_INDEX_SYSMODE机制)请参考各自页面。