dev.nlited.com

>>

XAudio2

<<<< prev
next >>>>

2016-01-24 03:39:23 chip Page 1534 📢 PUBLIC

Rocket has found its voice in XAudio2 today!

I used this tutorial as a starting point. XAudio2 is much simpler, with one odd exception, than DirectX. I went from knowing nothing about XAudio2 to playing audio in two hours.

XAudio2 is available on Win8 systems, it is not supported on Win7. _WIN32_WINNT must be defined to 0x0602 (or later) to compile.

The debug build caused problems as the debug version of the XAudio2 functions refer to the debug version of the C runtime (CRT) libraries. This forces the use of the debug multi-threaded libraries (/MTD) for the entire project, including my support libraries. I do not like linking to the debug libraries for a number of reasons, and typically link the release libraries even for debug builds. Rather than change my entire debug build to use libraries I don't want, it was easier to use the release version of the XAudio2 library. This was accomplished by undefining _DEBUG before including XAudio.h.


Sound.cpp: #undef _DEBUG #include <Windows.h> #include <xaudio2.h>

The XAudio2 functions are linked in by including XAudio2.lib.

XAudio2 uses a COM interface, so I need to call CoInitializeEx() and CoUninitialize() in WinMain(). There is some global initialization that needs to be performed, which I do in SoundMgrCreate().

WinMain.c: int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR CmdLine, int CmdShow) { int Err= ERR_OK; ghInst= hInst; Print(PRINT_DEBUG,"Rocket %s [%s]",gVerID.BuildStr,gVerID.BuildTime); CoInitializeEx(0,COINIT_MULTITHREADED); SoundMgrCreate(); if(IsErr(Err= MainCreate(&ghMain))) { Err= Error(Err,"WinMain: Unable to create Main window."); } else { MainLoop(ghMain); MainDestroy(ghMain); } SoundMgrDestroy(); CoUninitialize(); return(Err); }

I created a SoundMgr class that handles the global initializations and keeps track of the individual sounds.


Sound.cpp: #define SOUND_MAX 32 class SoundMgr { public: static SoundMgr *Ptr(HSOUNDMGR hMgr); static int Create(HSOUNDMGR *phMgr); int Destroy(void); int SoundCreate(HSOUND *phSound, const char *Name); int SoundDestroy(HSOUND hSound); int Play(HSOUND hSound, UINT Flags); int Stop(HSOUND hSound); //Data UINT Signature; private: SoundMgr(void); ~SoundMgr(void); int Create2(void); //Data IXAudio2 *pEngine; IXAudio2MasteringVoice *pMaster; HSOUND SoundList[SOUND_MAX]; };

The SoundMgr initialization is simple, just call XAudio2Create() to create a sound "engine" and then create the mastering voice.


