读书笔记——MIDI文件结构简介
庆祝论坛乔迁新居!我也把我几个月来研究MIDI文件的一些心得体会,贴到坛子上分享。哈哈,每天上下班在路上的一个多小时里,我拎着一个上网本,在公交车上慢慢地敲字~时间宝贵,这一个多小时的时间也要充分利用~
但这些东东毕竟是我个人的理解,难免有错误出现。也欢迎各位大虾批评指正,我会把一些修改的内容通过附录添加到文章末尾。
阅读本文的话,读者需要有基础性的电脑和MIDI音乐相关知识:
· 基本的计算机相关知识,如十六进制数的表示法,十六进制与十进制、二进制之间的转换方法等;
· 基本的乐理知识,如音高、时长、节拍、曲调等;
· MIDI协议基础,如各种MIDI事件的类型和含义,事件状态字、数据长度等的知识;
· MIDI文件的编辑制作经验,如音序器(硬件或软件)基于音轨的操作思想和使用方式等。
参考资料的话,推荐读者去找一下《MIDI原理与开发应用》这本书,国防工业出版社出版,陈学煌、刘永志、潘晓利、马俊编著,红色封皮。同时读者还可以去网上搜索相关的资料,有几个帖子写得也不错。
下面是正文:
- MIDI文件结构简介
MIDI文件是二进制文件,其内部主要记录了乐曲播放时,音序器应发送给音源的MIDI指令和每条指令发送的时间点。音序器读取这些时间信息和MIDI指令,通过在相应的时间发送相应的指令,以实现乐曲中音符的顺序播放和节拍信息。除了音序器需要发送的MIDI事件之外,MIDI文件内部也记录了一些辅助信息,如版权信息、音轨名、速度信息、拍号、调号等等,这些信息被称为Meta-event,只用于记录一些曲子的信息,通常并不发送给MIDI系统中的其他设备。
MIDI文件的数据结构被称为“chunk”。每个chunk由最初4字节的“chunk类型”,紧接着4字节的“Chunk大小”,和最后长度可变的“Chunk Data”构成。“Chunk data”的数据长度由“Chunk大小”来规定,即“Chunk大小”只描述了chunk中数据段的长度,而不是整个chunk的长度。
构成MIDI文件的Chunk主要有两种类型:一种是Header Chunk(MThd),另一种是Track Chunk(MTrk)。
Header Chunk位于整个MIDI文件的起始处,是必须存在的,其起始标记就是ASCII码形式的“MThd”字符串。Track Chunk的起始标记,依然是ASCII码形式的“MTrk”字符串,并且Track Chunk整块分布于MIDI文件之中的任何位置,数量也不定,从1块到若干块皆可。实际上一个MIDI文件就是由一个Header Chunk和若干Track Chunk组成。读者若使用一个十六进制编辑软件(如UltraEdit)打开并查看一个MID文件时,便能找到这两部分。
MIDI文件可能容纳的chunks只有Header Chunk和Track Chunk,其它的非法数据结构将被忽略(作者最新补充:有些厂商通过规定其他的Chunk以实现特殊功能,但有时候当其他厂商的音序器不支持这些Chunk时,有可能会破坏这些数据,所以要慎重编辑)。在MIDI的Chunk文件结构中,自长度区以后的数据格式,是严格规定好的。
这里要探讨不同数量的MTrk chunk所构成的MIDI文件的类型。MIDI文件的类型通常分为三种,分别是MIDI 0格式文件、MIDI 1格式文件、MIDI 格式2文件。
它们的相同点是:无论哪一种格式的MIDI文件,都要具备一个MThd chunk,和至少一条MTrk chunk。不同点是:它们各自的MTrk chunk数量不同,并且各个 chunk之间的播放方式略有区别。很多入门级电子琴和具备播放简单和弦铃声的手机,只能播放MIDI 0格式文件;平时用音序器软件编辑MIDI时,又最好保存为MIDI 1格式文件。这种现象是有原因的。
MIDI 0格式文件只有一个MTrk chunk。在这个chunk中,包含了整个MIDI文件中的MIDI事件,包括meta-event、演奏信息、效果器信息等等。所以播放器只需要顺序读取并解析文件,并发送实际的MIDI事件即可。播放MIDI 0格式文件,对于入门级电子琴或者手机这种性能较弱、资源紧张的嵌入式系统来说,相对容易一些。音序器不需要考虑在不同MTrk之间来回跳跃取数,只需要像流媒体文件那样顺序读取并解析就行了。
MIDI 1格式文件具有若干条MTrk chunk,并且chunk之间具有统一的时间信息,也就是说,各个chunk之间的播放是同步进行的。MIDI 1格式文件的第一条MTrk chunk是专用的,称为“Tempo Map”。它包括整个MIDI文件中所有的 meta-event。(笔者新增:后来笔者在玩MIDI时,发现这个Tempo Map有时可能是软件自己创建,也就是说用户在音序器中见到的第一轨,未必就是MIDI文件中的第一个MTrk Chunk即Tempo Map;反过来说,MIDI文件中的Tempo Map有可能不会出现在音序器中)从第二条MTrk chunk开始,每一条MTrk chunk都记录着各自的演奏和效果器等信息。音序器在播放时,将使用统一的时间等信息,同步播放各个chunk。这就像cakewalk软件播放MIDI文件一样,在一个时间轴的滚动下(音轨区的那条标志播放位置的竖线),各个音轨同时播放。实际上cakewalk软件也仅支持MIDI 0和MIDI 1文件。
MIDI 2格式文件也具有若干条MTrk chunk,但每个chunk具有独立的时间信息,也就是说各个chunk的播放并不是同步的,而是每个chunk都遵循自己的时间信息,chunk之间没有统一的时间联系,各自播放。这种文件目前笔者也未曾见过,所以笔者在此不再过多解释此格式的文件。
综上所述,在解读MIDI文件时,首先要找到各个块,也就是一个MThd chunk和若干MTrk chunk的ASCII字符串。这样用户或软件才能根据MThd和MTrk中所记录的信息,确定此MIDI文件的基本参数,并进行下一步更详细的解析。关于MThd和MTrk中详细的解析规则,笔者将在下文中具体解释。
以常用的音序器软件cakewalk为例,在cakewalk软件打开一个MIDI文件后,软件将读取MThd chunk内部的相关信息,从而确定此文件的类型、chunk数目、全局时钟设置等参数。然后软件将读取各个MTrk chunk,并将每个chunk内部包含的MIDI事件列在编辑区内对应的音轨部位。于是,在使用cakewalk打开一个MIDI文件后,用户就可以在编辑区内部看到若干条黄色的音轨,每个音轨内部含有该音轨所包含的MIDI事件,用户可以对不同音轨内的不同事件,甚至整条音轨,进行参数设置。
- MThd chunk结构
MThd chunk中保存了此MIDI的一些基本信息,如文件格式(MIDI 0、1、2格式)、此MIDI文件的音轨数(从1到多条)、时间类型(使用MIDI Tick或Frame来计时)。
当然,作为既定的标准,MThd Chunk一定是类似这样的数据结构(十六进制):
4D 54 68 64 // ① MThd的ASCII码,为Header Chunk的标志
00 00 00 06 // ② MThd中数据部分的长度,以目前标准均为6字节
hh hh // ③ MIDI文件类型
ii ii // ④ 此MIDI文件的音轨数目
jj jj // ⑤ 此MIDI文件的时间类型
Header Chunk之中的数据结构定义是严格遵循这个标准的。目前的标准中并未规定Header Chunk有其他的数据定义(但以后也许会扩充)。所以说,在Header Chunk中,前八个字节(即①②的数据结构)是固定样式的,数据段的大小也是固定为6字节的。
下面对数据段中的具体数据结构作一个介绍:
数据③标志着该MIDI文件的格式。MIDI文件格式有三种,0、1、2格式,所以可以分别用0000、0001、0002来表示。每种格式的具体含义请见上文。
数据④标志着该MIDI文件中所包含的音轨数目,也可以认为Track Chunk的数目。对于MIDI 0格式文件,此值仅为1,即只有一个Track Chunk;MIDI 1格式文件则可以有多个Track Chunk,而且Track Chunk数目为实际的音轨数目加一(因为第一个Track Chunk是Tempo Map,不记录实际的演奏信息)。MIDI 2格式文件因为笔者未曾遇见,所以不敢妄为解释。
数据⑤标志着该MIDI文件的时间类型。MIDI的时间类型通常有两种,一种是基于TPQN(Ticks Per Quarter-Note,每四分音符所具有的Midi Tick数)的时间度量法,另一种是基于SMPTE时间码的时间度量法。在这里,MIDI文件使用这个十六位数的最高位,标志这两种时间类型。也就是说,这个时间类型如果大于0x8000,则为SMPTE时间码度量法;如果小于0x8000,则为TPQN时间度量法。而此数的后十五位,则记录着具体的Midi Tick数量。
SMPTE本来是用于视频中的协议,所以它的计量单位为“帧”,就是“frame”。视频中有“帧率”的概念,单位为“帧/秒(fps)”。不同的视频标准中有不同的帧率,比如25fps、30fps等等。如果MIDI系统中使用这种时间度量法,那么它所定义的就是,在每一帧中,所具有的Midi Tick数目。这种度量法在单纯的MIDI系统中比较少见,笔者概念也显模糊,故不细谈。
对于大多数只有音频的MIDI系统中,MIDI文件多采用TPQN时间度量法。TPQN是“Ticks Per Quarter-Note(每四分音符中所包含的Midi Tick数量)”的缩写,它的意思可以从字面来理解。这个数值可以是十进制的60-480之间,数值越大,MIDI系统的时间分辨率就越大,也就是说可以演奏时值越小的音符。通常这个数都采用120、240、480,因为这些数都能被2、3、4甚至6、8整除,方便于八分音符、十六分音符、三连音甚至更短音符的演奏,换算成十六进制,就是0x78、0xF0、0x1E0。当然注意,这些十六进制数的最高位都是0。
- MTrk chunk结构
Track Chunk内部则包含了一个MIDI文件中记录的实际的MIDI信息和一些辅助信息(如meta-event)。
Track Chunk依然具有和Header Chunk类似的结构,就是“Chunk标志”+“数据段大小”+“数据”。所以它的结构如下所示(十六进制):
4D 54 72 6B // ① MTrk的ASCII码,为Track Chunk的标志
pp pp pp pp // ② MTrk中数据部分的长度
xx yy // ③ Delta-time及MIDI事件
xx yy // ③ Delta-time及MIDI事件
…… // (省略)③ Delta-time及MIDI事件
00 FF 2F 00 // ④ meta-event事件,此Track结束
数据①依然是Chunk标志,只不过该标志被换成了ASCII码的MTrk,代表接下来的数据为Track Chunk的数据。
数据②依然是此Chunk中所包含数据的大小。当然这个数就不是如同Header Chunk中那样的常数了,而是要精确描述接下来Track Chunk数据段的大小了。
接下来就是Track Chunk中所包含的真正数据了,就是由许多类似③那样的数据堆积起来的大段数据。xx代表了Delta-time,yy代表了真正的MIDI事件。这些MIDI事件才是音序器在播放MIDI文件时需要实时处理和发送的数据。这种结构笔者将在下文中详细介绍。
数据④从严格意义上讲,也属于③的类型。最初的00代表delta-time,随后的FF 2F 00为一段meta-event,代表了本Track结束。
- Delta-Time及MIDI事件结构
delta-time,实际上就是“Δt”。任何学习过数学和物理学的人,都能明白“Δt”的含义:它代表着时间差。
MIDI系统中的delta-time,表征着下一个事件距离上一个事件有多长时间,即两个事件之间的时间差。这个时间不是我们日常生活中的时分秒,而是MIDI Tick。音序器通过对自身产生的MIDI Tick进行计数,判断是否该处理下一个MIDI事件。如果Tick数到达delta-time,就处理下一个事件,然后继续判断下一个delta-time是否到达,周而复始。
为了能够表示足够长的时间,Delta-time使用可变长度数的格式,最长可以表示0x0fffffff。
MIDI事件则包括实际需要发送出去的MIDI事件,和meta-event事件。对于实际需要发送的数据,音序器就直接将数据发送出去;如果是meta-event事件,音序器则修改自身的相关参数。
这里要注意MIDI文件的“状态字省略”特点。为了减少MIDI文件的体积,人们规定:如果同一Track Chunk中的下一条MIDI事件,和上一条事件,属于同一类型同一通道的事件(即状态字相同)时,下一条事件的状态字可以省略,而只需记录数据。音序器碰到这种情况时,应自动填充上一事件的状态字。
举个例子,MIDI文件中的事件,大多数都是“Note-On”和“Note-Off”事件,其中“Note-Off”事件也可以用“Note-On”+“力度为0”来表示。所以在MIDI文件中,这种缩略形式会用到很多。比如连续演奏几个音符时,MIDI文件就会使用这种缩略法来减少文件体积。比如在MIDI文件中,常常会看到这种序列(十六进制):
00 93 3C 6B // 音符0x3C Note-On,力度为0x6B
70 3C 00 // 实际发送的指令为隔0x70 Ticks之后,发送93 3C 00。
因为力度为0,也就相当于让某个音符停止发音。83 3C 6B和93 3C 00的效果是一样的。所以通过这种写法,MIDI文件可以省略掉一个字节的空间。
总结起来,MIDI文件可以用以下的两张图来描述。通过这两张图,相信读者会对MIDI文件的数据结构建立一个直观的认识。本文的目的就在此。有关MIDI文件更细节的说明,请参考相关的MIDI文档和手册。相信在对MIDI文件有了一个基本认识之后,您再翻阅其他技术文档和手册时,就可以更加深刻地理解其含义了。
图一:MIDI 0格式文件的大概结构
图二:MIDI 1格式文件的大概结构