杰理 SDK 文档中心
首页
首页
  • 项目概览

    • AD23N SDK 概述与芯片平台
    • 工程结构与模块划分
  • 快速开始

    • 开发环境搭建与工具链
    • 编译构建指南
    • 烧录与固件升级工具
  • 应用框架与产品工作流

    • 应用入口与模式调度
    • 音乐播放应用
    • MIDI 解码与键盘演奏
    • 录音应用
    • LINEIN 与扩音应用
    • USB 从设备应用
    • 待机、软关机与空闲检测
    • 公共 UI 与 LED 显示
  • 音频子系统

    • 音频解码器框架
    • 音频编码器框架
    • 音效算法库
    • 音频管理与输出通路
  • 存储与文件系统

    • 文件系统层
    • NOR Flash 与虚拟机存储
    • 设备与设备管理
  • 系统服务与运行时

    • 消息机制与事件分发
    • 按键扫描与输入处理
    • 电源管理与低功耗控制
    • 定时器与系统任务
  • 外设驱动与平台

    • CPU 平台与启动流程
    • USB 协议栈与主机/设备驱动
    • SPI 与通用外设接口
  • 固件升级与构建工具

    • 固件升级机制
    • 编译后处理与镜像打包
    • 构建系统与命令行工具

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 持久化):

  1. 解码模式(DECODER_MIDI_EN 使能,midi_dec_mode.c):类似普通音乐播放器,但解码对象是 .mid 文件,目录固定为 /dir_midi,解码类型为 BIT_MIDI。
  2. 键盘模式(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_KIND0PCM 原始数据
ALAW_KIND1A-Law 压缩(1:2)
ADPCM_KIND2ADPCM 压缩(1:3)
ADPCM4_KIND34-bit ADPCM(1:4)
PCM_KIND_DOWN4PCM 降采样(1:2)
ADPCM4_KIND_DOWN7ADPCM4 降采样(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)
MidiMarkInfoA-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

关键时序语义:

  1. 按键驱动把按键事件转成 MSG_MIDICTRL_NOTE_ON_* 消息,应用模式在消息循环中翻译为"键号 = offset + scale×12"。
  2. midi_ctrl_note_on 走 BIT_MIDI_CTRL 控制链路:解码核心查 Instr_s.zoneMap[128] 得到音色 Zone 的 Flash 地址,以 Voice_t.indexIncr 定点重采样推进采样相位,经 vol_array 音量块与 panLft/panRgt 声像处理后送 DAC。
  3. 回调机制(CMD_MIDI_MELODY_TRIGGER)把"实际发声状态"回传应用层——注意回调在解码线程/中断上下文执行,只做计数,不做事关时序的操作,避免阻塞发声。
  4. 全部音符结束后解码器自动暂停(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

  1. 更换/扩充音色:修改 Channal_Prog_Tab(MIDI_PROG_* 枚举定义于 midi_prog.h);如需全键区演奏需扩充音色库 Zone 表并同步 midi_config.c。
  2. 增加琴键:在按键过滤器中新增 MSG_MIDICTRL_NOTE_ON_XX 消息及对应 case(仿 Do/Re/Mi/Fa 四键),或接入矩阵键盘(key_matrix)/触摸(key_touch)输入源。
  3. 八度切换:修改 scale(当前固定 5),可加按键消息实现高低八度演奏。
  4. 颤音:置 MIDI_VIBRATO_ENABLE = 1 启用 midi_ctrl_vel_vibrate 力度颤音。
  5. 背景音:midi_pctl[1] 已预留 BIT_A 无限循环播放块(loop = 255),放开注释的 MSG_A_PLAY 即可实现"演奏时播放伴奏/背景音"。
  6. 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 机制)请参考各自页面。
Prev
音乐播放应用
Next
录音应用