杰理 SDK 文档中心
首页
首页
  • 项目概览与快速开始

    • 项目概述与芯片支持
    • 环境搭建与工具链
    • 工程与构建系统
    • 烧录与升级工具
    • 文档与硬件资料
  • 系统架构与芯片平台

    • 芯片平台与启动流程
    • 预编译库与头文件体系
    • 消息、定时器与中断服务
    • 通用外设驱动
  • 存储与文件系统

    • 文件系统实现
    • 存储设备驱动
    • VM 参数存储系统
  • 音频处理

    • 音频解码器
    • 音频编码器
    • MIDI 合成与播放
    • 音效、变速变调与降噪
  • 语音玩具应用

    • 应用框架与状态机
    • 音乐播放与外部音源
    • MIDI 乐器模式
    • 录音应用
    • 待机、电源管理与 USB 从机
  • 小音箱应用

    • 应用框架与模式管理
    • 播放源:音乐、FM、录音与 LineIn
  • 应用层与示例工程

    • 通用 MCU 应用
  • 固件更新与补丁

    • 固件升级机制
    • AD14N 主动降噪补丁

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 子系统正是完成这一任务的软件合成器:

  1. 音色库(tone bank):预先生成、烧录在 SPI Flash 中的采样表(.mda/.bin 文件),每种乐器/按键对应一段压缩采样数据(支持 PCM/ALAW/ADPCM/ADPCM4 多种压缩格式)。
  2. 乐谱解码:读取 MIDI 文件(SMF 事件流),解析 delta-time、note on/off、控制变更(CC)、弯音(pitch bend)等事件。
  3. 实时合成:按采样率逐帧渲染,把每个活跃音符(voice/player)的采样混合输出,同时处理音量、声像、延音踏板、表情等控制参数。
  4. 解码器框架集成:作为 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

该函数的执行序列可以概括为:

  1. 校验音色库已加载(midi_tone_tab != 0),否则返回 E_MIDI_NO_CFG。
  2. 在关中断下清零全局解码器句柄 dec_midi_hld,避免多线程/中断竞争导致脏状态。
  3. 按需检查工作缓冲区:ops->need_dcbuf_size() 返回合成引擎所需的 RAM 大小(随 MAX_DEC_PLAYER_CNT 变化),若超过静态分配的 midi_decode_buff_nomark(位于 .midi_buf 段)则返回 E_MIDI_DBUF。这是 RAM 预算的第一道闸门。
  4. 初始化输出环形缓冲 cbuf_midi,把文件句柄、输出缓冲、操作表、事件表装配进 dec_obj。
  5. 格式检查:ops->format_check() 校验文件是否为支持的 MIDI 格式,失败返回 E_MIDIFORMAT。
  6. 读取 DAC 采样率并映射为引擎内部采样率索引(midi_musicsr_to_cfgsr,支持 48k/44.1k/32k/24k/22.05k/16k/12k/11.025k/8k)。
  7. 下发配置:MIDI_CONFIG_PARM 携带三个关键参数——player_t(最大同时发声数,即复音数)、sample_rate、spi_pos(音色库 Flash 地址),通过 CMD_INIT_CONFIG 传给引擎。注意 midi_init_info() 是 weak 函数,用户可覆写它来注入额外的模式/开关配置(mode_info、switch_info)。
  8. 注册 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

逐步说明:

  1. 初始化:midi_decode_init() 一次性完成音色库定位;失败不阻塞系统,后续播放返回 E_MIDI_NO_CFG。
  2. 打开:框架以 D_TYPE_MIDI 分发到 midi_decode_api(),先做 RAM 预算校验(need_dcbuf_size 与静态 buffer 比较),再 open + format_check 确认文件合法。
  3. 配置:读 DAC 采样率 → 组装 MIDI_CONFIG_PARM → 调用 weak 钩子 midi_init_info() 允许应用覆盖模式/开关 → CMD_INIT_CONFIG 下发,引擎完成通道 CC 默认值、tempo 换算、player 池分配。
  4. 输出挂接:解码器句柄通过 regist_dac_channel 进入 DAC 的 kick 驱动链,此后 DAC 每消费一帧,就触发 mp_output 从环形缓冲取新 PCM,形成"推拉结合"的播放节拍。
  5. 解码循环:mp_input 把文件流喂给引擎,MIDI_CTRL_MAIN 把 delta-time 换算为采样数,逐事件更新 16 通道控制状态并驱动 player 状态机,最终把各活跃 player 的采样混合(叠加)写入输出缓冲。多音符叠加即在此完成,叠加数由 MAX_DEC_PLAYER_CNT 决定。

配置选项

配置项类型默认/典型值说明
MAX_DEC_PLAYER_CNTint在 app_config.c 中定义解码时最大同时发声音符数(复音数),8~32 可配;越大音符叠加越多、所需解码 buffer 越大
MAX_CTR_PLAYER_CNTint引擎常量引擎硬上限,player_t 超过时被钳制,防止越界
MIDI_CONFIG_PARM.player_tintMAX_DEC_PLAYER_CNT下发到引擎的复音数
MIDI_CONFIG_PARM.sample_rateint由 midi_musicsr_to_cfgsr() 映射引擎采样率索引(48k/44.1k/32k/24k/22.05k/16k/12k/11.025k/8k)
MIDI_CONFIG_PARM.spi_posu16*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() 超过静态 buffer
  • E_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 音量/表情相乘叠加,输出前统一归一化,防止多音符叠加削波。

扩展点

  1. midi_init_info()(weak):应用可覆写,注入 MIDI_PLAY_CTRL_MODE mode_info 与 switch_info,例如指定播放模式(单曲/循环)、静音开关、变调等。
  2. MAX_DEC_PLAYER_CNT(app_config.c):编译期调整复音数,是"音质 ↔ 内存"的主要旋钮。
  3. 音色库格式:引擎同时兼容 0xABCD 压缩头与旧式非压缩头两代 .mda/.bin,后构建脚本(post_build/*/dir_midi、midi_cfg)负责把音色打包进文件系统镜像,更换音色只需替换文件而不改固件。
  4. 采样率适配:midi_musicsr_to_cfgsr() 的白名单决定了引擎可工作的采样率集合;若需新增采样率(如 96k),需同步扩展该表与 smpl_rate_tab。
  5. 控制事件集: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。

Prev
音频编码器
Next
音效、变速变调与降噪