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

    • AD16N 系列芯片与 SDK 能力总览
  • 快速开始

    • 环境搭建与编译工具链
    • 编译构建指南
    • 烧录与固件升级
  • SDK 工程架构

    • SDK 目录结构与模块分层
    • 构建系统与批处理工具
    • BSP 板级支持包
  • mbox_flash 小音箱应用

    • 应用初始化与启动流程
    • 应用配置系统
    • 按键、UI 与用户交互
  • 音频子系统

    • 音频解码框架与调度
    • 音频格式解码器实现
    • MIDI 合成与播放
    • 音频编码与录音
    • EQ/DRC 与音效处理
    • DAC/ADC 音频接口与采样
  • 存储与文件系统

    • 媒体 IO 抽象层 MIO
    • 存储设备驱动
    • 文件系统支持
  • 平台系统库

    • 系统基础服务
    • CPU 平台与运行库
    • 固件升级与更新机制
    • 蓝牙与扩展连接接口
  • 电源与低功耗管理

    • 电源管理与低功耗设计
    • 锂电池充电管理
  • 硬件与文档参考

    • SDK 文档中心与版本发布记录
    • 芯片数据手册与硬件设计参考

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

播放循环与合成过程

  1. 打开与校验:midi_decode_api 依次完成缓冲选择、ops->open、format_check(若 B_DEC_NO_CHECK 未置位则跳过校验)。
  2. 初始化配置:CMD_INIT_CONFIG 一次性下发采样率索引、SPI 音色地址、最大并发音数、播放模式、速度、默认音色、功能开关(见 midi_dec_confing 中 CMD_INIT_CONFIG 分支对 itt_info 各子结构的批量 memcpy 与 midi_play_ctrl_on/midi_switch_control 的调用)。
  3. 实时合成:DAC 通过 kick 机制拉取数据;引擎从 smf_data 字节流解析 delta-time 事件,维护 note_on_cnt、midi_players[] 等状态,按 MAX_PLAYER_CNTt 个 player 叠加采样,经 16 通道 mixer(CC 音量/表情/声像、pitchBend)混音后写入 cbuf_midi。
  4. 播放控制:应用层在播放中随时调用 midi_dec_confing_api(obj, cmd, parm) 调整速度、音色、变调、Mark、主旋律开关等,全部走命令分发,不直接触碰引擎内部状态。

功能开关映射

CMD_MIDI_SET_SWITCH 通过位掩码一次性控制多项合成行为(midi_switch_control 逐位解析):

开关位内部标志作用
MARK_ENABLEtriggered使能 Mark(段落标记)触发
MELODY_ENABLEmelody_on使能主旋律通道
TIM_DIV_ENABLEtmDiv_enable使能时间划分(跟唱判定)
MUTE_ENABLEmute_enable静音
SAVE_DIV_ENBALEsave_ptr_enable使能位置保存(MAX_DEC_PLAYER_CNT < 18 时强制关闭,因为回溯缓冲占用 player 数组空间)
EX_VOL_ENABLEex_flag使能扩展音量控制
SET_PROG_ENABLEins_set使能运行时换音色
MELODY_PLAY_ENABLEmelody_enable使能主旋律播放
BEAT_TRIG_ENABLEbeat_enable使能节拍触发
MELODY_STOP_ENABLEmelody_stop使能主旋律停止
MARK_LOOP_ENABLEloop_on使能 Mark 循环
SEMITONE_ENABLEpitch_enable使能变调(半音)
LIMITER_ENABLElimiter_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_CNTconst intapp_config.c 定义文件模式最大同时发音数(8~32),直接决定解码缓冲大小
MAX_CTR_PLAYER_CNTconst intapp_config.c 定义键盘模式最大同时发音数
MIDI_CTRL_OUT_CHANNELconst intapp_config.c 定义4byte 键盘模式输出声道数
MIDI_MAX_MARK_CNTextern 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_bufcbuf_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_bufcbuf_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_CFGmidi_tone_tab 为空(音色库未初始化)解码打开直接失败,应用应确保先调 midi_decode_cfg_init
E_MIDI_DBUFneed_dcbuf_size() 超出编译期缓冲提示需增大 MAX_DEC_PLAYER_CNT 对应缓冲;音符叠加数越大所需缓冲越大
E_MIDIFORMATformat_check 失败(非 MIDI 文件)解码打开失败;B_DEC_NO_CHECK 置位时跳过校验
E_MOUNT / E_OPENBYPATHVFS 挂载失败 / 音色库文件缺失启动配置失败,需检查镜像中 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/跟唱/回退功能的产品必须启用大缓冲。
  • 定点运算:速度换算用 MULSI 64 位中间量、衰减用查表+移位,避免浮点开销,适合无 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 等日志可用于定位打开失败、缓冲不足、音色库缺失三类高频问题。

扩展点

  1. midi_init_info()(weak 函数):应用层覆盖此函数即可在产品启动时注入 MIDI_INIT_STRUCT(播放模式、OKON/按键模式、默认音色 prog_info、主轨道 mainTrack_info、变调 semitone_info、Mark mark_info、主旋律 moledy_info、节拍 beat_info、看门狗 wdt_clear、变速录制 w2s_info 等),而无需改动引擎代码。这是产品差异化的主要入口。
  2. 编译期开关族:DECODER_MIDI_EN、DECODER_MIDI_KEYBOARD_EN、MIDI_VER_SELECT、MIDI_MAX_MARK_CNT 组合出"文件播放 / 键盘演奏 / 2byte / 4byte / 带 Mark / 无 Mark"等产品形态。
  3. 命令集扩展:在 midi_dec_confing 中新增 CMD_* 分支即可扩展控制能力,命令字与结构体定义集中在 MIDI_CTRL_API.h / MIDI_DEC_API.h / MIDIDefs.h,上层经 midi_dec_confing_api 统一访问。
  4. 应用层模式: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
Prev
音频格式解码器实现
Next
音频编码与录音