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

[JavaScriptでMIDIファイルを解析してみる 3](http://qiita.com/PianoScoreJP/items/f7f2aa915c4af21df856)
では、トラックチャンクの構造を確認しました。

ここまでわかればSMFをパースする方法はいろいろと思いつくかと思いますが、
一応「JavaScriptで」と銘打って進めてしまったので、JavaScriptを使って書いてみたいと思います。
ここでは、SMFを読み込み、チャンネル毎のデルタタイム、イベントを配列に時系列でならべるプログラムコードを紹介します。


#ファイルの読み込み
ファイルをサーバーサイドで読み込むなら、いろいろやり方は考えられるかと思いますが、
ここではFileReaderを使って読み込みしてみたいと思います。
バイナリファイルを読み込んで16進数並びにしたいなら、**readAsArrayBuffer**メソッドが使えます。

````loadSMF.html
・・・略
<input type=file id=loadFile>
・・・略 
````

````loadSMF.js
    //jQuery
    $("#loadFile").change(function(e){
        var file = e.target.files[0];  
        var reader = new FileReader();

        reader.onload = function() {   
                //読み込んだ結果を型付配列に
                var ar = new Uint8Array(reader.result);
                //データをparserに渡す
                var result = parseHeader(ar);
        }
        //ファイルを読み込み
        reader.readAsArrayBuffer(file);
    });
````
このようにファイルを読み込んで1バイトづつ読み込めるような形にしておき、
parseするメソッドに渡します。型付配列が使いにくければ、普通の配列に変換する等すればよいでしょう。

#ヘッダを取得

````parseSMF.js
//ヘッダを保持しておくクラス変数
var header = {};
function parseHeader(ar){
　　 
    //最初の4バイトがチャンクタイプ(4D 54 68 64)になっているかどうか
    if(ar[0]!==0x4D || ar[1] !== 0x54 || ar[2] !== 0x68 || ar[3] !== 0x64 ){
        //5～8バイト目(ヘッダのバイト数を表す)を取得
        header.size = getInt(ar.subarray(4,8));
        //SMFフォーマット
        header.format=ar[9];

        //トラック数取得
        header.trackcount = getInt(ar.subarray(10,12));

        //時間管理
        header.timemanage = ar[12];

        //分解能
        header.resolution = getInt(ar.subarray(12,14));

        //ヘッダ以降のデータをパース
        var tracks = parseTracks(ar.subarray(8+header.size,ar.length));
        
    }else{
        //正しいSMFじゃない!!　任意のエラー処理。
    }
    
    
}

//任意の型付配列から数値を求めるメソッド
function getInt(ar){
    var value = 0;
    for (var  i=0;i<ar.length;i++){
        value = (value << 8) + ar[i];
    }
    return value;
}

````
#トラックを取得
次にトラックチャンク
ここではメタイベント（ステータスバイトFF)と、MIIDイベント（ノートオン、ノートオフ)のみ取得する内容になっています。

````js
    var tracks=[]//トラックデータをトラック毎に保持しておくクラス変数
    function parseTracks(ar){
        //最初の4バイトがチャンクタイプ(4D 54 72 6B)になっているかどうか
        if(ar[0]===0x4D || ar[1] === 0x54 || ar[2] === 0x72 || ar[3] === 0x6B ){
            //サイズの取得
            var size = util.getInt(ar.subarray(4,8));
            //トラックのデータを取得
            var track = ar.subarray(8,8+size);
            //トラックデータを保持するための配列を作成
            tracks.push([]);
            //トラックのデータをパース
            parseTrackData(track);
            //次のトラックを解析
            if(ar.length > 8+size){
                   parseTracks(ar.subarray(8+size,ar.length))
            }        
        }else{
           //エラー処理
        }  
    }

    function parseData(ar){
          //デルタタイムを取得
          var result1 = getDeltaTime(ar);
          //イベントを取得
          var result2= getEvent(result1.ar);
          //トラックデータにデルタタイムとイベントを追加
          tracks[tracks.length-1].push({deltatime:result1.deltatime,event:result2.event});
          //残りのデータがあれば再帰的にパースする
          if(result2.ar.length>0){
             parseData(result2.ar);
          }

    }
    
    function getDeltaTime(ar){
        var value=0;
        //最上位ビットが1ならループ
        var i=0;
        while(ar[i]>=0x80){
             //1.最上位ビットのみ反転(例：1000 0001 => 0000 0001にする)
             var a = ar[i] ^ (1<<7);
             //2.valueに反転した値を保持しておく
             value = value<<7 | a;
             i++;
        }
        //最後の値を連結
        value = value | ar[i];
        //計算したデルタタイムと配列の残りをリターン
        return {ar:ar.subarray(i+1,ar.length),deltatime:value}
    }
    
    function getEvent(ar){
          var data={};
          //ステータスバイトを取得
          data.status = ar[0];
          //メタイベントの場合
          if(data.status === 0xFF){
                //イベントタイプ
               data.type = ar[1];
                //メタイベントのデータ量は3バイト目に保持されている
               data.size = util.getInt(ar.subarray(2,3));
                //データ
               data.data = ar.subarray(3,3+data.size);
                //残りの配列
               ar = ar.subarray(3+data.size,ar.length);
          } else if(data.status >=0x80 && data.status <=0x9F){
               //チャンネル 
               data.channel = data.status & 0xf
               //音高は2バイト目
               data.note = ar[1];
               //ヴェロシティ
               data.velocity=ar[2];
               //残りの配列
               ar = ar.subarray(3,ar.length);
          }
          //取得したイベントと配列の残りをリターン
          return {event:data,ar:ar};
    }    
````

かなり長くなりましたが、大まかにいうとトラック解析はこんな感じ
1.ヘッダチャンク判定
2.トラックサイズ取得
　2-1.デルタタイム取得
　2-2.イベント取得(以降2-1,2-2のループ)
3.トラックサイズ分取得、解析したら1に戻る
4.データがなくなったら終了


万が一変なデータに出会った際には、エラーで終了するようにしなければならないのでしょうが、
そういったことがなければ、問題なくデータを取得できるかと思います。
上記では基本的なイベントのみ拾うだけですが、他のイベントにもすべて対応するなら、
下記でどんなイベントがあって、どのようなデータ構造なのか確認することができます。
[MIDIメッセージテーブル一覧](http://www.midi.org/techspecs/midimessages.php)

#おわり
以上で、**JavaScriptでMIDIファイルを解析してみる**の連載!?を終わりにします。
ある程度汎用性のあるコードが完成しましたら、GitHubあたりにアップしてみたいと思います。

不備、間違い等ございましたら、コメントにてご連絡いただけると幸いです。