ホーム < ゲームつくろー! < DirectX技術編


その1 音を鳴らすのは簡単


 ゲームの要素として大変に大きなウェイトを閉めるのが「サウンド」です。サウンドは主に効果音とBGMに分かれますが、双方ともゲームには必要です。BGMは多少の遅延(レイテンシ)が許されますが、他方の効果音(SE)は非常にシビアなタイミングを要求されます。そんな厳しい仕様に耐えるのがDirectSoundです。DirectSoundは極めて短いレイテンシで効果音を鳴らす事ができます。扱いも比較的簡単かもしれません。ここでは、とにもかくにもDirectSoundを用いて音を鳴らす方法を見ていく事にします。



@ DirectSoundで音を鳴らす仕組み

 DirectSoundがどのようにして音を鳴らすのか簡単に説明します。DirectSoundが扱える音は「WAV(ウェーブデータ)」です。WAVフォーマットには波形情報がそのまま入っていて、セカンダリバッファと呼ばれるサウンド用のメモリにそれを置くと、音を出す準備が整います。セカンダリバッファは複数作る事ができ、それぞれに別の音を登録しておけます。実際に音を鳴らす時にはセカンダリバッファにある音を「プライマリバッファ」と呼ばれる高速なメモリ上に置きます。この時複数の音を同時に鳴らすとプライマリバッファ上で「合成」されます。

 DirectSoundで音を鳴らす仕組みはこのように非常に単純です。正しくデバイスやオブジェクトを作り、WAVファイルから音を抽出できれば、直ぐに音を鳴らせるんです。



A サウンド再生

 ではDirectSoundを使ってWAVを再生してみましょう。
 まず、DirectSoundを使うために次のライブラリを読み込みます:

ライブラリ追加
#pragma comment ( lib, "dxguid.lib" )
#pragma comment ( lib, "dsound.lib" )

dxguid.libはGUID(Global Unique ID)を定義しているライブラリです。dsound.libがDirect Soundの本体のライブラリです。#pragmaディレクティブを使わずにプロジェクトのプロパティ上に上記のライブラリを読み込むように設定しても構いません。

 続いてDirectSoundのヘッダーをインクルードします:

ヘッダー追加
#include <dsound.h>

 これでDirect Soundを使う準備が完了です。

 続いてサウンドデバイスを作成します。

サウンドデバイス作成
IDirectSound8 *pDS8;
DirectSoundCreate8(NULL, &pDS8, NULL);

 IDirectSound8というのがサウンドデバイスです。DirectSoundCreate8関数を使うとサウンドデバイスを作成できます。第1引数にはサウンドデバイスを選択するGUIDを指定するのですが、NULLにすると現在使用しているサウンドデバイスが指定されます。サウンドデバイスのGUIDを知るにはDSEnumCallback関数を使いますが、今は省略します。第2引数にサウンドデバイスのインターフェイスが格納されます。第3引数はNULLを指定しなければなりません。この書き方はお決まりだと思って頂いて構いません。

 次に「強調レベル」を設定します。強調レベルとはデバイスの優先度の事で、標準協調レベル(DSSCL_NORMAL)、優先協調レベル(DSSCL_PRIORITY)、書き込み優先協調レベル(DSSCL_WRITEPRIMARY)があります。普通は標準協調レベルでよいと思います。優先強調レベルはプライマリバッファのフォーマットを設定できます。書き込み優先強調レベルはプライマリバッファに直接書き込みができます。後者2つは中〜上級者向けです。強調レベルを設定するにはIDirectSound8::SetCooperativeLevelメソッドを使います:

協調レベル設定
pDS8->SetCooperativeLevel(hWnd, DSSCL_NORMAL);

サウンドは1つのウィンドウに対して制御されるため、第1引数にウィンドウハンドル(hWnd)が必要になります。大抵はゲームで使用するウィンドウのハンドルです。つまり、DirectSoundを使うにはウィンドウを作成する必要があるわけです。

 続いてデバイスの能力やスピーカの構成などを決めたりするのですが、無くても音はなりますのでそれらは省略してセカンダリバッファの作成に移ります。

 セカンダリバッファは音の貯蔵庫(メモリ)で、ここに波形データを置く事で音を鳴らす準備が整います。セカンダリバッファはCreateSoundBuffer関数で作成します:

