「りも」トップページへ
TIPS index へ

サウンドを鳴らす - 応用実装編

May.20.2000

ゲーム用途向けサウンドマネージャーの製作。


・ はじめに

ゲーム用途向けのサウンドマネージャーを設計し、製作します。

・ サウンドマネージャーとは

ゲームプログラムにおいて、サウンドを持つことは一層の表現力を持つことであり、 既にサウンド無しのゲームというのは考えられないと言っても過言ではないと 思います。
そういったサウンドを持ったゲームを作るために、サウンドを管理し、 簡易なコールでそれを鳴らしてくれるプログラムを作成します。 その作業内容はサウンドドライバーといったファウンデーションクラスでは なく、アプリケーションでもなく、その間を取り持つ立場にあることから サウンドマネージャーと呼びます。
サウンドマネージャーはアプリケーションとはシンクロナスに動作し、 サウンドドライバーを監視・コントロールします。アプリケーション側から 短いリクエストコマンドを受け取ったら、それを演奏しますが演奏中は アプリケーション側の進行を妨げません。
手短のCDプレイヤーを思い浮かべてください。利用者はCDプレイヤーの 再生ボタンを押すことにより曲を聞くことができます。演奏はCDプレイヤーが 行いますので、利用者は本を読んだり食事をしたり別なことが行えます。 演奏を止めるには停止ボタンを押すだけです。この、勝手に演奏してくれる CDプレイヤーがサウンドマネージャーにあたり、ボタンを押す行為が アプリケーション側からのリクエストにあたります。
主にゲームにおいて、サウンドに対するアプリケーション側の負担が 少なくなるようにサウンドを管理するマネージャーということです。

・ シンクロナスとリクエスト

PCMサウンドの再生するには波形データをひっきりなしに途切れなく サウンドデバイスへと流しこまなくてはなりません。 アプリケーション側でこれをやろうとすると実に頻繁にサウンドアクセスを しなければならなくて複雑なプログラムとなりがちです。
そこでサウンド部分はなるべくアプリケーション本体とは分離させ、 自立して動くように設計します。昔のアーケードゲームにはゲーム制御用 CPU とは別にサウンド専用CPUが載っていました、それと同じ理屈です。
シンクロナスに動作させるためにサウンドマネージャーを子プロセスと して fork します。fork して独立して動くサウンドマネージャーと アプリケーションとの連絡のためにソケット(パイプ)を繋ぎ、リクエストは ここを通して行うことにします。
この構成は、 わたなべごう氏 作の 「XLVNS」 のサウンド周りを参考にさせていただきました。
参考と言うよりそのままだったりするんですけど。
プロセスの fork やソケット(パイプ)の生成については省略します。 他の UNIX System 系プログラミング教材を御参照ください。 (また、これ系の本が書店にあんまし置いて無いんだ)

---- 該当部分の抜きだし

  /* --- 通信用ソケットの作成 */
  if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sock_fd) < 0) {
    perror("ERROR sound: socketpair can't make.\n");
    return(-1);
  }

  /* --- プロセスフォーク */
  pid = fork();
  if (pid < 0) {
    perror("sound fork error.\n");
    close(sock_fd[0]);
    close(sock_fd[1]);
    return(-1);
  }
  else if (pid) {
    /* -- 親プロセスなのでソケットfdを持ってリターン */
    close(sock_fd[1]);
    return(sock_fd[0]);
  }
  /* --- 子プロセス始動 */
  close(sock_fd[0]);
#ifdef DEBUG
  fprintf(stderr, "Limo Sound Manager is starting now ..\n");
#endif
---- ココマデ

Jun.28.2000
socketpair で AF_LOCAL/PF_LOCAL でなく AF_UNIX/PF_UNSPEC にしないと 互換性の問題で OS によっては通らないとの指摘をごう氏から受けていた ものを(ようやく)修正。

・ サウンドマネージャーの設計

さて実際にサウンドマネージャーに何をさせたいのかを考えます。
機能の設計と実装ですが、この「何をさせるか」というのを始めに考える事が 今回最大のトピックです。プログラム自体はそんなにすごいものじゃないと 思いますし。
ゲームを作る上でどの様な機能があったら良いのかなという部分(機能要求)は プログラムだけでは見えてこないところです。要するに仕様設計なんで すが、大抵の場合経験が物をいいます。あとは予測能力…。

