1. PianoScoreJP

    Posted

    PianoScoreJP
Changes in title
+JavaScriptでMIDIファイルを解析してみる 4
Changes in tags
Changes in body
Source | HTML | Preview

JavaScriptでMIDIファイルを解析してみる 3
では、トラックチャンクの構造を確認しました。

ここまでわかれば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));

        //結果を返す
        return {header:header,tracks:tracks}
    }else{
        //正しいSMFじゃない!! 任意のエラー処理。
    }


}

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

ヘッダは簡単ですね。次にトラックチャンク

    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 = Number(ar[0]).toString(16).substr(1,1)*1;
               //音高は2バイト目
               data.note = ar[1];
               //ヴェロシティ
               data.velocity=ar[2];
               //残りの配列
               ar = ar.subarray(2,ar.length);
          }
          //取得したイベントと配列の残りをリターン
          return {event:data,ar:ar};
    }    

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

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

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

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