MIDI檔案格式分析(補充和勘誤)

fulinux發表於2018-04-12

MIDI檔案格式分析(補充和勘誤)

本文是對《MIDI檔案格式分析》部落格連結的一點補充:

  • 原文複製
  • vim編輯二進位制檔案的方法
  • 文中製作的midi檔案內容和改動

原文複製

MIDI檔案屬於二進位制檔案,這種檔案一般都有如下基本結構: 檔案頭+資料描述

檔案頭一般包括檔案的型別,因為Midi檔案僅以.mid為副檔名的就有0類和1類兩種,而大家熟悉的點陣圖檔案的格式就更多了,所以才會出現檔案頭這種東西。

而資料描述部份是主體,我們現在來一起分析它的結構:

在每個Midi檔案的開頭都有如下內容,它們的十六進位制程式碼為:“4d 54 68 64 00 00 00 06 ff ff nn nn dd dd”。

前四個是ASCII字元“MThd”是用來鑑別是否Midi檔案,而隨後的四個位元組是指明檔案頭描述部分的位元組數,它總是6,所以一定是“00 00 00 06”,以下是剩餘部分的含義:
這裡寫圖片描述
以上就是MIDI檔案頭了,後面的所有內容都是真正做事的,我們先來看看它的構成。

MIDI的資料是由若干個格式相同的子資料構成的,這些子資料在多音軌的格式中記錄了一個軌道的所有資訊。多加一個音軌,就簡單地把資料追加在前一音軌的後面就可以了,不過不要忘記更改檔案頭中的nn nn(軌道數)。

先看全域性音軌。全域性音軌包括歌曲的附加資訊(比如標題和版權)、歌曲速度和系統碼(Sysx)等內容。

不管是全域性音軌還是含有音符的音軌,都以“4D 54 72 6B”開頭,它其實是ASCII字元“MTrk”,其後跟著一個4個位元組的整數,它標誌了該軌道的位元組數,這不包括前面的4個位元組和本身的4個位元組。這一點,我們可以在後面的例子中去理解。

接著就是記錄資料的地方了,每一個資料有著相同的結構:時間差+事件。

所謂時間差,指的是前一個事件到該事件的時間數,它的單位是tick(MIDI的最小時間單位)。它的構成比較特殊,這裡要用二進位制來說明。

一個位元組有8位,如果僅使用7位,它可以表示0~127這128個數,而剩下的一位,則用來作為標誌。如果要表示的數在以上範圍,則這個標誌為0,這時,一個7位的位元組可以表示0~127tick。如果要表示的數超出了這個範圍(比如240),則把標誌設定成1,然後記錄下高7位,剩下的留給下一個位元組,在該例中240可以分解成128*1+112,這裡的1就是第一個位元組要記錄的,加上標誌位,應該為10000001,即十六進位制的81;而112是下一個位元組記錄的,它的十六進位制為70:所以要表示240這個時間,要寫成81 70。同理,如果要表示65535tick,則可以先計算出65535=1282*3+1281*127+1280*127,然後得出結果:83 FF 7F。由此,我們反過來也可以知道如何確定時間差:只要標誌位為0,則表示結束讀取時間差。比如82 C0 03表示1282*2+1281*64+1280*3=40963,如果基本時間為120,則有341:043個四分音符。

以這種方式記錄整數的位元組稱為動態位元組,它根據記錄的整數改變自身的長度,這在後面還要用到,所以必須熟練計算。

看完了這麼麻煩的東西,我們再來看個更麻煩的東西:事件。在這些標準的解釋後面,我們會通過一些例子來進一步掌握這些內容。

事件大體上可以分為音符、控制器和系統資訊這幾個種類。對於這些事件,都有統一的表達結構:種類+引數。

對於一個音符,由於它的有效範圍是0~127,所以直接用00~7F作為“種類”,可以認為是個音符,比如3C表示中央C。而一個音符的最重要的引數是力度(也叫速度:velocity)。比如,3C 64 表示一個力度為十進位制100的中央C音符。

因為一個位元組有8位,所以剩餘的一位如果置1,再聯合其他的7位,則可以表示各種資訊。我們暫且無視一個音軌到底是全域性的還是用於記錄音符的。它們歸根結底都是用來記錄各種事件的,只不過有些應出現在全域性音軌比較合乎邏輯而已。既然這樣,我們就可以從下面的表來看事件:

下表中,x表示音軌0~F,比如81表示鬆開第二軌的音符。
這裡寫圖片描述
下表詳細地列出了FF的詳細情況,對於位元組數由資料決定的情況,表中以“–”表示。
這裡寫圖片描述
這些就是MIDI結構的全部內容,在下一講,我們將通過一個例項來分析。