今回サウンドデバイスは PCM(dsp) と CD-DA を扱います。
デバイスへのアクセスは EsounD libcdaudio を用います。OS や環境によって異なるデバイス周りのアクセスに対し 個別に対応するコードを用意するのではなく、そういった対策が既に 行われている既存ライブラリを用いる事によって手間とコードを減らそう というのが意図です。それに下手に自前で作るよりも出来の良い 既存ライブラリを用いた方が全体として大きなメリットでもあるわけで。

表現したい音声形式として、

  1. CD-DA による BGM
  2. PCM による SE
  3. PCM による BGM
の 3つを挙げます。
「PCM による BGM」というのがちょっと珍しいかもしれませんが、 実行環境において CD の無いシステムというのも割と多いであろうというのと ネットワーク配布しようとすると CD-DA は現実的じゃないというあたりから 必要な要素ということで。
MIDI out による演奏は最近需要が低くなってきた上に環境差を吸収して くれる様なライブラリが見付からなかったので見送りとします。

PCM が扱う音声ファイルは無圧縮PCM形式の(Windows)WAV ファイルとします。 これは開発側として用意しやすい形式であると共に EsounD で鳴らすに 不都合無く、いくつかの周波数を用意できるからです。

・ リクエストコマンド

アプリケーションからサウンドマネージャーへ、音声発呼のリクエストを 依頼する際のリクエストコマンドを決定します。
このリクエストコマンドを決定するということは、サウンドマネージャーの 細かい挙動を設計するということでもあります。

プログラムを書くよりもなによりも、まず最初にした事は今回製作する サウンドマネージャーのリクエストコマンド表を書くことでした。 以下のファイルがそのコマンド表です。
リクエストコマンド一覧
頭一文字が出力デバイスの指定だったり終了コードだったりするのは、 ごう氏のコードを参考にしたなごりです。 :-)

これらコマンドをソケット(パイプ)に流し込むだけで後はサウンドマネージャー が演奏してくれます。アプリケーションにとってはかなり負荷が減って いると思います。
コマンドは一度に複数列挙して指定する事が可能です。ただし現在の インプリメントでは 32トークンという制限がありますので、 適度な所で切らないとならないかもしれません。

コマンド群の中で BGM と CD-DA に READY というリクエストがあります。 これは演奏要求をした直後にポーズをかけるというもので C PLAY 1 C PAUSE と連続でコマンドを送信するのと等価です。 一見無意味なようですが実は CD-DA を BGM に使うには大きな意味を 持ちます。
CD は演奏する際回転します。一見当り前の事ですがこれが扱い難い原因の 一つでもあります。停止状態に演奏要求を出した場合、CD プレイヤーは スピンドルを回転させ、トラックを読み取り演奏を開始します。この、 スピンドルの立ち上がりというのが結構遅く、演奏要求から実際に音が 出力されるまで結構な時間待たされます。その空白時間というのが ゲームにとってはかなりマイナスに作用してしまうのです。
そこであらかじめスピンドルを回転させておき、いつでも演奏可能状態と したうえでゲームの進行とシンクロして BGM が流れるようリクエスト したあと即座に PAUSE 状態にしておきます。あとはゲームで「ここだ!」 というタイミングに合わせて RESUME を発呼すればばっちりとゲーム 画面(演出)に合わせた BGM 演奏を行えます。
格闘ゲームや落ち物パズルにおける「れでぃー・ごー☆」という場面を 思い浮かべてもらうとわかりやすいと思います。

・ 再生トラック(チャンネル)

PCM による BGM および SE にはそれぞれ最大発音数があり、1発音の事を BGM では「トラック」 SE では「チャンネル」と呼びます。呼称は違いますが 実装的には同じものです。デフォルトでは 2トラック 4チャンネルとなって おり、この状態では BGM が 2曲、SE が 4音同時に鳴らせるわけです。
リクエストを発呼した際、演奏中のトラック(チャンネル)だった場合 演奏を中止して新しい演奏を開始します。既に演奏中でも、それが別の トラック(チャンネル)だった場合、演奏は重なり同時発音されます。
SE とかは重なって欲しい音と重なって欲しくない音の 2パターンが存在する ものですので、その場合チャンネルを使い分けることによってコントロール します。

