S98プレイヤを作ってみる

s98の構造

fmgenの応用例としてS98プレイヤを作ってみました。 S98とは、OPN(A)レジスタへの出力をダンプしたデータです。 つまり、ダンプデータに従ってレジスタをいじるだけでプレイヤの完成、 というわけで、fmgenいじりの初歩には最適でしょう。

S98の形式については こちらの s98ampのソース内のドキュメントを参考にさせていただきました。 SMFっぽい作りになってるようです。

s98のデータの中身をおおざっぱに分けると

になります。fmgenで再生する場合は、レジスタ書き込みのときは SetRegで書き込み、待ちのときはその時間だけMixでデータを 受けとる、非常に簡単な流れでOkです。

実装例

今回はソースの解説が目的だったりするので... ダウンロード / GLOBALで見る(frameなし)

S98のファイル取り扱い

s98.hs98.cppで やっています。 S98Headerが、S98ファイルのヘッダ構造そのものです。 s98クラスは以下のような構造になっております。
S98(S98_opnareg& mopna, S98_waitsync& mwsync)
constructor。S98データ内で、OpnaReg、WaitSyncが来たときに するべき操作を実装したクラス、S98_opnareg、S98_waitsyncを渡します。
Open(string filename)
指定されたs98ファイルを開いて読み込みます。
NextData()
データを1つずつ読んでゆきます。 OpnaRegが来たときは、opnareg.DoProc、WaitSyncが来たときは、 waitsync.DoProcを呼び出します。OpnaReg/WaitSyncでは0、 loop時は1を返し、それ以外の困った状態のときは-1を返します。
Loop()
LoopPtrが0以外なら、そこにジャンプします。
Dump()
ヘッダの中身を表示します
今の実装では、Openで全部メモリに読み込んで、NextDataでポインタを 辿っていますが、NextDataで必要分だけ読むようにする実装も可能でしょう。

S98の処理取り扱い

s98クラスから呼ばれるクラスです。s98data.hs98data.cpp

OPNAWRAPクラスは、FM::OPNAといっしょにサンプルレートを渡したかっただけで でっちあげたクラスです。

S98_opnareg

OpnaRegが来たときに、s98.NextDataから呼ばれるDoProcメソッドを実装します。 具体的にはOPNA::SetRegに中継するだけです。(code)

S98_waitsync

WaitSyncが来たときに、s98.NextDataから呼ばれるDoProcメソッドを実装します。 OPNA::MixでPCMデータを受けとって、しかるべき場所に流すわけですが、 ここでは出力先に依存しないようにmymixer クラスで仮想化します。(code)

OPNA::Mixから渡されてくるデータは、FM_SAMPLETYPE型で、 デフォルトではint32です。mymixer側では16ビットにしたいため、 shortに丸める処理が入っています。

mymixerまわり

mymixerは純粋仮想関数を含むクラスです。実際に 出力したい環境(OSSとかaRtsとかesdとかSDLとか)にあわせて 実装します。

mymixerは以下の4つのmethodの実装を要求します。

artsmixer

SDL向けの実装はちょっと複雑なので、先にaRtsの実装例を示します (code)。 OpenでaRtsを初期化し、MixでaRtsに書き込み、Closeで閉じるという 実に素直な実装になっております。

sdlmixer

さてSDLへの実装例です(code)。 SDLのオーディオの仕組を箇条書にすると以下のような感じになります。

コールバック関数の第一引数は未使用になっています。 ここではコールバック関数をクラスのfriend関数として、 OpenAudio時にインスタンスのポインタ(this)を渡すようにしてみました (他にこのポインタの用途ってあるのだろうか)。

インスタンスmethodのポインタをcallbackに渡してもいいような 気がしますが、渡しかたが分からず断念。 コンパイルエラーが出てしまうのでした。調べりゃいいような気もしますが、 friendでいいかなーとか思って妥協した記憶があります。 あとfriendの方が、インスタンスがなくなった後にまちがって callbackが呼ばれたても安全かなと一瞬思いましたが別に callback内でポインタの正当性のチェックしてるわけでもないし意味ないじゃん。 (独り言ループ)

さて、aRtsで言うところのarts_write_streamに相当するのは、SDL_MixAudioと 言えるのですが、SDL側が準備できたということをcallbackで知ってから 書き込む必要がありますので、aRtsのときみたいにいつでも実行して よいというものでもないようです。

DoProcでs98クラスから送られてきたデータが callbackに渡されるまでDoProc内で待つようなことをしたくなりますが、 そうするとs98クラス側の処理がお留守になってしまって 大変困ります。 そのため、DoProcでs98クラスから渡されたデータを一時保管する 仕組が必要となります。

