Posts for: #Music

讀 MIDI

這篇是基於我一年前的 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,或是 這篇

一些聽音樂的怪習慣

這是我的「BlogBlog 同樂會 - 2026 年 2 月」的投稿文章。
本月主題是「只有我這樣嗎?」,由 Wiwi 主持。如果你有自己的部落格,歡迎一起來參加!

經由二月的部落格同樂會看到了 Timo 的這篇,想說也寫寫看關於聽音樂時的行為。

不自量力

首先,我是一個木耳 + 音痴 + 節奏痴, 但偏偏我又愛聽 prog metal 這種我顯然聽不懂的風格,我幾乎聽不出來哪裡轉調了, 也聽不出和弦是如何進行,甚至常常不知道現在的拍號長怎樣。但這不影響我認為這些 音樂好聽,而且我似乎可以靠感覺和多次重複聽試著多聽懂一些,例如 Dream Theater 的 A Nightmare To Remember 中間那段 “lost in this wonderful misery”, “bathing in beautiful agony” 我不知道這些和弦是怎麼動的,但可以聽出唱到在 “misery” 和 “agony” 時有一些很美麗的轉換。

可能是"聽懂音樂"對我而言不是簡單的事所以會給我某種成就感,我蠻享受試著聽懂的過程和聽懂後 跟上音樂的感覺,所以我可能會很投入在聽音樂,可能因此有了以下的怪習慣。

不是 Bass 手的空氣 Bass

我聽的音樂之中有不少用了 odd time signature,我會用食指(i)、中指(m)、無名指(a)去數跟的上的節奏, 有次朋友看到以為我在練 bass 的 finger picking,但我根本不會 bass,只是用三隻手指比較好數而已。 像是 Genesis 的 Firth of Fifth 前奏會像這樣:1

Firth of Fifth by Genesis, intro


 | 1 e an ah 2 e an ah | 1 an ah 2 an ah 3 an ah 4 an ah 5 an 6 an |
 | a m i  m  a m i  m  | a m  i  a m  i  a m  i  a m  i  m i  m i  |

 | 1 e an ah 2 e an ah | 1 an ah 2 an ah 3 an ah 4 an 5 an |
 | a m i  m  a m i  m  | a m  i  a m  i  a m  i  m i  m i  |

或是 Liquid Tension Experiment 的 Hypersonic 前奏:2

Hypersonic by Liquid Tension Experiment, intro

                                      X3
 | 1 e an ah 2 e an ah 3 an ah 4 an ah | 1 e an ah 2 e an ah 3 e an ah 4 e an ah |
 | m   i     m   i     a m  i  a m  i  | m   i     m   i     m   i     m   i     |

因為16分的 m i 交替對我這弱雞而言太快,所以我改打8分。

然後因為是節奏白痴,所以其實我大多只跟的上這種把等長音符分組的樂句, 那種附點加三連音的大概會讓我直接當機。

可以參考 Dream Theater 的 The Shadow Man Incident:3

The Shadow Man Incident by Dream Theater, about 01:58


 | ? ? ? |

    ... wait, it's in 4/4?!

EMOTIONAL

除了跟著節奏亂打,有時太投入音樂時會跟著有些輕微的(希望啦)肢體表現, 像是樂曲 resolve 到一段很 epic 段落的那一刻,會點一下頭、之後趨於抬頭挺胸的狀態, 像是 Dream Theater 的 Hell’s kitchen 最後一段、Illumination Theory - Surrender, Trust & Passion、 Octavarium - Razor’s Edge。還有各個 solo 的那顆充滿張力的音,通常都是吉他的 bending note, 像 Octavarium - Razor’s Edge 吉他 solo 最後推到 F 時(我超愛 Octavarium),和 Octavision 的 Three Lives,感覺臉都會不由自主的扭曲一下,只差沒有不小心秀出 guitar face 了。

THAT HIGH F!!!

THAT HIGH F!!!

只有我這樣嗎

應該不是,畢竟這篇文章是看到同樂會有人分享相似的狀況 才想到要寫的,而且居然連樂團都一樣,還是說聽金屬的都會變這樣?


  1. sheet music ↩︎

  2. tab
    tab(六線譜) 是吉他、bass 可以用的記譜方式,每條線代表對應的弦,數字代表第幾格 ↩︎

  3. tab ↩︎