・ サウンドリスト

PCM BGM や SE では WAV ファイルを読み込みそれを音声として出力します。 それら WAV ファイル群を管理し、WAV ファイルを通し番号で管理するため サウンドマネージャーはサウンドリストを要求し、それを読み込みます。
ようするに BGM と SE に使う WAV ファイルの一覧です。
実際のサウンドリスト例

書式は

の連続です。 と書くと nyo.wav を 23番に割り当てます。空白は含めないでください。
サウンドが BGM か SE かは「\BGM」「\SE」というタグで区別されます。 「\BGM」が出現したらその後「\SE」が出て来るかファイルの終端に達するまで リストは BGM の物として登録されます。逆に「\SE」は「\BGM」で出て来るか ファイルの終端に達するまでリストを SE の物として扱います。
BGM の登録番号と SE の登録番号は別に扱われますので同じナンバーに別々の WAV を割り当てることができます。また、同じ登録番号が複数出て来た場合は 後で出て来た項目で上書きされていき、前の方で宣言したものが無効となり ます。
最大登録数はインプリメントに因りますが、デフォルトの設定では BGM が 256番、SE が 65536番まで可能になっています。もちろん番号は間が飛んで いてもまったくかまいません。

今回は鳴らす SE や BGM の数だけ WAV ファイルを用意しなくてはなりません。 たくさんのファイルが必要になります。これらをアーカイブしたり、その アーカイブから利用するといった形態に関しては考慮していません。 ストリームで読み込んで出力していくからというのが理由の一つですが。

・ ライブラリとしての使い方

アプリケーション側から利用するインターフェース関数はとりあえず

  1. int lm_sound_init(char *sound_list, char *esd_host, char *cd_dev)
    サウンドマネージャーの初期化とプロセスフォーク
  2. void lm_sound_close(int sock)
    サウンドマネージャーの停止と後かたづけ
  3. int lm_sound_request(int sock, char *command)
    サウンドマネージャーへのリクエストラッパ
の 3つだけです。
起動して、リクエストして、用が無くなったら止める、といった感じです。

サウンドリストのパスと EsounDのホスト(ローカルなら null)を CDROM デバイス のパス(/dev/cdrom とか)を持たせて lm_sound_init を起動。起動成功時は サウンドマネージャーとのソケット(パイプ)ファイルディスクリプタを持って 帰って来ます。EsounD がいなかったり、CD がセットされていなくても 起動して帰って来ますが、それらは使えなくなりリクエストが無効になります。
lm_sound_close に lm_sound_init が持って来たファイルディスクリプタを 持たせてコールすることによりサウンドマネージャーを停止、開放します。
lm_sound_request はリクエストを容易に行うために用意したおまけです。 lm_sound_init が持って来たファイルディスクリプタと、リクエストコマンド の文字列を渡します。不正なリクエストはファイルマネージャーの中で 暗黙のうちに無視されます。

・ バグ/不具合

既知の不具合事項です

・ 実際のコード

ライブラリとサンプルのコードです。
利用と配布の条件として GPL(Ver.2) に準じます。(LGPL ではありません)

lm_sound_man.tar.gz (10902byte)

今回は面白そうなサンプルを用意できませんでした、簡素な Makefile と 取り敢えず鳴らすテストコードだけです。
各人テストの為の WAV ファイルを用意して、sound_list.dat にそのファイル名 を記述してください。プログラムを起動すると入力待ちになりますので コンソールからリクエストコマンドを直に打ち込んでサウンドマネージャー に渡してください。リクエストに応じてサウンドマネージャーが音を鳴らして くれると思います。
終了するには「E」と一文字だけ入力します、数秒ウエイトを取った後に サンプルの実行を終了します。

・ おわりに

プログラム内容についても色々と解説したい気分ですが、ここまででも大分 長くなってしまったので省略しておきます。
実際のコードが全てを語ってくれることでしょう :-)


れろれろ@ふみ rero2@yuumu.rim.or.jp