KuinでSMF(MIDIファイル)を解析しよう
MIDIを使った演奏のおもちゃを作ろうと思ったのですが、使いたいマイコンでSMFを扱うライブラリが無かったのでゼロからコーディングする事となりました。
いきなりマイコンでは敷居が高いので、Kuinという言語を使って解析方法を学習します。
SMFについて
Standard MIDI Fileの略で、電子楽器等を演奏するためのデータであるMIDI通信をファイルとして保存したものです。
普段は単純にMIDI、あるいはMIDIファイルと呼ばれていることが多い不憫なファイル形式です。
拡張子も「.smf」がありますが「.mid」で使われるほうが圧倒的に多いのでますます目にする機会がないですね…
今回はSMFやMIDIの構造については詳しく解説しませんので、私が参考にしたサイト等で確認してください。
使用する環境
Kuin Programming Language (Version 2018.9.17)
zipを解凍しただけで使えるという、環境構築の手間がないステキな言語ですよ。
プログラミング言語「Kuin」
解析の前準備
Kuinのfileライブラリはバイナリデータをbit8型の配列として取得します。
このままでは扱いにくいので、数値に変換するメソッドを先に作っておきます。
1. 固定長数値表現
難しそうな名前を使いましたが、2byteか4byteで表される単純な数値です。
file@Reader.read
で取得したデータをbit32型の変数にビットシフトしながら論理和します。
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
2. 可変長数値表現
これは8bitのうち、7bitを数値データとして扱い、最上位ビットを次のバイトデータも数値表現の続きかどうかを判断するフラグにしています。
例えば10進数の127
を2進数で表すと0111 1111
となりますね。
この場合、最上位ビットは 0
となっているため、次に続くバイトデータは別の物として判断できます。
では、読み込んだデータが1000 0001
となっていた場合はどうでしょう? 10進数の129
でしょうか?
この場合、最上位ビットが1
となっているため、次に続くバイトデータを読み込んで見る必要があります。
さらに1byte読み込むと1000 0001, 0000 1010
となりました。
ここで、それぞれの7bitの部分を抜き出して結合すると1000 1010
となり、10進数の138
になります。
このように、最上位ビットを確認しながらデータを結合するメソッドを作成します。
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
(byte $ bit32).and(0x7Fb32)
の部分で読み込んだデータの最上位ビットをマスクし、
さらにvalue.shl(7).or()
の部分で7bitずつ左にシフトしながら論理和して数値を再現しています。
SMFのヘッダチャンクを解析する
SMFはバイナリデータの先頭に次のような14byteのヘッダチャンクを持っています。
4D 54 68 64 00 00 00 06 00 01 00 02 01 E0
ヘッダーに記録されている情報は以下の通り。
1. チャンクタイプ (4byte)
2. データ長 (4byte)
3. フォーマット形式 (2byte)
4. トラック数 (2byte)
5. 時間単位 (2byte)
1. チャンクタイプの解析
4D 54 68 64
の部分です。文字にすると「MThd」となります。
これがなければ何も始まりません。きっとSMFの拡張子の皮をかぶった別物です。
const MTHD: int :: 0x4D546864
var chunk: int :: me.castInt(me._reader.read(4))
if(chunk <> MTHD)
throw ERR_INVALID_HEADER
end
先程作った、bit8の配列をintに変換するメソッドを利用し、比較しています。
2. データ長の解析
00 00 00 06
の部分です。
これは、ヘッダチャンクがこの後何バイト続いているかを示しているのですが、現状では固定値で6
となっています。
フォーマット形式、トラック数、時間単位がそれぞれ2byteで、これ以外にデータはありません。
4byteも準備してあるのは将来の拡張性でしょうか?
var headerSize: int :: me.castInt(me._reader.read(4))
if(headerSize <> 6)
throw ERR_INVALID_HEADER
end if
3. フォーマット形式の解析
00 01
の部分です。
読み込んでいるSMFがどのフォーマットで作成されているかを示しています。
SMFのフォーマットは現在「0,1,2」の3タイプが存在しています。
各フォーマットの詳細は解説サイトで確認してください。
var formatType: int :: me.castInt(me._reader.read(2))
if(formatType < 0 | formatType > 2)
throw ERR_FORMAT_TYPE
end if
4. トラック数の解析
00 02
の部分です。
ヘッダチャンクの後に続くトラックチャンクが何個あるかを示しています。
上の例では、ヘッダチャンクの後ろにトラックチャンクが2個並んでいることが分かります。
var trackSize: int :: me.castInt(me._reader.read(2))
5. 時間単位の解析
01 E0
の部分です。
4分音符あたりの分解能、または秒あたりのフレーム数を示しています。
若干ややこしいので、詳しくは解説サイトで確認してください。
var timeDivision: int :: me.castInt(me._reader.read(2))
ヘッダチャンク解析のまとめ
これでヘッダチャンクの解析ができるようになりました。
ここまでのプログラムをまとめます。
func main()
var parser: @SMFParser :: #@SMFParser
do parser.parse("test.mid")
end func
+class SMFParser()
const MTHD: int :: 0x4D546864
const ERR_INVALID_HEADER: int :: 0x00003100
const ERR_FORMAT_TYPE: int :: 0x00003101
const ERR_INVALID_TRACK_HEADER: int :: 0x00003102
var _reader: file@Reader
+func parse(filename: []char)
do me._reader :: file@makeReader(filename)
do me.parseHeader()
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
var formatType: int :: me.castInt(me._reader.read(2))
if(formatType < 0 | formatType > 2)
throw ERR_FORMAT_TYPE
end if
; track size
var trackSize: int :: me.castInt(me._reader.read(2))
; time division
var timeDivision: int :: me.castInt(me._reader.read(2))
; debug print
do cui@print("format type : \{formatType}\n")
do cui@print("track size : \{trackSize}\n")
do cui@print("time division : \{timeDivision}\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
ここまで解析できたら、次はトラックチャンクを解析していく訳ですが、長くなりそうなので記事を分割します。
KuinでSMF(MIDIファイル)を解析しよう2