LoginSignup
1
1

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