🎵

XAudio2導入:最低限Waveファイルの音源を鳴らすところまで

2023/12/18に公開

この記事は、神戸電子専門学校 ゲーム技研部 Advent Calendar 2023の18日目の記事です。
https://qiita.com/advent-calendar/2023/kdgamegiken

はじめに(飛ばしていいよ)

この記事では、XAudio2というオーディオAPIを使用して最低限音を鳴らせるようにするところまで実装したいと思います。

本記事の目的は、自作でフレームワークを作る際に内部でどういうことをやっているか理解する必要があります。モデル描画であれば頂点がこうでこうで描画されるとか、Window出すのにもこうでこうでこうなってるみたいなのです。
サウンドも同じようにちゃんと理解して実装したいですよね?ね?
というわけで自作で音を鳴らせれるようにしましょうというのが目的です。
(深堀などはせずにとりあえず音が鳴るところまで実装します。)

まだまだプログラミング勉強中なので間違った知識を記事にしてしまっていたり、もっと良い書き方があれば修正しますので教えていただけると幸いです。
(優しく教えてくださいm(__)m)
また、この記事は自分がプログラムを初めて1年目の頃でも理解できるような記事を目標に作成したので当たり前のことが書かれているかもしれませんが気にしないでください。

XAudio2とは?

MicrosoftのXAudio2のページ曰く「XAudio2 は、ゲーム用の高性能オーディオ エンジンを開発するための信号処理とミキシング基盤を提供する低レベルのオーディオ API です。」らしいです!

とりあえず、音を鳴らすためのAPIです!
Windowsアプリケーション以外にも、XBoxで使えます!(試したことないけど^^)

早速実装

前置きは短めにしたいので、さっそく実装していきましょう。
今回はとりあえず音を鳴らすだけなので、main.cppにすべてまとめて書きます。
各自新しいプロジェクトを作成してmain.cppを作成しておいてください。

そしたら、雛形的なものを用意したのでこちらをコピペしてください

main.cpp
//=============================
// 1.各種インクルード・リンカー設定
//=============================


//=============================
// 2.オブジェクト・変数の宣言
//=============================

//=============================
// 6.Waveファイルの読み込み
//=============================

//=============================
// 7.再生関数の作成
//=============================

int main()
{
	//=============================
	// 3.COMの初期化
	//=============================

	//=============================
	// 4.XAudio2の初期化
	//=============================

	//=============================
	// 8.実際に再生
	//=============================

	//=============================
	// 5.忘れる前に解放するものはしとく
	//=============================

	return 0;
}

あと、今回鳴らしてもらう音も配布しておきます。
https://drive.google.com/file/d/102WPd5P2r7qnXOiHgjlBecV3jtzf2P0l/view?usp=sharing
(びっくりしたらすみません)
いい感じの素材探してたけど、2次配布になってしまうのでこれしかなかった...sry

ダウンロードできたら(もしくは、自分でとってきたWaveファイルは)、ソリューションファイルがあるフォルダと同じ場所に入れといてください。

1.各種インクルード・リンカー設定

先に、インクルードとリンカーを設定してしまいます。

1.各種インクルード・リンカー設定
//=============================
// 1.各種インクルード・リンカー設定
//=============================

// XAudio2関連
#pragma comment(lib, "xaudio2.lib")
#include<xaudio2.h>

// マルチメディア関連
#pragma comment ( lib, "winmm.lib" )
#include<mmsystem.h>

// 文字列
#include<string>

上は、XAudio2を使用するために必要なものです。

真ん中は、Waveファイルを読み込む際に使用します。
(mmの意味は、マルチメディア)

stringは文字列です。はい。

2.オブジェクト・変数の宣言

次に、使う物を宣言しておきましょう。
今回は、main.cppにすべて書くのでグローバル変数として宣言しますが、実際にフレームワークとかに実装する際は、メンバ変数にしてね

2.オブジェクト・変数の宣言
//=============================
// 2.オブジェクト・変数の宣言
//=============================
IXAudio2* pXAudio2;
IXAudio2MasteringVoice* pMasteringVoice;
IXAudio2SourceVoice* pSourceVoice;

IXAudio2」 というのは本体ですね。

そして、ちょっとややこしいのが「IXAudio2MasteringVoice」と「IXAudio2SourceVoice」。