Sound.cpp: int SoundMgr::Create2(void) { int Err= ERR_OK; HRESULT hErr; if(!SUCCEEDED(hErr= XAudio2Create(&pEngine))) { Err= Error(ERR_SYSCREATE,"SoundMgr:Create2: Unable to create XAudio2 engine. [%X]",hErr); } else if(!SUCCEEDED(hErr= pEngine->CreateMasteringVoice(&pMaster))) { Err= Error(ERR_SYSCREATE,"SoundMgr:Create2: Unable to create mastering voice. [%X]",hErr); } else { //OK } return(Err); }

A Sound class instance is created for each sound effect and they can be created and destroyed as needed. The sounds are managed by the SoundMgr.


Sound.cpp: class Sound { public: static Sound *Ptr(HSOUND hSound); static int Create(HSOUND *phSound,const char *Name,IXAudio2 *pEngine); int Destroy(void); int Play(UINT Flags); int Stop(void); //Data UINT Signature; private: Sound(void); ~Sound(void); int Create2(const char *Name,IXAudio2 *pEngine); //Data Wave Src; IXAudio2SourceVoice *pVoice; bool IsPlaying; }; int SoundMgr::SoundCreate(HSOUND *phSound, const char *Name) { int Err= ERR_OK; UINT n1; for(n1=0;n1<SOUND_MAX && SoundList[n1];n1++); if(n1>=SOUND_MAX) { Err= Warn(ERR_TOO_MANY,"SoundMgr:SoundCreate(%s): Too many sounds [%d]",Name,n1); } else if(IsErr(Err= Sound::Create(&SoundList[n1],Name,pEngine))) { Err= Warn(Err,"SoundMgr:SoundCreate(%s): Unable to create sound.",Name); } else { *phSound= SoundList[n1]; } return(Err); } int SoundMgr::SoundDestroy(HSOUND hSound) { int Err= ERR_OK; UINT n1; Sound *pSound= Sound::Ptr(hSound); if(!pSound) { Err= ERR_BADHANDLE; } else { Err= pSound->Destroy(); for(n1=0;n1<SOUND_MAX && SoundList[n1]!=hSound;n1++); if(n1<SOUND_MAX) SoundList[n1]= 0; } return(Err); } int SoundMgr::Play(HSOUND hSound, UINT Flags) { int Err= ERR_OK; Sound *pSnd= Sound::Ptr(hSound); if(!pSnd) { Err= ERR_BADHANDLE; } else { Err= pSnd->Play(Flags); } return(Err); } int SoundMgr::Stop(HSOUND hSound) { int Err= ERR_OK; Sound *pSnd= Sound::Ptr(hSound); if(!pSnd) { Err= ERR_BADHANDLE; } else { Err= pSnd->Stop(); } return(Err); }

I create two sounds: hSndFire for when a bullet is fired, and hSndThrust that plays continuously while the rocket's engine is "flame on".

Game.cpp: int Game::Create2(void) { int Err2,Err= ERR_OK; if(IsErr(Err2= SoundCreate(&hSndFire,"Resource\\Fire.wav"))) Warn(Err2,"Game:Create2: Unable to create Fire sound."); if(IsErr(Err2= SoundCreate(&hSndThrust,"Resource\\Thrust.wav"))) Warn(Err2,"Game:Create2: Unable to create thrust sound."); if(IsErr(Err= ItemsCreate(&hItems,(HGAME)this))) { Err= Error(Err,__FUNCTION__": Unable to create Items."); } else if(IsErr(Err= CreateItems())) { Err= Error(Err,__FUNCTION__": Unable to create starting items."); } return(Err); } int Game::Destroy(void) { int Err= ERR_OK; SoundDestroy(hSndFire); SoundDestroy(hSndThrust); delete this; return(Err); }

I play the sounds during the Game update. The Fire sound is a one-shot sound, so calling Play() while it is already playing will have no effect and there will be no overlapping sounds. Alternatively, I could create multiple instances of the fire sound and cycle through them to create overlapped sound effects.

The Thrust sound is played in a loop for as long as the player is firing the rocket engine.

The logic behind whether to actually start or stop a sound is handled inside the Sound class, so there is no harm in stupidly calling Play() and Stop().

Game.cpp: void Game::Sounds(void) { if(CtrlState & CTRL_FIRE) SoundPlay(hSndFire,0); if(CtrlState & CTRL_THRUST) SoundPlay(hSndThrust,SNDPLAY_LOOP); else SoundStop(hSndThrust); }

Creating a sound requires first determining data format of the audio samples, then feeding it to XAudio2. This is actually the most complex part of getting started. I was surprised that XAudio2 does not have an API that accepts a file handle to a WAV file. Especially since XAudio2 CreateSourceVoice() takes the WAVEFORMATEX header verbatim. Instead, I need to write the code to parse the WAV header, extract the WAVEFORMATEX chunk, and set the pointers to the beginning of the audio sample data.

Fortunately, Jay Tennant was kind enough to provide that code in the Wave.h file. The Wave class expects the audio to be in an external file; I will need to rewrite it if I want to generate my own waveforms or read the data from a resource. It is easier to manage the audio assets as separate files, so this approach makes sense even for production releases.

Assuming the file parser is already done, creating the Sound object is easy.


Sound.cpp: int Sound::Create2(const char *Name, IXAudio2 *pEngine) { int Err= ERR_OK; HRESULT hErr; if(!Src.load(Name)) { Err= Warn(ERR_SYSCREATE,"Sound:Create(%s): Unable to load WAV data.",Name); } else if(!SUCCEEDED(hErr= pEngine->CreateSourceVoice(&pVoice,Src.wf()))) { Err= Warn(ERR_SYSCREATE,"Sound:Create(%s): Unable to create voice. [%X]",Name,hErr); } else { //OK } return(Err); }

Only 8 or 16 bit, mono or stereo, audio is supported. Calling CreateSourceVoice() with 32bit audio will fail with a generic (unhelpful) error code.

Playing the sound is also easy. The only trick here is handling both one-shot and looping audio. Calling Play() on a one-shot sound that is still playing (still has at least one pending audio buffer) will return immediately, allowing the prior sound to complete. Calling Play() on a looping sound that is still playing will also return immediately.

There needs to be a matching Stop() for every Start(), so if IsPlaying is set and there are no pending buffers I call Stop() just to be polite.

This function assumes the entire sound is found in a single memory buffer. I create a copy of the original XAUDIO2_BUFFER because I need to set additional parameters if the sound is looped.

I start the audio voice and submit the buffer. SubmitSourceBuffer() will return immediately and the audio will play asynchronously.

Stop() is simple, in all cases I simply call pVoice->Stop() and clear the IsPlaying flag.


Sound.cpp: int Sound::Play(UINT Flags) { int Err= ERR_OK; HRESULT hErr; XAUDIO2_VOICE_STATE State; pVoice->GetState(&State); if(IsPlaying && State.BuffersQueued>0) { Err= 1; } else { if(IsPlaying) { pVoice->Stop(); IsPlaying= false; } XAUDIO2_BUFFER Buf= *Src.xaBuffer(); if(Flags & SNDPLAY_LOOP) { Buf.LoopBegin= Buf.PlayBegin; Buf.LoopLength= Buf.PlayLength; Buf.LoopCount= XAUDIO2_LOOP_INFINITE; } if(!SUCCEEDED(hErr= pVoice->Start())) { Err= Warn(ERR_SYSCREATE,"Sound:Play: Unable to start voice. [%X]",hErr); } else if(!SUCCEEDED(hErr= pVoice->SubmitSourceBuffer(&Buf))) { Err= Warn(ERR_SYSCREATE,"Sound:Play: Failed [%X]",hErr); } else { IsPlaying= true; } } return(Err); } int Sound::Stop(void) { int Err= ERR_OK; if(IsPlaying) { pVoice->Stop(); IsPlaying= false; } return(Err); }

That is all there is to it, for basic audio playback. I found it was fun and easy to record my own sound effects by simply making noises with my voice and then tweaking the audio.



WebV7 (C)2018 nlited | Rendered by tikope in 34.332ms | 18.190.153.77