4. 标准 MIDI 文件格式规范
MIDI 协议解决的是音乐设备之间的即时通讯问题,它本质上是一个硬件之间的通信协议。而当我们想把 MIDI 演奏保存在磁盘上则需要用到标准 MIDI 文件格式规范(Standard MIDI-File Format Spec)。和 MIDI 通信协议一样,MIDI 文件也是 8 位字节流,下文将会说明 MIDI 文件一些最基本的格式规范。 4.1 ChunkChunk 是构成 MIDI 文件的基本单元。一个 Chunk 由三个部分组成:Chunk 类型 、Chunk 长度以及 Chunk 数据。Chunk 类型是 4 个 ASCII 字符,之后使用 32 位表示 Chunk 数据的长度,最后才是 Chunk 需要存储的数据。 MIDI 中一共有两种 Chunk,分别为 Header Chunk 和 Track Chunk。Header Chunk 标记为 MThd,存储的是整个 MIDI 文件的基本信息,和 PNG 等文件的 Header Chunk 类似。Track Chunk 标记为 MTrk,每个 Track Chunk 都存储了一个 MIDI 事件流,一个事件流可以包含 16 个 MIDI 信道的消息。一个典型 MIDI 文件的结构如下: MThd <length>
<MThd data>
MTrk <length>
<MTrk data>
MTrk <length>
<MTrk data>

