52880.fb2
In this tutorial we will learn how to play music and sounds with DirectX. We will use DirectX Audio to play wav and midi files, we will also use DirectShow to play mp3 files. In this tutorial we will have a simple application that plays a background track (in mp3 format) and we will allow the user to use the mouse to play some sound effects (in wav format) whenever they click on a coloured number. You can download the full source code by clicking the "Download Source" link above.
So far in our tutorials we have used Direct3D and DirectInput for graphics and user input. Now I'm going to introduce you to two more components of DirectX: DirectX Audio and DirectShow. We use DirectX Audio to play wav and midi audio files. We use DirectShow to play streaming media such as full motion video (avi) and high quality audio (mp3) files. In this tutorial we will only be looking at how to play mp3's with DirectShow.
So when should I use which type of file format? Well, that is largely a matter of personal preference. Before I tell you my preference, here is some information about each format.
Wav files
Wav files are pure, uncompressed digital audio. It is an actual, digital recording similar to that stored on a CD. Uncompressed digital audio is the only true "CD quality" audio. But wav files can be massive. Even a short track can take up 20 or 30 megabytes of space, often much more.
Midi files
Midi stands for "Musical Instrument Digital Interface". Midi files do not actually contain music recordings, instead they hold a set of instructions on how to play a tune. Midi files are very small which is good if you plan to have a downloadable version of your game on a website. The quality of playback dependents on the sound card of your user's machine. A Midi sequence that sounds great on a high-end card may sound terrible on a cheap one. Also, Midi is for instrumentals only, not vocals.
Mp3 files
As with wav files, mp3 files are actual digital recordings. But the major difference between mp3 and wav is that mp3 files are compressed, and are typically one-tenth the size of uncompressed files. Because mp3 files are compressed, they are a lossy format. This means that depending on how they are compressed, a certain degree of quality will be lost. However, you can still get "almost" CD quality audio from an mp3 file as long as the compression settings are right. Also, mp3 files are a "streaming media" that means that when they are played the whole track is not loaded at the start. Instead, only a part of the track is loaded from disk at a time as it is required.
My preference
I would say that you probably want to use mp3 or midi files for background music. Especially if you want your users to download you game from a website. Mp3 and midi files are both small and therefore good for long background tracks. I would then tend to use wav files for short sound effects like explosions and power-ups for that extra bit of quality. If file size is not a problem, then why not use wav files for all sounds and music?
Before we can start the actual coding, we need to add some new header and library files to our project. I've included the new header files in Game.h just below where I have included d3dx8.h. You can add the library files by going to Project > Settings… then on the Link tab, type the new library file names into the Object/Library Modules input box. The new files are listed below:
· dmusici.h
· dsound.h
· dshow.h
· dsound.lib
· strmiids.lib
To setup DirectX Audio we need to create two objects: the performance object and the loader object. The performance object is the top-level object in DirectX Audio, it handles the flow of data from the source to the synthesizer. The loader object loads the files (wav and midi) into sound segments that can be played later. We only need one of each of these objects for the whole application, so we will create them as member variables of our CGame class. Their definitions are shown below:
IDirectMusicPerformance8* m_pDirectAudioPerformance;
IDirectMusicLoader8* m_pDirectAudioLoader;
Before we can create our objects, we need to initialise the COM library. We need to do this because DirectX Audio is pure COM. Don't worry too much about what this means, all you need to do is call the CoInitialize function (shown below) before you can create the DirectX Audio objects. To keep it simple, this is done in the CGame constructor.
CoInitialize(NULL);
Now that we have initialised COM, we need to create and initialise our two DirectX Audio objects. To do this, we have a new method of CGame called InitialiseDirectAudio which is shown below. This method is called from our Initialise method, take a look at the code and I'll explain in a moment.
bool CGame::InitialiseDirectAudio(HWND hWnd) {
LogInfo("<br>Initialise DirectAudio:");
//Create the DirectAudio performance object
if (CoCreateInstance(CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance8, (void**) &m_pDirectAudioPerformance) != S_OK) {
LogError("<li>Failed to create the DirectAudio perfomance object.");
return false;
} else {
LogInfo("<li>DirectAudio perfomance object created OK.");
}
//Create the DirectAudio loader object
if (CoCreateInstance(CLSID_DirectMusicLoader, NULL, CLSCTX_INPROC, IID_IDirectMusicLoader8, (void**) &m_pDirectAudioLoader) != S_OK) {
LogError("<li>Failed to create the DirectAudio loader object.");
return false;
} else {
LogInfo("<li>DirectAudio loader object created OK.");
}
//Initialise the performance object
if (FAILED(m_pDirectAudioPerformance->InitAudio(NULL, NULL, hWnd, DMUS_APATH_SHARED_STEREOPLUSREVERB, 64, DMUS_AUDIOF_ALL, NULL))) {
LogError("<li>Failed to initialise the DirectAudio perfomance object.");
return false;
} else {
LogInfo("<li>Initialised the DirectAudio perfomance object OK.");
}
//Get the our applications "sounds" directory.
CHAR strSoundPath[MAX_PATH];
GetCurrentDirectory(MAX_PATH, strSoundPath);
strcat(strSoundPath, "\\Sounds");
//Convert the path to unicode.
WCHAR wstrSoundPath[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, strSoundPath, –1, wstrSoundPath, MAX_PATH);
//Set the search directory.
if (FAILED(m_pDirectAudioLoader->SetSearchDirectory(GUID_DirectMusicAllTypes, wstrSoundPath, FALSE))) {
LogError("<li>Failed to set the search directory '%s'.", strSoundPath);
return false;
} else {
LogInfo("<li>Search directory '%s' set OK.", strSoundPath);
}
return true;
}
So, what does this code do? Well, we use CoCreateInstance to create our performance and loader objects. Once we have done this, we need to initialise the performance object by calling the InitAudio method. The parameters used above for InitAudio are fairly typical, so you will probably just want to use the same. Take a look in the SDK for a full description of the InitAudio method and it's parameters. Finally, we set the search directory for our loader object. The search directory is the folder that the loader object will look in to find the files that you want to load. In the code above, we will set the search directory to the "Sounds" folder which is inside our project folder. Now we are ready to load and play sounds.
Now that we have created and initialised DirectX Audio, we can load and play sounds and music. To help us with this, I have created a new class called CSound which is shown below. Take a quick look at the methods, and I'll explain how it works.
CSound::CSound() {
m_pDirectAudioPerformance = NULL;
m_pDirectAudioLoader = NULL;
m_pSegment = NULL;
m_pGraph = NULL;
m_pMediaControl = NULL;
m_pMediaPosition = NULL;
m_enumFormat = Unknown;
LogInfo("<li>Sound created OK");
}
void CSound::InitialiseForWavMidi(IDirectMusicPerformance8* pDirectAudioPerformance, IDirectMusicLoader8* pDirectAudioLoader) {
m_pDirectAudioPerformance = pDirectAudioPerformance;
m_pDirectAudioLoader = pDirectAudioLoader;
m_enumFormat = WavMidi;
}
void CSound::InitialiseForMP3() {
CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC, IID_IGraphBuilder, (void**)&m_pGraph);
m_pGraph->QueryInterface(IID_IMediaControl, (void**)&m_pMediaControl);
m_pGraph->QueryInterface(IID_IMediaPosition, (void**)&m_pMediaPosition);
m_enumFormat = MP3;
}
CSound::~CSound() {
Stop();
SafeRelease(m_pSegment);
SafeRelease(m_pGraph);
SafeRelease(m_pMediaControl);
SafeRelease(m_pMediaPosition);
LogInfo("<li>Sound destroyed OK");
}
bool CSound::LoadSound(const char* szSoundFileName) {
WCHAR wstrSoundPath[MAX_PATH];
CHAR strSoundPath[MAX_PATH];
switch(m_enumFormat) {
case MP3:
//Get the our applications "sounds" directory.
GetCurrentDirectory(MAX_PATH, strSoundPath);
strcat(strSoundPath, "\\Sounds\\");
strcat(strSoundPath, szSoundFileName);
//Convert the path to unicode.
MultiByteToWideChar(CP_ACP, 0, strSoundPath, –1, wstrSoundPath, MAX_PATH);
m_pGraph->RenderFile(wstrSoundPath, NULL);
break;
case WavMidi:
//Convert the filename to unicode.
MultiByteToWideChar(CP_ACP, 0, szSoundFileName, –1, wstrSoundPath, MAX_PATH);
//Load a sound
m_pDirectAudioLoader->LoadObjectFromFile(CLSID_DirectMusicSegment, IID_IDirectMusicSegment8, wstrSoundPath, (void**) &m_pSegment);
m_pSegment->Download(m_pDirectAudioPerformance);
break;
default:
return false;
}
return true;
}
bool CSound::Play(DWORD dwNumOfRepeats) {
switch(m_enumFormat) {
case MP3:
//Make sure that we are at the start of the stream
m_pMediaPosition->put_CurrentPosition(0);
//Play mp3
m_pMediaControl->Run();
break;
case WavMidi:
//Set the number of times the sound repeats
m_pSegment->SetRepeats(dwNumOfRepeats); //To loop the sound forever, pass in DMUS_SEG_REPEAT_INFINITE
//Play the loaded sound
m_pDirectAudioPerformance->PlaySegmentEx(m_pSegment, NULL, NULL, 0, 0, NULL, NULL, NULL);
break;
default:
return false;
}
return true;
}
bool CSound::Stop() {
switch(m_enumFormat) {
case MP3:
m_pMediaControl->Stop();
break;
case WavMidi:
//Stop the loaded sound
m_pDirectAudioPerformance->StopEx(m_pSegment, 0, 0);
break;
default:
return false;
}
return true;
}
bool CSound::IsPlaying() {
switch(m_enumFormat) {
case MP3:
REFTIME refPosition;
REFTIME refDuration;
m_pMediaPosition->get_CurrentPosition(&refPosition);
m_pMediaPosition->get_Duration(&refDuration);
if (refPosition < refDuration) {
return true;
} else {
return false;
}
break;
case WavMidi:
if (m_pDirectAudioPerformance->IsPlaying(m_pSegment, NULL) == S_OK) {
return true;
} else {
return false;
}
break;
default:
return false;
}
}
So, how can I play a sound with CSound? Simple really, first create a new CSound object, then call either InitialiseForWavMidi or InitialiseForMP3 depending on what type of sound you want to play. Next, call LoadSound which is where you specify which file to play. Finally, call Play to play the sound that you have loaded. That's it! There are also two other methods: Stop which will stop the sound from playing and IsPlaying which tells you if a sound is playing or not. Below is a brief explanation of how each function works.
InitialiseForWavMidi
InitialiseForWavMidi initialises the CSound object for playing wav or midi files. The two parameters are the performance and loader pointers that we created earlier in CGame. These pointers are then saved as member variables for later use. We also set the format of this CSound object to WavMidi.
InitialiseForMP3
InitialiseForMP3 initialises the CSound object for playing mp3 files. There are no parameters. InitialiseForMP3 uses CoCreateInstance to create a DirectShow filter graph manager object. We can use CoCreateInstance here because CoInitialize has already been called from our CGame constructor. From the filter graph manager object we use the QueryInterface method to create two other objects: a media control object and a media position object. These three pointers are saved as member variables for later use. We also set the format of this CSound object to MP3.
LoadSound
Once we have initialised the CSound object, we can use LoadSound to load a given file. The single parameter of LoadSound is the filename of the sound to load. This is not a path because all sounds will be loaded from the Sounds folder which is inside our project folder.
If the format of this CSound object is MP3, we first build up the full path of the file and convert it to a unicode string. Next, all we need to do is call the RenderFile method of the filter graph manager to construct a filter graph that will play the specified file.
If the format of this CSound object is WavMidi, we first convert the filename passed into a unicode string. Next we call LoadObjectFromFile which loads the file and returns a segment pointer. Lastly, we call the Download method of our segment which downloads the band data to the performance.
We are now ready to start playing sounds.
Play
Once a file has been loaded, we can play it. This single parameter of the Play method is optional (default is 0) and is the number of times you would like the sound to repeat. This parameter is only used if you are playing wav or midi files.
If the format of this CSound object is MP3, we first make sure that we are at the start of the stream by calling the put_CurrentPosition method of the media position object that we created earlier. Once this is done, we play the mp3 by calling the Run method of the media control object.
If the format of this CSound object is WavMidi, we first set the number of times that the sound should repeat. Then we play the loaded segment by calling the PlaySegmentEx method. If you would like the sound to repeat forever, pass in the constant DMUS_SEG_REPEAT_INFINITE.
Stop
This very simply stops the sound from playing. We use the Stop method of the media control object to stop the sound if it's an mp3 file. If it's a wav or midi file we simply call the StopEx method of the perfomance object passing in the segment to stop.
IsPlaying
Finally, the IsPlaying method will return true if the sound is playing and false if it is not. If the file is an mp3, we get the current position that playback has reached and the total duration of the track. If the current position is not at the end, then we must still be playing the sound. If the file is a wav or midi, we simply use the IsPlaying method of the performance object passing in the segment to check.
In the CSound destructor we simply stop the sound from playing and release the objects. In CGame there is a new function called CleanUpDirectAudio which is shown below:
void CGame::CleanUpDirectAudio() {
//Stop all sounds.
m_pDirectAudioPerformance->Stop(NULL, NULL, 0, 0);
//CleanUp
m_pDirectAudioPerformance->CloseDown();
SafeRelease(m_pDirectAudioLoader);
SafeRelease(m_pDirectAudioPerformance);
LogInfo("<li>CleanUpDirectAudio finished.");
}
Here we first stop all sounds from playing (just in case), then we close down the performance object before releasing it and the loader object. Also, in the CGame deconstructor we need to call CoUninitialize to close down the COM library (shown below).
CoUninitialize();
So now we can play music and sounds in our applications. When the application starts, the background mp3 file plays. Move the mouse and click on a number to hear a different "sound effect". The background mp3 track is just a short loop so that I could keep the download size low, you can replace this with a larger track if you wish.
In this tutorial we learnt how to play sounds and music using two new DirectX components, DirectX Audio and DirectShow. In the next tutorial we will use all of the skills we have seen so far to create our first game: 3D Pong. This game will be simple, but will have all of the key features that you would expect from any game.