LoginSignup
2
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-19

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

MIDIを使った演奏のおもちゃを作ろうと思ったのですが、使いたいマイコンでSMFを扱うライブラリが無かったのでゼロからコーディングする事となりました。
いきなりマイコンでは敷居が高いので、Kuinという言語を使って解析方法を学習します。

SMFについて

Standard MIDI Fileの略で、電子楽器等を演奏するためのデータであるMIDI通信をファイルとして保存したものです。
普段は単純にMIDI、あるいはMIDIファイルと呼ばれていることが多い不憫なファイル形式です。
拡張子も「.smf」がありますが「.mid」で使われるほうが圧倒的に多いのでますます目にする機会がないですね…

今回はSMFやMIDIの構造については詳しく解説しませんので、私が参考にしたサイト等で確認してください。
- SMF(Standard MIDI File)フォーマット解説 | 技術的読み物 | FISH&BREAD
- SMF (Standard MIDI Files) の構造

使用する環境

Kuin Programming Language (Version 2018.9.17)
zipを解凍しただけで使えるという、環境構築の手間がないステキな言語ですよ。
プログラミング言語「Kuin」

解析の前準備

Kuinのfileライブラリはバイナリデータをbit8型の配列として取得します。
このままでは扱いにくいので、数値に変換するメソッドを先に作っておきます。

1. 固定長数値表現

難しそうな名前を使いましたが、2byteか4byteで表される単純な数値です。
file@Reader.readで取得したデータをbit32型の変数にビットシフトしながら論理和します。

main.kn
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になります。

このように、最上位ビットを確認しながらデータを結合するメソッドを作成します。

main.kn
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の拡張子の皮をかぶった別物です。

main.kn
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も準備してあるのは将来の拡張性でしょうか?

main.kn
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タイプが存在しています。
各フォーマットの詳細は解説サイトで確認してください。

main.kn
var formatType: int :: me.castInt(me._reader.read(2))
if(formatType < 0 | formatType > 2)
    throw ERR_FORMAT_TYPE
end if

4. トラック数の解析

00 02 の部分です。
ヘッダチャンクの後に続くトラックチャンクが何個あるかを示しています。
上の例では、ヘッダチャンクの後ろにトラックチャンクが2個並んでいることが分かります。

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

5. 時間単位の解析

01 E0 の部分です。
4分音符あたりの分解能、または秒あたりのフレーム数を示しています。
若干ややこしいので、詳しくは解説サイトで確認してください。

main.kn
var timeDivision: int :: me.castInt(me._reader.read(2))

ヘッダチャンク解析のまとめ

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

main.kn
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

2
2
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
2
2