要書寫二進位制(十六進位制)檔案,應該準備好一些工具,比如我自己用的是VC++,因為學習MIDI格式無非是想寫它的軟體,既然VC++可以編輯二進位制檔案,就將就著用吧。其次,應該找個可以編輯和播放MIDI檔案的軟體,比如Cakewalk,這樣就可以開始了。

首先書寫檔案頭“4d 54 68 64 00 00 00 06”,我們直接寫同步多音軌的格式,先寫1個音軌,並以120為一個音符的基本時間。這樣,隨後的位元組是:“00 01 00 01 00 78”。

現在,如果用Cakewalk開啟會失敗,因為我們指定的音軌數為1,但是並沒有書寫任何音軌,如果改成“00 01 00 00 00 78”再開啟,就不會出問題了。所以,今後如果更改了音軌數,千萬不要忘記向“上頭”彙報。

把軌道數改回01,繼續我們的實驗。先寫音軌的頭資訊:“4D 54 72 6B”(MTrk),因為我們還不能確定後面有多少位元組,所以先把它假設成“00 00 00 00”,以後再回來改。

我們先嚐試設定歌曲的速度和節拍等基本資訊。假設一個四分音符的時間是半秒,即0.5*106微秒。它的十六進位制數是07A120,再看事件表,設定速度是51,但是在其前面必須是FF,然後它須要3個位元組作為引數,因此位元組數為03,引數為“07 A1 20”,也就是“FF 51 03 07 A1 20”。這是事件部分,不要忘記在其之前有個引數——時間差。這是一開始就應該設定的引數,因此時間差為00。所以,完整的事件應該是“00 FF 51 03 07 A1 20”,我們把這一段追加在Midi檔案末尾。

這時先不要急著用Cakewalk驗證,因為我們還沒有向“上級”報告,沒錯,把前面表示位元組數的“00 00 00 00”改成“00 00 00 07”,如果用VC++作為二進位制檔案的編輯器,選擇了事件後,可以在狀態列看到選擇的位元組長。儲存後,再用Cakewalk開啟,就可以看見速度是120。

我們再來設定節拍和調號,因為一般用Cakewalk新建一個Midi會預設地設定成4/4,C大調,我們就改設成6/8,A大調。查閱事件表知道,58和59是分別用來設定節拍和調號的。雖然設定節拍的引數很多,但在現在的系統中,後兩個引數是被忽略的,而且Cakewalk還會對其進行修正。因此,我們只要設定好實際有用的就可以了。分子是6,分母是8,所以第一個引數是06,第二個引數是03(23=8)。最後,補上前面的時間差和後面的兩個被忽略的引數,它應該是“00 FF 58 04 06 03 00 00”;再看調號,A調有3個升號,因此可以這樣的事件可以表示為“00 FF 59 02 03 00”。事實上,大小調是個被忽略的引數。我們統計一下至今為止事件的位元組數,然後更改前面的引數,即把“00 00 00 07”改成“00 00 00 15”。儲存後用Cakewalk開啟,再進入五線譜視窗,就可以馬上驗證了。細心的你可能已經發現,進入五線譜視窗前和平常有些延遲,這是因為我們並沒有設定好那些可以忽略的位元組,而Cakewalk就是在對其進行重新驗證,這一點,我們以後再討論。

用同樣的方法,您可以很容易地設定歌曲的標題和版權,這作為一個練習,在這裡就不多寫了。我們現在學習寫一個含有音符的軌道。首先您應該知道要做哪些事:1、寫新音軌的資訊頭;2、向上級彙報多了一個音軌。接下來,我們開始寫入一個簡單的音符。

假設向第一拍寫一箇中音A,這裡可能要先說明一下,音符是從C0開始一起向上數的,數到中央C(C5)是十六進位制的3C,則中音A應該為45,在附件中有詳細的計算方法。我們知道在音樂中一個音符通常有三個屬性:音高、力度和時值。可是我們在事件表中並沒有看見有什麼可以直接設定音符時值的標誌。不錯,事實上,音符的時值是由按下的時間和鬆開的時間決定的。我們假設要寫入一個八分音符。它的Tick數是四分音符的一半,即60,十六進位制表示成3C。我們先來看看與音符有關的標誌。

在事件表中,9x是用來開啟一個音符,我們這裡假設使用第7個通道(注意到MIDI有16個通道(Channel),而第10個被預設地用作打擊樂,所以,我們在這個階段(沒有學習Sysx之前),先不要使用第10個通道),則9x中的x是6;再看它的引數,一個是音符,這裡我們寫入45,第二個是力度,我們用70,因為是一開始就觸發的,所以前面的時間差還是00。這樣我們就在第5個通道以力度112按下了一箇中音A。對應的位元組描述是“00 96 45 70”。它的時值不用想都知道一定是0,這取決於什麼時候把它鬆開。

