クラスまみれのゲームプログラミング入門

30:効果音を再生しよう

 30年前のゲームじゃあるまいし、やっぱり「ちゅどーん」など迫力のある効果音をつけて盛り上げたい。と、いうわけで今回は効果音を再生する方法についてご紹介します。

 DirectXにはWAVEファイルの再生に特化したDirectSoundと呼ばれる機能が存在し、wav形式のファイルに格納されている各種情報をDirectSoundにあらかじめコピーしておくことで、レスポンスの早い音楽の再生が可能となります。

 まずは効果音再生の基盤となるIDirectSound8インターフェースを作成します。実際に波形データを格納するためにはIDirectSoundBuffer8が必要なのですが、このIDirectSoundBuffer8はIDirectSound8を元に生成することになるため、IDirectSound8はクラス共通のstatic変数として、基盤クラスに置いておくことにします。

 IDirectSound8をstaticにしたため、コンストラクタ、デストラクタで生成・解放するとややこしくなるため、CGameObject::Initialize()で行った方法と同様に、生成・解放する専用の関数を作成し、WinMain()内で実行させるようにしています。

 IDirectSound8を作成したら、今動かしているソフトウェアとは別のソフトが音楽を再生したときに対する再生の優先度を指定します(サンプルプログラムではゲームの音楽再生を優先するようにしています)。

Sound1.h
#pragma once

#include "GameObject.h"

// DirectSoundを使うためのヘッダ定義とライブラリリンクの指示
#include <dsound.h>
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")

class CSound : public CGameObject
{
private:
	friend int APIENTRY _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int);
	static LPDIRECTSOUND8 pDSound;
	static void CreateDirectSound(HWND hWnd);
	static void ReleaseDirectSound();
};

Sound1.cpp
#include "Sound.h"

LPDIRECTSOUND8 CSound::pDSound = NULL;

void CSound::CreateDirectSound(HWND hWnd)
{
	// IDirectSound8の作成
	DirectSoundCreate8(
		&DSDEVID_DefaultPlayback,	// 標準のハードウェアを利用
		&pDSound,					// 格納するIDirectSound8インターフェース
		NULL						// 現行バージョンではNULLを指定
		);
	
	// 優先度の指定
	pDSound->SetCooperativeLevel(
		hWnd,						// 音楽再生に利用するウィンドウのハンドル
		DSSCL_PRIORITY				// 優先度
		);
}

void CSound::ReleaseDirectSound()
{
	RELEASE(pDSound);
}


ゲーム用パソコンならドスパラへ!Galleriaシリーズが大人気!

 waveファイルから取得したデータをDirectSoundのバッファに記録するための関数を作成します。手順としてはIDirectSound8::CreateBuffer()でデータを記録するためのインターフェース(IDirectSoundBuffer)を作成し、そのインターフェースにファイルから読み込んだデータをコピーします。ただし、コピーする際にはIDirectSoundBuffer::Lock()でメモリを確保してからコピーしなくてはならず、また、コピーが完了したら速やかにIDirectSoundBuffer::Unlock()を使ってIDirectSoundBufferが再生状態に入れるようにしなければなりません。

Sound2.h
/* 省略 */

class CSound : public CGameObject
{
private:
	/* 省略 */

	LPDIRECTSOUNDBUFFER pDSBuffer;

public:
	CSound();
	CSound(LPCTSTR filename);
	~CSound();

	BOOL Load(LPCTSTR filename);
};

Sound2.cpp
CSound::CSound()
{
	pDSBuffer = NULL;
}

CSound::CSound(LPCTSTR filename)
{
	pDSBuffer = NULL;
	Load(filename);
}

CSound::~CSound()
{
	RELEASE(pDSBuffer);
}

