讀 MIDI
這篇是基於我一年前的 MIDI 筆記整理,可能只有 cover 到當時需要的部份。
這篇筆記主要針對 MIDI file,沒有 MIDI protocol。
About MIDI#
MIDI, Musical Instrument Digital Interface, 是一種 1980 年代就存在的 open format, 常被用於操控音樂相關的訊號。
MIDI 本身不包含音訊,只包含了要彈哪顆音、彈多大力、這些音之間間隔多久這類的訊息, 所以傳輸 MIDI 訊號不需要很大的流量,檔案也不佔什麼空間。
更多關於 MIDI 檔案的介紹可以參考 wiwi 的這篇。 這篇文章會專注在如何看懂 MIDI 檔案本身,像是這樣:

當然這裡無法窮盡所有可能出現在 MIDI 檔案裡的訊息, 但應該能包含足夠的資訊讓讀者知道如何解讀 MIDI 的"文法", 再進一步透過查表得知感興趣的"單字",或是正確的跳過它。
Binary#
MIDI 檔案和大部分電腦裡的檔案一樣,不是設計給人讀的,而是以 binary (二進位資料)的形式儲存, 以方便電腦讀取。
雖然是這樣,但還是要有些人懂這些檔案,才能寫出相關的軟體。 因為 MIDI 有開放它的 spec,所以任何人都可以(至少試著)讀懂 MIDI。
二進位的原始形式就是一堆 0 和 1,直接拿它來編一首 djent 可能都比讀懂它容易, 所以我們通常會用 16 進位(Hexadecimal)的形式來看, 在 linux 你可以直接用 xxd 把 MIDI 轉成 16 進位:
$ xxd input.mid >> output.hex
或是直接用 vim 打開 MIDI 檔,輸入指令:
:%!xxd
Hexadecimal#
本段落簡單講解二進位和 16 進位的關係,如果已經了解可以跳到下一段。
Binary 只有 0 和 1,我們可以用多個 Binary 位數來表達一個數值, 在常見的編碼方式,如果不考慮負數的話 (這邊不贅述,有興趣的可以查 signed number 和 unsigned number) 有 n 個位數可以從 0 數到 2^n - 1,所以 16 進位需要四位二進位(4 bit):
| Binary | Hexadecimal | Decimal(十進位) |
|---|---|---|
| 0000 | 0x0 | 0 |
| 0001 | 0x1 | 1 |
| 0010 | 0x2 | 2 |
| 0011 | 0x3 | 3 |
| 0100 | 0x4 | 4 |
| 0101 | 0x5 | 5 |
| 0110 | 0x6 | 6 |
| 0111 | 0x7 | 7 |
| 1000 | 0x8 | 8 |
| 1001 | 0x9 | 9 |
| 1010 | 0xA | 10 |
| 1011 | 0xB | 11 |
| 1100 | 0xC | 12 |
| 1101 | 0xD | 13 |
| 1110 | 0xE | 14 |
| 1111 | 0xF | 15 |
注意到我們用 0x 開頭來表示這是 16 進位。
8 個 bit 稱為 1 byte,有 256 種可能值,需要兩個 hex number 表示, 因為我們常用 byte 當最小單位,可以習慣一下兩個 hex number 是 1 byte。 1 byte 已經夠存 ASCII 了,所以 hex 也能顯示成對應到的 ASCII 字元。
把 1 byte 展開,例如 0xAC = 1010 1100,我們稱最左邊的那一個二進位 most significant bit (MSB),而最右邊的叫做 least significant bit(LSB)。
Chunks#
MIDI 檔案由多個 chunks 組成,分為 header chunk 和 track chunk, header chunk 會是檔案的開頭,固定有以下格式:
4d54 6864 0000 0006
{MT hd} {length=6}
000X XXXX XXXX
{type} {ntrks} {division}
開頭一定是 4d54 6864(對應到 ASCII 的 MThd),標示這個檔案是 MIDI,以及 header chunk 的開頭。
所有 chunk 都會用 4 byte 標示這個 chunk 有多少 byte 的長度(不含表示長度的 4 byte 本身), header chunk 固定是 6 byte 長。
接下來固定按照以下順序出現,各 2 byte: {type}:
- X 可能是 0/1/2
- 0: 檔案中只有一個 track,可以有多個 channel
- 1: 每個 track 只有一個 channel,同時撥放
- 2: 檔案中有一個或以上個獨立的 track,可能像是多首曲子
{ntrks}: 這個檔案中有多少 track chunks
{division}:
- MSB = 0:後面的 15 bits 表示一個 quarter note 等於多少 ticks
- MSB = 1:後面的 15 bits 可以換算一個 tick 對應到多少秒
Header chunk 結束後會是 track chunk,開頭固定是 4d54 726b(ASCII:MTrk),標示 track chunk 的開始。
4d54 726b XXXX XXXX
{MT rk} {length}
{dt}{event} {dt}{event}....
之後一樣是固定 4 byte 的 track 長度,接下來就是這個 chunk 的"內文",透過 midi message 紀錄, midi message 有固定的格式: {delta time} {event},意思是經過多少時間後執行下一個 event, 如此不斷的累積時間、進行 event,我們就可以得到一個"在什麼時間做什麼事"的譜。
delta time#
- variable-length quanity
MIDI 為了省空間(那時記憶體還不是可以盡情揮霍的東西)利用一種可變長度的格式 (variable-length quanity)表示 delta time,每個 delta time 至少 1 byte,但是我們只用其中的 7 bit 存資料, 剩下的 1 bit(MSB) 則用來表示這筆資料是否還有接續,MSB = 1 表示這筆資料還沒完,所以我們會持續讀取 1 byte, 直到 MSB = 0,再把之前所有讀到的 7 bits 合併。
for example:
| variable-length(hex) | variable-length(bin) | data read(bin) | data read(hex) |
|---|---|---|---|
| 7E | 01111110 | 1111110 | 7E |
| C001 | 1100 0000 0000 0001 | 10 0000 0000 0001 | 2001 |
| BABE00 | 1011 1010 1011 1110 0000 0000 | 0 1110 1001 1111 0000 0000 | 0E9F00 |
如此就可以最多存到等效 28 bit 的資料,且不用每次都花那麼多空間存小的 delta time。
- delta time
現在我們了解如何判斷 delta time 的長度以及如何換算它,所以我們可以得到一個代表 delta time 的 數值,這個值的單位是 tick,在 header chunck: {division} 我們知道一個 tick 代表什麼, 例如 division MSB = 0 的情況下,可以換算一個 tick 是多少個四分音符,並且再進一步利用 meta event 的 temple 換算對應到的時間。
events#
event 分為三種: MIDI event, sysex event, meta event,所有 event 都包含兩部份: {status byte}{data bytes}。
所有 event 都由 status byte 開始,status byte 的 MSB = 1,用來宣告接下來的訊息代表什麼,data bytes 則是 接在 status byte 後面的內容,視 status byte 而言長度不一定。
- sysex, meta event
sysex(System Exclusive) event 看起來是主要用於和 MIDI devices 溝通的,我們之前的應用中沒有用到 MIDI file 裡 的這種 event 所以沒有深究,只知道這種 event 和 meta event 的類似,它們沒有固定長度而是在 data byte 中宣告。 (宣告長度的 data byte 也是用之前提到的 variable-length quanity 形式紀錄)
sysex event 的 status byte 只會是 F0 或 F7,接著用 {length} 宣告在這之後有多少 byte 的資料。
| status | data |
|---|---|
| F0 | {length}{{length} bytes of data … } |
| F7 | {length}{{length} bytes of data … } |
meta event 包含像是歌詞、節奏、time signature 等資訊,statu byte 是 FF,data byte 的第一個 byte {type} 決定接下來 是哪種資訊,接下來的 {length} 宣告之後還有多少長度,以下大概列出一些可能常用的 meta event,其他的可以去查表1。
| status | type | data | comment |
|---|---|---|---|
| FF | 51 | 03{TTTTTT} | Set tempo to {TTTTTT} microseconds per quarter note |
| FF | 58 | 04{NN}{DD}{CC}{BB} | Set time signature to {NN}/(2^{DD})* time, {CC} and {BB} are related to MIDI clock |
| FF | 59 | 02{SF}{MI} | {SF} = numbers of sharps/flats(-1 = 1 flat), {MI} = 0:major key, {MI} = 1:minor key |
| FF | 2F | 00 | End of Track |
* FF 58 04 04 02 18 08 = 4/4, FF 58 04 06 03 18 08 = 6/8
預設 120 BPM, 4/4。
- MIDI event:
| status | data | comment |
|---|---|---|
| 8{X} | {NN}00 | turn note {NN} off on channel {X} |
| 9{X} | {NN}{VV} | trun note {NN} on on channel {X}, with velocity {VV} |
| A{X} | {NN}{VV} | aftertouch, change the velocity to {VV} of note {NN} on channel {X} |
| B{X} | {CC}{DD} | Control Change(CC) on channel {X}, see the reference2 for detail |
| C{X} | {??} | program change |
| D{X} | {??} | channel pressure |
| E{X} | {XX}{XX} | pitch wheel |
MIDI event 的長度都是固定的,讀取到 status byte 就決定了接下來要再讀幾個 byte 表示該 event 的 結束。
- running status
所以我們現在知道如何判斷 delta time 和 event 的長度。理論上我們從 delta time 開始讀,直到讀到一個 byte 的 MSB = 0,這時我們知道這是 delta time 的最後一個 byte,下一個 byte 是 event 的 status byte,所以我們 預期它的 MSB = 1,但其實我們常常會遇到 delta time 的下一個 byte 的 MSB = 0,這是 MIDI 的另一個小規則: running status,當這個 MIDI event 的 status byte 和上一個一樣時,該 status byte 會被省略(對,連 1 byte 都要偷), 例如我要同時把兩顆音打開,MIDI 會長這樣:
00 90 3C
{dt} {Note ON} {0x3C=60,Note=C4}
50 00 40 50
{velocity = 0x50} {dt} {Note: E4} {Velocity}
而不是:
00 90 3C 50
{dt} {Note ON} {Note: C4} {velocity}
00 90 40 50
{dt} {Note On} {Note: E4} {Velocity}
所以如果要停止一顆音,我們可能更常看到利用 Velocity = 0 的 Note ON(9X),而不是直接用 Note OFF(8X), 因為通常這樣可以省一個 byte。注意只有 MIDI message 會用 running message, sysex 和 meta event 不會。
Conclusion#
至今我們應該要可以正確的解析一個 MIDI 檔案,我們知道 chunks 的開頭、長度,以及在一個 track 中,如何 判斷哪裡表示時間,哪裡告訴我們該做什麼事,只要注意 variable-length quanity 格式以及 running status 應該就能 避免混淆,流程大概是這樣:

大概吧