這篇是基於我一年前的 MIDI 筆記整理,可能只有 cover 到當時需要的部份。
這篇筆記主要針對 MIDI file,沒有 MIDI protocol。

About MIDI#

MIDI, Musical Instrument Digital Interface, 是一種 1980 年代就存在的 open format, 常被用於操控音樂相關的訊號。

MIDI 本身不包含音訊,只包含了要彈哪顆音、彈多大力、這些音之間間隔多久這類的訊息, 所以傳輸 MIDI 訊號不需要很大的流量,檔案也不佔什麼空間。

更多關於 MIDI 檔案的介紹可以參考 wiwi 的這篇。 這篇文章會專注在如何看懂 MIDI 檔案本身,像是這樣:

A MIDI file interpretation example

當然這裡無法窮盡所有可能出現在 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 應該就能 避免混淆,流程大概是這樣:

flow chart of reading a MIDI file

大概吧

如果想要研究細節,可以去看看官方的 MIDI spec,或是 這篇