ゆとりーなの日記

日記的な事を書いて行くと思はれる

XAudioの時代

最近MSDNなんかではDirectSoundよりもXAudio使った方がいいよ的な空気を出していましたが、サンプルをいじったのをコンパイルして実行してもなんかエラーで落ちるので今までは華麗にスルーしていました。去年の初めのころの話です。当時はどういうわけか解説ページも殆どなく、まあDirectSoundで間に合っているしいいかみたいなノリでここまで来たわけです。あとXAudioは遅いらしいという話を聞いていたのも敬遠していた理由です。しかしね、やはり新しい雰囲気を醸し出しているXAudioをいつかはいじってみたいとは思っていました。何せVista以降DirectSoundでは3D音声は使えませんし(私が使うことあるかは不明ですが)、特に通知関係の方でデバイスに因っては上手く動かないので自前でスレッド、或いは代替物を回してストリーミング再生する必要があったりと不満もありましたので。
そんな中、XAudio2を使ってみる。まとめ - while( c++ );にて、XAudioの使い方が纏められていたので、これを参考にDirectSoundで書いていた効果音再生クラスを置き換えてみました。
とりあえずインターフェイス的なものを。

class SoundPlayer {
public:
  explicit SoundPlayer(const char *fileName);
      ~SoundPlayer();
  void     play();
  void     stop();
  void     setVolume(float volume);
  float    volume() const;
  bool     fail() const;
  operator void*() const;
  bool     operator !() const;
private:
  IXAudio2SourceVoice *m_voice;
  bool                 m_isNoError;
  std::vector<char>    m_buffer;
  bool                 initSoundPlayer(const char *fileName);
};

メソッドの働きは名前からお察し下さい。
それでは実装です。まずはコンストラクタから。

SoundPlayer::SoundPlayer(const char *fileName) : m_voice(NULL), m_isNoError(true), m_buffer(0) {
  m_isNoError = initSoundPlayer(fileName);
}

取り敢えず初期化するためのプライベートメソッドを呼びます。返り値が成功したかどうかのboolなんで、これでエラーフラグに代入しときます。例外なげてもいいんですが、IXAudio2SourceVoiceをスマートポインタ化するのが面倒なのと(これは地味にIUnknown継承ではないという事実におととい気付きました。Release()がないって怒られました。)効果音読み込みに失敗しても何事もなかったかのように無音で動く同人ゲームが多いので、あえてfstream系のようにしてみました。explicitを付けるかどうかはCプラーのステータスらしいです。
初期化するためのプライベートメソッドです。

bool SoundPlayer::initSoundPlayer(const char *fileName) {
  std::ifstream fin(fileName, std::ios::in | std::ios::binary);

  if (fin) {
	char riff[4] = {'R', 'I', 'F', 'F'};
	char readData[4];
	fin.read(readData, sizeof(readData));
	if (memcmp(riff, readData, 4)) {
	  assert((0) && ("RIFFじゃない"));
	  return false;
	} 

	fin.ignore(4);

	char wave[4] = {'W', 'A', 'V', 'E'};
	fin.read(readData, sizeof(readData));
	if (memcmp(wave, readData, 4)) {
	  assert((0) && ("WAVEじゃない"));
	  return false;
    } 

	char fmt[4] = {'f', 'm', 't', ' '};
	fin.read(readData, sizeof(readData));
	if (memcmp(fmt, readData, 4)) {
	  assert((0) && ("fmt じゃない"));
	  return false;
	} 

	unsigned long fmtSize;
	fin.read(reinterpret_cast<char*>(&fmtSize), sizeof(fmtSize));

	unsigned short id;
	fin.read(reinterpret_cast<char*>(&id), sizeof(id));

	unsigned short formatTag;

	switch (id) {
	  case 1:
		formatTag = WAVE_FORMAT_PCM;
		break;
	  case 2:
		formatTag = WAVE_FORMAT_ADPCM;
		break;
	  default:
		return false;
	}

	unsigned short channel;
	fin.read(reinterpret_cast<char*>(&channel), sizeof(channel));

	unsigned long sample;
	fin.read(reinterpret_cast<char*>(&sample), sizeof(sample));

	unsigned long speed;
	fin.read(reinterpret_cast<char*>(&speed), sizeof(speed));
		
	unsigned short block;
	fin.read(reinterpret_cast<char*>(&block), sizeof(block));

	unsigned short bit;
	fin.read(reinterpret_cast<char*>(&bit), sizeof(bit));

	fin.ignore(fmtSize - 16);

	char data[4] = {'d', 'a', 't', 'a'};
	fin.read(readData, sizeof(readData));
	if (memcmp(data, readData, 4)) {
	  assert((0) && ("dataじゃない"));
	  return false;
	} 

	unsigned long size;
	fin.read(reinterpret_cast<char*>(&size), sizeof(size));

	m_buffer.resize(size);
	fin.read(&m_buffer[0], size);

	WAVEFORMATEX format    = {0};
	format.wFormatTag      = formatTag;
	format.nChannels       = channel;
	format.wBitsPerSample  = bit;
	format.nSamplesPerSec  = sample;
	format.nBlockAlign     = block;
	format.nAvgBytesPerSec = speed;
		
	if (!createSoundBuffer(m_voice, format)) {
	  return false;
	}

	XAUDIO2_BUFFER submit = {0};
	submit.AudioBytes     = m_buffer.size(); 
	submit.pAudioData     = reinterpret_cast<unsigned char*>(&m_buffer[0]);
	submit.Flags          = XAUDIO2_END_OF_STREAM;

	m_voice->SubmitSourceBuffer(&submit);

	  return true;
	} else {
	  assert((0) && ("ファイルオープンに失敗"));
	  return false;
	}
}

