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

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

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

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

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

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

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

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

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

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 乐器模式让设备可以:

  1. 播放 MIDI 乐谱文件:将 .mid/00_MIDI.mda 等格式的乐谱数据交给解码器,解码器逐事件驱动合成引擎发声;
  2. 作为电子琴演奏:应用层把按键事件翻译成 Note On/Off 控制消息,驱动同一个合成引擎实时发声(这也是 midi_ctrl_api.c 与 MIDI_CTRL_PARM 控制参数存在的意义);
  3. 多音符复音合成:同时最多支持 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

关键步骤与设计意图:

  1. 前置检查:midi_tone_tab == 0 直接返回 E_MIDI_NO_CFG,避免未初始化音色库时进入合成导致跑飞。
  2. 缓冲区预算校验:need_dcbuf_size() 返回的所需解码缓冲随 MAX_DEC_PLAYER_CNT(复音数)变化——复音越多,需要同时驻留的 MIDI_PLAYER 与 play_key 表越大;若超出静态分配的 midi_decode_buff_nomark[MIDI_DEC_BUF_SIZE/4],返回 E_MIDI_DBUF,防止内存越界。
  3. IO 接口注入:midi_dec_io0 把 mp_input/mp_output 作为解码器的读写回调,输入来自文件(dec_midi_hld.p_file),输出写入 cbuf_midi(obuf_midi)。
  4. 采样率对齐:midi_musicsr_to_cfgsr() 把 DAC 当前采样率(48000/44100/32000/24000/22050/16000/12000/11025/8000)映射成引擎内部索引,不匹配时 ASSERT(0, "midi error sr !!!"),说明合成引擎对采样率是固定表驱动、不支持任意值。
  5. 应用可定制:midi_init_info() 是 __attribute__((weak)) 弱符号,应用可在自己的代码里实现同名强符号,在打开时注入自定义的 mode_info/switch_info(由 get_midi_mode()/get_midi_switch_info() 暴露给应用层查询当前演奏模式与开关状态)。
  6. 注册 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

循环机制:

  1. 节拍驱动采样数:每次进入主循环把 now_srTicks 累加到 theTick,右移 MIDI_RESAMPLE_SHIFT 得到本次应合成的采样数 sample_cnt,余数保留在 theTick 供下次累积——这就是"非整数采样率比"的定点实现,避免长时间播放产生累积误差。
  2. 分块输出:sample_cnt 超过 MIDI_OBUF_BLOCK 时按块合成,保证输出缓冲(out_val)尺寸固定、可被 output 回调逐块消费。
  3. 按 player 合成:midi_player_on 位图标记当前活跃的发声体;每个活跃 player 的 WaveInfo_t 独立维护音量包络状态(attack/decay/env),midi_ctrl_gen_sample() 生成 len_out 个采样并累加进混音缓冲 out_val。
  4. 自然结束回收:当 voice.isend 置位且无 pendingStop 时,清 exclOn、熄灭 player 位,若注册了 melody_stop_trigger 回调则通知应用"某键的旋律已播完",应用层可据此点亮/熄灭按键 LED 或触发下一动作。
  5. 续传机制: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_CNTint(app_config.c 定义)8~32乐谱解码最大同时发声 key 数,决定解码 buffer 大小,由 need_dcbuf_size() 换算
MAX_CTR_PLAYER_CNT宏引擎上限控制/演奏模式最大 player 数,MIDI_CTRL_OPEN 中对 player_t 做上限钳制
MAX_CHANNEL_NUM宏16MIDI 通道数(channel_mixer[]、play_key[] 维度)
CTRL_CHANNEL_NUM宏—控制通道数,用于 decay_speed[] 初始化
midi_t_parm.player_tu8MAX_DEC_PLAYER_CNT注入 MIDI_CONFIG_PARM 的复音数(8~32 可配)
midi_t_parm.sample_rateu8由 dac_sr_read() 映射引擎采样率索引(9 档固定表)
midi_t_parm.spi_posu16*midi_tone_tab音色库 SPI cache 起始地址
midi_tone_tabu320音色库地址,midi_decode_init 成功后由 app_addr + sclust 得到;为 0 时 midi_decode_api 返回 E_MIDI_NO_CFG
midi_tempo_vu321024初始速度倍率(1024 = 100%),now_srTicks 实时变速基准
mute_thresholdu321L << 29静音阈值,低于此振幅的采样判定为可忽略/静音
decay_speed[chn]u3232768各通道自然衰减速度初值
通道 CC 默认值u8VOL=127 / EXPR=127 / PAN=64channel_mixer[].cc[] 初始控制器值
pitchBend_v[chn]u16256初始弯音值(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_now 1.30 格式),否则会出现音量跳变。
  • 音色库放 SPI Flash 不搬 RAM:spi_pos 直读 cache 地址,随机访问时延取决于 SPI Flash 控制器;压缩格式(0xABCD 头)可减小音色体积,但读取时要多一步解压,属于"空间换时间"的取舍。

扩展点

  1. midi_init_info(弱符号):应用可定义强符号覆盖默认空实现,在每次 CMD_INIT_CONFIG 时写入自定义 mode_info/switch_info,实现不同产品(电子琴/音乐盒/乐器玩具)的差异化初始状态。
  2. get_midi_mode() / get_midi_switch_info():暴露 init_info 句柄,应用层可随时读写演奏模式与开关(如"单音/多音模式切换")。
  3. melody_stop_trigger 回调:注册在 MIDI_DECODE_VAR 上的播完通知(携带 main_key[j] 与通道号),可用于按键 LED 熄灭、翻谱、连奏逻辑等。
  4. decoder_ops 接口:open/format_check/dec_confing/need_dcbuf_size 由 get_midi_ops() 提供,若要支持新的 MIDI 事件类型或音色格式,可在 midi_2byte 内部扩展事件表 midi_evt[] 与对应处理函数。
  5. 双格式音色头: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 注册)、按键控制模式(按键→音符映射)
Prev
音乐播放与外部音源
Next
录音应用