如何看懂一份MIDI文件

前言

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內(nèi)容在GarageBand中的展示

這份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種:
      1. 00 00 表示只含一個音軌
      2. 00 01 表示含有多個同步音軌
      3. 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為單位芽唇。

如果細(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é)表示秀睛,有兩個缺點:

  1. 可能造成空間浪費尔当,比如我們用4個字節(jié)表示頭塊數(shù)據(jù)長度,為 00 00 00 06蹂安,其實前面的3個字節(jié)是用不到的椭迎,浪費空間。
  2. 可能出現(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 10000x78)。
如果要表示的數(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 01000x8374)煌贴。
在舉一個例子:御板,解析一個動態(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微秒蛔钙。
上面我們說過第一個字符時長為1560ticks锌云,也就是 1560 * 1157.40625 / 1000 / 1000 = 1.8056秒。

The End?

實際應(yīng)用的MIDI文件可能比我舉個例子復(fù)雜很多吁脱,因為還可能出現(xiàn)多音軌桑涎,還有上面沒有描述的消息,比如模式消息兼贡、實時消息攻冷、公共消息等等。但是解析方式都是同一個套路遍希,只是可能消息的作用不同而已等曼。所以希望本文,可以幫助到你理解MIDI就滿足了孵班。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涉兽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子篙程,更是在濱河造成了極大的恐慌枷畏,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虱饿,死亡現(xiàn)場離奇詭異拥诡,居然都是意外死亡触趴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門渴肉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冗懦,“玉大人,你說我怎么就攤上這事仇祭∨叮” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵乌奇,是天一觀的道長没讲。 經(jīng)常有香客問我,道長礁苗,這世上最難降的妖魔是什么爬凑? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮试伙,結(jié)果婚禮上嘁信,老公的妹妹穿的比我還像新娘。我一直安慰自己疏叨,他們只是感情好潘靖,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著考廉,像睡著了一般秘豹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昌粤,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音啄刹,去河邊找鬼涮坐。 笑死,一個胖子當(dāng)著我的面吹牛誓军,可吹牛的內(nèi)容都是我干的袱讹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼昵时,長吁一口氣:“原來是場噩夢啊……” “哼捷雕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起壹甥,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤救巷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后句柠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浦译,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡棒假,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了精盅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帽哑。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叹俏,靈堂內(nèi)的尸體忽然破棺而出妻枕,到底是詐尸還是另有隱情,我是刑警寧澤粘驰,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布佳头,位于F島的核電站,受9級特大地震影響晴氨,放射性物質(zhì)發(fā)生泄漏康嘉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一籽前、第九天 我趴在偏房一處隱蔽的房頂上張望亭珍。 院中可真熱鬧,春花似錦枝哄、人聲如沸肄梨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽众羡。三九已至,卻和暖如春蓖租,著一層夾襖步出監(jiān)牢的瞬間粱侣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工蓖宦, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留齐婴,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓稠茂,卻偏偏與公主長得像柠偶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子睬关,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容