一、MIDI 基础:定义、起源与核心价值(开发前置认知)
1.1 精准定义:MIDI 不是音频,是'设备指令语言'
很多开发者初期会混淆 MIDI 与音频文件,核心认知需明确:MIDI 是通信协议,而非音频存储格式。
MIDI 的核心作用是定义电子乐器、计算机、效果器等设备之间的'控制指令传输规则',本质是设备间的通用通信语言。这些指令包含'音符启停、力度大小、乐器切换、音效调节'等所有音乐演奏相关参数,不存储任何声音波形。
MIDI 协议底层原理、硬件连接及消息结构,涵盖二进制文件解析(Chunk、Delta-Time、VLQ)与 Python Mido 库实操(生成、编辑、硬件交互)。同时介绍 MIDI 2.0 新特性(16 位精度、双向通信)及开发工具集,适用于音频开发、AI 作曲及嵌入式设备控制场景。
一、MIDI 基础:定义、起源与核心价值(开发前置认知)
很多开发者初期会混淆 MIDI 与音频文件,核心认知需明确:MIDI 是通信协议,而非音频存储格式。
MIDI 的核心作用是定义电子乐器、计算机、效果器等设备之间的'控制指令传输规则',本质是设备间的通用通信语言。这些指令包含'音符启停、力度大小、乐器切换、音效调节'等所有音乐演奏相关参数,不存储任何声音波形。
| 对比维度 | MIDI(.mid/.midi) | 音频文件(MP3/WAV/FLAC) |
|---|---|---|
| 存储内容 | 音乐控制指令(音符、力度、乐器、音效参数等) | 声音波形采样数据(直接记录声音本身) |
| 文件体积 | 极小(一首 3 分钟乐曲仅几 KB~几十 KB) | 较大(3 分钟 MP3 约 3MB,WAV 约 30MB) |
| 编辑灵活性 | 极高,可精准修改单个音符的参数(力度、时长、乐器),支持批量编辑 | 较低,仅能整体调节音量、均衡器,无法拆分单个音符 |
| 播放依赖 | 依赖音源(软音源/硬音源),由音源解析指令生成声音 | 直接播放波形,无需额外音源 |
| 开发场景 | AI 作曲、游戏配乐、音乐编程、设备控制 | 音频剪辑、语音识别、声音降噪 |
MIDI 的诞生源于 1980 年代初电子乐器行业的'兼容性危机',其发展历程直接决定了当前的协议规范,开发时需了解核心时间节点对应的技术迭代。
1980 年代初,电子琴、合成器等设备快速普及,但不同厂商的设备采用各自的通信协议,导致'甲厂键盘创作的曲子,乙厂合成器无法正常播放'——音色错乱、节奏偏移、力度失效等问题频发,严重阻碍了行业发展。
1981 年,Sequential Circuits 公司的 Dave Smith 首次提出'统一电子乐器通信标准'的构想;1983 年,Yamaha、Roland、Korg、Sequential Circuits 等 13 家厂商联合召开会议,正式确立 MIDI 1.0 协议,免费开放给全行业使用,奠定了现代电子音乐与音频开发的基础。
MIDI 的应用已渗透多个技术领域,不同场景对应不同的开发重点:
二、MIDI 硬件与连接:端口、拓扑与适配方案(硬件交互重点)
开发 MIDI 相关硬件交互功能时,需掌握端口定义、连接拓扑及适配方案,避免兼容性问题。本节覆盖从传统硬件到现代 USB 适配的全场景。
传统 MIDI 设备均配备 3 个标准端口,分别对应不同的通信方向,硬件连接时需明确端口功能:
传统 MIDI 端口采用 5 针 DIN 接口(圆形接口),引脚定义如下(开发硬件驱动时需对应):
| 引脚编号 | 功能定义 | 电平标准 |
|---|---|---|
| 1 | 未使用(预留) | 无 |
| 2 | MIDI 信号(负逻辑) | 0-5V TTL 电平 |
| 3 | 接地(GND) | 无 |
| 4 | 未使用(预留) | 无 |
| 5 | +5V 电源(部分设备支持,非标准) | 5V DC |
注意:MIDI 信号采用负逻辑,高电平(5V)表示'0',低电平(0V)表示'1',传输速率固定为 31250 bps,无校验位,停止位 1 位,这是硬件通信的基础参数。
实际开发中常涉及多设备连接,需掌握两种核心拓扑结构,适配不同场景:
适用场景:少量设备(≤5 台),无同步需求,结构简单。
连接方式:设备 1 OUT → 设备 2 IN,设备 2 Thru → 设备 3 IN,依次串联,最后一台设备无需连接 Thru。
优缺点:
适用场景:多设备(>5 台)、需要同步控制、高稳定性需求(如舞台演出、专业录音棚)。
连接方式:所有设备的 IN 端口连接到 MIDI 接口盒(或 MIDI 路由器)的 OUT 端口,接口盒作为中心节点,统一转发消息。
核心设备:MIDI 接口盒(支持多进多出,如 4 进 8 出、8 进 16 出),部分高端接口盒支持 USB 连接电脑,实现电脑对多设备的集中控制。
优缺点:
传统 5 针 DIN 接口逐渐被 USB 替代,无线 MIDI 也逐步普及,开发时需适配这些主流方案:
USB-MIDI 将 MIDI 消息封装在 USB 数据包中传输,兼容 USB 1.1/2.0/3.0,无需额外电源(部分设备除外),是当前电脑、手机与 MIDI 设备交互的主要方式。
核心优势:
开发注意:USB-MIDI 设备在系统中被识别为'MIDI 设备'和'USB 设备'双身份,需通过系统 API(如 Windows 的 MMMIDI API、macOS 的 CoreMIDI)读取设备信息及消息。
无线 MIDI 主要基于蓝牙(BLE MIDI)和 WiFi(MIDI over WiFi),适用于移动场景(如舞台演出、移动端音乐制作)。
开发注意:BLE MIDI 需遵循蓝牙 SIG 定义的 MIDI 服务规范(UUID:00001130-0000-1000-8000-00805F9B34FB),WiFi MIDI 需自定义消息封装格式,确保同步性。
实际开发中常遇到设备识别失败、消息丢失、延迟过高问题,对应解决方案如下:
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 设备无法被系统识别 | 驱动未安装、USB 线故障、端口冲突 | 1. 安装设备官方驱动;2. 更换 USB 线及端口;3. 关闭占用端口的其他程序 |
| MIDI 消息丢失、卡顿 | 传输速率不足、设备供电不稳、链路过长 | 1. 减少串联设备数量,改用星型拓扑;2. 更换优质 USB 线/音频线;3. 为设备单独供电 |
| 播放延迟过高(>20ms) | 软音源缓冲设置过大、系统资源不足、无线干扰 | 1. 减小软音源缓冲(至 128ms 以下);2. 关闭后台冗余程序;3. 无线设备切换至 5G WiFi 或近距离 BLE |
| 多设备同步错乱 | 无统一时钟源、设备延迟不一致 | 1. 采用 MIDI 同步时钟(MIDI Clock);2. 通过接口盒统一管理设备时钟;3. 校准各设备延迟参数 |
三、MIDI 消息:二进制结构与核心类型(协议层核心)
MIDI 消息是设备间传输的核心数据,所有 MIDI 开发(文件解析、设备控制、AI 生成)都需基于消息结构实现。本节从二进制层面拆解消息格式,覆盖所有核心类型。
MIDI 消息采用串行传输,以字节为单位,分为'状态字节'和'数据字节'两部分,部分消息还包含扩展数据。
为减少数据量,MIDI 支持'运行模式':连续发送同一类型、同一通道的消息时,仅第一个消息发送状态字节,后续消息可省略状态字节,直接发送数据字节。
示例:连续发送 3 个通道 1 的 Note On 消息,正常模式需发送 3 组(状态字节 +2 数据字节),运行模式仅发送 1 个状态字节 +6 个数据字节,大幅减少传输量。
开发注意:解析 MIDI 消息时需处理运行模式,记录上一个状态字节,直至遇到新的状态字节。
MIDI 消息分为'通道消息'和'系统消息'两大类,通道消息对应单个通道的控制(如音符、音色),系统消息对应全局控制(如同步、启停)。
通道消息共 7 类,均包含通道号(0-15),可独立控制 16 个通道的设备,是开发中最常用的消息类型。
功能:通知设备开始播放某个音符,是最核心的消息之一。
结构:1 个状态字节 + 2 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0x90-0x9F | 0x9n,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 音符音高(C4=60,对应中央 C,每半音递增 1) |
| 3 | 数据字节 2 | 0-127 | 演奏力度(0=无声,1-127=逐渐增强,64 为标准力度) |
特殊说明:当数据字节 2(力度)为 0 时,该消息等价于 Note Off 消息(部分设备支持此简化写法),但开发时建议单独发送 Note Off,避免兼容性问题。
功能:通知设备停止播放某个音符,必须与 Note On 配对使用,否则音符会持续发声。
结构:1 个状态字节 + 2 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0x80-0x8F | 0x8n,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 音符音高(与对应的 Note On 一致) |
| 3 | 数据字节 2 | 0-127 | 释放力度(部分设备支持,控制音符衰减速度,0 为默认) |
功能:控制单个音符的触后力度(按完琴键后持续施加的力度),实现音色变化,仅支持复音设备(可同时播放多个音符)。
结构:1 个状态字节 + 2 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0xA0-0xAF | 0xAn,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 目标音符音高 |
| 3 | 数据字节 2 | 0-127 | 触后力度(0=无效果,127=效果最强) |
开发注意:多数民用设备不支持复音触后,仅高端合成器支持,可作为扩展功能实现。
功能:控制设备的音效参数(音量、混响、颤音等),共支持 128 个控制器(CC0-CC127),是功能最丰富的通道消息。
结构:1 个状态字节 + 2 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0xB0-0xBF | 0xBn,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 控制器编号(CC0-CC127,对应不同功能) |
| 3 | 数据字节 2 | 0-127 | 控制器参数值(0-127 对应功能的强弱/开关) |
核心控制器编号及功能(开发高频使用,完整列表见附录):
功能:切换通道的乐器音色(如钢琴→小提琴),需配合 GM/GS/XG 音色表使用。
结构:1 个状态字节 + 1 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0xC0-0xCF | 0xCn,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 音色编号(对应 GM/GS/XG 音色表,0=钢琴,16=小提琴等) |
特殊说明:通道 10(状态字节 0xCF)为 GM 标准规定的鼓组通道,Program Change 消息对其无效,鼓组音色通过 Note On 的音高控制(见附录鼓组表)。
功能:控制整个通道的触后力度,所有音符同时受影响,与复音触后(0xA0)的区别是不针对单个音符。
结构:1 个状态字节 + 1 个数据字节
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0xD0-0xDF | 0xDn,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 触后力度(0=无效果,127=效果最强) |
功能:控制音符音高的连续变化(滑音效果),默认范围为±2 个半音,可通过 CC 控制器扩展范围。
结构:1 个状态字节 + 2 个数据字节(14 位精度,高 7 位 + 低 7 位)
| 字节位置 | 字节内容 | 取值范围 | 功能说明 |
|---|---|---|---|
| 1 | 状态字节 | 0xE0-0xEF | 0xEn,n 为通道号(0-15) |
| 2 | 数据字节 1 | 0-127 | 弯音值低 7 位 |
| 3 | 数据字节 2 | 0-127 | 弯音值高 7 位 |
计算方式:弯音值 = 高 7 位 × 128 + 低 7 位,取值范围 0-16383。中间值 8192 为标准音高(无弯音),<8192 为降音,>8192 为升音。
示例:弯音值 8192+2048=10240 → 升 1 个半音;弯音值 8192-2048=6144 → 降 1 个半音。
系统消息不包含通道号(状态字节高 4 位为 0xF),用于全局控制,如设备同步、启停、版权信息等,分为 3 子类:系统实时消息、系统通用消息、系统专属消息。
功能:用于多设备同步(如播放、暂停、节拍同步),优先级最高,可插入其他消息中间传输,不影响正常播放。
| 状态字节 | 消息名称 | 数据字节 | 功能说明 |
|---|---|---|---|
| 0xF8 | MIDI Clock(时钟同步) | 无 | 每 24 个时钟消息对应 1 个四分音符,用于多设备节拍同步 |
| 0xF9 | MIDI Tick(节拍脉冲) | 无 | 每 4 个 Tick 对应 1 个 Clock,用于细分节拍(较少使用) |
| 0xFA | Start(开始播放) | 无 | 通知设备从当前位置开始播放 |
| 0xFB | Continue(继续播放) | 无 | 通知设备从暂停位置继续播放 |
| 0xFC | Stop(停止播放) | 无 | 通知设备停止播放 |
| 0xFE | Active Sensing(活性检测) | 无 | 设备定期发送,检测链路是否正常,超时无响应则停止发声 |
| 0xFF | System Reset(系统重置) | 无 | 重置设备至初始状态(音色、音量、控制器均恢复默认) |
功能:用于全局设置(如调号、拍号、速度)、文件传输等,部分消息包含扩展数据。
| 状态字节 | 消息名称 | 数据字节 | 功能说明 |
|---|---|---|---|
| 0xF0 | System Exclusive(系统专属) | 可变长度 | 厂商自定义消息,用于设备参数调节、固件升级(如 Roland、Yamaha 专属指令) |
| 0xF1 | MIDI Time Code Quarter Frame(时间码) | 1 个 | 传输 SMPTE 时间码,用于音视频同步 |
| 0xF2 | Song Position Pointer(歌曲位置指针) | 2 个 | 设置播放位置,以 MIDI Clock 为单位(0-16383) |
| 0xF3 | Song Select(歌曲选择) | 1 个 | 选择设备中的预设歌曲(0-127) |
| 0xF6 | Tune Request(调音请求) | 无 | 通知设备自动调音(仅部分硬件支持) |
| 0xF7 | End of Exclusive(专属消息结束) | 无 | 标记系统专属消息结束 |
系统专属消息(SysEx)是厂商自定义的消息,用于控制设备的特殊功能(如参数调节、固件升级、音色备份),结构灵活,不同厂商格式不同。
通用结构:0xF0(开始) + 厂商 ID(1-3 字节) + 自定义数据(可变长度) + 0xF7(结束)
厂商 ID:由 MMA 分配,如 Yamaha(0x43)、Roland(0x41)、Korg(0x42),未分配厂商使用 0x7D。
开发注意:SysEx 消息兼容性差,仅针对特定设备,一般用于硬件驱动开发,AI 作曲、文件解析场景可忽略。
四、MIDI 文件格式:Chunk 结构、Delta-Time 与事件解析(文件处理核心)
MIDI 文件(.mid)是 MIDI 消息的持久化存储格式,所有文件解析、生成开发都需基于其结构实现。本节从二进制层面拆解文件结构,覆盖 Chunk、Delta-Time、事件解析全流程。
MIDI 文件采用'Chunk(块)'式存储,每个 Chunk 包含'类型标识、长度、数据'三部分,整体由 1 个 Header Chunk(头部块)和 1 个及以上 Track Chunk(音轨块)组成。
文件结构示意图:
MIDI 文件 → Header Chunk(头部信息) + Track Chunk 1(音轨 1) + Track Chunk 2(音轨 2) + ... + Track Chunk N(音轨 N)
核心规则:所有多字节数据采用'大端序'存储(高位字节在前,低位字节在后),开发解析时需注意字节序转换(如 C++ 的 htons 函数、Python 的 struct 模块)。
Header Chunk 是 MIDI 文件的'总说明书',存储文件格式、音轨数量、时间基准等全局信息,位于文件最开头,长度固定为 6 字节(不含类型和长度标识)。
Header Chunk 完整结构包含'类型标识(4 字节)+ 长度(4 字节)+ 数据(6 字节)'三部分,总长度为 14 字节,二进制层面逐字节解析如下,开发解析时需严格按此顺序读取:
字节偏移 | 字节长度 | 字段名称 | 取值范围/格式 | 功能说明 | 开发注意事项 |
0-3 | 4 字节 | Chunk 类型标识 | ASCII 码'MThd'(十六进制:0x4D546864) | 标识当前 Chunk 为 Header Chunk,是 MIDI 文件的起始标识 | 解析时先验证此标识,不匹配则为非法 MIDI 文件 |
4-7 | 4 字节 | Chunk 长度 | 固定值 6(十六进制:0x00000006) | 表示后续 Header 数据部分的长度为 6 字节 | 大端序存储,需转换为十进制后验证是否为 6 |
8-9 | 2 字节 | 文件格式类型 | 0、1、2(大端序) | 定义 MIDI 文件的音轨组织方式,对应三种格式: 1. 格式 0:仅 1 个 Track Chunk,所有 MIDI 消息混合存储; 2. 格式 1:多个 Track Chunk,同步播放(如旋律轨、鼓轨分离); 3. 格式 2:多个 Track Chunk,独立播放(较少使用,适用于多首独立乐曲) | 主流 MIDI 文件以格式 1 为主,格式 0 次之,需适配前两种格式 |
10-11 | 2 字节 | 音轨数量(NumTracks) | 1-65535(大端序,十进制) | 表示当前 MIDI 文件包含的 Track Chunk 数量 | 解析时需先读取此值,再循环读取对应数量的 Track Chunk |
12-13 | 2 字节 | 时间基准(Division) | 两种格式(大端序): 1. 高位 bit 为 0:值为每四分音符的 ticks 数(0-32767); 2. 高位 bit 为 1:值为负,低 15 位表示每帧的 ticks 数,帧速率固定(如 24、25、30 帧/秒) | 定义 MIDI 消息的时间精度,是节奏同步的核心参数: 例 1:Division=96 → 每四分音符对应 96 个 ticks,节奏精度为 96PPQ; 例 2:Division=0x8008(二进制 1000000000001000)→ 高位 bit=1,低 15 位=8,帧速率 30 帧/秒(默认) | 绝大多数民用 MIDI 文件采用第一种格式(PPQ),第二种(SMPTE)用于音视频同步 |
时间基准直接决定 MIDI 消息的时间戳计算方式,是解析节奏、时长的关键,需区分两种格式的具体逻辑:
Division 值为正数(0-32767),表示'每四分音符对应的 ticks 数',记为 PPQ(Pulses Per Quarter Note)。ticks 是 MIDI 文件内部的时间单位,所有事件的时间戳(Delta-Time)均以 ticks 为单位。
核心计算公式:
四分音符时长(秒)= 60 / BPM(每分钟节拍数);
每个 tick 时长(秒)= 四分音符时长 / PPQ;
事件实际时长(秒)= Delta-Time × 每个 tick 时长。
示例:若 Division=96(PPQ=96),BPM=120(每分钟 120 拍),则:
四分音符时长=60/120=0.5 秒;每个 tick 时长=0.5/96≈0.0052 秒;Delta-Time=48 的事件,实际时长=48×0.0052≈0.25 秒(即八分音符)。
Division 值为负数(0x8000-0xFFFF),此时需将值拆分为两部分:
帧速率对应关系(前 3bit 取值):
| 前 3bit 取值 | 帧速率(帧/秒) | 适用场景 |
|---|---|---|
| 000 | 24 | 电影标准 |
| 001 | 25 | PAL 电视标准 |
| 010 | 30(非丢帧) | NTSC 电视标准 |
| 011 | 30(丢帧,实际 29.97) | NTSC 广播标准 |
示例:Division=0x8008(二进制 1000000000001000),低 15bit=0x0008,前 3bit=000 → 帧速率 24 帧/秒,每帧 8 个 ticks,每个 tick 时长=1/24/8≈0.0052 秒。
基于 Python 的 struct 模块解析 Header Chunk,处理大端序转换,适配两种格式的时间基准,可直接嵌入项目:
import struct
def parse_header_chunk(file):
"""
解析 MIDI 文件的 Header Chunk
:param file: 打开的 MIDI 文件对象(二进制读模式)
:return: header_info: 字典,包含 Header Chunk 所有信息
"""
# 读取 Chunk 类型标识(4 字节)
chunk_id = file.read(4)
if chunk_id != b'MThd':
raise ValueError("非法 MIDI 文件:缺少 MThd 标识")
# 读取 Chunk 长度(4 字节,大端序)
chunk_length = struct.unpack('>I', file.read(4))[0]
if chunk_length != 6:
raise ValueError(f"Header Chunk 长度异常:预期 6 字节,实际{chunk_length}字节")
# 读取 Header 数据(6 字节,大端序)
format_type, num_tracks, division = struct.unpack('>HHH', file.read(6))
# 解析时间基准(Division)
division_info = {}
if division & 0x8000: # 高位 bit=1,SMPTE 格式
division_info['type'] = 'SMPTE'
# 拆分帧速率标识和每帧 ticks 数
frame_rate_flag = (division & 0x7000) >> 12 # 前 3bit
ticks_per_frame = division & 0x0FFF # 后 12bit
# 映射帧速率
frame_rate_map = {0: 24, 1: 25, 2: 30, 3: 29.97}
division_info['frame_rate'] = frame_rate_map.get(frame_rate_flag, 24)
division_info['ticks_per_frame'] = ticks_per_frame
else: # 高位 bit=0,PPQ 格式
division_info['type'] = 'PPQ'
division_info['ppq'] = division
# 组织返回结果
header_info = {
'format_type': format_type,
'num_tracks': num_tracks,
'division': division,
'division_info': division_info
}
return header_info
# 测试代码
if __name__ == "__main__":
with open("test.mid", "rb") as f:
header = parse_header_chunk(f)
print("Header Chunk 解析结果:")
print(header)
代码说明:通过 struct.unpack 的'>'符号指定大端序,验证关键标识和长度,拆分时间基准格式,返回结构化信息,便于后续 Track Chunk 解析调用。
Track Chunk 是 MIDI 文件的核心数据载体,每个音轨对应一条独立的演奏序列(如旋律轨、鼓轨、伴奏轨),文件中音轨数量由 Header Chunk 的 NumTracks 字段定义。所有音轨按时间同步播放,共同构成完整乐曲。
Track Chunk 结构与 Header Chunk 一致,均遵循'类型标识 + 长度 + 数据'的 Chunk 通用规范,但数据部分为可变长度(由 Chunk 长度字段指定),且包含大量带时间戳的 MIDI 事件。
Track Chunk 总长度 = 4 字节(类型标识)+ 4 字节(长度)+ N 字节(数据,N 为 Chunk 长度字段值),二进制逐字段解析如下:
字节偏移 | 字节长度 | 字段名称 | 取值范围/格式 | 功能说明 | 开发注意事项 |
0-3(相对于 Track Chunk 起始) | 4 字节 | Chunk 类型标识 | ASCII 码'MTrk'(十六进制:0x4D54726B) | 标识当前 Chunk 为 Track Chunk,区分于 Header Chunk | 解析时需循环验证此标识,确保音轨块格式合法 |
4-7 | 4 字节 | Chunk 长度 | 0-4294967295(大端序,十进制) | 表示后续 Track 数据部分的总字节数,即音轨事件的总长度 | 需按此长度读取完整音轨数据,避免读取越界或遗漏事件 |
8-N+7 | N 字节(Chunk 长度值) | 音轨数据(事件序列) | Delta-Time + MIDI 事件 重复序列,以 0xFF 0x2F 0x00 结尾 | 存储音轨的所有演奏事件,含音符、控制器、节拍等信息 | 必须解析至结束事件(0xFF 0x2F 0x00),确保事件完整性 |
核心规则:所有 Track Chunk 的事件均以'Delta-Time + 事件内容'为单位存储,Delta-Time 表示当前事件与上一事件的时间间隔(单位:ticks),需结合 Header Chunk 的 Division 字段换算为实际时长。
Delta-Time 是 MIDI 文件时间同步的核心,采用'可变长度量(Variable-Length Quantity, VLQ)'存储,目的是减少时间戳的数据量(短间隔用少字节表示,长间隔用多字节表示)。
VLQ 由 1-4 字节组成,每个字节的最高位(bit7)为'延续位',低 7 位为有效数据,具体规则:
解码时按字节读取,剥离延续位后拼接有效数据,计算方式:当前字节低 7 位 + 前一字节低 7 位×128 + 前两字节低 7 位×128² + ... ,示例如下:
| VLQ 字节序列(十六进制) | 解码过程 | 解码结果(十进制 ticks) |
|---|---|---|
| 0x40 | 单字节,延续位 0,有效数据 0x40(64) | 64 |
| 0x81 0x01 | 第一字节延续位 1(0x81→低 7 位 0x01),第二字节延续位 0(0x01→低 7 位 0x01),结果=0x01 + 0x01×128=129 | 129 |
| 0x8F 0x7F | 0x8F→低 7 位 0x0F,0x7F→低 7 位 0x7F,结果=0x0F + 0x7F×128=0x0F+0x3F80=0x3F8F=16271 | 16271 |
def decode_vlq(file):
"""
解码 Delta-Time 的 VLQ 格式,读取可变长度字节并返回对应的 ticks 值
:param file: 打开的 MIDI 文件对象(二进制读模式)
:return: vlq_value: 解码后的 ticks 值(十进制)
"""
vlq_value = 0
while True:
byte = ord(file.read(1)) # 读取 1 字节并转换为十进制
vlq_value = (vlq_value << 7) | (byte & 0x7F) # 拼接低 7 位有效数据
if not (byte & 0x80): # 若延续位为 0,结束解码
break
# 限制 VLQ 最大长度为 4 字节,避免异常数据导致死循环
if vlq_value > 0x7F7F7F7F:
raise ValueError("VLQ 值超出最大范围(4 字节),非法 MIDI 文件")
return vlq_value
Track 数据部分由一系列'Delta-Time + 事件'组成,事件分为三大类:通道事件(对应 3.2.1 节的通道消息)、系统通用事件(对应 3.2.2 节的系统消息)、元事件(MIDI 文件专属,用于描述乐曲信息)。
所有事件以状态字节开头,需结合'运行模式(Running Status)'解析,即连续相同类型、相同通道的事件可省略状态字节,直接复用前一事件的状态字节。
元事件仅存在于 MIDI 文件中,不发送给硬件设备,用于描述乐曲的元信息(如标题、作者、节拍、速度),状态字节固定为 0xFF,结构为:0xFF + 元事件类型(1 字节) + 数据长度(VLQ) + 数据(N 字节)。
高频元事件类型及解析规则(开发必适配):
| 元事件类型(十六进制) | 事件名称 | 数据格式 | 功能说明 | 解析示例 |
|---|---|---|---|---|
| 0x00 | 序列编号 | 数据长度 2 字节(大端序) | 标识当前序列编号(多序列文件用,较少见) | 0xFF 0x00 0x02 0x00 0x01 → 序列编号 1 |
| 0x01 | 乐曲标题 | 数据长度 VLQ,数据为 ASCII 字符串 | 存储乐曲名称,可用于文件解析后的展示 | 0xFF 0x01 0x05 48 65 6C 6C 6F → 标题'Hello' |
| 0x02 | 版权信息 | 数据长度 VLQ,数据为 ASCII 字符串 | 存储版权声明,非必需字段 | 0xFF 0x02 0x0A 4D 49 44 49 20 54 65 73 74 → 版权'MIDI Test' |
| 0x03 | 音轨名称 | 数据长度 VLQ,数据为 ASCII 字符串 | 标识当前音轨功能(如'旋律轨''鼓轨') | 0xFF 0x03 0x06 44 72 75 6D 20 54 72 61 63 6B → 音轨名'Drum Track' |
| 0x08 | 调号 | 数据长度 2 字节:字节 1 为调号(-7~+7),字节 2 为调式(0=大调,1=小调) | 定义乐曲调号,用于乐理分析和显示 | 0xFF 0x08 0x02 0x00 0x00 → C 大调 |
| 0x09 | 拍号 | 数据长度 4 字节:分子、分母(2 的幂次)、时钟数、三十二分音符数 | 定义乐曲节拍(如 4/4、3/4),配合 BPM 计算时长 | 0xFF 0x09 0x04 0x04 0x02 0x18 0x08 → 4/4 拍(分母 2²=4) |
| 0x0A | 速度(BPM) | 数据长度 3 字节(大端序),表示每四分音符的微秒数 | 核心参数,用于计算实际播放速度,BPM=60000000/微秒数 | 0xFF 0x0A 0x03 0x00 0x7A 0x12 → 微秒数 31250 → BPM=120 |
| 0x2F | 音轨结束 | 数据长度 0 字节 | 标识当前音轨事件结束,必需字段 | 0xFF 0x2F 0x00 → 音轨结束 |
通道事件(0x80-0xEF)和系统事件(0xF0-0xF7、0xF8-0xFF)的结构与 3.2 节的 MIDI 消息一致,但需结合运行模式解析,步骤如下:
开发提醒:系统实时事件(0xF8-0xFF,如时钟同步、启停)无 Delta-Time,可直接插入事件序列中,不影响其他事件的时间间隔。
结合 VLQ 解码、事件分类解析,实现 Track Chunk 全流程解析,返回结构化事件列表,可直接对接 Header Chunk 解析结果使用:
def parse_track_chunk(file, track_index):
"""
解析单个 Track Chunk,返回音轨信息及事件列表
:param file: 打开的 MIDI 文件对象(二进制读模式)
:param track_index: 音轨索引(从 0 开始)
:return: track_info: 字典,包含音轨名称、事件列表等信息
"""
# 验证 Track Chunk 标识
chunk_id = file.read(4)
if chunk_id != b'MTrk':
raise ValueError(f"第{track_index+1}个音轨块格式非法:缺少 MTrk 标识")
# 读取 Track Chunk 长度(大端序)
chunk_length = struct.unpack('>I', file.read(4))[0]
track_end_pos = file.tell() + chunk_length # 计算音轨数据结束位置
events = []
running_status = None # 存储运行模式下的上一状态字节
track_name = f"Track {track_index+1}" # 默认音轨名
while file.tell() < track_end_pos:
# 步骤 1:解析 Delta-Time
delta_time = decode_vlq(file)
# 步骤 2:解析事件(处理运行模式)
current_byte = ord(file.read(1))
if current_byte & 0x80: # 是状态字节
running_status = current_byte
# 判断事件类型
if running_status == 0xFF: # 元事件
meta_type = ord(file.read(1))
# 解析元事件数据长度(VLQ)
meta_len = 0
while True:
len_byte = ord(file.read(1))
meta_len = (meta_len << 7) | (len_byte & 0x7F)
if not (len_byte & 0x80):
break
# 读取元事件数据
meta_data = file.read(meta_len)
# 解析关键元事件(提取音轨名、速度等)
if meta_type == 0x03: # 音轨名称
track_name = meta_data.decode('ascii', errors='replace')
elif meta_type == 0x2F: # 音轨结束事件
events.append({
'delta_time': delta_time,
'event_type': 'Meta Event',
'meta_type': 'Track End',
'data': meta_data
})
break # 音轨结束,退出循环
# 其他元事件(标题、版权、拍号等)按需扩展解析
events.append({
'delta_time': delta_time,
'event_type': 'Meta Event',
'meta_type': meta_type,
'data': meta_data
})
elif 0xF0 <= running_status <= 0xF7: # 系统通用/专属事件
# 系统事件数据长度处理(SysEx 用 VLQ,其他固定长度)
if running_status == 0xF0 or running_status == 0xF7:
sys_len = 0
while True:
len_byte = ord(file.read(1))
sys_len = (sys_len << 7) | (len_byte & 0x7F)
if not (len_byte & 0x80):
break
sys_data = file.read(sys_len)
else: # 其他系统事件(F1-F6)数据长度固定为 1 字节
sys_data = file.read(1)
events.append({
'delta_time': delta_time,
'event_type': 'System Event',
'status': running_status,
'data': sys_data
})
elif 0x80 <= running_status <= 0xEF: # 通道事件
# 按通道事件类型读取数据字节(1 或 2 个)
if 0xC0 <= running_status <= 0xDF: # Program Change、Channel Pressure(1 字节数据)
data1 = ord(file.read(1))
events.append({
'delta_time': delta_time,
'event_type': 'Channel Event',
'status': running_status,
'channel': running_status & 0x0F,
'data': (data1,)
})
else: # 其他通道事件(2 字节数据)
data1 = ord(file.read(1))
data2 = ord(file.read(1))
events.append({
'delta_time': delta_time,
'event_type': 'Channel Event',
'status': running_status,
'channel': running_status & 0x0F,
'data': (data1, data2)
})
elif 0xF8 <= running_status <= 0xFF: # 系统实时事件(无数据字节)
events.append({
'delta_time': 0, # 实时事件无 Delta-Time
'event_type': 'System Real-Time Event',
'status': running_status,
'data': b''
})
else: # 非状态字节,复用运行状态
if running_status is None:
raise ValueError(f"第{track_index+1}个音轨块非法:无运行状态字节")
# 按运行状态类型补充数据字节
if 0xC0 <= running_status <= 0xDF: # 1 字节数据(当前字节为 data1)
events.append({
'delta_time': delta_time,
'event_type': 'Channel Event',
'status': running_status,
'channel': running_status & 0x0F,
'data': (current_byte,)
})
else: # 2 字节数据(当前字节为 data1,需再读 data2)
data2 = ord(file.read(1))
events.append({
'delta_time': delta_time,
'event_type': 'Channel Event',
'status': running_status,
'channel': running_status & 0x0F,
'data': (current_byte, data2)
})
# 组织音轨信息
track_info = {
'track_index': track_index,
'track_name': track_name,
'chunk_length': chunk_length,
'event_count': len(events),
'events': events
}
return track_info
# 组合 Header 和 Track 解析,完整解析 MIDI 文件
def parse_midi_file(file_path):
"""
完整解析 MIDI 文件,返回 Header 信息和所有音轨信息
:param file_path: MIDI 文件路径
:return: midi_info: 字典,包含完整 MIDI 文件信息
"""
with open(file_path, 'rb') as f:
# 解析 Header Chunk
header_info = parse_header_chunk(f)
# 解析所有 Track Chunk
tracks = []
for i in range(header_info['num_tracks']):
track = parse_track_chunk(f, i)
tracks.append(track)
# 组织完整结果
midi_info = {
'header': header_info,
'tracks': tracks,
'total_tracks': len(tracks)
}
return midi_info
# 测试完整解析
if __name__ == "__main__":
midi_info = parse_midi_file("test.mid")
print("MIDI 文件解析完成:")
print(f"文件格式:{midi_info['header']['format_type']},音轨数:{midi_info['total_tracks']}")
for track in midi_info['tracks']:
print(f"音轨{track['track_index']+1}:{track['track_name']},事件数:{track['event_count']}")
实际开发中,Track Chunk 解析易出现兼容性问题,核心避坑点如下:
结合 Header Chunk 和 Track Chunk 解析,MIDI 文件完整解析流程可分为 5 步,确保覆盖所有核心环节:
五、Python Mido 库实操:生成、解析与进阶改造(完整代码)
前文从二进制层面实现了 MIDI 解析的底层逻辑,实际开发中可借助成熟库快速落地——Mido 是 Python 生态最常用的 MIDI 处理库,封装了底层协议细节,支持 MIDI 文件的解析、生成、编辑及设备交互,兼顾易用性和灵活性。
本节基于 Mido 库实现全流程实操,包含环境搭建、文件解析、乐曲生成、进阶改造(如音色替换、节奏调整),代码可直接复制运行。
Mido 支持 Python 3.7+,通过 pip 安装,同时需安装 python-rtmidi 实现硬件设备交互(可选):
# 基础安装(仅文件处理)
pip install mido
# 完整安装(支持硬件设备交互)
pip install mido python-rtmidi
Mido 封装了 MIDI 协议的核心元素,关键概念对应关系如下,便于理解后续实操:
借助 Mido 可跳过底层二进制解析,直接提取 Header 信息、音轨事件、元数据等,适合快速开发需求:
import mido
def mido_parse_midi(file_path):
"""
使用 Mido 解析 MIDI 文件,提取关键信息
:param file_path: MIDI 文件路径
:return: 解析结果字典
"""
# 读取 MIDI 文件
mid = mido.MidiFile(file_path)
# 提取 Header 信息
header_info = {
'format_type': mid.type, # 文件格式(0/1/2)
'num_tracks': len(mid.tracks), # 音轨数
'division': mid.ticks_per_beat, # 时间基准(PPQ)
'length_seconds': mid.length # 乐曲总时长(秒,Mido 自动计算)
}
# 提取各音轨信息
tracks_info = []
for idx, track in enumerate(mid.tracks):
track_events = []
track_name = f"Track {idx+1}"
bpm = 120 # 默认 BPM
for msg in track:
# 提取音轨名
if msg.type == 'track_name':
track_name = msg.name
# 提取 BPM(速度事件)
elif msg.type == 'set_tempo':
bpm = mido.tempo2bpm(msg.tempo)
# 筛选关键事件(跳过实时事件,简化输出)
if msg.type in ['note_on', 'note_off', 'program_change', 'control_change', 'set_tempo']:
track_events.append({
'delta_ticks': msg.time,
'event_type': msg.type,
'details': msg.dict() # 事件详细参数
})
tracks_info.append({
'track_index': idx,
'track_name': track_name,
'bpm': bpm,
'event_count': len(track_events),
'events': track_events
})
return {
'header': header_info,
'tracks': tracks_info
}
# 测试解析
if __name__ == "__main__":
parse_result = mido_parse_midi("test.mid")
print("Header 信息:", parse_result['header'])
for track in parse_result['tracks']:
print(f"\n音轨{track['track_index']+1}:{track['track_name']}")
print(f"BPM:{track['bpm']},事件数:{track['event_count']}")
print("前 3 个事件:", track['events'][:3])
代码说明:Mido 自动处理二进制解析、VLQ 解码、运行模式等底层细节,可快速提取音轨名、BPM、音符事件等核心信息,大幅提升开发效率。
通过 Mido 创建 MidiFile、Track、Message 对象,可从零生成自定义 MIDI 乐曲,适合 AI 作曲、自动配乐等场景,以下示例生成一段 C 大调旋律:
import mido
from mido import MidiFile, MidiTrack, Message, MetaMessage
def generate_simple_midi(output_path):
"""
生成一段简单的 C 大调旋律 MIDI 文件
:param output_path: 输出文件路径(.mid)
"""
# 1. 创建 MIDI 文件(格式 1,PPQ=96)
mid = MidiFile(type=1, ticks_per_beat=96)
# 2. 创建旋律轨 melody_track = MidiTrack()
mid.tracks.append(melody_track)
# 添加音轨名元事件
melody_track.append(MetaMessage('track_name', name='C Major Melody', time=0))
# 设置速度(BPM=120)
melody_track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120), time=0))
# 设置拍号(4/4 拍)
melody_track.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
# 设置音色(钢琴,Program=0)
melody_track.append(Message('program_change', program=0, channel=0, time=0))
# 3. 定义旋律音符(音高、时长、力度)
# 音高对应:C4=60, D4=62, E4=64, F4=65, G4=67, A4=69, B4=71, C5=72
melody_notes = [
(60, 48, 64), # C4,时长 48ticks(八分音符),力度 64
(62, 48, 64), # D4
(64, 48, 64), # E4
(65, 48, 64), # F4
(67, 96, 64), # G4,时长 96ticks(四分音符)
(65, 48, 64), # F4
(64, 48, 64), # E4
(62, 96, 64), # D4
(60, 192, 64), # C4,时长 192ticks(二分音符)
]
# 4. 添加音符事件(Note On + Note Off)
for pitch, duration, velocity in melody_notes:
# Note On(开始发声)
melody_track.append(Message('note_on', note=pitch, velocity=velocity, channel=0, time=0))
# Note Off(停止发声,时间间隔为 duration)
melody_track.append(Message('note_off', note=pitch, velocity=velocity, channel=0, time=duration))
# 5. 添加音轨结束事件
melody_track.append(MetaMessage('end_of_track', time=0))
# 6. 创建鼓轨(可选,增加节奏)
drum_track = MidiTrack()
mid.tracks.append(drum_track)
drum_track.append(MetaMessage('track_name', name='Drum Track', time=0))
# 鼓组通道(通道 10,GM 标准)
# 底鼓(音高 36)、军鼓(38)、踩镲(42)
drum_pattern = [
(36, 48, 80), # 底鼓
(42, 24, 60), # 踩镲(闭合)
(38, 48, 80), # 军鼓
(42, 24, 60), # 踩镲(闭合)
]
# 循环 4 小节鼓点
for _ in range(4):
for pitch, duration, velocity in drum_pattern:
drum_track.append(Message('note_on', note=pitch, velocity=velocity, channel=9, time=0))
drum_track.append(Message('note_off', note=pitch, velocity=velocity, channel=9, time=duration))
drum_track.append(MetaMessage('end_of_track', time=0))
# 7. 保存 MIDI 文件
mid.save(output_path)
print(f"MIDI 文件已生成:{output_path}")
# 测试生成
if __name__ == "__main__":
generate_simple_midi("c_major_melody.mid")
代码说明:生成流程需遵循 MIDI 文件结构,先创建 MidiFile 对象,再添加音轨、元事件(速度、拍号)、音色设置,最后添加音符事件并保存。可通过调整音符的音高、时长、力度,生成任意旋律和节奏。
实际开发中常需对现有 MIDI 文件进行改造(如音色替换、节奏加速、添加效果),以下示例实现 3 种常见改造需求:
def replace_instrument(input_path, output_path, old_program, new_program, channel=0):
"""
替换 MIDI 文件指定通道的音色
:param input_path: 输入 MIDI 路径
:param output_path: 输出 MIDI 路径
:param old_program: 原音色编号
:param new_program: 新音色编号
:param channel: 目标通道(0-15)
"""
mid = mido.MidiFile(input_path)
for track in mid.tracks:
for msg in track:
# 找到目标通道的 program_change 事件,替换音色编号
if msg.type == 'program_change' and msg.channel == channel and msg.program == old_program:
msg.program = new_program
mid.save(output_path)
print(f"音色替换完成:通道{channel} 从{old_program}(钢琴)→{new_program}(小提琴)")
# 测试:将通道 0 的钢琴(0)替换为小提琴(41)
replace_instrument("test.mid", "violin_version.mid", old_program=0, new_program=41, channel=0)
def adjust_tempo(input_path, output_path, speed_ratio):
"""
调整 MIDI 文件速度(比例系数)
:param input_path: 输入 MIDI 路径
:param output_path: 输出 MIDI 路径
:param speed_ratio: 速度比例(1.0=原速,1.5=加速 50%,0.8=减速 20%)
"""
mid = mido.MidiFile(input_path)
for track in mid.tracks:
new_messages = []
for msg in track:
# 调整所有事件的 Delta-Time(按比例缩放)
msg.time = int(msg.time * speed_ratio)
# 调整速度事件(可选,进一步微调 BPM)
if msg.type == 'set_tempo':
original_bpm = mido.tempo2bpm(msg.tempo)
new_bpm = original_bpm * speed_ratio
msg.tempo = mido.bpm2tempo(new_bpm)
new_messages.append(msg)
# 替换音轨事件
track[:] = new_messages
mid.save(output_path)
print(f"速度调整完成:{speed_ratio}倍速,新 BPM 约为原 BPM×{speed_ratio}")
# 测试:加速 50%(1.5 倍速)
adjust_tempo("test.mid", "fast_version.mid", speed_ratio=1.5)
def add_reverb(input_path, output_path, channel=0, reverb_depth=90):
"""
为指定通道添加混响效果(通过 CC91 控制器)
:param input_path: 输入 MIDI 路径
:param output_path: 输出 MIDI 路径
:param channel: 目标通道
:param reverb_depth: 混响深度(0-127,越大混响越强)
"""
mid = mido.MidiFile(input_path)
# 找到第一个音轨(通常为旋律轨),在开头添加混响控制事件
melody_track = mid.tracks[0]
# 在速度事件后插入混响控制(CC91)
insert_pos = 0
for idx, msg in enumerate(melody_track):
if msg.type == 'set_tempo':
insert_pos = idx + 1
break
# 添加 CC91 事件(混响深度)
melody_track.insert(insert_pos, Message('control_change', channel=channel, control=91, value=reverb_depth, time=0))
mid.save(output_path)
print(f"混响效果添加完成:通道{channel},混响深度{reverb_depth}")
# 测试:为通道 0 添加混响(深度 90)
add_reverb("test.mid", "reverb_version.mid", channel=0, reverb_depth=90)
Mido 结合 python-rtmidi 可实现与 MIDI 硬件设备(如键盘、合成器)的实时交互,支持发送消息控制设备发声、接收设备输入的音符消息:
import mido
from mido import Message
def list_midi_devices():
"""列出所有可用的 MIDI 输入/输出设备"""
print("MIDI 输出设备:")
for idx, name in enumerate(mido.get_output_names()):
print(f" {idx}: {name}")
print("\nMIDI 输入设备:")
for idx, name in enumerate(mido.get_input_names()):
print(f" {idx}: {name}")
def send_note_to_device(device_idx, note, velocity=64, duration=1):
"""
向 MIDI 设备发送音符消息(控制发声)
:param device_idx: 输出设备索引
:param note: 音高(0-127)
:param velocity: 力度(0-127)
:param duration: 发声时长(秒)
"""
with mido.open_output(mido.get_output_names()[device_idx]) as port:
# 发送 Note On(开始发声)
port.send(Message('note_on', note=note, velocity=velocity))
# 等待指定时长
mido.sleep(duration)
# 发送 Note Off(停止发声)
port.send(Message('note_off', note=note, velocity=velocity))
def receive_notes_from_device(device_idx):
"""接收 MIDI 设备(如键盘)输入的音符消息"""
print(f"开始接收设备{device_idx}的音符消息(按 Ctrl+C 退出)")
with mido.open_input(mido.get_input_names()[device_idx]) as port:
for msg in port:
if msg.type in ['note_on', 'note_off']:
status = "按下" if msg.type == 'note_on' and msg.velocity > 0 else "松开"
print(f"音符{msg.note}(C4=60):{status},力度{msg.velocity}")
# 测试硬件交互
if __name__ == "__main__":
list_midi_devices()
# 发送音符到设备 0(替换为实际设备索引)
# send_note_to_device(device_idx=0, note=60, duration=1)
# 接收设备输入(注释掉发送部分,单独运行)
# receive_notes_from_device(device_idx=0)
代码说明:需先通过 list_midi_devices() 查看系统中的 MIDI 设备索引,再指定设备发送/接收消息。适用于开发 MIDI 键盘控制器、实时音效触发等硬件交互场景。
六、MIDI 2.0 协议核心升级:兼容性与新特性(前沿技术)
MIDI 1.0 协议自 1983 年发布以来,支撑了电子音乐、音频开发数十年的发展,但随着 AI 作曲、高保真音频、跨设备协同等需求的升级,其带宽限制、单向传输、低精度参数等短板逐渐凸显。2020 年 MIDI 制造商协会(MMA)与日本电子乐器工业协会(AMEI)联合发布 MIDI 2.0 协议,在保留向下兼容的基础上,实现了全方位技术突破,为新一代音乐开发提供了底层支撑。
本节将从升级背景、核心新特性、兼容性设计、落地现状及开发适配要点五个维度,拆解 MIDI 2.0 的技术细节,为开发者提供前沿技术参考。
MIDI 1.0 在设计之初针对的是 80 年代的电子乐器,受硬件性能限制,存在诸多难以突破的瓶颈,无法适配当下的技术场景:
基于上述瓶颈,MIDI 2.0 以'高精度、双向交互、高兼容性、可扩展'为核心目标,进行了全协议层的重构与升级,同时严格保证与 MIDI 1.0 设备的向下兼容,降低行业迁移成本。
MIDI 2.0 并非对 1.0 的简单修补,而是在消息结构、参数精度、传输机制、设备交互等层面实现了颠覆性升级,核心特性可概括为'高精度、双向化、可扩展、智能化'四大维度。
MIDI 2.0 将核心参数精度从 1.0 的 7 位(0-127)提升至 16 位(0-65535),同时保留 7 位模式供兼容使用,大幅提升了音乐表达的细腻度。
核心升级点:
开发注意:16 位参数传输需区分'原生 16 位模式'与'7 位兼容模式',可通过设备协商机制自动适配,确保对 1.0 设备的兼容。
双向通信是 MIDI 2.0 最核心的升级之一,打破了 1.0 单向传输的局限,实现'指令下发 + 状态回传'的闭环交互,为多设备协同、智能化控制奠定基础。
核心实现:
应用场景:在 AI 作曲辅助中,上位机可根据合成器回传的音色特性,动态调整生成的 MIDI 序列,确保演奏效果与设备匹配;在舞台演出中,控制台可实时监控所有设备状态,实现精准同步控制。
MIDI 2.0 突破了 1.0 的 16 通道限制,同时提升了传输带宽,适配多设备、多轨复杂乐曲的开发需求。
MIDI 2.0 新增标准化的元数据协议和设备配置框架,解决了 1.0 时代厂商自定义格式导致的兼容性问题,同时适配 AI 作曲、跨平台开发的需求。
MIDI 2.0 原生支持 BLE MIDI、WiFi MIDI 等无线传输方式,同时优化了对嵌入式设备、移动端的适配,满足物联网时代的跨终端音乐开发需求。
为降低行业迁移成本,MIDI 2.0 采用'双模式兼容'设计,确保 MIDI 1.0 设备可与 2.0 设备无缝协同,同时支持开发者逐步迭代升级应用。
目前 MIDI 2.0 已逐步进入商业化落地阶段,主流厂商(Yamaha、Roland、Korg、Native Instruments)均已发布 2.0 设备(如合成器、键盘、接口盒);软件层面,Ableton Live、Logic Pro、Cubase 等专业 DAW 均已支持 MIDI 2.0 特性;开发库层面,Mido 1.3.0+、RtMidi 5.0+、CoreMIDI(macOS 11+)、MMMIDI API(Windows 11+)均提供完整的 2.0 协议支持。
但需注意,民用市场仍以 MIDI 1.0 设备为主,2.0 设备主要集中在专业领域(录音棚、舞台演出、高端电子乐器),开发者需兼顾新旧协议的适配需求。
MIDI 2.0 的升级为音乐开发与新兴技术的融合奠定了基础,未来核心趋势包括:
综上,MIDI 2.0 并非简单的协议升级,而是开启了'高精度、智能化、跨终端'的音乐开发新时代。开发者需提前掌握其核心特性与适配逻辑,才能在专业音频开发、AI 作曲、物联网音乐设备等前沿领域占据先机。
七、全量附录:标准表、工具集与常见问题排查(开发必备)
本节整理 MIDI 开发高频使用的标准表、工具集及常见问题解决方案,可作为项目开发的工具书直接复用,大幅提升开发效率,规避常见坑点。
GM(General MIDI)标准统一了音色编号,确保不同设备播放一致性,是开发必备基础表,按编号对应如下:
通道 10 为 GM 标准鼓组通道,不响应 Program Change 消息,通过 Note On 音高控制鼓组音色,核心音高与音色对应如下:
音高(十进制) | 音色名称 | 音高(十进制) | 音色名称 | 音高(十进制) | 音色名称 |
35 | 低音鼓 1 | 53 | 拍手 | 71 | 钟铃 |
36 | 低音鼓 2 | 54 | 电颤琴 | 72 | 牛铃 |
37 | 边击底鼓 | 55 | 高音康加鼓 | 73 | 木鱼(高) |
38 | 军鼓 1 | 56 | 低音康加鼓 | 74 | 木鱼(低) |
39 | 边击军鼓 | 57 | 开镲 1 | 75 | tambourine |
40 | 军鼓 2 | 58 | 高邦戈鼓 | 76 | 响板 |
41 | 低嗵鼓 1 | 59 | 低邦戈鼓 | 77 | 三角铁(高) |
42 | 闭镲 1 | 60 | 开镲 2 | 78 | 三角铁(低) |
43 | 低嗵鼓 2 | 61 | 踩镲踏板 | 79 | 振动器 |
44 | 闭镲 2 | 62 | 高音嗵鼓 1 | 80 | 响铃 |
45 | 中嗵鼓 1 | 63 | 高音嗵鼓 2 | 81 | 铃鼓 |
46 | 开镲 3 | 64 | 铃鼓 | 82 | 钹(高) |
47 | 中嗵鼓 2 | 65 | 牛铃(低) | 83 | 钹(低) |
48 | 高嗵鼓 | 66 | 牛铃(高) | 84 | 雨声器 |
49 | 击镲 1 | 67 | 木鱼(中) | 85 | 风声器 |
50 | 击镲 2 | 68 | 定音鼓(高) | 86 | 雷鸣器 |
51 | 吊镲 1 | 69 | 定音鼓(中) | 87 | 海浪声 |
52 | 吊镲 2 | 70 | 定音鼓(低) | 88 | 鸟鸣声 |
Control Change 消息(0xBn)支持 128 个控制器,以下为开发高频使用的控制器及功能,剩余控制器为厂商自定义或预留:
CC 编号 | 功能名称 | 取值范围 | 核心说明 |
0 | 银行选择(MSB) | 0-127 | 配合 CC32 切换扩展音色库,高位字节 |
1 | 调制轮 | 0-127 | 控制颤音、音色亮度,默认对应调制深度 |
2 | 呼吸控制器 | 0-127 | 模拟呼吸力度,控制音量或音色变化 |
4 | 脚踏控制器 1 | 0-127 | 自定义功能,常见用于表情控制 |
5 | 端口amento 时间 | 0-127 | 滑音时间调节,值越大滑音越慢 |
6 | 数据入口 | 0-127 | 配合 CC98/99 设置 14 位参数,低位字节 |
7 | 主音量 | 0-127 | 控制对应通道整体音量,0=静音,127=最大 |
8 | 平衡 | 0-127 | 左右声道平衡,64=居中 |
10 | 声像 | 0-127 | 通道声像定位,0=左声道,127=右声道 |
11 | 表情强度 | 0-127 | 细腻音量调节,比 CC7 更精准 |
12 | 效果控制 1 | 0-127 | 自定义效果参数,常见用于混响前置调节 |
13 | 效果控制 2 | 0-127 | 自定义效果参数,常见用于延迟前置调节 |
16 | 通用滑块 1 | 0-127 | 自定义滑块控制,适配设备旋钮 |
17 | 通用滑块 2 | 0-127 | 自定义滑块控制,适配设备旋钮 |
18 | 通用滑块 3 | 0-127 | 自定义滑块控制,适配设备旋钮 |
19 | 通用滑块 4 | 0-127 | 自定义滑块控制,适配设备旋钮 |
32 | 银行选择(LSB) | 0-127 | 配合 CC0 切换扩展音色库,低位字节 |
64 | 延音踏板(sostenuto) | 0-127 | 0-63=抬起,64-127=按下,控制音符延音 |
65 | 端口amento 开关 | 0-127 | 0-63=关闭,64-127=开启,控制滑音功能 |
66 | sostenuto 踏板 | 0-127 | 保持踏板,仅保持按下踏板时的音符 |
67 | 软踏板 | 0-127 | 减弱音量并柔化音色,值越大效果越明显 |
71 | 谐振(亮度) | 0-127 | 控制音色明亮度,值越大音色越亮 |
72 | 释放时间 | 0-127 | 控制音符释放时长,值越大衰减越慢 |
73 | Attack 时间 | 0-127 | 控制音符起音时长,值越大起音越缓 |
74 | 亮度控制 | 0-127 | 辅助谐振调节,细化音色明亮度 |
75 | 衰减时间 | 0-127 | 控制音符衰减时长,值越大衰减越慢 |
76 | 延音时间 | 0-127 | 控制音符延音阶段时长 |
77 | 震音速率 | 0-127 | 控制颤音频率,值越大颤音越快 |
78 | 震音深度 | 0-127 | 控制颤音幅度,值越大颤音越明显 |
79 | 震音延迟 | 0-127 | 控制颤音启动延迟,值越大延迟越久 |
80 | 端口amento 深度 | 0-127 | 控制滑音幅度,值越大滑音越明显 |
91 | 混响深度 | 0-127 | 控制混响效果强度,0=无混响 |
92 | 延迟深度 | 0-127 | 控制延迟效果强度,0=无延迟 |
93 | 合唱深度 | 0-127 | 控制合唱效果强度,0=无合唱 |
94 | 音色过滤 | 0-127 | 控制音色滤波强度,细化音色质感 |
95 | 效果音量 | 0-127 | 控制整体效果音量,平衡干声与效果声 |
96 | 数据增量 | 0-127 | 增加数据参数值,配合数据入口使用 |
97 | 数据减量 | 0-127 | 减少数据参数值,配合数据入口使用 |
98 | 非注册参数(MSB) | 0-127 | 厂商自定义参数,高位字节 |
99 | 非注册参数(LSB) | 0-127 | 厂商自定义参数,低位字节 |
100 | 注册参数(MSB) | 0-127 | 标准化注册参数,高位字节 |
101 | 注册参数(LSB) | 0-127 | 标准化注册参数,低位字节 |
121 | 重置所有控制器 | 0 | 重置对应通道所有 CC 控制器至默认值 |
122 | 本地控制开关 | 0-127 | 0=关闭本地控制,64-127=开启 |
123 | 所有音符关闭 | 0 | 停止对应通道所有发声音符 |
124 | 所有声音关闭 | 0 | 停止对应通道所有声音(含效果声) |
125 | 单音模式开关 | 0-127 | 0=复音模式,64-127=单音模式 |
126 | 复音模式开关 | 0-127 | 0=单音模式,64-127=复音模式 |
127 | 触后灵敏度 | 0-127 | 调节触后效果灵敏度,值越大越灵敏 |
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 设备无法被系统识别 | 1. 驱动未安装;2. USB 线故障;3. 端口冲突;4. 设备供电不足 | 1. 安装厂商官方驱动(小众设备),主流设备即插即用;2. 更换优质 USB 线,尝试不同 USB 端口;3. 关闭占用端口的其他程序,重启电脑;4. 为设备单独供电(部分大功率设备) |
| 多设备串联无响应 | 1. 端口连接错误(如 OUT 接 OUT);2. 信号衰减;3. 未开启 Thru 端口 | 1. 严格按'OUT→IN'连接,Thru 端口用于转发;2. 减少串联设备数量(≤5 台),改用星型拓扑;3. 确认设备 Thru 端口开启,部分设备默认关闭 |
| 无线 MIDI 延迟过高 | 1. 距离过远;2. 干扰严重;3. 设备功耗设置过低 | 1. 缩短设备距离(BLE 建议≤10 米);2. 避开 WiFi、蓝牙干扰源;3. 调整设备功耗模式,优先保证传输速率 |
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| MIDI 文件解析失败 | 1. 缺少 MThd/MTrk 标识;2. 大端序转换错误;3. VLQ 解码异常;4. 事件未结束(无 0xFF 0x2F 0x00) | 1. 验证文件开头标识,非法文件需过滤;2. 所有多字节数据使用大端序转换(如 Python struct 的'>'符号);3. 限制 VLQ 最大 4 字节,避免死循环;4. 确保解析至音轨结束事件,补全缺失事件 |
| 运行模式下消息解析错乱 | 1. 未记录上一状态字节;2. 系统实时消息干扰 | 1. 维护状态字节变量,非状态字节时复用前一状态;2. 系统实时消息(0xF8-0xFF)无 Delta-Time,单独处理,不影响状态字节记录 |
| 时间计算偏差 | 1. Division 格式解析错误;2. BPM 未正确读取;3. VLQ 解码错误 | 1. 区分 PPQ 与 SMPTE 格式,正确解析时间基准;2. 读取元事件中的速度信息(0xFF 0x0A),计算 BPM;3. 验证 VLQ 解码逻辑,对照示例数据测试 |
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 音色错乱 | 1. 通道 10 误发 Program Change;2. 未适配 GM/GS/XG 标准;3. 音色库缺失 | 1. 通道 10 为鼓组通道,禁止发送 Program Change;2. 优先使用 GM 标准音色,如需扩展适配 GS/XG;3. 加载对应 SoundFont 音色库,确保音色编号匹配 |
| MIDI 2.0 消息无法识别 | 1. 设备仅支持 1.0 协议;2. 未协商协议版本;3. 16 位参数未降级 | 1. 检测设备协议版本,自动切换至 1.0 模式;2. 通过属性交换协议协商版本,避免调用不支持的特性;3. 16 位参数降级为 7 位(右移 9 位),确保 1.0 设备兼容 |
| 控制器参数无响应 | 1. CC 编号错误;2. 设备不支持该控制器;3. 未重置控制器状态 | 1. 对照 CC 控制器表验证编号,避免使用预留编号;2. 查询设备能力参数,仅调用支持的控制器;3. 初始化时发送 CC121(重置控制器),恢复默认状态 |
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| Mido 库无法读取设备 | 1. 端口名称错误;2. 权限不足;3. 库版本过低 | 1. 通过 mido.get_input_names()/get_output_names() 获取端口名称;2. 赋予程序设备访问权限(Linux/macOS);3. 升级 Mido 至 1.3.0+,支持 MIDI 2.0 |
| RtMidi 库编译失败 | 1. 缺少依赖库;2. 跨平台适配错误 | 1. 安装 PortAudio、ALSA 等依赖(Linux);2. 配置跨平台编译选项,确保 API 适配对应系统(如 Windows 用 MMMIDI,macOS 用 CoreMIDI) |
| Web MIDI API 无法调用设备 | 1. 浏览器不支持;2. 未获取用户授权;3. 仅支持 HTTPS | 1. 使用 Chrome、Edge 等现代浏览器;2. 调用 API 时请求用户授权,获取设备访问权限;3. 本地测试可使用 localhost,线上需部署 HTTPS |
本附录可作为开发手册随时查阅,结合前文的协议解析与实操代码,可覆盖从需求开发到问题排查的全流程,助力高效落地 MIDI 相关项目。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online