前回の記事 → KuinでSMF(MIDIファイル)を解析しよう1
前回はSMFのヘッダチャンクの解析までできました。
今回はトラックチャンクの解析を勉強していきます。
SMFのトラックチャンクを解析する
トラックチャンクは、ヘッダチャンク(14byte)から連なって次のような構成のデータになっています。
4D 54 72 6B 00 00 00 0C イベントデータ イベントデータ イベントデータ …
トラックチャンクに記録されている情報は以下の通り。
1.チャンクタイプ(4byte)
2.データ長(4byte)
3.イベントデータ(必要数繰り返し)
これでトラック1つ分のチャンクです。
複数のトラックがある場合は、前のチャンクの終端からまたトラックチャンクが並んでいます。
1. チャンクタイプの解析
4D 54 72 6B
の部分です。文字にすると「MTrk」となります。
const MTRK: int :: 0x4D54726B
var chunk: int :: me.castInt(me._reader.read(4))
if(chunk <> MTRK)
throw ERR_INVALID_TRACK_HEADER
end if
2. データ長の解析
00 00 00 0C
の部分です。
これは、イベントデータがこの後何バイト続いているかを示しています。
つまり、ここで読み込んだバイト数分を解析したら1つのトラックの解析が完了しています。
var trackSize: int :: me.castInt(me._reader.read(4))
3. イベントデータの解析
ここからはひたすらイベントデータが並んでいます。
しかし、イベントデータはたくさんの種類があり、データを表現するバイト数も様々です。
不要なデータだからといってスキップするにしても、とりあえずは解析をしなければ次のデータの在り処が分かりません。
若干面倒ですが、1つ1つ丁寧に解析をしていきます。
イベントデータの構成
イベントデータは大きく分けて3つのパターンがあり、以下の組み合わせになっています。
1. デルタタイム + MIDIイベント
2. デルタタイム + SysExイベント
3. デルタタイム + メタイベント
デルタタイムとは
デルタタイムは直前のイベントからの時間をあらわします。
デルタタイムの表現にはヘッダチャンクで解析した時間単位が絡んできますが、とてもややこしいので解説サイトで確認してください。
解析に必要な情報は「デルタタイムは可変長数値表現」ということのみです。
可変長なので前回の記事で作ったメソッドでそのまま読み込むことができます。
var time: int :: me.parseVariableInt()
MIDIイベントを解析しよう
演奏データそのものを示します。
シーケンサーで鍵盤を押したり離したりした場合に通信されるデータがこれになります。
MIDIイベントは「ステータスバイト」と「データバイト」で構成されていて、ステータスバイトが演奏命令の種類をあらわし、以下の7種類があります。
演奏命令 | ステータス | データ1 | データ2 |
---|---|---|---|
ノートオフ | 0x80〜8F | 0x00〜0F | 0x00固定 |
ノートオン | 0x90〜9F | 0x00〜0F | 0x00〜0F |
キープレッシャー | 0xA0〜AF | 0x00〜0F | 0x00〜0F |
コントロールチェンジ | 0xB0〜BF | 0x00〜0F | 0x00〜0F |
プログラムチェンジ | 0xC0〜CF | 0x00〜0F | なし |
チャンネルプレッシャー | 0xD0〜DF | 0x00〜0F | なし |
ピッチベンド | 0xE0〜EF | 0x00〜0F | 0x00〜0F |
この表から以下の特徴が分かります。
- ステータスバイトの上位4ビットで命令を判断できる。
- ステータスバイトの最上位ビットは
1
である。 - データ1の最上位ビットは
0
である。 - データ2は不要の場合もある。
以上より、MIDIイベントを判断するプログラムを作成します。
var status: bit8 :: me._reader.read(1)[0]
switch(status.and(0xF0b8))
case 0x80b8, 0x90b8, 0xA0b8, 0xB0b8, 0xE0b8
; データバイト2つを解析
case 0xC0b8, 0xD0b8
; データバイト1つを解析
default
throw ERR_INVALID_STATUS
end switch
status.and(0xF0b8)
でステータスバイトの上位4ビットだけを取り出し、データバイトが2つか1つかで処理を分けることにしました。
ランニングステータス
MIDIイベントは、より少ないバイト数で表現できるように1つ前のイベントと同じ演奏命令だった場合、ステータスバイトを省略することができます。
これをランニングステータスと呼び、デルタタイムの後はデータ1が続くようになります。
では、省略されたかどうかをどうやって判断するかというと、デルタタイムの後に読み込んだバイトデータの最上位ビットを確認することで判断ができるようになっています。
先の特徴で上げたように、ステータスバイトは最上位ビットが1
になっていて、データバイトは0
になっていることが決まっているので、読み込んだバイトデータが0x80
より小さければランニングステータスが適用されていると判断できます。
これを踏まえ、先のプログラムを修正してみます。
var status: bit8 :: me._reader.read(1)[0]
var data1: bit8
; ランニングステータスの適用チェック
if(status < 0x80b8)
; running status
do data1 :: status
do status :: (前回のステータスデータ)
else
do data1 :: me._reader.read(1)[0]
end if
switch(status.and(0xF0b8))
case 0x80b8, 0x90b8, 0xA0b8, 0xB0b8, 0xE0b8
; データバイト2つを解析
case 0xC0b8, 0xD0b8
; データバイト1つを解析
default
throw ERR_INVALID_STATUS
end switch
ランニングステータスが適用されている場合、読み込んだバイトはデータ1になるのでdata1
の変数に代入しなおし、status
には保存しておいた前回のステータスバイトを代入しています。
データ1を先読みすることになりますが、どの演奏命令にもデータ1は存在するため問題ありません。
SysExイベントを解析しよう
システムエクスクルーシブメッセージを表現するのに使用されます。
このイベントはステータスバイトがF0
かF7
になっており、その次にデータ長とメッセージが続きます。
データ長は可変長数値表現で格納されているため、ステータスの次のデータバイトの最上位ビットの役割が変わります。
そのため、MIDIイベントのようにランニングステータスを適用することはできません。
1. F0
+ データ長(可変長)
+ メッセージ
+ F7
2. F7
+ データ長(可変長)
+ メッセージ
どちらもステータスバイトの上位4ビットがF
になっているので、MIDIイベントのステータスバイトを確認したときに解析を分けることができます。
これを踏まえ、MIDIイベント解析プログラムの分岐部分を修正してみます。
switch(status.and(0xF0b8))
case 0x80b8, 0x90b8, 0xA0b8, 0xB0b8, 0xE0b8
; データバイト2つを解析
case 0xC0b8, 0xD0b8
; データバイト1つを解析
; SysExEvent
case 0xF0b8
do me.parseSysExEvent(status, data1)
default
throw ERR_INVALID_STATUS
end switch
続いてSysExイベントを解析するメソッドを作成します。
ランニングステータス確認のために先読みしておいたdata1
は不要となるので、可変長のデータ長を解析するために1バイト読み戻しをします。
func parseSysExEvent(status: bit8, data1: bit8)
if(status <> 0xF0b8 & status <> 0xF7b8)
throw ERR_INVALID_STATUS
end if
; data1 は不要なので 1byte 戻す
do me._reader.setPos(%cur, -1)
var length: int :: me.parseVariableInt()
var data: []bit8 :: me._reader.read(length)
end func
メタイベントを解析しよう
曲のタイトルや著作権、テンポなどの演奏データ以外の情報が格納されています。
このイベントはステータスバイトがFF
になっており、その次にメタイベントの種類、データ長とデータが続きます。
SysExイベント同様にランニングステータスを適用することはできません。
(メタイベントの種類は0x00〜0x7Fの範囲なのでやれそうな気もしますが、ダメみたいです。理由は分かりませんでした!)
ステータスバイトの上位4ビットがF
になっていますので、SysExイベントと同じに見えてしまいます。
SysExイベントを解析する前に、メタイベントかどうかを判断して分岐しましょう。
そして、次に続くメタイベントの種類でデータの意味とデータ長を判断できるようになっています。
ステータス | 種類 | データ長 |
---|---|---|
0xFF | 0x00 | 0x02 |
0xFF | 0x01 | 可変長 |
0xFF | 0x02 | 可変長 |
0xFF | 0x03 | 可変長 |
0xFF | 0x04 | 可変長 |
0xFF | 0x05 | 可変長 |
0xFF | 0x06 | 可変長 |
0xFF | 0x07 | 可変長 |
0xFF | 0x08 | 可変長 |
0xFF | 0x09 | 可変長 |
0xFF | 0x20 | 0x01 |
0xFF | 0x21 | 0x01 |
0xFF | 0x2F | 0x00 |
0xFF | 0x51 | 0x03 |
0xFF | 0x54 | 0x05 |
0xFF | 0x58 | 0x04 |
0xFF | 0x59 | 0x02 |
0xFF | 0x7F | 可変長 |
これを踏まえ、SysExイベント解析プログラムの分岐部分を修正してみます。
func parseSysExEvent(status: bit8, data1: bit8)
if(status = 0xFFb8)
do me.parseMetaEvent(status, data1)
ret
end if
; --- 省略 ----
end func
続いてメタイベントを解析するメソッドを作成します。
ランニングステータス確認のために先読みしておいたdata1
がメタイベントの種類になっています。
func parseMetaEvent(status: bit8, data1: bit8)
var length: int
switch(data1 $ int)
case 0x00 to 0x09, 0x20, 0x21, 0x2F, 0x51, 0x54, 0x58, 0x59, 0x7F
do length :: me.parseVariableInt()
default
throw ERR_INVALID_META_EVENT
end switch
var data: []bit8 :: me._reader.read(length)
end func
トラックチャンク解析のまとめ
これでトラックチャンクの解析ができるようになりました。
ここまでのプログラムをまとめます。
func main()
var parser: @SMFParser :: #@SMFParser
do parser.parse("tw008.mid")
end func
+class SMFParser()
const MTHD: int :: 0x4D546864
const MTRK: int :: 0x4D54726B
const ERR_INVALID_HEADER: int :: 0x00003100
const ERR_FORMAT_TYPE: int :: 0x00003101
const ERR_INVALID_TRACK_HEADER: int :: 0x00003102
const ERR_INVALID_STATUS: int :: 0x00003103
const ERR_INVALID_META_EVENT: int :: 0x00003104
var _reader: file@Reader
var _formatType: int
var _trackSize: int
var _timeDivision: int
var _lastStatus: bit8
+func parse(filename: []char)
do me._reader :: file@makeReader(filename)
do me.parseHeader()
for i(0, me._trackSize - 1)
do me.parseTrack(i)
end for
do me._reader.fin()
end func
func parseHeader()
; chunk
var chunk: int :: me.castInt(me._reader.read(4))
if(chunk <> MTHD)
throw ERR_INVALID_HEADER
end if
; header size
var headerSize: int :: me.castInt(me._reader.read(4))
if(headerSize <> 6)
throw ERR_INVALID_HEADER
end if
; format
do me._formatType :: me.castInt(me._reader.read(2))
if(me._formatType < 0 | me._formatType > 2)
throw ERR_FORMAT_TYPE
end if
; track size
do me._trackSize :: me.castInt(me._reader.read(2))
; time division
do me._timeDivision :: me.castInt(me._reader.read(2))
; debug print
do cui@print("format type : \{me._formatType}\n")
do cui@print("track size : \{me._trackSize}\n")
do cui@print("time division : \{me._timeDivision}\n")
end func
func parseTrack(track: int)
; chunk
var chunk: int :: me.castInt(me._reader.read(4))
if(chunk <> MTRK)
throw ERR_INVALID_TRACK_HEADER
end if
; track size
var trackSize: int :: me.castInt(me._reader.read(4))
var trackEnd: int :: me._reader.getPos() + trackSize
var lastStatus: bit8
while(me._reader.getPos() < trackEnd)
do me.parseMidiEvent(track)
end while
end func
func parseMidiEvent(track: int)
var time: int :: me.parseVariableInt()
var status: bit8 :: me._reader.read(1)[0]
var data1: bit8
if(status < 0x80b8)
; running status
do data1 :: status
do status :: me._lastStatus
else
do data1 :: me._reader.read(1)[0]
end if
switch(status.and(0xF0b8))
case 0x80b8, 0x90b8, 0xA0b8, 0xB0b8, 0xE0b8
do me.parse3ByteEvent(status, data1)
case 0xC0b8, 0xD0b8
do me.parse2ByteEvent(status, data1)
case 0xF0b8
do me.parseSysExEvent(status, data1)
default
throw ERR_INVALID_STATUS
end switch
end func
func parse3ByteEvent(status: bit8, data1: bit8)
var data2: bit8 :: me._reader.read(1)[0]
do me._lastStatus :: status
; debug print
do cui@print("status: \{status}, ")
do cui@print("data1: \{data1}, ")
do cui@print("data2: \{data2}\n")
end func
func parse2ByteEvent(status: bit8, data1: bit8)
do me._lastStatus :: status
; debug print
do cui@print("status: \{status}, ")
do cui@print("data1: \{data1}, ")
end func
func parseSysExEvent(status: bit8, data1: bit8)
if(status = 0xFFb8)
do me.parseMetaEvent(status, data1)
ret
end if
if(status <> 0xF0b8 & status <> 0xF7b8)
throw ERR_INVALID_STATUS
end if
; 1byte戻す
do me._reader.setPos(%cur, -1)
var length: int :: me.parseVariableInt()
var data: []bit8 :: me._reader.read(length)
; debug print
do cui@print("status: \{status}, data: ")
for i(0, ^data - 1)
do cui@print(" \{data[i]}")
end for
do cui@print("\n")
end func
func parseMetaEvent(status: bit8, data1: bit8)
var length: int
switch(data1 $ int)
case 0x00 to 0x09, 0x20, 0x21, 0x2F, 0x51, 0x54, 0x58, 0x59, 0x7F
do length :: me.parseVariableInt()
default
throw ERR_INVALID_META_EVENT
end switch
var data: []bit8 :: me._reader.read(length)
; debug print
do cui@print("status: \{status}, ")
do cui@print("type: \{data1}, data: ")
for i(0, ^data - 1)
do cui@print(" \{data[i]}")
end for
do cui@print("\n")
end func
func parseVariableInt(): int
var value: bit32
while loop(true)
var byte: bit8 :: me._reader.read(1)[0]
do value :: value.shl(7).or((byte $ bit32).and(0x7Fb32))
if(byte < 0x80b8)
break loop
end if
end while
ret value $ int
end func
func castInt(data: []bit8): int
var value: bit32
for i(0, ^data - 1)
do value :: value.shl(8).or(data[i] $ bit32)
end for
ret value $ int
end func
end class
「main.kn」と同じフォルダに「test.mid」というSMFを置いてCUIモードで実行すると、解析結果が出力されると思います。
私が持っているファイルでは以下のようになりました。
format type : 1
track size : 2
time division : 480
status: 0xFF, type: 0x03, data: 0x73 0x61 0x74 0x6F
status: 0xFF, type: 0x2F, data:
status: 0x90, data1: 0x32, data2: 0x64
status: 0x80, data1: 0x32, data2: 0x00
status: 0xFF, type: 0x2F, data:
まとめ
長くなってしまいましたが、なんとかSMF全体の解析ができるようになりました。
初めにSMFをバイナリエディタで開いた時は途方に暮れましたが、1バイトずつ解析していけば何とかなるものですね。
さて、このままでは解析しただけで扱いにくいので、各トラックやイベントをクラスにまとめたものをGithubにアップしました。
正直、MIDIのすべてを理解できていないため解析できないファイルもあるかもしれません。
詳しい方いましたら教えてください!
Github satonayu/SMF
参考にしたサイト
MIDIやSMFの解説
SMF(Standard MIDI File)フォーマット解説 | 技術的読み物 | FISH&BREAD
SMF (Standard MIDI Files) の構造