如何看懂一份MIDI文件

前言

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内容在GarageBand中的展示

这份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种:
      1. 00 00 表示只含一个音轨
      2. 00 01 表示含有多个同步音轨
      3. 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为单位。

如果细心的同学可能会想到,那一个4分音符时长是多少呢?如果不能确定一个4分音符时长,就不能确定每单位ticks的具体时长,后面的逻辑也就走不通了。而4分音符在不同的节拍下是不同时长,那么midi是怎么解决这个问题的。之前我也困惑过,不过现在我们先保留这个问题,后面会讲到。

总结一下,每个midi文件都会有一段相似的开头,用十六进制表示为“4d 54 68 64 00 00 00 06 ff ff nn nn dd dd”,这就是头块信息。

动态字节

在讲音轨块数据之前,必须先讲讲动态字节,因为音轨块数据中的数值是用到了动态字节来表示。在前面,我们讲文件头块的时候,说到会用4个字节来表示头块数据的长度,这样就是用固定字节表示。用固定字节表示,有两个缺点:

  1. 可能造成空间浪费,比如我们用4个字节表示头块数据长度,为 00 00 00 06,其实前面的3个字节是用不到的,浪费空间。
  2. 可能出现最大值不够用,比如我们用固定4个字节表示长度,然后它的范围 0 ~~ 2^64 - 1 。如果我们要指定更大的数值,就没有办法了。当然可以使用更大的固定字节,比如6字节或者8字节,但是这样缺点1可能造成浪费也就更大了。

说了这么多,正式来讲讲动态字节~~~~~
一个字节有8块,除了最高位用作标志位,还有7位,可以表示的范围为0 ~~ 2^7 - 1 (即为127)。如果要表示的数是在这个范围之内,那么标志位为0,然后用其余7位表示就好了。比如120,可以表示为0111 10000x78)。
如果要表示的数值超过这个范围,那么先记录低7位为一个字节,超过7位的数值移交给前面的字节,而这个前字节的标志位必须为1,表示它是进位的。如果前字节还是超过127,继续同样的步骤。举个栗子:我们要表示500这个数,二进制为:1 1111 0100 一共有9位。先记录下低7位在一个字节为0111 0100。高位还有11 ,存在一个字节为1000 0011。所以500这个数值用动态字节表示为1000 0011 0111 01000x8374)。
在举一个例子:,解析一个动态字节(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 6490说明是个按下音符,也就是发出音符。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微秒。
上面我们说过第一个字符时长为1560ticks,也就是 1560 * 1157.40625 / 1000 / 1000 = 1.8056秒。

The End?

实际应用的MIDI文件可能比我举个例子复杂很多,因为还可能出现多音轨,还有上面没有描述的消息,比如模式消息、实时消息、公共消息等等。但是解析方式都是同一个套路,只是可能消息的作用不同而已。所以希望本文,可以帮助到你理解MIDI就满足了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容