特別地,如果一個音符的力度為0,則MIDI認為使用者想鬆開這個鍵,因為9x已經開啟了通道,所以我們直接寫入一個帶00力度的同一音符就可以決定這個音符的時值了。根據前面的分析,這個時間差應該是3C,所以我們在寫入3C後寫上音符45和它的力度00,即“3C 45 00”。統計好位元組數並向這一軌的頭資訊中更新,然後儲存到磁碟,用Cakewalk開啟並進入事件列表視窗便可以驗證了。

在這個基礎上,我們再嘗試在A的後面增加一個四分音符中音#G。因為96已經開啟了通道,我們沒有必要每次都使用9x,只要輸入事件資訊即可。對於中音#G,它的十六進位制是44,相對剛才輸入00力度的A來說時間差為00,因此可以表示成“00 44 64”,這裡我們已經假設力度為100;然後是鬆開它,因為是四分音符,所以時間差是78H,別忘記力度是00,它的位元組應表示成“78 44 00”,做好後面的工作,然後驗證看對不對。

我們再做個稍微複雜一點的實驗:在原來的基礎上,在同一軌的第一拍加上一個附點四分中音D。這裡就不能再使用追加的方法了,因為前面的事件已經過了3個八分音符的時間,無論再加上什麼,都只會發生在後面,所以我們要在前面插入一些位元組。

9x已經開啟了通道,我們直接在9x按下的音符後加上一個音符事件“00 3E 64”,這裡的00顯然是個時間差,3E是中音D,64是力度,也就是說,在按下中音A的同時按下了中音D。我們又按下一個鍵了,要在什麼時候,在哪裡鬆開才能保證輸入的是個附點八分音符呢?首先,它的時值是3個八分音符,即180,這裡還有一點要注意,180是個大於128的數,它的動態位元組就應該表示成“81 34”,在哪裡輸入才好呢?如果你覺得在按下D後輸入,或者在任何什麼地方輸入這個時間差,然後再寫上“3E 00”可以表示鬆開的話就完全誤解了時間差的概念。其實,我們只要簡單地在鬆開#G的時候鬆開D就可以了,所以應該在末尾補上“00 3E 00”。統計好位元組數後到Cakewalk中去驗證驗證吧。這裡附有我們目前寫下的Midi檔案樣本。

到目前為此,我們應該可以輸入任何形式的音符了,不過MIDI除了音符以外,還可以包括各種控制器和系統碼,它們的地位不亞於音符,我們現在馬上學習如何使用控制器。

控制器比音符要簡單多了,我們嘗試在#G前加入相位控制(Pan),它的十進位制程式碼是10,十六進位制是0A,我們將引數設定成111,即十六進位制的6F。首先查得控制器是Bx,這裡的x和上面一樣,也是6。接下來寫入控制器號0A,然後是引數6F,別忘了前面的時間差是00。所以這段位元組是“00 B6 0A 6F”,它應放在鬆開#G的事件之前,與按下#G同時。不過,一旦使用了非音符,而後面還有音符事件時,則必須重新通知開啟音符,這說起來複雜,做起來還是比較容易的,我們只要稍微改寫下一個音符事件即可:原本是“時間差+音符+力度”,我們加入一個開啟音符的標誌,成為“時間差+9x+音符+力度”即可。校驗過頭資訊後,去Cakewalk中進行更進一步的檢驗便知它的可行。

其實,時間差為00的控制事件如果出現的時間也是00,則在Cakewalk中會盡可能地把它們放在軌道資訊中,而不在事件列表中重複。我們可以利用這一點給音軌設定初始樂器和音量,這就作為一個練習,在此就不再說明了。

至於其他的諸如觸後鍵等與控制器類似的格式在此就不多說了。在這裡有必要提醒的是滑音。滑音的樂理範圍是-8192~8191,但是在使用時引數是個正數,比如要設定成0,則應該是0-(-8192)=8192,它才是引數。8192的7位雙位元組表示成“8192 mod 128=00H;8192 div 128=40H”。如果時間差是00,則應表示成“00 E6 00 40”

最後我們看看系統碼。系統碼的構成本來是“F0 廠家ID 裝置號碼 格式程式碼 傳送命令 具體引數 F7”,而在檔案中,則不以開頭的“F0”為系統碼,而位元組數也僅記錄剩餘的系統碼,比如XG的復位碼是“F0 43 10 4C 00 00 7E 00 F7”,則在檔案中應寫成“00 F0 08 43 10 4C 00 00 7E 00 F7”,其中第一個00是時間差,F0是系統碼標誌,08是後面的位元組數。有一點要注意的是,幾個系統碼不可以寫在一起,比如“00 F0 0D 43 10 4C 00 00 7E 00 F7 F0 AA BB CC F7”或“00 F0 0C 43 10 4C 00 00 7E 00 F7 AA BB CC F7”都是不好的寫法。如果存在以上系統碼集,可以分成兩個事件:“00 F0 08 43 10 4C 00 00 7E 00 F7 00 F0 04 AA BB CC F7”