正直長すぎますね。要分割ですかね。まあ主な原因はmmioを使っていないい事に起因するのですがあれってmmioOpenの第一引数がLPSTRことchar*なんでちょっと使いにくいんですよね。std::stringとかとも相性悪いですし。実際std::stringをmmioOpenに渡すのって、どう書くのが正しいんですかね。まさかこれだったりすることはないはずです。

  std::string str("seikimatsu.wav");
  MMIOINFO mmioInfo;
  mmioOpen(const_cast<LPSTR>(str.c_str()), &mmioInfo, MMIO_READ);

というわけで、自前でstd::ifstream使ってwaveファイルを読み込みます。ヘッダ情報を順番に読み込むだけです。普通のwaveファイルならこれで読み込めるはずです。多分。この辺りは別クラスにするのがいいかもしれません。
読み込んだ情報をWAVEFORMATEXに渡してやります。計算でも止まるメンバもありますが、せっかくファイルから読み込んだのでその値を使ってあげています。あと、データサイズ分のバッファとして、std::vectorをresizeしてやります。std::vectorの中身はcharにするかunsigned charにするか悩むところですが、どっちの方がキャストが多くなるかと考えると、恐らくunsigned charなので今回はcharにしてみました。実際oggを読むときもcharのほうが相性がいいです。ファイル読み込み系はchar*が基本ですからね。対してunsigned charが便利なのはXAudio側にバッファを渡すとき位ですかね。未だにreinterpret_castの綴りが覚えられないんで、キャストは少ない方がいいんです。そういう問題ではないですね。はい。
正規ルートの一番最後でバファとIXAudio2SourceVoiceをつないでいます。DirectSoundの時は、メモリをロックして書きこんでアンロック、デバイスロストにも対処してと結構複雑だったことがなんか5行位で済んでしまいます。
最後の方にcreateSoundBufferという関数が出てきますが、これが指定フォーマットでIXAudio2SourceVoice*を初期化する関数で、こんな感じになっています。

bool createSoundBuffer(IXAudio2SourceVoice *&voice, WAVEFORMATEX &format) {
  if (FAILED(g_xAudio->CreateSourceVoice(&voice, &format))) {
	return false;
  } else {
	return true;
  }
}

ポインタの参照の宣言は正直気持ち悪いです。*と&どっちが左かよく忘れます。実際に試してコンパイル通った方が正しいに違いない戦法はどこまで通じるんですかね。正直ダブルポインタでもいい気がしてきました。
g_xAudioというグローバル丸出しの変数が、XAudioの親玉です。プログラムの最初の方に次のような初期化コードを実行しておく必要があるのはいうまでもありません。

