The T3DLIB3 Sound and Music Library
I have taken all the sound and music technology that we've been building and used it to create the next component to your game engine, T3DLIB3. It is composed of two main source files:
You'll also need to include the DirectSound import library, DSOUND.LIB, to make anything link. However, DirectMusic doesn't have an import library because it's pure COM, 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.H
DMKSCTRL.H
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.
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:
// number of midi segments that can be cached in memory
#define DM_NUM_SEGMENTS 64
// 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 macros; just a macro to help convert from 0–100 to the Microsoft decibels scale and one to convert multibyte characters to wide:
#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)
WARNING
The column width of this book is too small to fit the whole macro, so the definition is on two lines. This is a no-no in real life. Macros must be on a single line!
Next are the types for the sound engine.
The Types
First is the DirectSound object. 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, respectively, will be stored by the engine in the preceding two structures. Now let's take a look at the globals.
Global Domination
T3DLIB3 contains a number of globals. Let's take a look. First 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
// the directmusic performance manager
IDirectMusicPerformance *dm_perf;
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
The highlighted lines show 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 to do so. 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, and then DirectMusic.
The DirectSound API Wrapper
DirectSound can be simple or complicated, depending on how you use it. If you want a "do it all" API, you're going to end up using most of the DirectSound functions themselves. But if you want a simpler API that allows you to initialize DirectSound and load and play sounds of a specific format, that's a lot easier to wrap up into a few functions.
So what I've done is take much of your work in the DirectSound part of this chapter and formalize it into 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 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 11 KHz 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() initializes 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:
if (!DSound_Init(void))
{ /* error */ }
Function Prototype:
int DSound_Shutdown(void);
Purpose:
DSound_Shutdown() shuts down and releases all the COM interfaces created during DSound_Init(). However, DSound_Shutdown() will not release all the memory allocated to all the sounds. You must do this yourself with another function. Anyway, here's how you would shut down DirectSound:
if (!DSound_Shutdown())
{ /* error */ }
Function Prototype:
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 from the 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:
int fire_id = DSound_Load_WAV("FIRE.WAV");
// test for error
if (fire_id==-1)
{ /* error */}
Of course, it's up to you 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, 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:
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, let's say you have a gunshot sound and you want to fire three gunshots, one right after another. 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 solution—it's possible to create a duplicate (or replicant, if you're a Blade Runner fan) of the sound buffer, excluding for the actual sound data. Instead of copying it, you just point a pointer to it, and DirectSound is smart enough to be used as a "source" for multiple sounds using the same data. If you wanted to play a gunshot up to eight times, for example, you would 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, except that 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:
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 wish, 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 flags—0 for a single sound, or DSBPLAY_LOOPING to loop—and the sound will start playing. And if the sound is already playing, it will restart at the beginning. Here's an example of loading and playing a sound:
int fire_id = DSound_Load_WAV("FIRE.WAV");
DSound_Play_Sound(fire_id,0);
Or, you can leave 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, send DSBPLAY_LOOPING for the flags parameter.
Function Prototype:
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's playing already). 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:
DSound_Stop_Sound(fire_id);
And 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(), like this:
//...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:
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 ID number of the sound to the function, and the function will return one of these values:
If the value returned from DSound_Status_Sound() is neither of the these constants, the sound is not playing. Here's a complete example that waits until a sound has finished playing and then deletes it:
// 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 required 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 0–100, and the sound will change instantly. Here's an example of reducing the volume of a sound to 50 percent of what it was loaded as:
DSound_Set_Sound_Volume(fire_id, 50);
You can always change the volume back to 100 percent, 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 11 KHz mono, here's how you would double the perceived playback rate:
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:
DSound_Set_Sound_Pan(fire_id, 10000);
The DirectMusic API Rapper—Get It?
The DirectMusic API is even simpler than the DirectSound API. I have created functions to initialize DirectMusic and created all the COM objects for you to allow you to focus on loading and playing MIDI files. Here's the basic functionality list:
Initializing and shutting down DirectMusic with single calls.
Loading MIDI files from disk.
Playing a MIDI file.
Stopping a MIDI that is currently playing.
Testing the play status of a MIDI segment.
Automatically connecting to DirectSound if it's already initialized.
Deleting MIDI segments 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 DMusic_Init(void);
Purpose:
DMusic_Init() initializes DirectMusic and creates all necessary COM objects. You make this call before any other calls to the DirectMusic library. In addition, if you want to use DirectSound, make sure to initialize DirectSound before calling DMusic_Init(). Here's an example of using the function:
if (!DMusic_Init())
{ /* error */ }
Function Prototype:
int DMusic_Shutdown(void);
Purpose:
DMusic_Shutdown() shuts down the entire DirectMusic engine. It releases all COM objects in addition to unloading all loaded MIDI segments. Call this function at the end of your application, but before the call to shut down DirectSound (if you have DirectSound support). Here's an example:
if (!DMusic_Shutdown())
{ /* error */ }
// now shutdown DirectSound...
Function Prototype:
int DMusic_Load_MIDI(char *filename);
Purpose:
DMusic_Load_MIDI() loads a MIDI segment into memory and allocates a record in the midi_ids[] array. The function returns the ID of the loaded MIDI segment, or -1 if unsuccessful. The returned ID is used as a reference for all other calls. Here's an example of loading a couple MIDI files:
// load files
int explode_id = DMusic_Load_MIDI("explosion.mid");
int weapon_id = DMusic_Load_MIDI("laser.mid");
// test files
if (explode_id == -1 || weapon_id == -1)
{ /* there was a problem */ }
Function Prototype:
int DMusic_Delete_MIDI(int id);
Purpose:
DMusic_Delete_MIDI() deletes a previously loaded MIDI segment from the system. Simply supply the ID to delete. Here's an example of deleting the previously loaded MIDI files in the preceding example:
if (!DMusic_Delete_MIDI(explode_id) ||
!DMusic_Delete_MIDI(weapon_id) )
{ /* error */ }
Function Prototype:
int DMusic_Delete_All_MIDI(void);
Purpose:
DMusic_Delete_All_MIDI() simply deletes all MIDI segments from the system in one call. Here's an example:
// delete both of our segments
if (!DMusic_Delete_All_MIDI())
{ /* error */ }
Function Prototype:
int DMusic_Play(int id);
Purpose:
DMusic_Play() plays a MIDI segment from the beginning. Simply supply the ID of the segment you want to play. Here's an example:
// load file
int explode_id = DMusic_Load_MIDI("explosion.mid");
// play it
if (!DMusic_Play(explode_id))
{ /* error */ }
Function Prototype:
int DMusic_Stop(int id);
Purpose:
DMusic_Stop() stops a currently playing segment. If the segment is already stopped, the function has no effect. Here's an example:
// stop the laser blast
if (!DMusic_Stop(weapon_id))
{ /* error */ }
Function Prototype:
int DMusic_Status_MIDI(int id);
Purpose:
DMusic_Status() tests the status of any MIDI segment based on its ID. The status codes are
#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
Here's an example of changing state based on a MIDI segment completing:
// main game loop
while(1)
{
if (DMusic_Status(explode_id) == MIDI_STOPPED)
game_state = GAME_MUSIC_OVER;
} // end while
For a demo of using the library, check out DEMO10_5.CPP|EXE and DEMO10_6.CPP|EXE. The first program is a demo of DirectMusic using the new library, which allows you to pick a MIDI file from a menu and instantly play it. The second demo is a mixed-mode application that uses both DirectSound and DirectMusic at the same time. The important detail of the second demo is that DirectSound must be initialized first. The sound library detects this and then connects to DirectSound. Otherwise, the sound library would create its own DirectSound object.
WARNING
Both DEMO10_5.CPP and DEMO10_6.CPP use external cursor, icon, and menu resources contained in DEMO10_5.RC and DEMO10_6.RC, respectively. So make sure to include these files in the projects when you're trying to compile. And you need to include T3DLIB3.CPP|H to compile, but you knew that!
|