前言
MIDI 文件是在做音乐应用时,很可能会遇到的一种文件格式。Github上面有相关的类库,可以用来解析MIDI,因为不想满足于仅仅能够拿来能用就好,还是希望能够了解MIDI到底是怎么解析,所以自己找了一下资料看了一下,但是发现在网上还没有找到一篇讲MIDI比较详细的,可以让人看一遍,就知道还MIDI是怎么一回事。因此我尝试自己写一篇,个人的水平有限,可能有一些说不清楚的地方。如果大家有啥意思或者问题,可以留言讨论。
什么是MIDI
MIDI(Musical Instrument Digital Interface)乐器数字接口 ,是20 世纪80 年代初为解决电声乐器之间的通信问题而提出的。MIDI是编曲界最广泛的音乐标准格式,可称为“计算机能理解的乐谱”。MIDI是电子乐器和计算机使用的标准语言,是一套消息(即指令)的约定,它不产生声音信号,而是在电缆传送各种消息,由接收消息的设备或其它电子装置产生声音或执行某个动作。
MIDI的文件格式
在开始说明之前,我们先来看看一份MIDI文件是怎么样子的。如下所示:
4D 54 68 64 00 00 00 06 00 01 00 03 01 E0 4D 54
72 6B 00 00 00 1A 00 FF 03 03 31 32 33 00 FF 51
03 08 7A 23 00 FF 58 04 04 02 18 08 00 FF 2F 00
4D 54 72 6B 00 00 00 67 00 FF 03 13 5B 47 4D 20
30 35 34 5D 20 56 6F 69 63 65 20 4F 6F 68 73 8F
00 90 3C 64 8C 18 80 3C 40 82 68 90 3E 64 8C 18
80 3E 40 82 68 90 40 64 86 48 80 40 40 78 90 41
64 86 48 80 41 40 78 90 43 64 86 48 80 43 40 78
90 45 64 86 48 80 45 40 78 90 47 64 87 40 80 47
40 87 40 90 48 64 8F 00 80 48 40 00 FF 2F 00 4D
54 72 6B 00 00 00 0E 00 FF 03 06 4D 61 72 6B 65
72 00 FF 2F 00
一串16进制的数字,看不懂对不对,那就好了。如果能够看懂了,本文可能就不太适合你,可以关了页面,去干别的事了。
然后我们用Mac自带GarageBand(中文名字为库乐队)来打开这份MIDI文件(可以把这些数据写到文件,保存为文件后缀是midi就可以了)。
这份MIDI文件其中是包含了3条音轨,但是演奏的主音轨只有一条。而图中两条绿线的区域就是这个演奏音轨的内容,里面每一段绿色的长条就是一个音符的演奏信息。我们先记下来第一个音符的信息,它的音高是C3,力度是100。
OK,目前为止,我们已经通过GarageBand看到一个MIDI信息是怎么的。接下来,我们就要来讲讲怎么从上面的那串十六进制的数据,也懂出这些信息。
MIDI文件基本由两块组成
<文件头块> + <音轨块数据>
其中音轨块数据就是由若干个格式相同的子数据构成。
先来看看文件头块,头块主要有三块:
<标志符串>(4字节) + <头块数据区长度>(4字节) + <头块数据区>(6字节)
- 标志符串,指的是"MThd"或"MTrk",MThd是头块类型,MTrk是音轨类型。所以头块标志就是MThd的ASCII码,用十六进制表示就是4d 54 68 64。
- 头块数据区长度,指的是后面接着的头块数据区长度,因为长度是6字节,所以固定显示为00 00 00 06。
- 头块数据区,共有6字节,分别为ff ff nn nn dd dd。
- 前两个字节ff ff 指定midi文件格式,一般有3种:
- 00 00 表示只含一个音轨
- 00 01 表示含有多个同步音轨
- 00 10 表示含有多个独立音轨
(大多数的midi文件都是第二种情况,也就是00 01)
- nn nn 指定轨道数,一般都会大于1,因为除了演播主音轨外,还会有全局音轨。
- dd dd 指定基本时间格式,dd dd 的最高位为标记位,0为采用ticks计时,后面的数据为一个4分音符的ticks;1为SMPTE格式计时,后面的数值则是定义每秒中SMTPE帧的数量及每个SMTPE帧的tick。用我们举例的midi来看看,dd dd 的数据为01 E0,表示采用ticks计时,1E0转十进制为480,也就是每个4分音符,包含480ticks。后面事件时间都是以ticks为单位。
- 前两个字节ff ff 指定midi文件格式,一般有3种:
如果细心的同学可能会想到,那一个4分音符时长是多少呢?如果不能确定一个4分音符时长,就不能确定每单位ticks的具体时长,后面的逻辑也就走不通了。而4分音符在不同的节拍下是不同时长,那么midi是怎么解决这个问题的。之前我也困惑过,不过现在我们先保留这个问题,后面会讲到。
总结一下,每个midi文件都会有一段相似的开头,用十六进制表示为“4d 54 68 64 00 00 00 06 ff ff nn nn dd dd”,这就是头块信息。
动态字节
在讲音轨块数据
之前,必须先讲讲动态字节,因为音轨块数据中的数值是用到了动态字节来表示。在前面,我们讲文件头块的时候,说到会用4个字节来表示头块数据的长度,这样就是用固定字节表示。用固定字节表示,有两个缺点:
- 可能造成空间浪费,比如我们用4个字节表示头块数据长度,为 00 00 00 06,其实前面的3个字节是用不到的,浪费空间。
- 可能出现最大值不够用,比如我们用固定4个字节表示长度,然后它的范围 0 ~~ 2^64 - 1 。如果我们要指定更大的数值,就没有办法了。当然可以使用更大的固定字节,比如6字节或者8字节,但是这样缺点1可能造成浪费也就更大了。
说了这么多,正式来讲讲动态字节~~~~~
一个字节有8块,除了最高位用作标志位,还有7位,可以表示的范围为0 ~~ 2^7 - 1 (即为127)。如果要表示的数是在这个范围之内,那么标志位为0,然后用其余7位表示就好了。比如120,可以表示为0111 1000
(0x78
)。
如果要表示的数值超过这个范围,那么先记录低7位为一个字节,超过7位的数值移交给前面的字节,而这个前字节的标志位必须为1,表示它是进位的。如果前字节还是超过127,继续同样的步骤。举个栗子:我们要表示500这个数,二进制为:1 1111 0100
一共有9位。先记录下低7位在一个字节为0111 0100
。高位还有11 ,存在一个字节为1000 0011
。所以500这个数值用动态字节表示为1000 0011 0111 0100
(0x8374
)。
在举一个例子:,解析一个动态字节(0x83FF7F
),先读取第一个字节83
,因为最高标志位为1,所以它是进位的,不是最终字节,表示的数值为3 R
。读取第二个字节FF
,同理因为最高标志位为1,也是进位的,不是最终字节,表示的数值为127 R
。读取第三个字节7F
,因为最高标志位为0,表示是最终字节,动态字节取值结束,该字节表示的数值为127 R
。
所以动态字节(0x83FF7F
)表示的值为 3 * 128^2 + 127 * 128^1 + 127 * 128^0 = 65535.
音轨块
midi文件,在头块之后,剩余是一个或者多个音轨块。每个音轨块的结构如下所示也是包含3部分。
<标志符串>(4字节) + <音轨块数据区长度>(4字节) + <音轨块数据区>(多个MIDI事件构成)
上面说过,音轨块的标志符串为"MTrk",也是记录ASCII码,用十六进制表示就是4d 54 72 6b。音轨块数据区长度也为固定4字节,指定后面的数据区长度。
其中MIDI事件的构成是
<delta time> + <MIDI 消息>
其中delta time 就是采用动态字节来表示,单位就是tick。
MIDI 消息,由一个状态字节 + 多个数据字节 构成。状态字节可以理解为方法
,数据字节可以理解为这个方法的参数
。状态字节的最高位永远为1,因为它的范围介于128~ 255之间,而数据字节最高位永远为0,所以的它的范围介于0 ~ 127 之间。消息根据性质可分成通道消息和系统消息两大类。
通道消息是对单一的MIDI Channel起作用,其Channel是利用状态字节的低 4 位来表示,从0~F共有16个。
下表为通道消息的同类,其中X为0~16.
状态字节 | 功能描述 | 数据字节描述 |
---|---|---|
8X | 松开音符 | 1字节:音符号(00~7F) / 2字节:力度(00~7F) |
9X | 按下音符 | 1字节:音符号(00~7F) / 2字节:力度(00~7F) |
AX | 触后音符 | 1字节:音符号(00~7F) / 2字节:力度(00~7F) |
BX | 控制器变化 | 1字节:控制器号码(00~79) / 2字节:控制器参数(00~7F) |
CX | 改变乐器 | 1字节:乐器号码(00~7F) |
DX | 通道触动压力 | 1字节:压力(00~7F) |
EX | 弯音轮变换 | 1字节:弯音轮变换值的低字节 / 2字节:弯音轮变换值的高字节 |
还有一种特殊的状态字节FF
,表示非MIDI事件(Non- MIDI events),也叫meta-event(元事件)。元事件的语法定于如下:
FF + <种类字节>(1字节) + <数据字节长度> + <数据字节>
FF
的部分功能,其他如果数据字节数不是固定,而是有前面的动态字节制定,则用--
表示
种类 | 功能描述 | 数据字节长度 | 数据字节描述 |
---|---|---|---|
00 | 设置轨道音序 | 2 | 音序号 00 00-FF FF |
01 | 文字事件 | -- | 文本信息 |
02 | 版权公告 | -- | 版权信息 |
03 | 指定歌曲/音轨的名称 | -- | 歌曲名称(用于全局音轨时)/音轨的名称 |
04 | 指定乐器 | -- | 乐器名称 |
05 | 歌词 | -- | 歌词 |
06 | 标记 | -- | 标记(通常在一个格式0的音轨,或在格式1的第一个音轨。) |
07 | 注释 | -- | 描述一些在这一点上发生的动作或事件 |
2F | 音轨终止 | -- | 音轨结束标志(必须有的) |
51 | 指定速度 | -- | 设定速度,以微妙为单位,是四分音符的时值 |
58 | 指定节拍 | -- | 略 |
上面两个表是常见消息的状态字节,还有一些其他消息没有列举出来,但是这两个表已经够用了。
开始看midi
上面讲了那么多,现在我们再看看上面的midi文件:
4D 54 68 64 00 00 00 06 00 01 00 03 01 E0 4D 54
72 6B 00 00 00 1A 00 FF 03 03 31 32 33 00 FF 51
03 08 7A 23 00 FF 58 04 04 02 18 08 00 FF 2F 00
4D 54 72 6B 00 00 00 67 00 FF 03 13 5B 47 4D 20
30 35 34 5D 20 56 6F 69 63 65 20 4F 6F 68 73 8F
00 90 3C 64 8C 18 80 3C 40 82 68 90 3E 64 8C 18
80 3E 40 82 68 90 40 64 86 48 80 40 40 78 90 41
64 86 48 80 41 40 78 90 43 64 86 48 80 43 40 78
90 45 64 86 48 80 45 40 78 90 47 64 87 40 80 47
40 87 40 90 48 64 8F 00 80 48 40 00 FF 2F 00 4D
54 72 6B 00 00 00 0E 00 FF 03 06 4D 61 72 6B 65
72 00 FF 2F 00
4D 54 68 64 00 00 00 06 00 01 00 03 01 E0
头块数据,然后就是4D 54 72 6B
转为字符就是MTrk
,说明这是一个音轨块信息。接下来4个固定字节表示数据长度00 00 00 1A
,所以接下来需要读取26个字节的数据。
接下来就是读取事件,根据语法第一个事件是00 FF 03 03 31 32 33
。其中00
表示时间间隔为0ticks;FF
说明是元事件;03
是状态字节,说明指定歌曲名称;下面的03
指定下面还有3个字节作为文本信息;31 32 33
就是文本信息。
第二个事件为00 FF 51 03 08 7A 23
,这里不一个一个字节解释了,整个事件就是指定演奏速度,则每拍的时间555555微秒。用每拍所占的时间而不是单位时间内的拍数表示速度,使得依据一个基于时间的(例如SMPTE时间代码或MIDI时间代码)实现时间的绝对同步成为可能。
每个音轨最后肯定是以00 FF 2F 00结束,因为这是一个音轨结束事件。
其他事件就不说明,通过事件的类型,我们可以得知这是一个全局事件。
我们找到下一个4D 54 72 6B
,一直到00 FF 2F 00
为止,把下一个音轨的数据截取出来。
4D 54 72 6B 00 00 00 67 00 FF 03 13 5B 47 4D 20
30 35 34 5D 20 56 6F 69 63 65 20 4F 6F 68 73 8F
00 90 3C 64 8C 18 80 3C 40 82 68 90 3E 64 8C 18
80 3E 40 82 68 90 40 64 86 48 80 40 40 78 90 41
64 86 48 80 41 40 78 90 43 64 86 48 80 43 40 78
90 45 64 86 48 80 45 40 78 90 47 64 87 40 80 47
40 87 40 90 48 64 8F 00 80 48 40 00 FF 2F 00
除去音轨头块数据,第一个事件就是FF 03
指定音轨的名称。
重点来了,第二个事件就是00 90 3C 64
,90
说明是个按下音符,也就是发出音符。3C
是音符号,64
是力度。大家还记得我们从GarageBand观察时候,记下第一个音符是C3,力度是100。
3C
表示为第60号音符。从MIDI音符号表可以找到第60号的音符为C4。
等等为什么是C4,这个问题,我也疑惑过。其实,这是对中央C的标号不同导致,在GarageBand,钢琴弹音域中央C为C3,其他乐器还是C4。只要降低一个八度,做个转换就好了。
力度64
,即为100 R
,所以力度为100。跟我们在GarageBand看到是一致的。
需要注意,90
事件是个note_on事件就是发音事件,但是如果参数力度为0 ,它实际上就是一个note_off事件,不会发音。
第二个事件就是8C 18 80 3C 40
,整个事件就是经过1560ticks之后,松开音符3C
,力度为60。两个事件串联起来就是,音符C4发出声音时长为1560ticks。
其他事件和音轨就不看,大概读的方法是一样的思路。
怎么计算时间
我们还留着一个问题没回答,那就是怎么确定时间单位ticks。
我们从头块信息,可以得知到一个4分音符的ticks数为480
,然后从全局音轨得到播放速度为,每个节拍555555
微秒。1个4分音符为1节拍,也就是说1tick为555555 / 480 = 1157.40625
微秒。
上面我们说过第一个字符时长为1560
ticks,也就是 1560 * 1157.40625 / 1000 / 1000 = 1.8056
秒。
The End?
实际应用的MIDI文件可能比我举个例子复杂很多,因为还可能出现多音轨,还有上面没有描述的消息,比如模式消息、实时消息、公共消息等等。但是解析方式都是同一个套路,只是可能消息的作用不同而已。所以希望本文,可以帮助到你理解MIDI就满足了。