KuinでSMF(MIDIファイル)を解析しよう２

前回の記事　→　[KuinでSMF(MIDIファイル)を解析しよう１](https://qiita.com/satonayu/items/422f36df940b4bf9d13b)
前回はSMFのヘッダチャンクの解析までできました。
今回はトラックチャンクの解析を勉強していきます。

## SMFのトラックチャンクを解析する
トラックチャンクは、ヘッダチャンク(14byte)から連なって次のような構成のデータになっています。
`4D 54 72 6B 00 00 00 0C　イベントデータ　イベントデータ イベントデータ …`

トラックチャンクに記録されている情報は以下の通り。
　1.チャンクタイプ(4byte)
　2.データ長(4byte)
　3.イベントデータ(必要数繰り返し)

これでトラック１つ分のチャンクです。
複数のトラックがある場合は、前のチャンクの終端からまたトラックチャンクが並んでいます。

### 1. チャンクタイプの解析
`4D 54 72 6B`の部分です。文字にすると「MTrk」となります。

```Kuin:main.kn
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` の部分です。
これは、イベントデータがこの後何バイト続いているかを示しています。
つまり、ここで読み込んだバイト数分を解析したら１つのトラックの解析が完了しています。

```Kuin:main.kn
var trackSize: int :: me.castInt(me._reader.read(4))
```

### 3. イベントデータの解析
ここからはひたすらイベントデータが並んでいます。
しかし、イベントデータはたくさんの種類があり、データを表現するバイト数も様々です。
不要なデータだからといってスキップするにしても、とりあえずは解析をしなければ次のデータの在り処が分かりません。
若干面倒ですが、１つ１つ丁寧に解析をしていきます。

## イベントデータの構成
イベントデータは大きく分けて３つのパターンがあり、以下の組み合わせになっています。
　1.　デルタタイム　＋　MIDIイベント
　2.　デルタタイム　＋　SysExイベント
　3.　デルタタイム　＋　メタイベント

### デルタタイムとは
デルタタイムは直前のイベントからの時間をあらわします。
デルタタイムの表現にはヘッダチャンクで解析した時間単位が絡んできますが、とてもややこしいので解説サイトで確認してください。
解析に必要な情報は「デルタタイムは可変長数値表現」ということのみです。
可変長なので前回の記事で作ったメソッドでそのまま読み込むことができます。

```Kuin:main.kn
var time: int :: me.parseVariableInt()
```


### MIDIイベントを解析しよう
演奏データそのものを示します。
シーケンサーで鍵盤を押したり離したりした場合に通信されるデータがこれになります。
MIDIイベントは「ステータスバイト」と「データバイト」で構成されていて、ステータスバイトが演奏命令の種類をあらわし、以下の７種類があります。

| 演奏命令　|ステータス　|データ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 |

この表から以下の特徴が分かります。

- ステータスバイトの上位４ビットで命令を判断できる。
- ステータスバイトの最上位ビットは`1`である。
- データ1の最上位ビットは`0`である。
- データ2は不要の場合もある。

以上より、MIDIイベントを判断するプログラムを作成します。

```Kuin:main.kn
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)`でステータスバイトの上位４ビットだけを取り出し、データバイトが２つか１つかで処理を分けることにしました。

#### ランニングステータス
MIDIイベントは、より少ないバイト数で表現できるように１つ前のイベントと同じ演奏命令だった場合、ステータスバイトを省略することができます。
これをランニングステータスと呼び、デルタタイムの後はデータ１が続くようになります。

では、省略されたかどうかをどうやって判断するかというと、デルタタイムの後に読み込んだバイトデータの最上位ビットを確認することで判断ができるようになっています。
先の特徴で上げたように、ステータスバイトは最上位ビットが`1`になっていて、データバイトは`0`になっていることが決まっているので、読み込んだバイトデータが`0x80`より小さければランニングステータスが適用されていると判断できます。

これを踏まえ、先のプログラムを修正してみます。

```Kuin:main.kn
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` + `データ長（可変長)`　＋　`メッセージ`

どちらもステータスバイトの上位４ビットが`F`になっているので、MIDIイベントのステータスバイトを確認したときに解析を分けることができます。
これを踏まえ、MIDIイベント解析プログラムの分岐部分を修正してみます。

```Kuin:main.kn
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バイト読み戻しをします。

```Kuin:main.kn
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の範囲なのでやれそうな気もしますが、ダメみたいです。理由は分かりませんでした！）

ステータスバイトの上位４ビットが`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イベント解析プログラムの分岐部分を修正してみます。

```Kuin:main.kn
func parseSysExEvent(status: bit8, data1: bit8)
	if(status = 0xFFb8)
		do me.parseMetaEvent(status, data1)
		ret
	end if
	
	; --- 省略 ----
end func
```

続いてメタイベントを解析するメソッドを作成します。
ランニングステータス確認のために先読みしておいた`data1`がメタイベントの種類になっています。

```Kuin:main.kn
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
```

## トラックチャンク解析のまとめ
これでトラックチャンクの解析ができるようになりました。
ここまでのプログラムをまとめます。

```Kuin:main.kn
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をバイナリエディタで開いた時は途方に暮れましたが、１バイトずつ解析していけば何とかなるものですね。

さて、このままでは解析しただけで扱いにくいので、各トラックやイベントをクラスにまとめたものをGithubにアップしました。
正直、MIDIのすべてを理解できていないため解析できないファイルもあるかもしれません。
詳しい方いましたら教えてください！
[Github satonayu/SMF](https://github.com/satonayu/SMF)


## 参考にしたサイト

### MIDIやSMFの解説
[SMF(Standard MIDI File)フォーマット解説 | 技術的読み物 | FISH&amp;BREAD](http://maruyama.breadfish.jp/tech/smf/)
[SMF (Standard MIDI Files) の構造](https://sites.google.com/site/yyagisite/material/smfspec)

### 解析プログラム
[JavaScriptでMIDIファイルを解析してみる 1](https://qiita.com/PianoScoreJP/items/2f03ae61d91db0334d45)