當然系統碼可以寫在任何音軌,不過一般我們會考慮把歌曲播放前傳送的系統碼寫在全域性音軌中,並把時間差設成00。

作為一個參考,這裡再附上一個MIDI樣本。

雖然我們只討論了同步多音軌的格式,其實對於其他兩種,比如較常見的單音軌格式,所有的事件只寫在一個音軌中,即只要存在一個“MTrk”就足夠了。而相對地,用於記錄音軌數的兩個位元組也永遠為“00 01”,連續事件如果出現的通道不同,也必須重新指定通道(8x~Ex)。在此不詳細討論了。

音符十六進位制的計算
關於樂器選擇
RPN和NRPN

在MIDI中,中央C是C5,最低音是C0,最高音是G5,要計算任何一個音符對應的十六進位制,可以使用這個公式:

假設音符是NO,表示第O八度的音名N,比如G2中,N為G,O為2,則它的十進位制為O*12+N,N的值為了簡便起見,用下表給出:

這樣G2的十進位制值為2*12+7=31,十六進位制為1F。

若知道音符的十六進位制,也可以很容易求出音符,比如64(16)=100(10),

而100 div 12=8,100 mod 12=4,對應音符為E8。寫成公式就是:

N=B mod 12;O=B div 12;(設B為表示音符的位元組的十進位制數)

樂器是MIDI中比較重要的因素,要選擇所有的樂器不僅僅只是使用Cx號標誌就能完成的,還必須結合BankSelect(樂隊選擇),而BankSelect其實是由0號控制器和32號控制器完成的,它們的十六進位制程式碼分別是00和20.比如要選擇出XG標準中的Slow Violin,它在第08H個樂隊中的第28H號樂器中,所以它的完整程式碼應為“00 B0 00 00 00 20 08 00 C0 28”。我們來分析它的構成:這裡我們假設時間差為00,所有資訊都發給通道00,所以第一個00是時間差,B0是開啟控制器的標誌,並指定傳送到通道00,接下來的00 00,是由控制器號00和控制器引數00構成的,它事實上是表示0號控制器的引數為0,即BankSelect-MSB的引數為0,然後是下一個事件,它是“00 20 08”,即時間差是00,使用20H號控制器,引數為08H,即BankSelect-LSB的引數是08H,這樣就指定了Bank(樂隊)。再下來就是“00 C0 28”,就是所謂的Patch Change事件了,它的時間差為00,引數是28H。這樣就完成了標準的樂器選擇。

事實上,它是由三個事件共同完成的,如果某次選擇樂器和上次的樂器有共同的引數,可以不必重複使用相關的操作。

在本例中,您可能發現了一點,當連續使用同類操作時可以不必每次指定操作種類,比如這裡的連續再次使用控制器,所以第二個控制器並沒有使用B0作為標誌,而是直接使用控制器號碼和它的引數。這一點和音符是一樣的。其實,如果連續使用Patch Change事件(我是說如果),則也可不必每次都寫Cx,開啟了一次就可以了;不過就Patch Change事件而言,連續地更換樂器的結果是僅最後一個有效而已。

在前面的文件中並沒有提及RPN和NRPN,其實它們是由四個連續的控制器來實現的。我們假設要使用RPN事件的Coarse Turning,它的引數假設是4096,則它的位元組是“00 B0 65 00 00 64 02 00 06 20 00 26 00”,我們來分析這段位元組:首先我們先看看RPN是由哪四個控制器組成的——首先設定RPN-MSB和RPN-LSB,分別對應的控制器是65H和64H,Coarse Turning的RPN碼是2,所以MSB為0,LSB為2;然後是設定Data Entry MSB和Data Entry LSB,對應的控制器是06H和26H,而4096 div 128=32,4096 mod 128=0,對應的十六進位制數分別是20H和00H。因此就構成了上面的位元組。而NRPN和RPN原理是一樣的,只不過不用RPN-MSB和RPN-LSB,而改用NRPN-MSB和NRPN-LSB而已,它們對應的十六進位制數分別為63H和62H。

原文
MIDI檔案屬於二進位制檔案,這種檔案一般都有如下基本結構: 檔案頭+資料描述

檔案頭一般包括檔案的型別,因為Midi檔案僅以.mid為副檔名的就有0類和1類兩種,而大家熟悉的點陣圖檔案的格式就更多了,所以才會出現檔案頭這種東西。

