EFFECTS
Proper use of real-time effects can make your game audio really shine. Add a slight echo when the player steps into a cave or a slight distortion when the communication signal breaks up, and you can really add to your player's sense of immersion.DirectX Audio provides several stock effects, such as echo, flange, chorus, distortion, and gargle (for a complete list, consult your DirectX documentation, at DirectX Audio\Using DirectX Audio\Using Effects\Standard Effects). Also, developers can write custom effects and use them from within their games.There are two main ways to use effects: you can set them up from inside DirectMusic Producer or you can apply them at runtime. Which method you should use depends on how your game audio is designed.Adding effects using DirectMusic Producer is handy if the effects won't change during the course of playback. For example, if you're authoring a DirectMusic project for a level of your game that takes place entirely within a cave, you could easily edit your default audio path so that all of your sound effects play through an echo effect. Conversely, if your player is going in and out of caves, this method won't work—you'll want to engage and disengage the echo effect at runtime, based on, say, the type of terrain (cave or not cave) underneath the current player position.The following sections will teach you both methods.
Adding Effects Using DirectMusic Producer
DirectMusic Producer makes adding effects easy. All you need to do is create an audio path for the segment you want to add an effect to. Do this by right-clicking on the segment and selecting New Audiopath. Usually, you'll want to base your audio path on a standard path, such as stereo sound or 3D sound, so select the appropriate option (when in doubt, just base it on standard stereo). The audio path editor, shown in Figure 12.7, will appear.

Figure 12.7: the audio path editor.
Right-click on the 1-128 mix group, and select Properties. To add custom effects, you'll need to uncheck the Use Standard Buffer checkbox (see Figure 12.8). Then, close the Properties dialog, and just drag-and-drop the effects you want from the effects palette on the right-hand side of your editing window to the mix group you want to apply them to. You should see a new effect instance appear in the Effects List column.

Figure 12.8: Ucheck the Use standard Buffer check - box to add custom effect to your audio path.
Once you've got an effect hooked up, you can edit its properties by right-clicking on it and selecting Properties, then clicking Edit Effect Properties. Clicking that button will bring up the Effect Properties dialog (see Figure 12.9). You can also cut, copy, and paste effects, and you can hear your changes at any time by playing the object whose audio path you're editing. Cool, isn't it?

