The T3DLIB3 Sound and Music Library
I have taken all the sound and music technology from the first Tricks and used it to create the last component to our game engine T3DLIB3. It is composed of two main source files:
T3DLIB3.CPPThe main C/C++ source
T3DLIB3.HThe header file
However, you'll also need to include the DirectSound import library DSOUND.LIB to make anything link. However, DirectMusic does not have an import library because it's a pure COM object, so there isn't a DMUSIC.LIB. On the other hand, you still need to point your compiler to the DirectSound and DirectMusic .H header files, so it can find them during compilation. Just to remind you, they are:
DSOUND.HThe standard DirectSound header.
DMKSCTRL.HAll of these are for DirectMusic.
DMUSICI.H
DMUSICC.H
DMUSICF.H
With all that in mind, let's take a look at the main elements of the T3DLIB3.H header file.
NOTE
With DirectX 8.0+, Microsoft integrated DirectSound and DirectMusic much more tightly and called it DirectAudio. I see no point in changing to DirectAudio, so we will simply use each component separately in this book.
The Header
The header file T3DLIB3.H contains the types, macros, and externals for T3DLIB3.CPP. Here are the #defines you'll find in the header:
#define DM_NUM_SEGMENTS 64 // number of midi segments that can be cached in memory // midi object state defines #define MIDI_NULL 0 // this midi object is not loaded #define MIDI_LOADED 1 // this midi object is loaded #define MIDI_PLAYING 2 // this midi object is loaded and playing #define MIDI_STOPPED 3 // this midi object is loaded, but stopped #define MAX_SOUNDS 256 // max number of sounds in system at once // digital sound object state defines #define SOUND_NULL 0 // " " #define SOUND_LOADED 1 #define SOUND_PLAYING 2 #define SOUND_STOPPED 3
Not much for macrosthis is just a macro to help convert from 0100 to the Microsoft decibels scale, and one to convert multibyte characters to wide characters:
#define DSVOLUME_TO_DB(volume) ((DWORD)(-30*(100 - volume))) // Convert from multibyte format to Unicode using the following macro #define MULTI_TO_WIDE( x,y ) MultiByteToWideChar( CP_ACP,MB_PRECOMPOSED, y,-1,x,_MAX_PATH)
CAUTION
The column width of this book might be too small to fit the whole macro, so the definition might be on two lines. This is a no-no in real life. Macros must be on a single line!
Next up are the types for the sound engine. First, the DirectSound object.
The Types
There are only two types for the sound engine: one to hold a digital sample, and the other to hold a MIDI segment:
// this holds a single sound typedef struct pcm_sound_typ { LPDIRECTSOUNDBUFFER dsbuffer; // the directsound buffer // containing the sound int state; // state of the sound int rate; // playback rate int size; // size of sound int id; // id number of the sound } pcm_sound, *pcm_sound_ptr;
And now the DirectMusic segment type:
// directmusic MIDI segment typedef struct DMUSIC_MIDI_TYP { IDirectMusicSegment *dm_segment; // the directmusic segment IDirectMusicSegmentState *dm_segstate; // the state of the segment int id; // the id of this segment int state; // state of midi song } DMUSIC_MIDI, *DMUSIC_MIDI_PTR;
Both sounds and MIDI segments will be stored by the engine in the preceding two structures, respectively. Now let's take a look at the globals.
Global Domination
T3DLIB3 contains a number of globals. Let's take a look. First, here are the globals for the DirectSound system:
LPDIRECTSOUND lpds; // directsound interface pointer DSBUFFERDESC dsbd; // directsound description DSCAPS dscaps; // directsound caps HRESULT dsresult // general directsound result DSBCAPS dsbcaps; // directsound buffer caps pcm_sound sound_fx[MAX_SOUNDS]; // array of sound buffers WAVEFORMATEX pcmwf; // generic waveformat structure
And here are the globals for DirectMusic:
// direct music globals IDirectMusicPerformance *dm_perf ; // the directmusic performance manager IDirectMusicLoader *dm_loader; // the directmusic loader // this hold all the directmusic midi objects DMUSIC_MIDI dm_midi[DM_NUM_SEGMENTS]; int dm_active_id; // currently active midi segment
NOTE
I have highlighted lines in the arrays that hold sounds and MIDI segments.
You shouldn't have to mess with any of these globals, except to access the interfaces directly if you want. In general, the API will handle everything for you, but the globals are there if you want to tear them up.
There are two parts to the library: DirectSound and DirectMusic. Let's take a look at DirectSound first.
The DirectSound API Wrapper
DirectSound can be complicated or simple, depending on how you use it. If you want a "do it all" API, you're going to end up simply using most of the DirectSound functions themselves. But if you want a simpler API that enables you to initialize DirectSound and load and play sounds of a specific format, that is a lot easier to wrap up in a few functions.
So, what I've done is take much of our work in setting up DirectSound and formalize it into a set of functions for you. In addition, I've created an abstraction around the sound system, so you refer to a sound with an ID (same for the DirectMusic part) that is given to you during the loading process. Thus, you can use this ID to play the sound, check its status, or to terminate it. This way, there aren't any ugly interface pointers that you have to mess with. The new API supports the following functionality:
Initializing and shutting down DirectSound with single calls
Loading .WAV files with 11KHz 8-bit mono format
Playing a loaded sound file
Stopping a sound
Testing the play status of a sound
Changing the volume, playback rate, or stereo panning of a sound
Deleting sounds from memory
Let's take a look at each function one by one.
NOTE
Unless otherwise stated, all functions return TRUE (1) if successful and FALSE (0) if not.
Function Prototype:
int DSound_Init(void);
Purpose:
DSound_Init() is used to initialize the entire DirectSound system. It creates the DirectSound COM object, sets the priority level, and so forth. Just call the function at the beginning of your application if you want to use sound. Here's an example:
Example:
if (!DSound_Init(void)) { /* error */ }
Function Prototype:
int DSound_Shutdown(void);
Purpose:
DSound_Shutdown() is used to shut down and release all the COM interfaces created during DSound_Init(). However, DSound_Shutdown() will not release all the memory allocated to all the sound. You must do this yourself with another function. Anyway, here's how you would shut down DirectSound:
Example:
if (!DSound_Shutdown()) { /* error */ }
Function Prototype(s):
int DSound_Load_WAV(char *filename);
Purpose:
DSound_Load_WAV() creates a DirectSound buffer, loads the sound data file into memory, and prepares the sound to be played. The function takes the complete path and filename of the sound file to be loaded (including the extension .WAV) and loads the file off disk. If successful, the function returns a non-negative ID number. You must save this number, because it is used as a handle to reference the sound. If the function can't find the file, or too many sounds are loaded, it will return -1. Here's an example of loading a .WAV file named FIRE.WAV:
Example:
int fire_id = DSound_Load_WAV("FIRE.WAV"); // test for error if (fire_id==-1) { /* error */}
Of course, it's up to you on how you want to save the IDs. You might want to use an array or something else.
Finally, you might wonder where the sound data is, and how to mess with it. If you really must, then you can access the data within the pcm_sound array sound_fx[] using the ID you get back from either load function as the index. For example, here's how you would access the DirectSound buffer for the sound with ID sound_id:
Example:
sound_fx[sound_id].dsbuffer
Function Prototype:
int DSound_Replicate_Sound(int source_id); // id of sound to copy
Purpose:
DSound_Replicate_Sound() is used to copy a sound without copying the memory used to hold the sound. For example, suppose you have a gunshot sound, and you want to fire three gunshots, each right after the other? The only way to do this right now would be to load three copies of the gunshot sound into three different DirectSound memory buffers, which would be a waste of memory.
Alas, there is a solutionit's possible to create a duplicate or replicant (if you're a Blade Runner fan) of the sound buffer, except for the actual sound data. Instead of copying it, we just point a pointer to it, and DirectSound is smart enough to use it as a "source" for multiple sounds using the same data. The bottom line is, if you want to play a gunshot up to eight times, for example, you should load the gunshot once, make seven copies of it, and acquire a total of eight unique IDs. Replicated sounds work exactly the same as normal sounds, but instead of using DSound_Load_WAV() to load and create them, you copy them with DSound_Replicate_Sound(). Get it? Good! I'm starting to get dizzy! Here's an example of creating eight gunshots:
Example:
int gunshot_ids[8]; // this holds all the id's // load in the master sound gunshot_ids[0] = Load_WAV("GUNSHOT.WAV"); // now make copies for (int index=1; index<8; index++) gunshot_ids[index] = DSound_Replicate_Sound(gunshot_ids[0]); // use gunshot_ids[0..7] anyway you which they all go bang!
Function Prototype:
int DSound_Play_Sound(int id, // id of sound to play int flags=0, // 0 or DSBPLAY_LOOPING int volume=0, // unused int rate=0, // unused int pan=0); // unused
Purpose:
DSound_Play_Sound() plays a previously loaded sound. You simply send the ID of the sound along with the play flags0 for a single time, or DSBPLAY_LOOPING to loop it, and the sound will start playing. If the sound is already playing, it will restart at the beginning. Here's an example of loading and playing a sound:
Example:
int fire_id = DSound_Load_WAV("FIRE.WAV"); DSound_Play_Sound(fire_id,0);
I could have also left out the 0 for flags entirely, because its default parameter is 0:
int fire_id = DSound_Load_WAV("FIRE.WAV"); DSound_Play_Sound(fire_id);
Either way, the FIRE.WAV sound will play once and then stop. To make it loop, you would send DSBPLAY_LOOPING for the flags parameter.
Function Prototype(s):
int DSound_Stop_Sound(int id); int DSound_Stop_All_Sounds(void);
Purpose:
DSound_Stop_Sound() is used to stop a single sound from playing (if it is playing). You simply send the ID of the sound, and that's it. DSound_Stop_All_Sounds() will stop all the sounds currently playing. Here's an example of stopping the fire_id sound:
Example:
DSound_Stop_Sound(fire_id);
At the end of your program, it's a good idea to stop all the sounds from playing before exiting. You could do this with separate calls to DSound_Stop_Sound() for each sound, or a single call to DSound_Stop_All_Sounds():
//...system shutdown code DSound_Stop_All_Sounds();
Function Prototype:
int DSound_Delete_Sound(int id); // id of sound to delete int DSound_Delete_All_Sounds(void);
Purpose:
DSound_Delete_Sound() deletes a sound from memory and releases the DirectSound buffer associated with it. If the sound is playing, the function will stop it first. DSound_Delete_All_Sounds() deletes all previously loaded sounds. Here's an example of deleting the fire_id sound:
Example:
DSound_Delete_Sound(fire_id);
Function Prototype:
int DSound_Status_Sound(int id);
Purpose:
DSound_Status_Sound() tests the status of a loaded sound based on its ID. All you do is pass the function the ID number of the sound, and the function will return one of these values:
DSBSTATUS_LOOPINGThe sound is currently playing and is in loop mode.
DSBSTATUS_PLAYINGThe sound is currently playing and is in single play mode.
If the value returned from DSound_Status_Sound() is neither of the preceding constants, the sound is not playing. Here's a complete example that waits until a sound has completed playing and then deletes it.
Example:
// initialize DirectSound DSound_DSound_Init(); // load a sound int fire_id = DSound_Load_WAV("FIRE.WAV"); // play the sound in single mode DSound_Play_Sound(fire_id); // wait until the sound is done while(DSound_Sound_Status(fire_id) & (DSBSTATUS_LOOPING | DSBSTATUS_PLAYING)); // delete the sound DSound_Delete_Sound(fire_id); // shutdown DirectSound DSound_DSound_Shutdown();
Pretty cool, huh? A lot better than the couple hundred or so lines of code to do it manually with DirectSound!
Function Prototype:
int DSound_Set_Sound_Volume(int id, // id of sound int vol); // volume from 0-100
Purpose:
DSound_Set_Sound_Volume() changes the volume of a sound in real time. Send the ID of the sound along with a value from 0100 and the sound will change instantly. Here's an example of changing the volume of a sound to 50% of what it was loaded as:
Example:
DSound_Set_Sound_Volume(fire_id, 50);
You can always change the volume back to 100% like this:
DSound_Set_Sound_Volume(fire_id, 100);
Function Prototype:
int DSound_Set_Sound_Freq( int id, // sound id int freq); // new playback rate from 0-100000
Purpose:
DSound_Set_Sound_Freq() changes the playback frequency of the sound. Because all sounds must be loaded at 11KHz mono, here's how you would double the perceived playback rate:
Example:
DSound_Set_Sound_Freq(fire_id, 22050);
And to make you sound like Darth Vader, do this:
DSound_Set_Sound_Freq(fire_id, 6000);
Function Prototype:
int DSound_Set_Sound_Pan( int id, // sound id int pan); // panning value from -10000 to 10000
Purpose:
DSound_Set_Sound_Pan() sets the relative intensity of the sound on the right and left speakers. A value of -10,000 is hard left and 10,000 is hard right. If you want equal power, set the pan to 0. Here's how you would set the pan all the way to the right side:
Example:
DSound_Set_Sound_Pan(fire_id, 10000);