4.2 Header ChunkMIDI 文件的 Header Chunk 包含的信息非常简单,我们以上面这个文件为例: 4D 54 68 64 // MThd 的 ASCII 码
00 00 00 06 // MThd 的数据长度,MThd Data 固定为 6 字节
---- DATA 部分 ----
00 01 // MIDI 文件格式,有 0、1、2 三种
00 02 // MIDI 文件的包含的音轨数量,即 Track Chunk 数量
00 DC // MIDI 文件的时间类型
前两条数据已经介绍过,这里不再赘述。我们来解释一下 MIDI 文件格式与 MIDI 时间类型: MIDI 文件格式(MIDI File Formats) MIDI 文件格式分为三种,格式 0 的 MIDI 文件只有一个 Header Chunk 和一个 Track Chunk。对于只有一个轨道的程序可以采用这种格式。 格式 1 有一个 Header Chunk ,和多个 Track Chunk 。其中第一条 Track Chunk 是特殊的,负责记录 MIDI 文件的所有 Meta Event(后面会讲到),而从第二条 Track Chunk 开始才会记录 MIDI Event,所以我们上图中的 MIDI 文件实际上只有一条用于演奏的音轨。目前绝大部分的支持多音轨的程序都采用这种格式,笔者也建议读者尽量使用这种格式。 格式 2 的 MIDI 文件也有多个 Track Chunk,但不同的是格式 1 所有 Track Chunk 共用一条时间轴,所有 Track 应当被视作同时播放的。而格式 2 中 Track Chunk 都有自己独立的时间信息,这种格式非常少见,不建议使用。 我们用一张表总结一下: [td]
| 音轨数量 | 时间轴 | 格式 0 | 1 个 | 1 条 | 格式 1 | 多个 | 1 条 | 格式 2 | 多个 | 多条 |
MIDI 时间类型 MIDI 时间类型主要有两种,为了方便介绍读者可以简单将其理解为“按音符分割的”和“按帧分割的”: “按音符分割的”时间类型 15 位为 0,被称为 TPQN(Ticks Per Quarter-Note),即一个四分音符中包含了多少 Tick。在前文的例子中 00 DC 表示 TPQN 为 220,那么一个八分音符为 110 Ticks,一个二分音符为 440 Ticks。另外 TPQN 也被称为 Pulses Per Quarter-Note (每四分音符的脉冲数),如果你在代码中看到 PPQ、PPQN 这样的简写,你知道他们是一个意思即可。 “按帧分割的”时间类型 15 为 1,这种格式单纯 MIDI 文件中几乎不用而且比较复杂,建议读者跳过。其编码规则简单说就是使用了 SMPTE 时间码的规范。其 14 - 8 位包含了包含 -24、-25、-29 或 -30 四个值之一,对应于四种标准 SMPTE 时间码格式(-29 对应于 30 个丢帧),并表示每秒的帧数。第 7 到 0 位表示帧内分辨率。我们依然用一张表总结一下:
| 15 位 | 14-8位 | 7-0位 | 按音符 | 0 | 四分音符的Tick数 | 按帧 | 1 | SMPTE格式 | 每帧Tick数 | 4.3 Track ChunkTrack Chunk 的主要功能是用于存储实际的演奏数据。它的 Chunk Data 中存储的是一串事件流,被 Track Chunk 记录的事件我们称为 MTrk 事件,其结构如下: <MTrk event> = <delta time> <event>
在这个结构中,事件可以指代三类事件:midi 事件、系统独有事件、元事件: <event> = <midi event> | <sysex event> | <meta event>
delta time MIDI 通信时所有信息都是即时执行,所以 MIDI 消息并没有记录时间,但是 MIDI 文件则需要记录时间在时间轴上的位置。MIDI 文件采用差量时间来记录 MIDI 事件,即 Δt。delta time 表示的是当前事件与上一个时间相差的 Tick 数。如果要表示同时发生的数个任务,则记录一串 delta time 为 0 的事件流即可。比如我们控制器一章中切换乐器的事件流可以表示为: Delta time : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0000 0000 // 0 = Sound bank selection (MSB)
Data byte 2 : 0000 0101
Delta time : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0010 0000 // 32 = Sound bank selection (LSB)
Data byte 2 : 0000 0001
Delta time : 0000 0000
Status byte : 1100 0000
Data byte 1 : 0000 0010
sysex event 即系统独占的消息事件,具体可以参考前文中的系统独占消息。 meta event 所有元事件以 1111 1111 开头,这个指令在 MIDI 消息中表示系统复位。这个指令是一个系统实时信息,通常在使用 MIDI 文件的程序并不会用到,所以在这里用于表示元事件。元事件主要用于指定拍号、调号、速度等。 需要注意的是FF 2F 00 是一个特殊的元事件,表示轨道结束。所有 Track Chunk 都以这个元事件结束。下面这张表是标准中已定义的元事件: [td]
| 意义 | FF 00 02 | 序列号 | FF 01 len text | 文本事件 | FF 02 len text | 版权声明 | FF 03 len text | 轨道名称 | FF 04 len text | 轨道中使用的乐器类型 | FF 05 len text | 歌词 | FF 06 len text | 某个点的名称,比如“第一乐章” | FF 07 len text | Cue Point 某个舞台事件描述 | FF 20 01 cc | MIDI 通道前缀 | FF 2F 00 | End of Track | FF 51 03 tttttt | 设置速度 | FF 54 05 hr mn se fr ff | SMPTE Offset | FF 58 04 nn dd cc bb | 拍号 | FF 59 02 sf mi | 调号 | 5. MIDI 协议的缺陷与改良方案5.1 MIDI 2.0 & MPEMIDI 通信协议目前看来主要有两个较明显的缺陷。第一个缺陷是许多值可以表示的范围实在有限,比如 note off 的 velocity 就只有 128 个、乐器也只有 128 个、只有 16 个信道。 另一个问题更为麻烦,MIDI 中控制器、和弯音消息只能发送给某个信道,你根本就没法将它和某个音联系在一起。这一局限在以前并没有引起多少问题,因为传统乐器很少碰到按音处理控制器的情况。而弯音用得最频繁的更多是单声部乐器。 但电子音乐界向来不缺乏整活健将,工程师总是会想方设法突破现有限制。最典型的例子就是 seaboard 键盘,这玩意儿可以在每个键上提供弯音能力。你可以从下面这段演奏上感受到这一乐器的神奇魅力:
,时长01:01
[color=rgba(255, 255, 255, 0.8)]
为了解决让控制器消息能按“音”发送,seaboard 的制造商 ROLI 制订了 MIDI Polyphonic Expression(MPE,MIDI 复音表示法)。其原理基本上可以概括为:让每个发声的音符都会在其 Note On 和 Note Off 之间临时分配一个 MIDI 通道。这样便把控制器消息和弯音消息与特定音符建立了联系,并且很好的兼容了 MIDI 协议。 上述问题现在都正在通过新的 MIDI 2.0 得到解决,在 MIDI 2.0 中 volocity 从 0 - 128 扩展到 0 - 65535,信道从 16 个增加到 256 个,同时 MIDI 2.0 也支持 MPE 以及远程控制。 5.2 如何拓展 MIDI如果 MIDI 2.0 和 MPE 这类现成的解决方案无法满足你的需求,那么你可以考虑自己来拓展 MIDI 协议或者 MIDI 标准格式。目前来看,可靠的拓展方式有几下几种: - 使用未定义的 MIDI 消息:比如系统消息 1111 0101 的行为在 MIDI 标准中就未被定义。这种方法的好处是不需要进行额外的解析工作,但缺点便是可以使用的指令十分有限。
- 使用自定义 Chunk:Chunk 在设计之初便考虑到了拓展的问题,你可以按照 Chunk 的格式自由地声明一个新的 Chunk 类型,主流解析工具在碰到无法解析的 Chunk 时会自动忽略掉,所以不用担心兼容的问题。如果你有整段的数据,既不属于 Track,又不能被 Heaer 所包含,那么可以考虑这种方式。
- 使用系统独占消息:如果你需要在 MIDI 通信协议上进行拓展,可以考虑使用系统独占消息,合成器会自动忽略无法解析的独占消息。具体可以参考附录中的系统独占消息一节。
- 其他:你也可以参考 MPE 的方式,基于现有的编码方案但是重新定义指令的意义和执行。
6. 思考与讨论6.1 什么时候使用 MIDI 格式,什么时候不用?首先我们需要认识到 MIDI 的优点,MIDI 记录的实际上是事件流,最适合的场景就是在现场演奏时用于硬件之间的通信。作为 MIDI 文件格式作为一种存储格式,其优点是数据十分紧凑,体积较小。但 MIDI 的缺点是十分明显的,一方面我们无法快速查询、访问其中某个具体内容的值:比如我们没法快速找到某一个轨道的拍号,或者某个音的音高。 所以我的建议是,尽量避免在现场演奏场景之外使用 MIDI 文件格式,但可以在抽象上对齐 MIDI。在内存中我们尽量把 MIDI 文件转化为实例对象,便于我们快速访问。在需要持久化的场景下则可以使用更容易解析的 JSON 或者 MusicXML 格式。只有在用户需要或者向其他编辑工具导出数据的时候,我们才考虑使用 MIDI 标准文件格式。 6.2 MIDI 协议无法满足的需求如何解决?绝大部分这类问题可以通过不使用 MIDI 编码来解决。原则很简单,只要不涉及现场演奏场景和向其他工具导出数据,就避免使用 MIDI 编码来做任何事情。只用确保在需要 MIDI 的场景可以导出 MIDI 文件就行。 6.3 如果多数场景不使用 MIDI,那有必要深入学习 MIDI 协议吗?如果你的开发工作涉及到音乐的“本体”部分,那么我建议多了解一些 MIDI 协议,因为虽然我们可能多数情况下不直接使用 MIDI 协议的编码,但是 MIDI 的事件流是创作场景和存储场景会大量用到的,同时 MIDI 中的多数抽象和概念是行业内通用的。 6.4 如何设计自定义的音乐数据格式?我的建议是用一个文档维护所有的基础字段和拓展字段,各项目在定义 Model 时尽量参考这个文档。如果现有的拓展字段可以解决你的需求,就不要新增拓展字段。 附录 可变长度数量(Variable-Length Quantities)由于单个字节表示的最大范围为 0 - 256,所以在 MIDI 文件中表示较大数字时会采用可变长度数量。其每一个字节使用第 7 位表示这个字节是否为最后一个字节,1 表示不是最后一个字节,0 表示是最后一个字节, 0 - 6 位则作为有效位。 举一个例子,数字 127 可以表示为 0111 1111 ,128 则表示为 1000 0001 0000 0000,这样理论上可以表示的数字可以无限大,不过在实践中通常不会使用超过 32 位。 总结一下就是: [td]速率的解释note on 中的 velocity 实际上是按键的“触发速率”,你可以把其视为从键盘能感知到下按到下按结束这个过程中的键程除以按下时间,note off 则是反向的“释放速率”。速率的计算方式和更多细节可以参考这篇论文:The Interpretation of MIDI Velocity 一堆速查表- 查十进制的 MIDI 消息:Expanded MIDI 1.0 Messages List (Status Bytes)
- 查 GM 乐器表:General MIDI Instrument List
- 查 MIDI 事件流:Standard MIDI-File Format Spec. 1.1, updated
参考文献推荐读物- 《音乐声学——音响、乐器、计算机音乐、MIDI、音乐厅声学原理及应用》- 龚镇雄
- The Computer Music Tutorial - Curtis Roads
加入我们
字节跳动音乐研发团队,业务包含字节旗下的音乐流媒体应用、字节音乐中台、抖音 & 西瓜视频中的音乐视频和音乐创作工具等场景。团队拥有良好的技术氛围,在 ByteTech 沉淀了大量优秀的视频课程和技术文章,欢迎各位同学加入。
|