namespace {

IXAudio2               *g_xAudio;
IXAudio2MasteringVoice *g_masteringVoice;

}

bool initXAudio2() {
  if (CoInitializeEx(NULL, COINIT_MULTITHREADED)) {
    return false;
  }

  UINT32 flag = 0;

#ifndef NDEBUG

  flag |= XAUDIO2_DEBUG_ENGINE;

#endif

  if (FAILED(XAudio2Create(&g_xAudio, flag))) {
    return false;
  }
	
  if (FAILED(g_xAudio->CreateMasteringVoice(&g_masteringVoice))) {
    return false;
  }

  return true;
}

DirectSoundに無理やり当てはめるとIXAudio2*がIDirectSound8*、IXAudio2MasteringVoice*がプライマリバッファといったところですね。どちらも初期コード的にはあまり変わりません。しいて言えばXAudioにはHWNDを渡す必要がないですね。コンソールでも音が鳴らしやすいはずです。あとは、CoInitializeExを呼ぶ必要があるみたいです。
クラスにしなかったのは、IXAudio2*もIXAudio2MasteringVoice*を二つ以上作りたくなる状況が想像できなかったからです。シングルトンは面倒臭いので作らない時は作りません。エラーコード返してますが、流石にこの初期化に失敗してプログラム継続することはなさそうなので例外でもいい気はします。因みに今までXAudioでミスっていたのは多分#ifndef NDEBUGの所ではないかと思っています。こんなのいらんべといって消していたのは若気の至りです。
プログラムの最後には一応後始末コードも読んでおくのが礼儀ですね。

void creanUpXAudio2() {
  if (g_masteringVoice) {
	g_masteringVoice->DestroyVoice();
	g_masteringVoice = NULL;
  }

  if (g_xAudio) {
	g_xAudio->Release();
	g_xAudio = NULL;
  }
  
  CoUninitialize();
}

CoInitializeExとCoUninitializeはスレッド毎に一回呼べばいいらしいです。COMを使うときのお約束ですね。因みにこれ、同スレッドで2回以上呼ぶとどうなるんですかね。Direct3DとかもCOMのはずなのに普通はCoInitializeExなんか書きません。Direct3DCreate9あたりで呼ばれているんですかね。もしかしてDirect3Dの初期化コードの後ならCoInitializeEx要らないとかいう説はあるんですかね。今までDirect3Dの初期化コードの後にCoInitializeEx呼んでも(逆もまた然り)落ちた記憶はないんであまり気にしなくていいのかもしれませんが。
SoundPlayerクラスに戻ります。
で、デストラクタです。

SoundPlayer::~SoundPlayer() {
  if (m_voice) {
    m_voice->Stop();
    m_voice->DestroyVoice();
  }
}

矢張り止めてから後始末するのが王道らしいです。Release()ではないことに当初はびっくりしていました。個人的に。
肝心の再生やボリューム関係は一瞬で書けます。このあたりもDirectSoundとは変わりません。

void SoundPlayer::play() {
  if (m_voice) {
	m_voice->Start();
  }
}

void SoundPlayer::stop() {
  if (m_voice) {
	m_voice->Stop();
  }
}

void SoundPlayer::setVolume(float volume) {
  if (m_voice) {
	m_voice->SetVolume(volume, XAUDIO2_COMMIT_NOW);
  }
}

float SoundPlayer::volume() const {
  float volume = 0;
  if (m_voice) {
	m_voice->GetVolume(&volume);
  }
  return volume;
}

ループ再生とかはまだ考慮していませんがそのうちに。DirectSoundと違ってボリュームにはlongではなくfloatを渡すみたいです。
最後にオペレータ系を。

bool SoundPlayer::fail() const {
  return m_isNoError ? false : true;
}

SoundPlayer::operator void*() const {
  return m_isNoError ? reinterpret_cast<void*>(1) : NULL;
}

bool SoundPlayer::operator !() const {
  return fail();
}

この辺りはfstream系と同じ感じにしておきました。しかしoperator boolではなくoperator void*だったとは驚きです。