Figure 12.9: The Effect Properties dialog for the echo effect.
Adding Effects at Runtime
Chapter 2, when you learned how to put raw bytes into a secondary DirectSound buffer and mix them out the primary buffer to the speakers. Underneath DirectMusic, this same process is happening, only it's not your code generating those bytes, it's the DirectMusic synthesizer.Every DirectMusic audio path is actually a DirectSound buffer, and every buffer can have multiple effects attached to it. As these secondary buffers get mixed onto the primary buffer, DirectX Audio applies the effects attached to them. Turning an effect on or off at runtime is accomplished by grabbing the correct DirectSound buffer and inserting or removing an effect from it.Each effect attached to a buffer has its own parameters structure and its own interface to set or retrieve the specific values for that effect. For example, if you wanted to change the delay of an echo, you'd grab the IDirectSoundFXEcho8 interface that you're interested in, fill in the appropriate members of a DSFXEcho parameter structure, and pass that structure to the SetAllParameters method of IDirectSoundFXEcho8.
Programming DirectX Audio Effects
Every effect has a GUID, defined in dsound.h. For example, the name of the echo effect's GUID is GUID_DSFX_STANDARD_ECHO. These GUIDs are how you tell DirectX Audio what kind of effects you want.To use an effect, you do two things. First, you have to get the DirectSound buffer interface of the audio path you want to add an effect to. Once you've got the buffer's interface, you call its SetFX method to attach all of your effects onto the buffer. Once the effect is attached, you can retrieve its interface and adjust its parameters.Hypothetically, here's how the process might look in code (assume that audiopath is the interface of the audio path you want to add an effect to):
// get audio path's underlying directsound buffer
hr = audiopath->GetObjectInPath(DMUS_PCHANNEL_ALL, DMUS_PATH_BUFFER, 0,
GUID_NULL, 0, IID_IDirectSoundBuffer8, (LPVOID*)&directsoundbuffer);
ThrowIfFailed(hr, "Couldn't get DirectSound buffer interface.");
// Add an echo effect.
DSEFFECTDESC dsEffect;
dsEffect.dwSize = sizeof(DSEFFECTDESC);
dsEffect.dwFlags = 0;
dsEffect.guidDSFXClass = GUID_DSFX_STANDARD_ECHO;
dsEffect.dwReserved1 = 0;
dsEffect.dwReserved2 = 0;
DWORD dwResults;
hr = directsoundbuffer->SetFX(1, &dsEffect, &dwResults);
ThrowIfFailed(hr, "Couldn't set effect.");
// get the effect's interface IDirectSoundFXEcho8* effect = NULL;
hr = audiopath->GetObjectInPath(DMUS_PCHANNEL_ALL,
DMUS_PATH_BUFFER_DMO, 0, GUID_All_Objects, 0, IID_IDirectSoundFXEcho8,
(LPVOID*) &effect);
ThrowIfFailed(hr, "Couldn't get effect interface.");
// set the effect's params
DSFXEcho params;
params.fWetDryMix = 50; // 50% echo (wet), 50% original wave (dry)
params.fFeedback = 50; // 50% of output fed back into input
params.fLeftDelay = 750; // wait 750ms, the echo out left speaker
params.fRightDelay = 500; // wait 500ms, then echo out right speaker
params.lPanDelay = 0; // don't swap echo
hr = effect->SetAllParameters(¶ms);
// release the effect's interface
SAFE_RELEASE(effect);
// release the buffer interface
SAFE_RELEASE(directsoundbuffer);
This code doesn't exist as written anywhere—in real-life it's scattered in a few different places (that you'll learn about later). But this is how it would look if you were dealing strictly with DirectX Audio; this code illustrates the minimum steps needed to apply an echo effect to a particular audio path (or should I say to the DirectSound buffer of a particular audio path).The code starts by grabbing the IDirectSoundBuffer8 interface contained in the audio path. That interface is stored in directsoundbuffer. Next, the code assembles a DSEFFECTDESC structure. This structure tells DirectX Audio what kind of effects are wanted. The SetFX method of the buffer interface allows you to add several effects at once; it expects an array of these DSEFFECTDESC structures, with each structure containing the GUID of an effect to be applied.The first parameter to SetFX specifies the number of DSEFFECTDESC structures you're sending; the second parameter is the address of the first structure in the array. The third parameter is the address of a DWORD array, with as many elements as there are DSEFFECTDESC structures. If anything goes wrong with a particular effect, DirectX Audio puts a code into the corresponding member of this array. This can be used to determine what specifically caused the call to SetFX to fail.Once SetFX returns successfully, the audio path has the echo effect attached to it, but the echo effect is using default values. So, the code grabs the interface for the echo effect's interface by calling GetObjectInPath. It then adjusts the parameters contained in the DSFXEcho structure, and sends the new values to the effect via a call to SetAllParameters. Finally, it releases the interfaces it has gathered.Again, that's a hypothetical code path—in reality, the different steps are spread over several classes, which you'll now learn about.
Adding Effects to the Audio Engine
The process of adding effects support to the audio engine is one that involves a few edits to the CSoundInstance class, as well as the creation of a handful of new classes (one for each type of standard effect supported by DirectX Audio: echo, flanger, chorus, and so on).When you're finished, you'll be able to use effects very simply, thanks to the C++ audio engine code provided in this section. Recall from Chapter 5 that if you want to play multiple sounds concurrently, you need a CSoundInstance for each instance of the sound:
CSoundInstance inst[numinsts];
for (int q=0; q < numinsts; q++) {
snd->Play(&inst[q]);
Sleep(500);
}
This code, taken from Ch5p2_SoundInstances, illustrates the basic idea: You create several CSoundInstance structures, and each time you play a sound, you give the Play method a unique instance it can use.I've designed the effects enhancements so that effects can be used following the same procedure. To use an effect, all you need to do is add it to a sound instance. Anything played on that sound instance will then have that effect. For example, here's how to do the same thing as above, only with an echo effect added to each instance:
CSoundInstance inst[numinsts];
for (int q=0; q < numinsts; q++) {
CEchoEffect *echo = new CEchoEffect;
// set parameters for echo effect
inst[q].AddEffect(echo);
// once you call AddEffect, the engine owns the effect pointer.
snd->Play(&inst[q]); // with echo!
Sleep(500);
}
Here you can see the required steps—first, you create a new echo effect class (CEchoEffect). Normally, right after you create the effect class, you'll want to fill up its m_Params member with the specific parameters for the effect, but I left that code out here to save space. Once you do that, you call the AddEffect method of CSoundInstance (which you'll learn about in the following sections), and the effect is magically applied to that instance. Any sounds played from that instance will have an echo applied to them!You can call AddEffect repeatedly to add additional effects, and if you want to clear everything and start over, you can call RemoveAllEffects. I didn't bother with making it easy to remove individual effects because games don't usually do that, but if you want to, it should be easy for you to add that method.
Tip | Be aware that the audio engine only supports one instance per effect type. That is, you can't have two echo effects on the same audio path. I did this so that the code would be easier to follow; if you want to implement support for multiple instances of the same effect, feel free! |
Now that you see how things work from a client's perspective, here's how to implement this functionality by changing the audio engine's code.
The Effects Classes
In the audio engine, each of the different effects is represented by an effects class. All of these effects classes derive from a common CEffect base class. CEffect looks like this:
class CEffect
{
public:
CEffect(); virtual ~CEffect();
virtual GUID GetGuid() = 0;
virtual void SetAllParameters(IDirectMusicAudioPath8 *audiopath) = 0;
};
The two important methods in the CEffect base class are GetGuid and SetAllParameters. These are both pure virtual functions, so they must be defined by all the derived effect classes. The GetGuid method returns the GUID for that particular effect (for example, GUID_DSFX_STANDARD_ECHO), and the SetAllParameters method applies the class's parameters to the effect on the given audio path. You'll learn more about SetAllParameters in the next section.Here's the echo effect class you saw earlier:
class CEchoEffect : public CEffect
{
public:
CEchoEffect() { ZeroMemory(&m_Params, sizeof(DSFXEcho)); }
virtual ~CEchoEffect() { }
virtual void SetAllParameters(IDirectMusicAudioPath8 *audiopath);
virtual GUID GetGuid() { return(GUID_DSFX_STANDARD_ECHO);}
DSFXEcho m_Params;
};
Here you can see that the CEchoEffect derives from CEffect and implements the two required pure virtuals. Notice also that I was lazy when writing the class, and just made the m_Params structure a public member. If you wanted to, you could protect this structure and create accessor functions to get and set a specific member of it (for example, GetWetDryMix and SetWetDryMix). You could also make the constructor fill in the m_Params structure with sensible default values, instead of just setting everything to zero.
Enhancements to CSoundInstance
The CSoundInstance class contains a few new functions and variables:
class CSoundInstance
{
public:
// existing stuff snipped to save space
void AddEffect(CEffect *effect);
void RemoveAllEffects();
void ApplyEffectsToAudioPath();
protected:
void ClearEffectsVector();
std::vector<CEffect *> m_Effects;
IDirectSoundBuffer8 *m_DirectSoundBuffer;
};
First, there's m_Effects, an STL vector of pointers to CEffect classes. This vector represents the list of all the effects attached to the instance. Second, there's a DirectSound buffer pointer, m_DirectSoundBuffer. This buffer is fetched from the audio path and is used to apply the effects by calling its SetFX method.There are also some new methods in CSoundInstance. The public effects interface is comprised of three methods: AddEffect, RemoveAllEffects, and ApplyEffectsToAudioPath. The ApplyEffectsToAudioPath method is a refresh method; if you change the parameters of an effect after you add it, you will need to manually call this function to send the new parameters to the effect. There's also a new protected method, ClearEffectsVector, that's called at destruction time and by RemoveAllEffects, and deletes all of the effects pointers in the m_Effects array.The AddEffect method is simple:
void CSoundInstance::AddEffect(CEffect *effect)
{
m_Effects.push_back(effect); ApplyEffectsToAudioPath();
}
As you can see, all it does is push the given effect pointer onto the vector, and then calls ApplyEffectsToAudioPath, which does the real work in getting the new effect onto the audio path.
RemoveAllEffects is similarly easy:
void CSoundInstance::RemoveAllEffects()
{
ClearEffectsVector();
ApplyEffectsToAudioPath();
}
The RemoveAllEffects method just clears the effect vector, and then relies on the ApplyEffectsToAudioPath method to inform DirectX Audio of the change.Here's the important method:
void CSoundInstance::ApplyEffectsToAudioPath()
{
HRESULT hr;
if (NULL == m_AudioPath || NULL == m_DirectSoundBuffer) return;
// deactivate audiopath
m_AudioPath->Activate(false);
// remove any effects possibly attached.
hr = m_DirectSoundBuffer->SetFX(0, NULL, NULL);
ThrowIfFailed(hr, "Couldn't remove effects chain.");
if (m_Effects.size() == 0) {
m_AudioPath->Activate(true);
return;
}
// our effects vector isn't empty, so loop through it and
// apply the effects to the audiopath
std::vector<DSEFFECTDESC> effectdescs;
std::vector<DWORD> effectdescresults;
for (std::vector<CEffect *>::iterator i = m_Effects.begin();
i != m_Effects.end(); ++i) {
DSEFFECTDESC edesc;
edesc.dwSize = sizeof(DSEFFECTDESC);
edesc.dwFlags = 0;
edesc.guidDSFXClass = (*i)->GetGuid();
edesc.dwReserved1 = 0;
edesc.dwReserved2 = 0;
effectdescs.push_back(edesc);
effectdescresults.push_back(0); // dummy value
}
hr = m_DirectSoundBuffer->SetFX(effectdescs.size(),
&effectdescs[0], &effectdescresults[0]);
ThrowIfFailed(hr, "Creation of effects chain failed.");
// feel free to add more detailed error msg processing here
// now that the effects are created, grab each of their
// interfaces and set their params.
for (std::vector<CEffect *>::iterator q = m_Effects.begin();
q != m_Effects.end(); ++q) {
(*q)->SetAllParameters(m_AudioPath);
}
// activate audiopath
m_AudioPath->Activate(true);
}
This code's job is to look at the m_Effect vector and put those effects onto the audio path. It starts by deactivating the audio path—this is a requirement, because the SetFX call will fail if the audio path is active. Next, it removes any effects that might already be present, by passing a zero and a couple of NULLs to SetFX. This is done in case any effects have been removed from the m_Effects vector. Note that at this point, the code returns if the m_Effects vector is empty.If the vector isn't empty, the code loops through it and creates a vector of DSEFFECTDESC structures. Vectors are really just arrays, so by using a vector, you avoid the hassle of having to allocate an array of the right size; instead, you just push things onto the vector. For each effect in the m_Effects vector, the code asks it for its GUID, puts that GUID into a new DSEFFECTDESC structure, and adds that structure to the effectdescs vector. Notice that it also pushes a zero onto the effectdescresults array. This is just so that by the time the loop ends, the effectdescresults array is big enough for SetFX.Once the loop ends, the code calls SetFX, giving it the vectors it created, along with the number of effects to create (a.k.a. the size of the m_Effects vector). The code doesn't do detailed error checking on SetFX—if it fails, you could enhance it by having it look at the effectdescresults array to figure out what effect caused it to fail.After SetFX returns successfully, the code again loops through each class in the m_Effects vector. It tells each effect class to apply its parameters to the audio path. Once that's done, the code activates the audio path and returns—everything's now set up!Now, all that remains to be seen is how the effects classes apply their parameters to the audio path.
Applying Effect Parameters
Each effect class has a SetAllParameters method that's supposed to apply the parameters contained in that effect class to the DirectX Audio effect object on the audio path. Here's what that code looks like:
void CEchoEffect::SetAllParameters(IDirectMusicAudioPath8 *audiopath)
{
HRESULT hr;
IDirectSoundFXEcho8* effect = NULL;
// get the effect's interface
hr = audiopath->GetObjectInPath(DMUS_PCHANNEL_ALL,
DMUS_PATH_BUFFER_DMO, 0, GUID_All_Objects, 0,
IID_IDirectSoundFXEcho8, (LPVOID*) &effect);
ThrowIfFailed(hr, "couldn't get effect interface.");
// set the effect's params
hr = effect->SetAllParameters(&m_Params);
// release the effect's interface
SAFE_RELEASE(effect);
}
The three main steps are to get the effect interface, call the SetAllParameters method of that interface, and release the interface. Notice that the interface is retrieved from the given audio path, not from a DirectSound buffer. Also, notice that all the effects use the DMUS_PATH_BUFFER_DMO flag; they differ only by their IID. This is because inside DirectX Audio, effects are actually DirectX Media Objects (DMOs)—they are used by other DirectX components, such as DirectShow. They're also used directly by other programs.Each effect class needs its own version of SetAllParameters, because each effect class must request a different DirectX Audio effect interface, and supply a different m_Params structure. You could probably write a template to eliminate some of the code duplication—if you're feeling adventurous, go for it!
Runtime Effect Programming Wrap-Up
You've now seen the complete workings of one particular effect: the echo effect. The source code on your CD contains complete implementations of the standard DirectX effects (for example, there's also a CGargleEffect, CFlangeEffect, and CParamEqEffect). The Ch12p2_Effects sample program demonstrates how a few of these effects sound. Feel free to edit the sample program to use the other effects, and to change the effects' parameters to achieve different sounds. (Although it's probably easier to do this in DirectMusic Producer so that you don't have to recompile each time you change an effect parameter!)
Effects Wrap-Up
A word of caution: use effects sparingly. Like lens flares and graphic violence, effects work best when applied strategically, and for a specific purpose. They are often applied with great subtlety. A common mistake is to look at all these cool new effects and start blanketing your game's audio with them. Resist the temptation to do this. Instead, apply effects with restraint and precision, and your game's audio will truly sound great.To learn more about effects, consult the DirectX documentation at DirectX Audio\Using DirectX Audio\Using Effects.