Home > Articles

  • Print
  • + Share This
This chapter is from the book

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.CPP—The main C/C++ source

  • T3DLIB3.H—The 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.H—The standard DirectSound header.

  • DMKSCTRL.H—All 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 macros—this is just a macro to help convert from 0–100 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 solution—it'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 flags—0 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_LOOPING—The sound is currently playing and is in loop mode.

  • DSBSTATUS_PLAYING—The 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 0–100 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);
  • + Share This
  • 🔖 Save To Your Account