セカンダリバッファの作成
DSBUFFERDESC DSBufferDesc;
IDirectSoundBuffer *ptmpBuf = 0;
IDirectSoundBuffer8 *pDSBuffer;
pDS8->CreateSoundBuffer( &DSBufferDesc, &ptmpBuf, NULL );
ptmpBuf->QueryInterface( IID_IDirectSoundBuffer8 ,(void**)&pDSBuffer );
ptmpBuf->Release();

 セカンダリバッファを作るには、その能力を表すDSBUFFERDESC構造体が必要です。その構造体にあらかじめ作成したいバッファのすべての情報を格納しておく必要があります。詳細は後述します。関数が成功すると第2引数にサウンドバッファであるIDirectSoundBufferインターフェイスが戻ります。ここで気をつけたいのは返るインターフェイスの型がIDirectSoundBuffer8ではない点です。返るのは「8」が無いインターフェイスです。実際に必要なのは「8」が付くバッファの方なんですが、それを取得するにはIDirectSoundBuffer::QueryInterface関数を通す必要があります。これはDirectSoundがCOMの仕様にしたがっているからなのですが、詳しい事はあまり考えずにこういうものだと割り切っても良いです。第3引数はNULLにします。

 セカンダリバッファの能力を指定するDSBUFFERDESC構造体の中身を見てみましょう:

DSBUFFERDESC構造体
typedef struct {

   DWORD dwSize;
   DWORD dwFlags;
   DWORD dwBufferBytes;
   DWORD dwReserved;
   LPWAVEFORMATEX lpwfxFormat;
   GUID guid3DAlgorithm;

} DSBUFFERDESC, *LPDSBUFFERDESC;

dwSizeはこの構造体の大きさです。sizeof演算子で与えてください。
dwFlagsはデバイスの付加能力を指定するフラグですが、今回は特に指定しないので0にします。
dwBufferByttesはセカンダリバッファのサイズでバイト単位で指定します。ここで指定した大きさのメモリが確保されます。どういう大きさにするべきかは書き込みする波形データによって異なります。
dwReservedは予約域で0にします。
lpwfxFormatはWAVのフォーマットをWAVEFORATEX構造体で指定します。これは後述します。
guid3DAlgorithmは使用する仮想3DエフェクトのGUIDを設定するのですが、エフェクトを使用しない時はGUID_NULLにします。

 5段目にあるWAVEFORMATEX構造体はWAVフォーマットに含まれる音質やチャンネル数(モノラルとかステレオの事です)の情報を格納する構造体で、次のようになっています:

WAVEFORMATEX構造体
typedef struct {

    WORD  wFormatTag;
    WORD  nChannels;
    DWORD nSamplesPerSec;
    DWORD nAvgBytesPerSec;
    WORD  nBlockAlign;
    WORD  wBitsPerSample;
    WORD  cbSize;

} WAVEFORMATEX;

wFormatTagはウェーブフォームオーディオフォーマットのタイプを指定します。いわゆるオーディオの圧縮技術のフォーマットタグを指定するのですが、非圧縮で良い場合はWAVE_FORMAT_PCMを与えます。
nChannnelsは通常はステレオ・モノラルの選択です。ステレオならば左右のスピーカーから独立した音がなるのでチャンネル数が2になります。
nSamplesPerSecはサンプリングレート(音質)をHz単位で指定します。サウンドボードによって能力が異なりますが、CDが再生できるなら44100(Hz)を扱えます。
nAvgBytesPerSecは平均データ転送速度を指定するのですが、wFormatTagにWAVE_FORMAT_PCM(非圧縮波形)が指定されている場合はnSamplePerSec*nBlockAlineになります。
nBlockAlineはwFormatTagで指定したフォーマット形式の最小単位(バイト単位)を指定します。wFormatTagにWAVE_FORMAT_PCMを指定した場合はnChannels*wBitsPerSample/8になります。
wBitsPerSamplesはサンプリングビットを指定します。サンプリングビットとしては8, 16, 32などを指定できます。数が大きいほどクリアな音質になります。
cbSizeはこの構造体の後に付加的な情報がある場合にそのサイズを指定します。wFormatTagがWAVE_FORMAT_PCMの場合は無視されるので0を入れておきます。

 以上から、例えばCD並みのクリアなステレオ音質を与える場合のWAVEFORMATEXの設定は次のようになります:

WAVEFORMATEX wFmt;
wFmt.wFormatTag = WAVE_FORMAT_PCM;
wFmt.nChannels = 2;
wFmt.nSamplesPerSec = 44100;
wFmt.wBitsPerSample = 16;
wFmt.nBlockAlign = wFmt.nChannels * wFmt.wBitsPerSample / 8;
wFmt.nAvgBytesPerSec = wFmt.nSamplesPerSec * wFmt.nBlockAlign;
wFmt.cbSize = 0;