まずは、「IXAudio2SourceVoice」(ソースボイス)から
これは、音の起点であって、BGMやSEを鳴らすごとに必要になります。同じSEを複数鳴らすなら、その分作成する必要があります。

そして、「IXAudio2MasteringVoice」(マスターボイス)は
1個だけです。最終的に音を出力してくれます。
イメージ的には、画像の様にマスターボイスの中にソースボイスをまとめて、それが音声として出力される感じです。

(ソースボイスと、マスターボイスの間に「サブミックスボイス」というものを挟むこともできます。SE、BGMとかでグループ分けして、それらの音量調節を行ったりできます(ゲームのオプション画面っぽい)。ここでは説明を割愛するので、興味があれば調べてみてください。)

3.COMの初期化

これはおまじないです。

3.COMの初期化
//=============================
// 3.COMの初期化
//=============================
HRESULT result;
result = CoInitializeEx(NULL,COINIT_MULTITHREADED);
if (FAILED(result))
{
	return 0;
}

HRESULT型 というのは、こういう初期化関数とか作成関数がうまくいったかとかどういった理由で失敗したか教えてくれるものです。成功すれば、S_OKが帰ってきます。
万が一失敗すれば、終わりです。

4.XAudio2の初期化

XAudio2で使う物の初期化をしていきましょう。
初期化するものは、本体である「IXAudio2」と、ソースボイスを総まとめする「IXAudio2MasteringVoice」(マスターボイス)です。
ソースボイスは、実際に使うとき(音鳴らすとき)に作成します。

4.XAudio2の初期化
//=============================
// 4.XAudio2の初期化
//=============================
result = XAudio2Create(&pXAudio2);
if (FAILED(result))
{
	return 0;
}

result = pXAudio2->CreateMasteringVoice(&pMasteringVoice);
if (FAILED(result))
{
	return 0;
}

これも先程と同様に、ちゃんと作成できたか確認しましょう。

5.忘れる前に解放するものはしとく

先に、解放処理を書いておきましょう。(忘れるとメモリリークするよ)

5.忘れる前に解放するものはしとく
//=============================
// 5.忘れる前に解放するものはしとく
//=============================
pMasteringVoice->DestroyVoice();
pXAudio2->Release();

CoUninitialize();

ここ、順番注意
作成した順と逆順で解放していかないと、例外スロー起こるので注意

XAudio2の初期化はこれだけです。すごく簡単ですよね!
でも、ここからしんどいです

6.Waveファイルの読み込み

正直、ここが一番面倒です。
以下、すごく参考にさせていただいた読み込みの記事です。深く知りたい方はこちらを読んでください。この記事での説明は割愛します。
https://yttm-work.jp/directx/directx_0034.html

イメージだけは説明すると、音のデータは波形のデータになっています。それを、C++で使えるように読み込むといった形になります。
まずは、Waveデータの構造体を作ります。ここに、Waveファイルのデータを格納します。

6.Waveファイルの読み込み
//=============================
// 6.Waveファイルの読み込み
//=============================
struct WaveData
{
	WAVEFORMATEX m_wavFormat;
	char* m_soundBuffer;
	DWORD m_size;

	~WaveData() { free(m_soundBuffer); }
};
WaveData waveData;

一番上の WAVEFORMATEX 構造体は、Waveファイルのフォーマットに関する情報が入っています。
2つ目のsoundBufferは、音の波形データが入っています。
3つ目はそのまま。サイズです。

注意するのは、デストラクタで解放を行っています。これ、なかったら凄い容量のメモリリークが起こるので、書き忘れの無いようにしてください。

次に、これを使用して実際に読み込んでいきます。(ちょっと長いけど頑張れ👍)
先程引用した記事を見ながら書くと、ここで行われていることが理解できると思います。

6.Waveファイルの読み込み・続き
bool LoadWaveFile(const std::wstring& wFilePath,WaveData* outData)
{
	// 中身入ってるもの来たら、一旦解放しとく
	// (じゃないと、もとの中身のサウンドバッファーがある場合、メモリリークする)
	if (outData)
	{
		free(outData->m_soundBuffer);
	}
	// nullptrが来たらリターンする
	else
	{
		return false; 
	}


	HMMIO mmioHandle = nullptr;

	// チャンク情報
	MMCKINFO chunkInfo{};

	// RIFFチャンク用
	MMCKINFO riffChunkInfo{};


	// WAVファイルを開く
	mmioHandle = mmioOpen(
		(LPWSTR)wFilePath.data(),
		nullptr,
		MMIO_READ
	);

	if (!mmioHandle)
	{
		// Wavファイルを開けませんでした
		return false;
	}

	// RIFFチャンクに侵入するためにfccTypeにWAVEを設定をする
	riffChunkInfo.fccType = mmioFOURCC('W', 'A', 'V', 'E');

	// RIFFチャンクに侵入する
	if (mmioDescend(
		mmioHandle,		//MMIOハンドル
		&riffChunkInfo,	//取得したチャンクの情報
		nullptr,		//親チャンク
		MMIO_FINDRIFF	//取得情報の種類
	) != MMSYSERR_NOERROR)
	{
		// 失敗
		// Riffチャンクに侵入失敗しました
		mmioClose(mmioHandle, MMIO_FHOPEN);	
		return false;
	}

	// 侵入先のチャンクを"fmt "として設定する
	chunkInfo.ckid = mmioFOURCC('f','m','t',' ');
	if (mmioDescend(
		mmioHandle,
		&chunkInfo,
		&riffChunkInfo,
		MMIO_FINDCHUNK
	) != MMSYSERR_NOERROR)
	{
		// fmtチャンクがないです
		mmioClose(mmioHandle, MMIO_FHOPEN);	
		return false;
	}

	// fmtデータの読み込み
	DWORD readSize = mmioRead(
		mmioHandle,						//ハンドル
		(HPSTR)&outData->m_wavFormat,	// 読み込み用バッファ
		chunkInfo.cksize				//バッファサイズ
	);

	if (readSize != chunkInfo.cksize)
	{
		// 読み込みサイズが一致していません
		mmioClose(mmioHandle, MMIO_FHOPEN);
		return false;
	}
	
	// フォーマットチェック
	if (outData->m_wavFormat.wFormatTag != WAVE_FORMAT_PCM)
	{
		// Waveフォーマットエラーです
		mmioClose(mmioHandle, MMIO_FHOPEN);
		return false;
	}

	// fmtチャンクを退出する
	if (mmioAscend(mmioHandle, &chunkInfo, 0) != MMSYSERR_NOERROR)
	{
		// fmtチャンク退出失敗
		mmioClose(mmioHandle, MMIO_FHOPEN);
		return false;
	}

	// dataチャンクに侵入
	chunkInfo.ckid = mmioFOURCC('d','a','t','a');
	if (mmioDescend(mmioHandle, &chunkInfo, &riffChunkInfo, MMIO_FINDCHUNK) != MMSYSERR_NOERROR)
	{
		// dataチャンク侵入失敗
		mmioClose(mmioHandle, MMIO_FHOPEN);
		return false;
	}
	// サイズ保存
	outData->m_size = chunkInfo.cksize;

	// dataチャンク読み込み
	outData-> m_soundBuffer= new char[chunkInfo.cksize];
	readSize = mmioRead(mmioHandle, (HPSTR)outData->m_soundBuffer, chunkInfo.cksize);
	if (readSize != chunkInfo.cksize)
	{
		// dataチャンク読み込み失敗
		mmioClose(mmioHandle, MMIO_FHOPEN);
		delete[] outData->m_soundBuffer;
		return false;
	}

	// ファイルを閉じる
	mmioClose(mmioHandle, MMIO_FHOPEN);

	return true;
}

一番しんどい部分は終わりました。

次に、実際に再生する関数を作ります。

7.再生関数の作成

今から作る関数をmain関数で呼び、音を再生する形にしたいと思います。
ここで、先程説明した「ソースボイス」を作成します。

7.再生関数の作成

//=============================
// 7.再生関数の作成
//=============================

