JavaScriptでMIDIファイルを解析してみる 3
では、トラックチャンクの構造を確認しました。
ここまでわかればSMFをパースする方法はいろいろと思いつくかと思いますが、
一応「JavaScriptで」と銘打って進めてしまったので、JavaScriptを使って書いてみたいと思います。
ここでは、SMFを読み込み、チャンネル毎のデルタタイム、イベントを配列に時系列でならべるプログラムコードを紹介します。
#ファイルの読み込み
ファイルをサーバーサイドで読み込むなら、いろいろやり方は考えられるかと思いますが、
ここではFileReaderを使って読み込みしてみたいと思います。
バイナリファイルを読み込んで16進数並びにしたいなら、readAsArrayBufferメソッドが使えます。
・・・略
<input type=file id=loadFile>
・・・略
//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するメソッドに渡します。型付配列が使いにくければ、普通の配列に変換する等すればよいでしょう。
#ヘッダを取得
//ヘッダを保持しておくクラス変数
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あたりにアップしてみたいと思います。
不備、間違い等ございましたら、コメントにてご連絡いただけると幸いです。