而資料描述部份是主體,我們現在來一起分析它的結構:

在每個Midi檔案的開頭都有如下內容,它們的十六進位制程式碼為:“4d 54 68 64 00 00 00 06 ff ff nn nn dd dd”。

前四個是ASCII字元“MThd”是用來鑑別是否Midi檔案,而隨後的四個位元組是指明檔案頭描述部分的位元組數,它總是6,所以一定是“00 00 00 06”,以下是剩餘部分的含義:

以上就是MIDI檔案頭了,後面的所有內容都是真正做事的,我們先來看看它的構成。

MIDI的資料是由若干個格式相同的子資料構成的,這些子資料在多音軌的格式中記錄了一個軌道的所有資訊。多加一個音軌,就簡單地把資料追加在前一音軌的後面就可以了,不過不要忘記更改檔案頭中的nn nn(軌道數)。

先看全域性音軌。全域性音軌包括歌曲的附加資訊(比如標題和版權)、歌曲速度和系統碼(Sysx)等內容。

不管是全域性音軌還是含有音符的音軌,都以“4D 54 72 6B”開頭,它其實是ASCII字元“MTrk”,其後跟著一個4個位元組的整數,它標誌了該軌道的位元組數,這不包括前面的4個位元組和本身的4個位元組。這一點,我們可以在後面的例子中去理解。

接著就是記錄資料的地方了,每一個資料有著相同的結構:時間差+事件。

所謂時間差,指的是前一個事件到該事件的時間數,它的單位是tick(MIDI的最小時間單位)。它的構成比較特殊,這裡要用二進位制來說明。

一個位元組有8位,如果僅使用7位,它可以表示0~127這128個數,而剩下的一位,則用來作為標誌。如果要表示的數在以上範圍,則這個標誌為0,這時,一個7位的位元組可以表示0~127tick。如果要表示的數超出了這個範圍(比如240),則把標誌設定成1,然後記錄下高7位,剩下的留給下一個位元組,在該例中240可以分解成128*1+112,這裡的1就是第一個位元組要記錄的,加上標誌位,應該為10000001,即十六進位制的81;而112是下一個位元組記錄的,它的十六進位制為70:所以要表示240這個時間,要寫成81 70。同理,如果要表示65535tick,則可以先計算出65535=1282*3+1281*127+1280*127,然後得出結果:83 FF 7F。由此,我們反過來也可以知道如何確定時間差:只要標誌位為0,則表示結束讀取時間差。比如82 C0 03表示1282*2+1281*64+1280*3=40963,如果基本時間為120,則有341:043個四分音符。

以這種方式記錄整數的位元組稱為動態位元組,它根據記錄的整數改變自身的長度,這在後面還要用到,所以必須熟練計算。

看完了這麼麻煩的東西,我們再來看個更麻煩的東西:事件。在這些標準的解釋後面,我們會通過一些例子來進一步掌握這些內容。

事件大體上可以分為音符、控制器和系統資訊這幾個種類。對於這些事件,都有統一的表達結構:種類+引數。

對於一個音符,由於它的有效範圍是0~127,所以直接用00~7F作為“種類”,可以認為是個音符,比如3C表示中央C。而一個音符的最重要的引數是力度(也叫速度:velocity)。比如,3C 64 表示一個力度為十進位制100的中央C音符。

因為一個位元組有8位,所以剩餘的一位如果置1,再聯合其他的7位,則可以表示各種資訊。我們暫且無視一個音軌到底是全域性的還是用於記錄音符的。它們歸根結底都是用來記錄各種事件的,只不過有些應出現在全域性音軌比較合乎邏輯而已。既然這樣,我們就可以從下面的表來看事件:

下表中,x表示音軌0~F,比如81表示鬆開第二軌的音符。

下表詳細地列出了FF的詳細情況,對於位元組數由資料決定的情況,表中以“–”表示。

這些就是MIDI結構的全部內容,在下一講,我們將通過一個例項來分析。

要書寫二進位制(十六進位制)檔案,應該準備好一些工具,比如我自己用的是VC++,因為學習MIDI格式無非是想寫它的軟體,既然VC++可以編輯二進位制檔案,就將就著用吧。其次,應該找個可以編輯和播放MIDI檔案的軟體,比如Cakewalk,這樣就可以開始了。

首先書寫檔案頭“4d 54 68 64 00 00 00 06”,我們直接寫同步多音軌的格式,先寫1個音軌,並以120為一個音符的基本時間。這樣,隨後的位元組是:“00 01 00 01 00 78”。

現在,如果用Cakewalk開啟會失敗,因為我們指定的音軌數為1,但是並沒有書寫任何音軌,如果改成“00 01 00 00 00 78”再開啟,就不會出問題了。所以,今後如果更改了音軌數,千萬不要忘記向“上頭”彙報。

把軌道數改回01,繼續我們的實驗。先寫音軌的頭資訊:“4D 54 72 6B”(MTrk),因為我們還不能確定後面有多少位元組,所以先把它假設成“00 00 00 00”,以後再回來改。

我們先嚐試設定歌曲的速度和節拍等基本資訊。假設一個四分音符的時間是半秒,即0.5*106微秒。它的十六進位制數是07A120,再看事件表,設定速度是51,但是在其前面必須是FF,然後它須要3個位元組作為引數,因此位元組數為03,引數為“07 A1 20”,也就是“FF 51 03 07 A1 20”。這是事件部分,不要忘記在其之前有個引數——時間差。這是一開始就應該設定的引數,因此時間差為00。所以,完整的事件應該是“00 FF 51 03 07 A1 20”,我們把這一段追加在Midi檔案末尾。

這時先不要急著用Cakewalk驗證,因為我們還沒有向“上級”報告,沒錯,把前面表示位元組數的“00 00 00 00”改成“00 00 00 07”,如果用VC++作為二進位制檔案的編輯器,選擇了事件後,可以在狀態列看到選擇的位元組長。儲存後,再用Cakewalk開啟,就可以看見速度是120。

我們再來設定節拍和調號,因為一般用Cakewalk新建一個Midi會預設地設定成4/4,C大調,我們就改設成6/8,A大調。查閱事件表知道,58和59是分別用來設定節拍和調號的。雖然設定節拍的引數很多,但在現在的系統中,後兩個引數是被忽略的,而且Cakewalk還會對其進行修正。因此,我們只要設定好實際有用的就可以了。分子是6,分母是8,所以第一個引數是06,第二個引數是03(23=8)。最後,補上前面的時間差和後面的兩個被忽略的引數,它應該是“00 FF 58 04 06 03 00 00”;再看調號,A調有3個升號,因此可以這樣的事件可以表示為“00 FF 59 02 03 00”。事實上,大小調是個被忽略的引數。我們統計一下至今為止事件的位元組數,然後更改前面的引數,即把“00 00 00 07”改成“00 00 00 15”。儲存後用Cakewalk開啟,再進入五線譜視窗,就可以馬上驗證了。細心的你可能已經發現,進入五線譜視窗前和平常有些延遲,這是因為我們並沒有設定好那些可以忽略的位元組,而Cakewalk就是在對其進行重新驗證,這一點,我們以後再討論。

用同樣的方法,您可以很容易地設定歌曲的標題和版權,這作為一個練習,在這裡就不多寫了。我們現在學習寫一個含有音符的軌道。首先您應該知道要做哪些事:1、寫新音軌的資訊頭;2、向上級彙報多了一個音軌。接下來,我們開始寫入一個簡單的音符。

假設向第一拍寫一箇中音A,這裡可能要先說明一下,音符是從C0開始一起向上數的,數到中央C(C5)是十六進位制的3C,則中音A應該為45,在附件中有詳細的計算方法。我們知道在音樂中一個音符通常有三個屬性:音高、力度和時值。可是我們在事件表中並沒有看見有什麼可以直接設定音符時值的標誌。不錯,事實上,音符的時值是由按下的時間和鬆開的時間決定的。我們假設要寫入一個八分音符。它的Tick數是四分音符的一半,即60,十六進位制表示成3C。我們先來看看與音符有關的標誌。

在事件表中,9x是用來開啟一個音符,我們這裡假設使用第7個通道(注意到MIDI有16個通道(Channel),而第10個被預設地用作打擊樂,所以,我們在這個階段(沒有學習Sysx之前),先不要使用第10個通道),則9x中的x是6;再看它的引數,一個是音符,這裡我們寫入45,第二個是力度,我們用70,因為是一開始就觸發的,所以前面的時間差還是00。這樣我們就在第5個通道以力度112按下了一箇中音A。對應的位元組描述是“00 96 45 70”。它的時值不用想都知道一定是0,這取決於什麼時候把它鬆開。

特別地,如果一個音符的力度為0,則MIDI認為使用者想鬆開這個鍵,因為9x已經開啟了通道,所以我們直接寫入一個帶00力度的同一音符就可以決定這個音符的時值了。根據前面的分析,這個時間差應該是3C,所以我們在寫入3C後寫上音符45和它的力度00,即“3C 45 00”。統計好位元組數並向這一軌的頭資訊中更新,然後儲存到磁碟,用Cakewalk開啟並進入事件列表視窗便可以驗證了。

