Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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あたりにアップしてみたいと思います。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした