JSSの内部(2/2)
意味解析
JSSは意味解析を変数SakuraMidiContext
の中で実装しています。
意味解析では、構文解析された情報を元にMIDIデータ化を行います。MIDIデータは、メッセージの種類ごとに書式が決められており、1メッセージあたり2バイト以上のバイト列になっています。
バイト列 | 意味 |
8n kk vv | ノートオフ。鍵盤から指を離すことに相当します。kk=ノート番号。vv=打鍵の強さ。JSSではノートオフ打鍵の強さは0にしています |
9n kk vv | ノートオン。鍵盤を指で押すことに相当します。kk=ノート番号。vv=打鍵の強さ |
An kk vv | ポリフォニックキープレッシャー。kk=ノート番号 |
Bn kk mm | コントロールチェンジ。kk=コントローラー番号。mm=パラメータ値 |
Cn pp | プログラムチェンジ。pp=プログラム番号(音色番号) |
Dn vv | チャンネルプレッシャー。vv=チャンネルプレッシャー値 |
En ll mm | ピッチベンドチェンジ。ll=下位ピッチベンド値、mm=上位ピッチベンド値 |
Fx xx .. | エクスクルーシブメッセージ/メタイベント他 |
nはチャンネル番号[0~F]です。MIDIでは最大16種類の楽器を取り扱うことができ、その区別にチャンネル番号を使用します。
MMLでは音符を一文字で表現していましたが、MIDIではノートオンとノートオフの合計2メッセージになることが分かると思います。MIDIメッセージ自体に時間情報は含まれていませんので、MIDIファイル化を前提にした場合、時間管理が必要となります。すなわち、意味解析では、「時間とMIDIメッセージバイト列」の配列を結果として生成します。
なお、JSSでは4分音符の長さを120としてMML開始からの時間を管理しています。この時間数値のことをMIDIではステップと言います。
また、JavaScriptではバイト列を取り扱えないので、整数(0~255)の配列としています。ここではいくつかの例を元に、意味解析がどんな処理になっているのかを解説します。
ケース1:ノート
該当MIDIメッセージは、8n(ノートオフ)および、9n(ノートオン)になります。これらメッセージには「ノート番号」と「ベロシティ」が必要です。最初にノートオンメッセージを作り、その後適切な時間後にノートオフメッセージを作ることになります。
ノート番号とは音符に割り当てられた番号であり、ノート番号60がピアノで言うところの中央の「ド」になります。そこから半音移動するたびに1が増減されます。例えば、ド#は61、レは62となります。JSSでは、中央のドをオクターブ5と定義しているのでオクターブ4(1オクターブ下)のドだとノート番号は48になります。
ベロシティは鍵盤をたたく強さです。0~127で表します。0のときはノートオフです。
例えば次のようなノートだと、
c+4,75,90,5
意味解析の結果は、次のようになります。
パラメータ | 意味解析結果 |
ノート | c |
臨時記号 | + |
長さ | 4 |
ゲート | 75 |
ベロシティ | 90 |
タイミング | 5 |
この場合、MIDIメッセージに必要なパラメータは次のようになります。
パラメータ名 | 実際の例 |
ノート番号 | オクターブを5としてcはノート番号60なので、これに臨時記号のシャープ分を追加して、合計61(16進数だと3D)となります |
ノートオンまでの時間 | 現在時刻からタイミング分進めた時間になります。現在をTcとするとTc+5となります |
ノートオフまでの時間 | 4分音符のステップ数は120です。ゲートが75なので、実際に鳴っている時間は120ステップの75%である90ステップになります。タイミングを考慮して、ノートオフ時刻は、現在時刻をTcとしてTc+95になります |
ベロシティ | 90(16進数だと5A)です |
ここから、出来上がるMIDIメッセージは、現在時刻をTcとすると、次のようになります。
時刻 | MIDIメッセージ |
Tc+5 | 90 3D 5A |
Tc+95 | 80 3D 00 |
この音符は4分音符ですので、現在時刻は120ステップ進みます。この処理は、JSSではSakuraMidiContext.event_note
で実装しています。
ケース2:コントロールチェンジの場合
MIDIにはコントローラーと呼ばれる一連の音源操作機能があります。コントロールチェンジメッセージを使うことで、それぞれの機能を操作できます。例えば、次のようなコントローラーをMMLから指定できるようになっています(一例)。
バイト列 | 意味 | MMLでの表記 |
Bn 01 vv | モジュレーション。音の揺れ具合を指定 | M |
Bn 07 vv | ボリューム。音の大きさを指定 | V |
Bn 5B vv | エフェクト1(リバーブ)。残響効果の強さを指定 | REV |
Bn 5D vv | エフェクト3(コーラス)。コーラスの強さを指定 | CHO |
それぞれの値は、すべて0~127の範囲で指定します。
例えば次のようなパラメータだと、
CHO(100)
構文解析の結果は、次のようになります。
パラメータ | 意味解析結果 |
パラメータ | CHO |
引数 | 100(16進数だと64) |
ここから、現在時刻をTcとすると、出来上がるMIDIメッセージは、次のとおりです。
時刻 | MIDIメッセージ |
Tc | B0 5D 64 |
この処理は、JSSではSakuraMidiContext.event_control_change
で実装しています。
ケース3:コンテキストの場合
MIDIメッセージには変換されませんが、コンテキストとして保持しなければならないパラメータがあります。一例を挙げると、オクターブ番号はoで指定しますが、この値は保持しなければなりません。例えば、
o5 c o4 c
というMMLの場合、最初のノートcはオクターブ5なのでノート番号60ですが、あとのほうのノートcはオクターブ4なのでノート番号は48になります。また、
o5 c < c
というMMLの場合も「<」はオクターブを一つ下げるという意味なので先ほどのMMLとまったく同じ処理になります。
このように、ノートにおいて省略できるパラメータである長さ、ゲート、ベロシティ、タイミングすべては、コンテキストとして保持する必要があります。JSSでは、コンテキストをSakuraMidiContext.tracks
に保持しています。
SMF変換
SMF変換では、意味解析によりMIDIメッセージ化されたデータをSMFに変換します。JSSはSakuraSMFFormatter
の中で実装しています。
SMFはヘッダチャンクおよび複数のトラックチャンクから成っています。トラックチャンクは複数のMIDIメッセージを時間順に含みます。
SMFはフォーマット0、1、2の3種類が規定されていますが、ここではFormat1専用としています。
ヘッダチャンクは14バイトで固定です。4バイトの固定文字列MThd
、32bitビッグエンディアンのデータ長、6バイトのデータにより構成されます。最初の10バイトは次のようになっています。
header_chunk = [0x4d, 0x54, 0x68, 0x64, // Magic, MThd 0x00, 0x00, 0x00, 0x06, // length 0x00, 0x01 // format 1 ];
この後にトラック数とステップ数が、16bitビッグエンディアンで格納されます。
トラックチャンクのヘッダは8バイトです。4バイトの固定文字列MTrk
、32bitビッグエンディアンのデータ長により構成されます。その後、MIDIメッセージの数だけチャンクが伸びていきます。
var chunk = [0x4d, 0x54, 0x72, 0x6b, // Magic, MTrk 0x00, 0x00, 0x00, 0x00 // length ];
意味解析の段階で既に時刻つきのMIDIメッセージ配列を作っています。SMF変換では、SakuraSMFFormatter.midis.tracks[i].Events
(iはトラック番号)にトラック単位で格納されています。本稿では示していませんが、JSSのMMLでは休符にマイナス長を指定して時刻を逆戻りできる仕様になっているので、MIDIメッセージの時刻並びは前後することがあります。
例えば、次のようなメッセージの並びもあり得ます。
時間 | バイト列 |
5 | 90 3D 5A |
120 | B0 5D 64 |
95 | 80 3D 00 |
そこで、最初に時刻をキーにソートします。JavaScriptでは配列にソート用関数があるので、比較関数を与えてやればどんなデータ型でもソートが可能です。該当のトラック情報をtr
として、次のようなコードでソートできます。
tr.Events = tr.Events.sort(function(a, b){ return (a.time > b.time ? 1 : -1); });
結果として次のようなMIDIメッセージ順になります。
時間 | バイト列 |
5 | 90 3D 5A |
95 | 80 3D 00 |
120 | B0 5D 64 |
SMFでは、時刻を相対時間で表すことになっているので、前後のMIDIメッセージ間で時刻を減算し、メッセージ間の相対時間を求めます。結果として、次のようになります。
時間 | バイト列 |
5 | 90 3D 5A |
90 | 80 3D 00 |
25 | B0 5D 64 |
これをトラックチャンクに並べてデータ長を調整すればSMFの出来上がりです。
CGIへの渡し方
JSSでできることは、MMLをSMFのバイト列へ変換することです。実際にSMFのバイナリファイルを生成することはできないので、CGIへ渡すようにしています。バイト列は、そのままでは整数の配列にすぎないので、Hexbinary化したあとxmlhttprequest
を使ってCGIへ渡しています。
Hexbinary化は変数Hexbinary
に、xmlhttprequest
によるHTTP通信については変数Ajax
に実装しています。実際のコード例は次のようなものです。
var result = "type=hex&midi=" + Hexbinary.encode(SakuraSMFFormatter.results); // SMFバイト列をHexbinary化 var ajax = new Ajax.Updater(this.target_div, this.url,{ // xmlhttprequestを使ってPOST method: 'POST', parameters: result });
CGI側では渡されたHexbinary
をバイナリファイルとして保存し、そのURLを返せばよいことになります。