在這個基礎上,我們再嘗試在A的後面增加一個四分音符中音#G。因為96已經開啟了通道,我們沒有必要每次都使用9x,只要輸入事件資訊即可。對於中音#G,它的十六進位制是44,相對剛才輸入00力度的A來說時間差為00,因此可以表示成“00 44 64”,這裡我們已經假設力度為100;然後是鬆開它,因為是四分音符,所以時間差是78H,別忘記力度是00,它的位元組應表示成“78 44 00”,做好後面的工作,然後驗證看對不對。

我們再做個稍微複雜一點的實驗:在原來的基礎上,在同一軌的第一拍加上一個附點四分中音D。這裡就不能再使用追加的方法了,因為前面的事件已經過了3個八分音符的時間,無論再加上什麼,都只會發生在後面,所以我們要在前面插入一些位元組。

9x已經開啟了通道,我們直接在9x按下的音符後加上一個音符事件“00 3E 64”,這裡的00顯然是個時間差,3E是中音D,64是力度,也就是說,在按下中音A的同時按下了中音D。我們又按下一個鍵了,要在什麼時候,在哪裡鬆開才能保證輸入的是個附點八分音符呢?首先,它的時值是3個八分音符,即180,這裡還有一點要注意,180是個大於128的數,它的動態位元組就應該表示成“81 34”,在哪裡輸入才好呢?如果你覺得在按下D後輸入,或者在任何什麼地方輸入這個時間差,然後再寫上“3E 00”可以表示鬆開的話就完全誤解了時間差的概念。其實,我們只要簡單地在鬆開#G的時候鬆開D就可以了,所以應該在末尾補上“00 3E 00”。統計好位元組數後到Cakewalk中去驗證驗證吧。這裡附有我們目前寫下的Midi檔案樣本。

到目前為此,我們應該可以輸入任何形式的音符了,不過MIDI除了音符以外,還可以包括各種控制器和系統碼,它們的地位不亞於音符,我們現在馬上學習如何使用控制器。

控制器比音符要簡單多了,我們嘗試在#G前加入相位控制(Pan),它的十進位制程式碼是10,十六進位制是0A,我們將引數設定成111,即十六進位制的6F。首先查得控制器是Bx,這裡的x和上面一樣,也是6。接下來寫入控制器號0A,然後是引數6F,別忘了前面的時間差是00。所以這段位元組是“00 B6 0A 6F”,它應放在鬆開#G的事件之前,與按下#G同時。不過,一旦使用了非音符,而後面還有音符事件時,則必須重新通知開啟音符,這說起來複雜,做起來還是比較容易的,我們只要稍微改寫下一個音符事件即可:原本是“時間差+音符+力度”,我們加入一個開啟音符的標誌,成為“時間差+9x+音符+力度”即可。校驗過頭資訊後,去Cakewalk中進行更進一步的檢驗便知它的可行。

其實,時間差為00的控制事件如果出現的時間也是00,則在Cakewalk中會盡可能地把它們放在軌道資訊中,而不在事件列表中重複。我們可以利用這一點給音軌設定初始樂器和音量,這就作為一個練習,在此就不再說明了。

至於其他的諸如觸後鍵等與控制器類似的格式在此就不多說了。在這裡有必要提醒的是滑音。滑音的樂理範圍是-8192~8191,但是在使用時引數是個正數,比如要設定成0,則應該是0-(-8192)=8192,它才是引數。8192的7位雙位元組表示成“8192 mod 128=00H;8192 div 128=40H”。如果時間差是00,則應表示成“00 E6 00 40”

最後我們看看系統碼。系統碼的構成本來是“F0 廠家ID 裝置號碼 格式程式碼 傳送命令 具體引數 F7”,而在檔案中,則不以開頭的“F0”為系統碼,而位元組數也僅記錄剩餘的系統碼,比如XG的復位碼是“F0 43 10 4C 00 00 7E 00 F7”,則在檔案中應寫成“00 F0 08 43 10 4C 00 00 7E 00 F7”,其中第一個00是時間差,F0是系統碼標誌,08是後面的位元組數。有一點要注意的是,幾個系統碼不可以寫在一起,比如“00 F0 0D 43 10 4C 00 00 7E 00 F7 F0 AA BB CC F7”或“00 F0 0C 43 10 4C 00 00 7E 00 F7 AA BB CC F7”都是不好的寫法。如果存在以上系統碼集,可以分成兩個事件:“00 F0 08 43 10 4C 00 00 7E 00 F7 00 F0 04 AA BB CC F7”

當然系統碼可以寫在任何音軌,不過一般我們會考慮把歌曲播放前傳送的系統碼寫在全域性音軌中,並把時間差設成00。

作為一個參考,這裡再附上一個MIDI樣本。

雖然我們只討論了同步多音軌的格式,其實對於其他兩種,比如較常見的單音軌格式,所有的事件只寫在一個音軌中,即只要存在一個“MTrk”就足夠了。而相對地,用於記錄音軌數的兩個位元組也永遠為“00 01”,連續事件如果出現的通道不同,也必須重新指定通道(8x~Ex)。在此不詳細討論了。

音符十六進位制的計算
關於樂器選擇
RPN和NRPN

在MIDI中,中央C是C5,最低音是C0,最高音是G5,要計算任何一個音符對應的十六進位制,可以使用這個公式:

假設音符是NO,表示第O八度的音名N,比如G2中,N為G,O為2,則它的十進位制為O*12+N,N的值為了簡便起見,用下表給出:
這裡寫圖片描述
這樣G2的十進位制值為2*12+7=31,十六進位制為1F。

若知道音符的十六進位制,也可以很容易求出音符,比如64(16)=100(10),

而100 div 12=8,100 mod 12=4,對應音符為E8。寫成公式就是:

N=B mod 12;O=B div 12;(設B為表示音符的位元組的十進位制數)

樂器是MIDI中比較重要的因素,要選擇所有的樂器不僅僅只是使用Cx號標誌就能完成的,還必須結合BankSelect(樂隊選擇),而BankSelect其實是由0號控制器和32號控制器完成的,它們的十六進位制程式碼分別是00和20.比如要選擇出XG標準中的Slow Violin,它在第08H個樂隊中的第28H號樂器中,所以它的完整程式碼應為“00 B0 00 00 00 20 08 00 C0 28”。我們來分析它的構成:這裡我們假設時間差為00,所有資訊都發給通道00,所以第一個00是時間差,B0是開啟控制器的標誌,並指定傳送到通道00,接下來的00 00,是由控制器號00和控制器引數00構成的,它事實上是表示0號控制器的引數為0,即BankSelect-MSB的引數為0,然後是下一個事件,它是“00 20 08”,即時間差是00,使用20H號控制器,引數為08H,即BankSelect-LSB的引數是08H,這樣就指定了Bank(樂隊)。再下來就是“00 C0 28”,就是所謂的Patch Change事件了,它的時間差為00,引數是28H。這樣就完成了標準的樂器選擇。

事實上,它是由三個事件共同完成的,如果某次選擇樂器和上次的樂器有共同的引數,可以不必重複使用相關的操作。

在本例中,您可能發現了一點,當連續使用同類操作時可以不必每次指定操作種類,比如這裡的連續再次使用控制器,所以第二個控制器並沒有使用B0作為標誌,而是直接使用控制器號碼和它的引數。這一點和音符是一樣的。其實,如果連續使用Patch Change事件(我是說如果),則也可不必每次都寫Cx,開啟了一次就可以了;不過就Patch Change事件而言,連續地更換樂器的結果是僅最後一個有效而已。

在前面的文件中並沒有提及RPN和NRPN,其實它們是由四個連續的控制器來實現的。我們假設要使用RPN事件的Coarse Turning,它的引數假設是4096,則它的位元組是“00 B0 65 00 00 64 02 00 06 20 00 26 00”,我們來分析這段位元組:首先我們先看看RPN是由哪四個控制器組成的——首先設定RPN-MSB和RPN-LSB,分別對應的控制器是65H和64H,Coarse Turning的RPN碼是2,所以MSB為0,LSB為2;然後是設定Data Entry MSB和Data Entry LSB,對應的控制器是06H和26H,而4096 div 128=32,4096 mod 128=0,對應的十六進位制數分別是20H和00H。因此就構成了上面的位元組。而NRPN和RPN原理是一樣的,只不過不用RPN-MSB和RPN-LSB,而改用NRPN-MSB和NRPN-LSB而已,它們對應的十六進位制數分別為63H和62H。

vim編輯二進位制檔案的方法

參考連結

開啟
vim -b x.mid
設定16進位制格式顯示:
:%!xxd
只用編輯16進位制部分的內容,修改其他部分的內容不會有其他影響期間不要儲存改動
最後儲存前返回二進位制形式:
:%!xxd -r
:wq

文中製作的midi檔案內容和改動

原文描述中每個音軌結束後面沒有加“00 ff 2f 00”, 同時在位元組數中加上4,可以直接通過windows中的音樂播放器播放:

00000000: 4d54 6864 0000 0006 0001 0002 0078 4d54 MThd………xMT
00000010: 726b 0000 0019 00ff 5103 07a1 2000 ff58 rk……Q… ..X
00000020: 0406 0300 0000 ff59 0203 0000 ff2f 004d …….Y…../.M
00000030: 5472 6b00 0000 0b00 9645 703c 4500 00ff Trk……Ep

相關文章