25
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-06-18

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));
        
    }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イベント(ノートオン、ノートオフ)のみ取得する内容になっています。

    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メッセージテーブル一覧

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

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

25
20
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
25
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?