ただこれらの情報はWAVファイル内から吸い出せるので、上のように手打ちする事はあまり無いかもしれません。この情報をDSBUFFERDESC::lpwfxFormatに渡します:

DSBUFFERDESC DSBufferDesc;
DSBufferDesc.dwSize = sizeof(DSBUFFERDESC);
DSBufferDesc.dwFlags = 0;
DSBufferDesc.dwBufferBytes =
          wFmt.nSamplesPerSec
*
          wFmt.wBitsPerSample *
          wFmt.nChannels *
          m_dwTime
;
DSBufferDesc.dwReserved = 0;
DSBufferDesc.lpwfxFormat = &wFmt;
DSBufferDesc.guid3DAlgorithm = GUID_NULL;

 dwBufferBytesは格納する音の長さや音質等で決まるバッファサイズです。これもWAVデータから抽出されるので、実際は上のようではなくて直接サイズ指定が可能です。詳しくはWAVファイルから情報を吸い出す時に説明します。正しく値を設定したこの構造体をCreateSoundBuffer関数の第1引数に与えれば、セカンダリバッファが作成されます。

 作成したセカンダリバッファに対して、次に実際の波形データを書き込みます。これはIDirectSoundBuffer8::Lock関数でメモリをロックすることで行えます:

メモリロック
// セカンダリバッファにWaveデータ書き込み
LPVOID lpvWrite = 0;
DWORD dwLength = 0;
if ( DS_OK == pDSBuffer->Lock( 0, 0, &lpvWrite, &dwLength, NULL, NULL, DSBLOCK_ENTIREBUFFER ) ) {
memcpy( lpvWrite, pWaveData, dwLength);
pDSBuffer->Unlock( lpvWrite, dwLength, NULL, 0);
}

pWaveDataには波形データが格納されていると思ってください。Lockメソッドが成功するとlpWriteに書き込み先のポインタが、dwLengthにはそのサイズが返るので、そこにmemcpyで波形データをコピーしています。Lockメソッドの引数は次の通りです:

Lock関数の第1引数にはロックするオフセット位置を指定します。
第2引数はロックするメモリの大きさをバイト単位で指定します。一部分だけにコピーする場合は値をセットする必要があります。上の例ではサイズが0と指定されていますが、これは第7引数で全メモリをロックするDSBLOCK_ENTIREBUFFERフラグが指定されているためで、このフラグがあるとここは無視されます。
第3引数にはロック位置のポインタが返されます。
第4引数はロックした大きさです。
第5および第6引数はロックした範囲がメモリの終端を越えてしまった場合に分割された位置と大きさが格納されます。DirectSoundは「循環バッファ」を採用しているため、ロックがメモリの最後と最初をまたぐ事があります。そのために2つのメモリ領域の指定が必要になるわけです。ただし、ここにNULLを指定するとバッファをまたぐ事はなくなります。
第7引数にはロックの方法を指定します。DSBLOCK_FROMWRITECURSORを指定すると現在の書き込みカーソル位置から指定の大きさだけロックされます。よって第1引数が無視されます。

 ロックに成功したら、後は必要な音のデータをメモリコピーするだけです。書き込みが終わったら必ずIDirectSoundBuffer8::Unlock関数でロックを解除しなければいけません。忘れると音が再生されません(失敗する)

 セカンダリバッファに波形データを書き込めれば、すぐに再生できるようになります。音を再生するにはIDirectSoundBuffer8::Playメソッドを用います;

再生
pDSBuffer->Play(0, 0, 0);

引数がすべて0でも再生します。第1引数は予約なので0にします。第2引数は優先度を数字で表現します。0がもっとも優先順位が低く0xffffffffが最高です。絶対に鳴らさなければならない音は最高値に設定すると良いでしょう。第3引数は再生方法を指定するフラグです。たくさんあるのですが、通常再生(最後まで行ったら音がやむ)なら0でかまいません。

 以上で音を鳴らすプロセスは終了です。こうしてみてみると、セカンダリバッファの作成が面倒ですが、再生までのプロセスはそれほど難しいものではないのがわかると思います。

 DirectSound以上に大変なのはwavファイルから音のデータを抽出する作業です。実は、DirectSoundには音抽出用の関数群が一切ありません。よってこれはユーザが実装するしかありません。次の章ではwavからDirectSoundに必要なデータを抜き出す部分を作成します。