これは過去に書いたコードの使い回しになるが、MIDIプレイヤーを作るためには、MIDIまわりを操作するライブラリが必要になる(なまのバイトデータを直接送受信するのでもない限り)。MIDIプレイヤーというのは具体的には(少なくとも現状では)標準MIDIファイル (SMF; Standard MIDI File) のプレイヤーだから、SMFを解析してMIDIメッセージにしてデバイスに送信する仕組みが必要になる。
CライブラリとしてSMFを解析できるライブラリは既にいくつもあるので、P/Invokeしてしまえば必ずしも自前で作る必要はないが、明らかに無駄なパフォーマンスコストが発生するし、SMFを読むのは難しくないので、C#ネイティブで実装した。
SMF解析について難しいことはあまりないが、注意点はいくつかある。まず、SMFに含まれるMIDIメッセージには、(1)デルタタイムと(2)MIDIイベントが含まれている。(1)デルタタイムは、「可変長数値表現」を用いた、「8ビット目を用いない数値」の連続で表現する。7ビットの表現範囲を超える値については、デルタタイムは2バイト以上になる。14ビットの表現範囲を超えると(全休符が何小節も続くとこれが発生しうる)、3バイト目が必要になる。
また、通常の(2)MIDIイベントには、(2a)ステータスコードと呼ばれる「命令」と、(2b)MSB、(2c)LSBという各1バイト(ただしデータとしては各7ビット)の「データ」が含まれるが、通常の(2a)ステータスコードは必ず7ビット目が1になっている(16進数であれば0x80〜0xFFとなる)ところ、SMFでは、直前のMIDIイベントと同一のイベントが送信される場合、この部分は完全に省略することができる。これは特にエクスプレッションやモジュレーションなどを連続的に増大させる場合などに圧縮効果が大きい(とは言ってもほんの数バイトなので現代的な問題ではないが)。MIDIイベントの省略を検出するには、データの先頭ビットを見る。1(16進数なら0x80〜0xFF)であれば、それは省略されていないMIDIイベントだ。もしデータの先頭ビットが0なら、それはランニングステータスの省略を意味している。
解析されたSMFは、ヘッダ(デルタタイムの扱いを除いては意味のある情報はほとんど無い)とトラック群から成り、トラックにはMIDIメッセージのみが含まれる、単純な構成だ。
SMFの解析が終わったら、次はこれを時系列に沿って演奏することになるが、その前にひとつ処理しておくべきことがある。SMFフォーマット1のSMFは、複数のトラックで構成されているが、実際に演奏する前に、それらを1つのイベントの列にしておくことが望ましい。というのは、演奏命令はタイマーを使用して逐次送信するため、単一のタイマーを使用してイベントを送信するほうが効率的だし、何より同期のミスマッチにかかる不安がない(小さい)。
そのため、MIDIメッセージを時系列に沿ってマージすることが必要になるが、これは単純に「デルタタイムを変換して得られた絶対時間」をキーにして全トラックのメッセージを合わせてソートすれば良いというものではない。ひとつのトラックにおいて「前のノートをオフ、同じタイミングで同じノートをオン」といったイベントの流れがある場合、この順序は保持されなければならない(時間をおいて2つのノートオンが続いた後、1つのノートオフが送信された場合、2番目のノートは即座に消えてしまい、何も発音されないことになる)。単純なソートではこの「意味のある順序」が保持されない。というわけで、トラックのマージは「同じタイミングで送信されるイベント群」を並べ替える必要がある。
本当はこの後プレイヤーのAPIについても書くつもりだったのだけど、どうも長くなってきたみたいなので、今日はこの辺にして明日続きを書くことにする。