前言
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就滿足了。