前言
MIDI 文件是在做音樂應(yīng)用時乾蓬,很可能會遇到的一種文件格式。Github上面有相關(guān)的類庫,可以用來解析MIDI吧趣,因為不想滿足于僅僅能夠拿來能用就好,還是希望能夠了解MIDI到底是怎么解析耙厚,所以自己找了一下資料看了一下强挫,但是發(fā)現(xiàn)在網(wǎng)上還沒有找到一篇講MIDI比較詳細(xì)的,可以讓人看一遍薛躬,就知道還MIDI是怎么一回事俯渤。因此我嘗試自己寫一篇,個人的水平有限型宝,可能有一些說不清楚的地方八匠。如果大家有啥意思或者問題絮爷,可以留言討論。
什么是MIDI
MIDI(Musical Instrument Digital Interface)樂器數(shù)字接口 梨树,是20 世紀(jì)80 年代初為解決電聲樂器之間的通信問題而提出的坑夯。MIDI是編曲界最廣泛的音樂標(biāo)準(zhǔn)格式,可稱為“計算機(jī)能理解的樂譜”抡四。MIDI是電子樂器和計算機(jī)使用的標(biāo)準(zhǔn)語言柜蜈,是一套消息(即指令)的約定,它不產(chǎn)生聲音信號指巡,而是在電纜傳送各種消息跨释,由接收消息的設(shè)備或其它電子裝置產(chǎn)生聲音或執(zhí)行某個動作。
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進(jìn)制的數(shù)字,看不懂對不對阔涉,那就好了缆娃。如果能夠看懂了,本文可能就不太適合你瑰排,可以關(guān)了頁面贯要,去干別的事了。
然后我們用Mac自帶GarageBand(中文名字為庫樂隊)來打開這份MIDI文件(可以把這些數(shù)據(jù)寫到文件椭住,保存為文件后綴是midi就可以了)崇渗。
這份MIDI文件其中是包含了3條音軌,但是演奏的主音軌只有一條京郑。而圖中兩條綠線的區(qū)域就是這個演奏音軌的內(nèi)容宅广,里面每一段綠色的長條就是一個音符的演奏信息。我們先記下來第一個音符的信息些举,它的音高是C3跟狱,力度是100。
OK户魏,目前為止驶臊,我們已經(jīng)通過GarageBand看到一個MIDI信息是怎么的。接下來叼丑,我們就要來講講怎么從上面的那串十六進(jìn)制的數(shù)據(jù)关翎,也懂出這些信息。
MIDI文件基本由兩塊組成
<文件頭塊> + <音軌塊數(shù)據(jù)>
其中音軌塊數(shù)據(jù)就是由若干個格式相同的子數(shù)據(jù)構(gòu)成鸠信。
先來看看文件頭塊纵寝,頭塊主要有三塊:
<標(biāo)志符串>(4字節(jié)) + <頭塊數(shù)據(jù)區(qū)長度>(4字節(jié)) + <頭塊數(shù)據(jù)區(qū)>(6字節(jié))
- 標(biāo)志符串,指的是"MThd"或"MTrk"症副,MThd是頭塊類型店雅,MTrk是音軌類型政基。所以頭塊標(biāo)志就是MThd的ASCII碼贞铣,用十六進(jìn)制表示就是4d 54 68 64闹啦。
- 頭塊數(shù)據(jù)區(qū)長度,指的是后面接著的頭塊數(shù)據(jù)區(qū)長度辕坝,因為長度是6字節(jié)窍奋,所以固定顯示為00 00 00 06。
- 頭塊數(shù)據(jù)區(qū)酱畅,共有6字節(jié)琳袄,分別為ff ff nn nn dd dd。
- 前兩個字節(jié)ff ff 指定midi文件格式纺酸,一般有3種:
- 00 00 表示只含一個音軌
- 00 01 表示含有多個同步音軌
- 00 10 表示含有多個獨立音軌
(大多數(shù)的midi文件都是第二種情況窖逗,也就是00 01)
- nn nn 指定軌道數(shù),一般都會大于1餐蔬,因為除了演播主音軌外碎紊,還會有全局音軌。
- dd dd 指定基本時間格式樊诺,dd dd 的最高位為標(biāo)記位仗考,0為采用ticks計時,后面的數(shù)據(jù)為一個4分音符的ticks词爬;1為SMPTE格式計時秃嗜,后面的數(shù)值則是定義每秒中SMTPE幀的數(shù)量及每個SMTPE幀的tick。用我們舉例的midi來看看顿膨,dd dd 的數(shù)據(jù)為01 E0锅锨,表示采用ticks計時,1E0轉(zhuǎn)十進(jìn)制為480恋沃,也就是每個4分音符橡类,包含480ticks。后面事件時間都是以ticks為單位芽唇。
- 前兩個字節(jié)ff ff 指定midi文件格式纺酸,一般有3種:
如果細(xì)心的同學(xué)可能會想到顾画,那一個4分音符時長是多少呢?如果不能確定一個4分音符時長匆笤,就不能確定每單位ticks的具體時長研侣,后面的邏輯也就走不通了。而4分音符在不同的節(jié)拍下是不同時長炮捧,那么midi是怎么解決這個問題的庶诡。之前我也困惑過,不過現(xiàn)在我們先保留這個問題咆课,后面會講到末誓。
總結(jié)一下扯俱,每個midi文件都會有一段相似的開頭,用十六進(jìn)制表示為“4d 54 68 64 00 00 00 06 ff ff nn nn dd dd”喇澡,這就是頭塊信息迅栅。
動態(tài)字節(jié)
在講音軌塊數(shù)據(jù)
之前,必須先講講動態(tài)字節(jié)晴玖,因為音軌塊數(shù)據(jù)中的數(shù)值是用到了動態(tài)字節(jié)來表示读存。在前面,我們講文件頭塊的時候呕屎,說到會用4個字節(jié)來表示頭塊數(shù)據(jù)的長度让簿,這樣就是用固定字節(jié)表示。用固定字節(jié)表示秀睛,有兩個缺點:
- 可能造成空間浪費尔当,比如我們用4個字節(jié)表示頭塊數(shù)據(jù)長度,為 00 00 00 06蹂安,其實前面的3個字節(jié)是用不到的椭迎,浪費空間。
- 可能出現(xiàn)最大值不夠用藤抡,比如我們用固定4個字節(jié)表示長度侠碧,然后它的范圍 0 ~~ 2^64 - 1 。如果我們要指定更大的數(shù)值缠黍,就沒有辦法了弄兜。當(dāng)然可以使用更大的固定字節(jié),比如6字節(jié)或者8字節(jié)瓷式,但是這樣缺點1可能造成浪費也就更大了替饿。
說了這么多,正式來講講動態(tài)字節(jié)~~~~~
一個字節(jié)有8塊贸典,除了最高位用作標(biāo)志位视卢,還有7位,可以表示的范圍為0 ~~ 2^7 - 1 (即為127)廊驼。如果要表示的數(shù)是在這個范圍之內(nèi)据过,那么標(biāo)志位為0,然后用其余7位表示就好了妒挎。比如120绳锅,可以表示為0111 1000
(0x78
)。
如果要表示的數(shù)值超過這個范圍酝掩,那么先記錄低7位為一個字節(jié)鳞芙,超過7位的數(shù)值移交給前面的字節(jié),而這個前字節(jié)的標(biāo)志位必須為1,表示它是進(jìn)位的原朝。如果前字節(jié)還是超過127驯嘱,繼續(xù)同樣的步驟。舉個栗子:我們要表示500這個數(shù)喳坠,二進(jìn)制為:1 1111 0100
一共有9位鞠评。先記錄下低7位在一個字節(jié)為0111 0100
。高位還有11 丙笋,存在一個字節(jié)為1000 0011
谢澈。所以500這個數(shù)值用動態(tài)字節(jié)表示為1000 0011 0111 0100
(0x8374
)煌贴。
在舉一個例子:御板,解析一個動態(tài)字節(jié)(0x83FF7F
),先讀取第一個字節(jié)83
牛郑,因為最高標(biāo)志位為1怠肋,所以它是進(jìn)位的,不是最終字節(jié)淹朋,表示的數(shù)值為3 R
笙各。讀取第二個字節(jié)FF
,同理因為最高標(biāo)志位為1础芍,也是進(jìn)位的杈抢,不是最終字節(jié),表示的數(shù)值為127 R
仑性。讀取第三個字節(jié)7F
惶楼,因為最高標(biāo)志位為0,表示是最終字節(jié)诊杆,動態(tài)字節(jié)取值結(jié)束歼捐,該字節(jié)表示的數(shù)值為127 R
。
所以動態(tài)字節(jié)(0x83FF7F
)表示的值為 3 * 128^2 + 127 * 128^1 + 127 * 128^0 = 65535.
音軌塊
midi文件晨汹,在頭塊之后豹储,剩余是一個或者多個音軌塊。每個音軌塊的結(jié)構(gòu)如下所示也是包含3部分淘这。
<標(biāo)志符串>(4字節(jié)) + <音軌塊數(shù)據(jù)區(qū)長度>(4字節(jié)) + <音軌塊數(shù)據(jù)區(qū)>(多個MIDI事件構(gòu)成)
上面說過剥扣,音軌塊的標(biāo)志符串為"MTrk",也是記錄ASCII碼铝穷,用十六進(jìn)制表示就是4d 54 72 6b钠怯。音軌塊數(shù)據(jù)區(qū)長度也為固定4字節(jié),指定后面的數(shù)據(jù)區(qū)長度氧骤。
其中MIDI事件的構(gòu)成是
<delta time> + <MIDI 消息>
其中delta time 就是采用動態(tài)字節(jié)來表示呻疹,單位就是tick。
MIDI 消息,由一個狀態(tài)字節(jié) + 多個數(shù)據(jù)字節(jié) 構(gòu)成刽锤。狀態(tài)字節(jié)可以理解為方法
镊尺,數(shù)據(jù)字節(jié)可以理解為這個方法的參數(shù)
。狀態(tài)字節(jié)的最高位永遠(yuǎn)為1并思,因為它的范圍介于128~ 255之間庐氮,而數(shù)據(jù)字節(jié)最高位永遠(yuǎn)為0,所以的它的范圍介于0 ~ 127 之間宋彼。消息根據(jù)性質(zhì)可分成通道消息和系統(tǒng)消息兩大類弄砍。
通道消息是對單一的MIDI Channel起作用,其Channel是利用狀態(tài)字節(jié)的低 4 位來表示输涕,從0~F共有16個音婶。
下表為通道消息的同類,其中X為0~16.
狀態(tài)字節(jié) | 功能描述 | 數(shù)據(jù)字節(jié)描述 |
---|---|---|
8X | 松開音符 | 1字節(jié):音符號(00~7F) / 2字節(jié):力度(00~7F) |
9X | 按下音符 | 1字節(jié):音符號(00~7F) / 2字節(jié):力度(00~7F) |
AX | 觸后音符 | 1字節(jié):音符號(00~7F) / 2字節(jié):力度(00~7F) |
BX | 控制器變化 | 1字節(jié):控制器號碼(00~79) / 2字節(jié):控制器參數(shù)(00~7F) |
CX | 改變樂器 | 1字節(jié):樂器號碼(00~7F) |
DX | 通道觸動壓力 | 1字節(jié):壓力(00~7F) |
EX | 彎音輪變換 | 1字節(jié):彎音輪變換值的低字節(jié) / 2字節(jié):彎音輪變換值的高字節(jié) |
還有一種特殊的狀態(tài)字節(jié)FF
,表示非MIDI事件(Non- MIDI events)莱坎,也叫meta-event(元事件)衣式。元事件的語法定于如下:
FF + <種類字節(jié)>(1字節(jié)) + <數(shù)據(jù)字節(jié)長度> + <數(shù)據(jù)字節(jié)>
FF
的部分功能,其他如果數(shù)據(jù)字節(jié)數(shù)不是固定檐什,而是有前面的動態(tài)字節(jié)制定碴卧,則用--
表示
種類 | 功能描述 | 數(shù)據(jù)字節(jié)長度 | 數(shù)據(jù)字節(jié)描述 |
---|---|---|---|
00 | 設(shè)置軌道音序 | 2 | 音序號 00 00-FF FF |
01 | 文字事件 | -- | 文本信息 |
02 | 版權(quán)公告 | -- | 版權(quán)信息 |
03 | 指定歌曲/音軌的名稱 | -- | 歌曲名稱(用于全局音軌時)/音軌的名稱 |
04 | 指定樂器 | -- | 樂器名稱 |
05 | 歌詞 | -- | 歌詞 |
06 | 標(biāo)記 | -- | 標(biāo)記(通常在一個格式0的音軌,或在格式1的第一個音軌乃正。) |
07 | 注釋 | -- | 描述一些在這一點上發(fā)生的動作或事件 |
2F | 音軌終止 | -- | 音軌結(jié)束標(biāo)志(必須有的) |
51 | 指定速度 | -- | 設(shè)定速度住册,以微妙為單位,是四分音符的時值 |
58 | 指定節(jié)拍 | -- | 略 |
上面兩個表是常見消息的狀態(tài)字節(jié)瓮具,還有一些其他消息沒有列舉出來荧飞,但是這兩個表已經(jīng)夠用了。
開始看midi
上面講了那么多搭综,現(xiàn)在我們再看看上面的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
頭塊數(shù)據(jù)垢箕,然后就是4D 54 72 6B
轉(zhuǎn)為字符就是MTrk
,說明這是一個音軌塊信息兑巾。接下來4個固定字節(jié)表示數(shù)據(jù)長度00 00 00 1A
,所以接下來需要讀取26個字節(jié)的數(shù)據(jù)条获。
接下來就是讀取事件,根據(jù)語法第一個事件是00 FF 03 03 31 32 33
蒋歌。其中00
表示時間間隔為0ticks帅掘;FF
說明是元事件;03
是狀態(tài)字節(jié)堂油,說明指定歌曲名稱修档;下面的03
指定下面還有3個字節(jié)作為文本信息;31 32 33
就是文本信息府框。
第二個事件為00 FF 51 03 08 7A 23
吱窝,這里不一個一個字節(jié)解釋了,整個事件就是指定演奏速度,則每拍的時間555555微秒院峡。用每拍所占的時間而不是單位時間內(nèi)的拍數(shù)表示速度兴使,使得依據(jù)一個基于時間的(例如SMPTE時間代碼或MIDI時間代碼)實現(xiàn)時間的絕對同步成為可能。
每個音軌最后肯定是以00 FF 2F 00結(jié)束照激,因為這是一個音軌結(jié)束事件发魄。
其他事件就不說明,通過事件的類型俩垃,我們可以得知這是一個全局事件咱士。
我們找到下一個4D 54 72 6B
恩沽,一直到00 FF 2F 00
為止砸狞,把下一個音軌的數(shù)據(jù)截取出來荒勇。
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
除去音軌頭塊數(shù)據(jù)荷逞,第一個事件就是FF 03
指定音軌的名稱醉旦。
重點來了藏古,第二個事件就是00 90 3C 64
吧碾,90
說明是個按下音符俺孙,也就是發(fā)出音符辣卒。3C
是音符號,64
是力度睛榄。大家還記得我們從GarageBand觀察時候荣茫,記下第一個音符是C3,力度是100场靴。
3C
表示為第60號音符啡莉。從MIDI音符號表可以找到第60號的音符為C4。
等等為什么是C4旨剥,這個問題咧欣,我也疑惑過。其實轨帜,這是對中央C的標(biāo)號不同導(dǎo)致魄咕,在GarageBand,鋼琴彈音域中央C為C3蚌父,其他樂器還是C4哮兰。只要降低一個八度,做個轉(zhuǎn)換就好了苟弛。
力度64
喝滞,即為100 R
,所以力度為100膏秫。跟我們在GarageBand看到是一致的右遭。
需要注意,90
事件是個note_on事件就是發(fā)音事件,但是如果參數(shù)力度為0 窘哈,它實際上就是一個note_off事件言蛇,不會發(fā)音。
第二個事件就是8C 18 80 3C 40
宵距,整個事件就是經(jīng)過1560ticks之后腊尚,松開音符3C
,力度為60满哪。兩個事件串聯(lián)起來就是婿斥,音符C4發(fā)出聲音時長為1560ticks。
其他事件和音軌就不看哨鸭,大概讀的方法是一樣的思路民宿。
怎么計算時間
我們還留著一個問題沒回答,那就是怎么確定時間單位ticks像鸡。
我們從頭塊信息活鹰,可以得知到一個4分音符的ticks數(shù)為480
,然后從全局音軌得到播放速度為只估,每個節(jié)拍555555
微秒志群。1個4分音符為1節(jié)拍,也就是說1tick為555555 / 480 = 1157.40625
微秒蛔钙。
上面我們說過第一個字符時長為1560
ticks锌云,也就是 1560 * 1157.40625 / 1000 / 1000 = 1.8056
秒。
The End?
實際應(yīng)用的MIDI文件可能比我舉個例子復(fù)雜很多吁脱,因為還可能出現(xiàn)多音軌桑涎,還有上面沒有描述的消息,比如模式消息兼贡、實時消息攻冷、公共消息等等。但是解析方式都是同一個套路遍希,只是可能消息的作用不同而已等曼。所以希望本文,可以幫助到你理解MIDI就滿足了孵班。