1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

前回の記事 → 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」となります。

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

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

3. イベントデータの解析

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

イベントデータの構成

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

デルタタイムとは

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

main.kn
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イベントを判断するプログラムを作成します。

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)でステータスバイトの上位4ビットだけを取り出し、データバイトが2つか1つかで処理を分けることにしました。

ランニングステータス

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

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

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

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イベントを解析しよう

システムエクスクルーシブメッセージを表現するのに使用されます。
このイベントはステータスバイトがF0F7になっており、その次にデータ長とメッセージが続きます。

データ長は可変長数値表現で格納されているため、ステータスの次のデータバイトの最上位ビットの役割が変わります。
そのため、MIDIイベントのようにランニングステータスを適用することはできません。

 1. F0 + データ長(可変長) + メッセージ + F7
 2. F7 + データ長(可変長) + メッセージ

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

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バイト読み戻しをします。

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の範囲なのでやれそうな気もしますが、ダメみたいです。理由は分かりませんでした!)

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

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

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

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

トラックチャンク解析のまとめ

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

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

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

参考にしたサイト

MIDIやSMFの解説

SMF(Standard MIDI File)フォーマット解説 | 技術的読み物 | FISH&BREAD
SMF (Standard MIDI Files) の構造

解析プログラム

JavaScriptでMIDIファイルを解析してみる 1

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?