RingBuffer

という場合にはリングバッファというものがよく使われます。 典型的な実装は以下の通りです。
  1. リングバッファは4つの変数を管理します。 それぞれ、次に読むべき場所(rptr)、次に書くべき場所(wptr)、 バッファの大きさ(bufsize)、 書き込まれている(そしてまだ読まれていない)データ量(inlen)、です。
  2. rptr、wptrは、バッファ本体へアクセスするインデックス変数で、 0からbufsize - 1まで変化します。
  3. rptr、wptrは、値がbufsizになったら0に戻る、という以外は 普通にインクリメントできます。
  4. inlen == 0のときはリングバッファは空っぽです。
  5. inlen == bufsizeのときはリングバッファがいっぱいです。
  6. リングバッファから読み込むときは、rptrから1バイト読み込み、 rptrを(3のルールに従って)インクリメントします。 同時にinlenを1減らします。
  7. リングバッファに書き込むときは、wptrの位置に1バイト書き込み、 wptrを(3のルールに従って)インクリメントします。 同時にinlenを1増やします。
bufsizeを2の羃乗にしておいて、
++rptr; rptr &= bufsize - 1;
なんてことをして3の処理を高速化させたりしたもんですが、まあそれは 置いといて、このルールを 素直にコーディングしたものがこれです。

このコードはPC-98用RS-MIDIドライバ書いたときのリングバッファ管理ルーチン (実際はマクロでした)をC++で書き直したものです。が、 コードと上の説明を見て分かるように、1バイト(というか配列の中身1つ)単位での 操作しかできないのが難点です。 RS-MIDIドライバのときはこれで充分だったのですが(どうせ送受信は 1バイト単位でしかできなかったし)、音データを保存、取り出しするときに 1バイトずつやりとりしていたのではえらい遅くなってしまいます。

なんとかズルしようと考えた戦略は以下の通りでした。

んー戦略としては悪くないと思ったんですがー。結果としてはうまく ゆきませんでした。やはりRingBufferに生のデータがどのくらい残ってて、 といった情報をきっちり管理できるようにしとかないと駄目みたい。

RingBufferその2

やはりズルしないでRingBufferで複数バイトを一気に入れたり出したり できるようにしないとだめかなあということで 真面目に実装しなおしたのがこれです。 Getで中身を取り出し、Putで格納です。

DOS時代なんかだと、この程度のルーチンを面倒がっていてはまともにプログラム 書けなかったんですが...

inBufSize()でinlenを、maxBufSize()でbufsizeを得ることができます。 また、Capacity()でinlenとbufsizeの比率を知ることができます。 余計な関数ですがまああればあったで便利かなと思うわけですが...

sdlmixerその2

さて、RingBufferができたところでsdlmixerを仕上げます (ヘッダソース)。 注意点としてはSDL_MixAudioではUint8*で渡す必要があるので、 RingBufferをUint8で管理します。ので、sdlmixer::Mixでは shortのデータをUint8に分解して格納しております(endianどうすべ)

sdlmixer::Mix側では

とかいう処理をしています。RingBufferが溢れたらどうすんの?という 点に関してですがMixではやってきたデータを捌くまではwhileを抜けないので ずーっと待ち続けますのでなんとなくうまく動きます。

1番目は本当はcapacityじゃなくて時間で決めるべきだと思うんですが... まあ手抜きです。

まとめ

雑多なこと

S98_opnaregとS98_waitsync

何もこんなものを用意しなくても、 s98クラスの中にFM::OPNAとmymixerを持たせたほうが処理が楽になるのに... と思われるかもしれませんが、このクラスたちはS98データと それを鳴らすFM音源モジュール、それと実際に音を出すミキサ、の 三者の独立性を高めるためにやっぱり必要かなと 考えています。

つまり今回はfmgenを使いましたが、別のインターフェースを持つ FM音源エミュレータが出たときに、そちらへのサポートを容易にするには このようにデータ解釈部と処理部を分けたほうが都合がよいわけです。

また、s98クラスにOPNAやmymixerを入れてしまうというのは s98クラスに「ミキサを使って音を出すためのもの」という性質を持たせてしまう ことになるわけで、s98クラスの汎用性が損われているように思います。

mymixer

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からコンバートというのも考えないことはないですが、 今後の展開を待つべし...


戻る

Zinnia (zinnia@risky-safety.org)
このWebコンテンツ(ここから辿れるもの)に対する コメントのメールは許可なく公開することがあります。 (最近多い無礼なメール対策であって穏当なメールをいきなり公開したり することはありません)