bool PlayWaveSound(const std::wstring& fileName, WaveData* outData, bool loop)
{	
	if (!LoadWaveFile(fileName, outData))
	{
		//Waveファイル読み込み失敗
		return false;
	}

	//=======================
	// SourceVoiceの作成
	//=======================
	WAVEFORMATEX waveFormat{};

	// 波形フォーマットの設定
	memcpy(&waveFormat, &outData->m_wavFormat, sizeof(outData->m_wavFormat));

	// 1サンプル当たりのバッファサイズを算出
	waveFormat.wBitsPerSample = outData->m_wavFormat.nBlockAlign * 8 / outData->m_wavFormat.nChannels;

	// ソースボイスの作成 ここではフォーマットのみ渡っている
	HRESULT result = pXAudio2->CreateSourceVoice(&pSourceVoice, (WAVEFORMATEX*)&waveFormat);
	if (FAILED(result))
	{
		// SourceVoice作成失敗
		return false;
	}
	
	//================================
	// 波形データ(音データ本体)をソースボイスに渡す
	//================================
	XAUDIO2_BUFFER xAudio2Buffer{};
	xAudio2Buffer.pAudioData = (BYTE*)outData->m_soundBuffer;
	xAudio2Buffer.Flags = XAUDIO2_END_OF_STREAM;
	xAudio2Buffer.AudioBytes = outData->m_size;

	// 三項演算子を用いて、ループするか否かの設定をする
	xAudio2Buffer.LoopCount = loop ? XAUDIO2_LOOP_INFINITE : 0;

	pSourceVoice->SubmitSourceBuffer(&xAudio2Buffer);

	// 実際に音を鳴らす
	pSourceVoice->Start();

	return true;
}

上から順番に見ていきましょう。
まずは、先程作成したWaveファイルを読み込む関数で読み込んできます。

次に、ソースボイスを作成するために、読み込んできたフォーマットを用いて作成します。
そして作成(CreateSourceVoice)します。
次に、ここでは実際の波形データは渡っていないので、それを渡す準備をします。
その際に、ループするか決めれますので設定しておきましょう。SEは基本ループしないと思いますし、BGMはだいたいループするでしょう。

そして、波形データを渡します。(SubmitSourceBuffer)

最後に、Start関数を呼ぶと、音が再生されます。

追加で、5番の部分にソースボイスの解放処理を追加しておいてください。

追加
//=============================
// 5.忘れる前に解放するものはしとく
//=============================

//追加
pSourceVoice->DestroyVoice();

//以後は同じ
pMasteringVoice->DestroyVoice();


8.実際に再生

ここまで長かったですが、ようやく再生できます!音を鳴らしてみましょう。

8.実際に再生
//=============================
// 8.実際に再生
//=============================
bool isPlay=PlayWaveSound(L"KurataGorilla.wav",&waveData, false);

if (isPlay)
{
	bool isRunning = true;

	XAUDIO2_VOICE_STATE state;
	pSourceVoice->GetState(&state);

	while ((state.BuffersQueued > 0) != 0)
	{
		pSourceVoice->GetState(&state);
	}
}

再生するときに、ワイド文字列を渡しているので、「""」の前に「L」と書くのを忘れないようにお願いします。

if文の中身は、音が再生し終わるまでループするっていう処理を書いているだけなので深く気にしないでください。
どうですか?音なりましたか?

多分鳴ったと思います。
ただし鳴らない可能性として、Waveファイルのフォーマットの「wFormatTag」が現在「PCM形式」のものでないとならないようにしています。
ならない場合は、PCM形式に変換してきてください。
(私の渡したサンプルボイスは、問題なく動くはずです)

追加で

解放を自動でやってくれる「スマートポインタ」というものがあります。
XAudio2で使うスマートポインタは「ComPtr」です。
MicrosoftのXAudio2のページでも、ComPtrを使うことをおすすめしているので、このプロジェクトではComPtrにしなくてもいいと思いますが、実際にフレームワーク等に実装する際はComPtrを使って実装してみましょう。

おわりに

フレームワークを自作している人の手助けになれば幸いです。

ここから、例えば音量を調節したり、ピッチを調節したりすることもできます。
もっと言えば、音にエフェクトをかけることもできます。ローパスフィルタ・ハイパスフィルタとかで検索をかけてもらえばもっと可能性が広がるかもしれません。

これを実装してサウンドプログラムに興味を持った人は、もっと調べてみたり、XAudio2の書籍もあるので購入を検討するのもありかと思います。

引用

WAVEファイル読み込む際に参考にさせていただいた記事
https://yttm-work.jp/directx/directx_0034.html

神戸電子専門学校ゲーム技術研究部

Discussion