fmgenの応用例としてS98プレイヤを作ってみました。 S98とは、OPN(A)レジスタへの出力をダンプしたデータです。 つまり、ダンプデータに従ってレジスタをいじるだけでプレイヤの完成、 というわけで、fmgenいじりの初歩には最適でしょう。
S98の形式については こちらの s98ampのソース内のドキュメントを参考にさせていただきました。 SMFっぽい作りになってるようです。
s98のデータの中身をおおざっぱに分けると
s98クラスから呼ばれるクラスです。s98data.h、 s98data.cpp
OPNAWRAPクラスは、FM::OPNAといっしょにサンプルレートを渡したかっただけで でっちあげたクラスです。
OpnaRegが来たときに、s98.NextDataから呼ばれるDoProcメソッドを実装します。 具体的にはOPNA::SetRegに中継するだけです。(code)
WaitSyncが来たときに、s98.NextDataから呼ばれるDoProcメソッドを実装します。 OPNA::MixでPCMデータを受けとって、しかるべき場所に流すわけですが、 ここでは出力先に依存しないようにmymixer クラスで仮想化します。(code)
OPNA::Mixから渡されてくるデータは、FM_SAMPLETYPE型で、 デフォルトではint32です。mymixer側では16ビットにしたいため、 shortに丸める処理が入っています。
mymixerは純粋仮想関数を含むクラスです。実際に 出力したい環境(OSSとかaRtsとかesdとかSDLとか)にあわせて 実装します。
mymixerは以下の4つのmethodの実装を要求します。
SDL向けの実装はちょっと複雑なので、先にaRtsの実装例を示します (code)。 OpenでaRtsを初期化し、MixでaRtsに書き込み、Closeで閉じるという 実に素直な実装になっております。
さてSDLへの実装例です(code)。 SDLのオーディオの仕組を箇条書にすると以下のような感じになります。
インスタンスmethodのポインタをcallbackに渡してもいいような 気がしますが、渡しかたが分からず断念。 コンパイルエラーが出てしまうのでした。調べりゃいいような気もしますが、 friendでいいかなーとか思って妥協した記憶があります。 あとfriendの方が、インスタンスがなくなった後にまちがって callbackが呼ばれたても安全かなと一瞬思いましたが別に callback内でポインタの正当性のチェックしてるわけでもないし意味ないじゃん。 (独り言ループ)
さて、aRtsで言うところのarts_write_streamに相当するのは、SDL_MixAudioと 言えるのですが、SDL側が準備できたということをcallbackで知ってから 書き込む必要がありますので、aRtsのときみたいにいつでも実行して よいというものでもないようです。
DoProcでs98クラスから送られてきたデータが callbackに渡されるまでDoProc内で待つようなことをしたくなりますが、 そうするとs98クラス側の処理がお留守になってしまって 大変困ります。 そのため、DoProcでs98クラスから渡されたデータを一時保管する 仕組が必要となります。
++rptr; rptr &= bufsize - 1;なんてことをして3の処理を高速化させたりしたもんですが、まあそれは 置いといて、このルールを 素直にコーディングしたものがこれです。
このコードはPC-98用RS-MIDIドライバ書いたときのリングバッファ管理ルーチン (実際はマクロでした)をC++で書き直したものです。が、 コードと上の説明を見て分かるように、1バイト(というか配列の中身1つ)単位での 操作しかできないのが難点です。 RS-MIDIドライバのときはこれで充分だったのですが(どうせ送受信は 1バイト単位でしかできなかったし)、音データを保存、取り出しするときに 1バイトずつやりとりしていたのではえらい遅くなってしまいます。
なんとかズルしようと考えた戦略は以下の通りでした。
やはりズルしないでRingBufferで複数バイトを一気に入れたり出したり できるようにしないとだめかなあということで 真面目に実装しなおしたのがこれです。 Getで中身を取り出し、Putで格納です。
DOS時代なんかだと、この程度のルーチンを面倒がっていてはまともにプログラム 書けなかったんですが...
inBufSize()でinlenを、maxBufSize()でbufsizeを得ることができます。 また、Capacity()でinlenとbufsizeの比率を知ることができます。 余計な関数ですがまああればあったで便利かなと思うわけですが...
さて、RingBufferができたところでsdlmixerを仕上げます (ヘッダとソース)。 注意点としてはSDL_MixAudioではUint8*で渡す必要があるので、 RingBufferをUint8で管理します。ので、sdlmixer::Mixでは shortのデータをUint8に分解して格納しております(endianどうすべ)
sdlmixer::Mix側では
1番目は本当はcapacityじゃなくて時間で決めるべきだと思うんですが... まあ手抜きです。
何もこんなものを用意しなくても、 s98クラスの中にFM::OPNAとmymixerを持たせたほうが処理が楽になるのに... と思われるかもしれませんが、このクラスたちはS98データと それを鳴らすFM音源モジュール、それと実際に音を出すミキサ、の 三者の独立性を高めるためにやっぱり必要かなと 考えています。
つまり今回はfmgenを使いましたが、別のインターフェースを持つ FM音源エミュレータが出たときに、そちらへのサポートを容易にするには このようにデータ解釈部と処理部を分けたほうが都合がよいわけです。
また、s98クラスにOPNAやmymixerを入れてしまうというのは s98クラスに「ミキサを使って音を出すためのもの」という性質を持たせてしまう ことになるわけで、s98クラスの汎用性が損われているように思います。
ad-hocで作ったのでかなりいい加減ですが、methodを足して、 周辺部まで真面目に作れば それなりに汎用性のあるものが出来るのではないかと思っています。
利用者からsdlmixerを見てみると、面倒なcallbackの実装などは 内部で隠蔽されていますので、 SDL_mixerを使うとちょっと大袈裟な場面では有用かと思います。 また、今回の音源いじりみたいに、流れ作業で音を出すような場合は むしろSDL_mixerよりも使いやすいとも言えるでしょう。
出力先として今回はSDLとaRtsを用意しました。 mymixerという仮想基底クラスを用意したおかげで、
mymixer* p; p = new sdlmixer(...) // SDLのとき p = new artsmixer(...) // aRtsのとき S98_waitsync waitsync(opna, p);といった感じで対応できるのですが... aRtsがない環境でコンパイルすら通らないのではちと困ります。 SDLがやっているようにconfigureで選別して...という余裕が なかったので...
どう解決したのかはコード見てください(^^; 我ながらかなり 姑息です。
今回のコードは
この先、UIを増やすとか、なんだとか、相互作用をするクラスが 増えてゆけば破綻する可能性もあるわけですがそこはそれ。 個人の趣味のプログラムでは再設計を恐れずいじり続けたって いいじゃんと思っております。
横浜中華街offらしく横浜弁でしめてみました。え。駄目ですか。 ...しょんぼり。(死語か)
とはいうもののs98データをどうやって作るかという話が出てないので 試せないっすね。サンプルデータを用意するのでちとお待ちを。
fmpとかpmdからコンバートというのも考えないことはないですが、 今後の展開を待つべし...