BOOL CSound::Load(LPCTSTR filename)
{
	// waveファイルを開く
	FILE *fp;
	if(_tfopen_s(&fp, filename, _T("rb"))){
		DXTRACE_MSG(_T("ファイルが開けません"));
		return FALSE;
	}

	// 本当にwaveファイルかどうか調べる
	char buf[10];
	fread(buf, 4, 1, fp);
	if(memcmp(buf, "RIFF", 4) != 0){
		DXTRACE_MSG(_T("RIFFフォーマットではありません"));
		return FALSE;
	}

	// RIFFデータサイズは省略
	fseek(fp, 4, SEEK_CUR);

 	fread(buf, 8, 1, fp);
	if(memcmp(buf, "WAVEfmt ", 8) != 0){
		DXTRACE_MSG(_T("WAVEフォーマットではないか、フォーマット定義がありません。"));
		return FALSE;
	}

	// fmtデータサイズエリアを読み飛ばす
	fseek(fp, 4, SEEK_CUR);

    // フォーマット情報を取得
	WAVEFORMATEX wavf;
	fread(&wavf, sizeof(WAVEFORMATEX) - 2, 1, fp);

	// 音楽データの開始を意味する「data」の文字列があるか調べる
	ZeroMemory(buf, 10);
	while(strcmp("data", buf)){
		buf[0] = fgetc(fp);
		if(buf[0] == EOF){
			DXTRACE_MSG(_T("波形データ定義が見つかりません。"));
			fclose(fp);
			return FALSE;
		}
		if(buf[0] == 'd') fread(&buf[1], 1, 3, fp);
	}

	// 音楽データサイズ取得
	int wsize;
	fread(&wsize, sizeof(int), 1, fp);

	// CreateSoundBufferに送信するための音楽情報を作成
	DSBUFFERDESC desc;
	ZeroMemory(&desc, sizeof(DSBUFFERDESC));
	desc.dwSize = sizeof(DSBUFFERDESC);
	desc.dwFlags = LOCDEFER;	// ハードウェアでの再生を優先
	desc.dwBufferBytes = wsize;
	desc.lpwfxFormat = &wavf;

	RELEASE(pDSBuffer);
	pDSound->CreateSoundBuffer(&desc, &pDSBuffer, NULL);

    //アクセス可能なバッファのサイズ
    DWORD  buff_size;
    //WAVバッファアクセスポイントを格納する為のポインタ
    LPVOID pvAudioPtr;

	// バッファロック
	pDSBuffer->Lock(0, 0,			// バッファ全体をロックするため、数値の指定は不要
		&pvAudioPtr, &buff_size,	// 書き込むバッファを取得するためのポインタ
		NULL, NULL,					// 2つに分けて書き込むこともできる
		DSBLOCK_ENTIREBUFFER		// バッファすべてをロック
		);

	//サウンドデータをバッファへ書き込む
	fread(pvAudioPtr, buff_size, 1, fp);

	//ロック解除
	pDSBuffer->Unlock(pvAudioPtr, buff_size, NULL, NULL);

	fclose(fp);

	return TRUE;
}

 一度データをコピーしてしまえばあとは簡単です。IDirectSoundBuffer::Play()を実行すれば、即座に効果音が再生されます。再びPlay()を実行しても、はじめからは再生されませんので、再生位置を変更するSetCurrentPosition()を使って、音楽のはじめの位置までに一度戻しておいてからPlay()するのがよいでしょう。

play.cpp
void CSound::Play()
{
	if(pDSBuffer){
		pDSBuffer->SetCurrentPosition(0);	// 再生場所を先頭に移動
		pDSBuffer->Play(
			0,	// 必ず0を指定
			0,	// 再生の優先度。0が最も低く、0xFFFFFFFFが最も高い
			0	// 再生オプション。例えばDSBPLAY_LOOPINGならループ再生になる
			);
	}
}

 ついでですので、IDirectSoundBufferに元から用意されている停止およびボリュームやパン(音の出る位置)の処理もクラス化しておきましょう。ボリューム(パン)の指定を有効にするためには、IDirectSoundBuffer作成時に、利用する旨(DSBCAPS_CTRLVOLUME・DSBCAPS_CTRLPAN)を指示しておく必要があります。

 ボリューム調整の値はDSBVOLUME_MAX(=0)からDSBVOLUME_MIN(=-10000)の間で行うのですが、この単位は(小さくするデシベル数×100)となっています。再生するファイルによっては-5000に達する前でもほぼ無音状態になるものもありますので注意してください。

Sound3.h
/* 省略 */

class CSound : public CGameObject
{
private:
	/* 省略 */

public:
	/* 省略 */
	void Stop();
	void SetVolume(LONG volume);
	void SetPan(LONG lPan);
};

Sound3.cpp
BOOL CSound::Load(LPCTSTR filename)
{
	/* 省略 */

	DSBUFFERDESC desc;
	ZeroMemory(&desc, sizeof(DSBUFFERDESC));
	desc.dwSize = sizeof(DSBUFFERDESC);
	desc.dwFlags = DSBCAPS_LOCDEFER | DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME;
	desc.dwBufferBytes = wsize;
	desc.lpwfxFormat = &wavf;

	RELEASE(pDSBuffer);
	pDSound->CreateSoundBuffer(&desc, &pDSBuffer, NULL);

	/* 省略 */
}

void CSound::Play()
{
	if(pDSBuffer){
		pDSBuffer->SetCurrentPosition(0);
		pDSBuffer->Play(0, 0, 0);
	}
}

void CSound::Stop()
{
	if(pDSBuffer) pDSBuffer->Stop();
}

void CSound::SetVolume(LONG volume)
{
	if(pDSBuffer) pDSBuffer->SetVolume(volume);
}

void CSound::SetPan(LONG lPan)
{
	if(pDSBuffer) pDSBuffer->SetPan(lPan);
}

 では最後に動作を確認するためのサンプルプログラムを。以前に掲載した「戦闘機からミサイルを発射するデモ」のShipクラスの一部を変更します。

ship.h
class CShip : public CITCharactor
{
private:
	CSound shootsound;
	CInput *input;
protected:
	void Init();
	void Exec();
};

ship.cpp
void CShip::Init()
{
	shootsound.Load(_T("missile.wav"));

	/* 省略 */
}

void CShip::Exec()
{
	/* 省略 */

	if(input->IsKeyPressed(DIK_SPACE) || input->IsButtonPressed(0)){
		// ミサイル発射
		AppendObject(new CShoot(), 900, true);
		shootsound.Play();
	}

	sprite.Draw(x, y);
}

missile.wav
[右クリックしてダウンロード]

 IDirectSound8が先に解放されないよう、CSound::ReleaseDirectSound()の処理はCGameObject::Uninitialize()の後に行うようにしましょう。