Beginning Game Development Part IX –Direct Sound Part II
New Game Loop
In a number of post it was pointed out that the game was not running as fast as some of the DirectX samples. The reason behind this is that the game used an older and less efficient version of the game loop. Let's go ahead and change the code to use the new game loop based on this blog by Tom Miller.
First create a new class called NativeMethods and paste the Message struct and extern method declaration into it. The finished class should look like this.
C#1: using System;
2: using System.Runtime.InteropServices;
3:
4: namespace BattleTank2005
5: {
6: class NativeMethods
7: {
8: [StructLayout ( LayoutKind.Sequential )]
9: public struct Message
10: {
11: public IntPtr hWnd;
12: public uint msg;
13: public IntPtr wParam;
14: public IntPtr lParam;
15: public uint time;
16: public System.Drawing.Point p;
17: }
18:
19: [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
20: [DllImport ( "User32.dll", CharSet = CharSet.Auto )]
21: public static extern bool PeekMessage ( out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags );
22: }
23: }
Make sure to reference the System.Runtime.InteropServices namespace at the top of the class.
Next, add the OnApplicationIdle method into the GameEngine class
C#1: public void OnApplicationIdle ( object sender, EventArgs e )
2: {
3: while ( AppStillIdle )
4: {
5: // Render a frame during idle time (no messages are waiting)
6: _deltaTime = HiResTimer.GetElapsedTime ( );
7:
8: CheckForInput ( );
9:
10: Render ( );
11: }
12: }
Then add the AppStillIdle property to the GameEngine class.
C#
1: private bool AppStillIdle
2: {
3: get
4: {
5: NativeMethods.Message msg;
6: return !NativeMethods.PeekMessage ( out msg, IntPtr.Zero, 0, 0, 0 );
7: }
8: }
In the Program class change the Main method so it reads:
C#
1: [STAThread]
2: static void Main ( )
3: {
4: using ( GameEngine engine = new GameEngine ( ) )
5: {
6: engine.Initialize ( );
7: System.Windows.Forms.Application.Idle += new EventHandler ( engine.OnApplicationIdle );
8: Application.Run ( engine );
9: }
10: }
Finally, remove the entire OnPaint method from the GameEngine class. You can also remove the following line from the constructor of the game engine class.
C#
1: this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true);
You should see a pretty good improvement over the previous game loop. Just remember that there really is not big benefit it your game runs at 700 fps and is no fun to play, so don't get caught up in the speed over all other facets of the game.
Sound effects in Games
After adding the sound to BattleTank2005 in the last article, why would we bother with sound effects now? If we record the sounds ahead of time then they is really no reason to add effects to them - or is there. Sound effects are added to further manipulate sounds based on the active environment of the game. The simplest example would be to manipulate the base sound of footsteps and add an echo when the player is in a tunnel.
As you will see a little later, DirectSound also adds an effect which allows for the manipulation of sounds to match specific pre-defined environments.
Effects
There are nine sound effects in the DirectSound namespace which are referred to officially as Microsoft DirectX Media Objects (DMOs). Each class also has a large number of effect specific properties you can manipulate to pretty much get any sound effect you want. Here is the information from the SDK in a simple table format for easy reference.
Effect Name
|
Description
|
Chorus
|
A voice-doubling effect created by echoing the original sound with a slight delay and slightly modulating the delay of the echo.
|
Compressor
|
A reduction in the fluctuation of a signal above a certain amplitude.
|
Distortion
|
Adding harmonics to the signal in such a way that, as the level increases, the top of the waveform becomes squared off or clipped.
|
Echo
|
Causes sounds to be repeated after a fixed delay, usually at a diminished volume. As the repeated sounds are fed back into the mix, they are repeated again.
|
Flanger (Flange)
|
An echo effect in which the delay between the original signal and its echo is very short and varies over time.
|
Gargle
|
Modulates the amplitude of the signal.
|
Interactive3DLevel2Reverb (Reverb)
|
Environmental reverberation in accordance with the Interactive 3-D Audio, Level 2 (I3DL2) specification, published by the Interactive Audio Special Interest Group. See more on this later.
|
ParamEq
|
Amplifies or attenuates signals of a given frequency
|
WavesReverb
|
Intended for use with music and based on the Waves MaxxVerb
technology, which is licensed to Microsoft.http://www.waves.com/ |
Out of these effect classes the Interactive3DLevel2Reverb is a special class, since it provides what amounts to a large set of pre-built settings representing various environments.
The principle behind this class is based on the effects of the environment on the sounds we hear.
Basically sound we hear has three temporal components:
1. Direct Path: Straight from the source to the listener.
2. Early Reflections: Sounds that reach the listener after reflecting of one or two surfaces first. Surprisingly sounds that reflect of one surface are called first-order reflections; sounds that reflect of two surfaces are called second-order reflections and so on. Humans normally hear only first and second order reflections.
3. Late Reverberations: These are the combined of lower-order reflections.
In addition to the AllParameters property that all the other effect classes share, theReverb effect has two additional properties: Quality and Preset. The Quality property is simply an integer value with larger values representing higher quality at the cost of processing time. The default value is 2 and the values are bounded by QualityMax andQualityMin.
The Preset property is an EffectsEnvironmentPreset enumeration value that allows you to select pre-made environment settings without having to set the individual properties such as rolloff factors, diffusion etc. The enumeration includes 30 settings including settings for: Mountains, UnderWater, Bathroom and PaddedCell. This means that you can easily manipulate pre-recorded sounds to fit your game world better without having to record and ship sounds for each possible environment.
The ParamEqclass also has a non standard feature not shared by any of the other sound effect classes. You can use this effect class multiple times on the same buffer to achieve similar control over the sound as you would with a hardware equalizer.
Update the SoundEffects class
The first effect we are going to use in BattleTank2005 is the Interactive3DLevel2Reverb effect set to a preset value of Hangar (You have to play with the presets, some sounds do not come over very well. I originally wanted to use Plain as the preset, but one of the sounds ended up being distorted).
The first effect we are going to use in BattleTank2005 is the Interactive3DLevel2Reverb effect set to a preset value of Hangar (You have to play with the presets, some sounds do not come over very well. I originally wanted to use Plain as the preset, but one of the sounds ended up being distorted).
We want this effect to apply to all the sounds in the game, so we are going to add it to the constructor of the SoundEffectsClass.
First we need to update the SoundEffects class (slightly unfortunately named) and set the ControlEffect property of the buffer to true. This enables the buffer to be used with sound effects. In the constructor of the SoundEffects class add the following line of code.
C#
bufferDescription.ControlEffects = true;
Next we create the EffectDescription array, initializing it to the correct size. If you plan to add other effects make sure to change the size of the array accordingly. After the catch statement in the constructor add the following code.
C#
Microsoft.DirectX.DirectSound.EffectDescription[] effects = new Microsoft.DirectX.DirectSound.EffectDescription[ 1 ];
Next we set the desired effect class using the DSoundHelper class.
C#
effects[ 0 ].GuidEffectClass = DSoundHelper.StandardInteractive3DLevel2ReverbGuid;
Then we add the effect to the SecondaryBuffer for this sound.
C#
AddEffects ( effects );
Sidebar: Casting
As you may have noticed the order of the SoundEffect classes in the SecondaryBuffer is significant. If you attempt to cast the object returned by the GetEffects method to another Effect class you will get a System.InvalidCastException exception. You could soround the call with a try/catch but this means that the framework still has to create and throw the exception, just for you to turn around and catch it again. Never use exception handling as a flow control, if you can find another way do so.
C#
EchoEffect echo = (EchoEffect)_myEngineNoise.GetEffect ( 0 );
When you call this method and the first postion in the array is not an EchoEffect class you get an exception. Since exceptions are expensive in terms of resources, the best way to cast in this circumstance is to cast like this:
C#
EchoEffect echo = _myEngineNoise.GetEffect ( 0 ) as EchoEffect;
if ( echo != null ) { EffectsEcho echoParams = echo.AllParameters; echoParams.LeftDelay = 0.9f; echo.AllParameters = echoParams; }
If the cast fails then the assigned to object will be null. Easy and inexpensive.
|
After setting the effect we need to get it back out to set its parameters. Use the correct casting method as explained above.
C#
Interactive3DLevel2ReverbEffect enviroEffect = GetEffect ( 0 ) as Interactive3DLevel2ReverbEffect;
Check the effect variable for null before doing anything. If we have the right effect you have two choices.
C#
if ( enviroEffect != null ){}
You can either get the Effects parameters from the AllParameter property of the class and then set each separate value to give you very fine grained control over the effect you are adding.
C#
1: if ( enviroEffect != null )
2: {
3: // Set all the various parameters
4: EffectsInteractive3DLevel2Reverb enviroEffects = enviroEffect.AllParameters;
5:
6: enviroEffects.DecayHfRatio = 2.0f;
7: enviroEffects.DecayTime = 2.0f;
8: enviroEffects.Density = 1.0f;
9: enviroEffects.Diffusion = 2.0f;
10: enviroEffects.Reflections = 1;
11: enviroEffects.Reverb = 2;
12:
13: // Set them back on the effect class
14: enviroEffect.AllParameters = enviroEffects;
15: }
You must remember to assign the modified Effects structure back to the AllParameter property of the class.
Or, for the Interactive3DLevel2ReverbEffect you can chose one of the preset environments and simply update the Preset property of the effect class.
C#
1: if ( enviroEffect != null )
2: {
3: // Choose a preset
4: enviroEffect.Preset = EffectsEnvironmentPreset.Hangar;
5: }
Note that you do not have to set the effects class back to the SecondaryBuffer since you were manipulating only its value and you had to set it in the first place to get it. What's even cooler is that all effects can be changed while the buffer is playing but there is one caveat.
Changes to sounds may not be heard immediately since DirectX buffers about 100 milliseconds of sound starting at the play cursor. If you call any of these methods and then change the parameters then buffer rule is in effect and the changes are not heard immediately.
SecondaryBuffer.SetCurrentPosition
SecondaryBuffer.SetEffects
SecondaryBuffer.Stop
SecondaryBuffer.Write
SecondaryBuffer.SetEffects
SecondaryBuffer.Stop
SecondaryBuffer.Write
To overcome the buffer rule either call Stop or SetCurrentPostion. We take advantage of the built in Stop method of our SoundEffects class which stops the sound and resets the stream position to the beginning of the stream, in effect clearing the buffered sound.
C#
1: public void Stop()
2: {
3: _secondaryBuffer.Stop();
4: _secondaryBuffer.SetCurrentPosition(0);
5: }
In the SoundEffects class add the following method. Before you can add effects to aSecondaryBuffer you first need to stop the buffer. Then you can add the EffectDescription array passed into the method to the secondary buffer and restart it.
C#
1: public bool AddEffects ( EffectDescription[] effectDescription )
2: {
3: Stop ( );
4: EffectsReturnValue[] effectreturn = _secondaryBuffer.SetEffects ( effectDescription );
5: Play ( );
6:
7: return true;
8: }
Adding effects to a SecondaryBuffer simply requires you to pass an array ofEffectDescription structs to the SetEffects method of the secondary buffer. This method returns an array of EffectsReturnValue enumerations to report the result of the operation. The return value can be any combination of seven values, including failure.
Next we add the method to get the effect back from the buffer
C#
public object GetEffect ( int position )
{
return _secondaryBuffer.GetEffects ( position );
}
Note that the effects are returned as objects and must be cast to their correct types before use.
That's it for the changes required to the SoundEffect class. You have now successfully added you first effect to the game.
As a caveat, the documentation points out that effect might not work well on very small buffer of less than 150 millisecond length.
Update the GameEngine class
The first step is to reference the Microsoft.DirectX.DirectSound in the GameEngineclass since we are going to be creating the effects classes and structures in theConfigureSounds method of this class.
C#
using Microsoft.DirectX.DirectSound;
Since we now have two namespaces references that contain a Caps class we need to update the Caps class in the ConfigureDevice method to be fully qualified to avoid namespace collisions.
C#
Microsoft.DirectX.Direct3D.Caps caps = Microsoft.DirectX.Direct3D.Manager.GetDeviceCaps ( adapterOrdinal, Microsoft.DirectX.Direct3D.DeviceType.Hardware );
In the ConfigureSounds method of the GameEngine class add all the following code right after the creation of the engineSound1
C#
1: Microsoft.DirectX.DirectSound.EffectDescription[] effects = new Microsoft.DirectX.DirectSound.EffectDescription[ 1 ];
2: effects[ 0 ].GuidEffectClass = DSoundHelper.StandardGargleGuid;
3: engineSound1.AddEffects ( effects );
4:
5: // Get the effect back so we can set it to the value we want
6: GargleEffect gargle = engineSound1.GetEffect ( 0 ) as GargleEffect;
7:
8: if ( gargle != null )
9: {
10: EffectsGargle gargle_params = gargle.AllParameters;
11: gargle_params.WaveShape = 1;
12:
13: gargle.AllParameters = gargle_params;
14: }
Next we rinse and repeat for engineSound2 using a different effect.
1: effects = new Microsoft.DirectX.DirectSound.EffectDescription[ 1 ];
2: effects[ 0 ].GuidEffectClass = DSoundHelper.StandardGargleGuid;
3: engineSound2.AddEffects ( effects );
4:
5: // Get the effect back so we can set it to the value we want
6: FlangerEffect flanger = engineSound2.GetEffect ( 0 ) as FlangerEffect;
7:
8: if ( flanger != null )
9: {
10: EffectsFlanger flanger_params = flanger.AllParameters;
11: flanger_params.Waveform = 1;
12:
13: flanger.AllParameters = flanger_params;
14: }
All done. Combined with the 3DListener the three effects we have applied to the game should make it easier to change the few sound we have.
Granted, we did not make extensive use of all the available effects. But at this point you should know how to assign any effect you wish to the sounds in the game. Experiment with the various effects and their parameters and choose those that you think fit your game the best.
Summary
Now we have pretty much completed the pass through the basic functionality of the DirectSound namespace. There are a lot of different effects you can implement using the effects provided with DirectSound. As always, the best way to learn and understand all of them is to experiment. In the next article we are going to use theDirectXAudioVideoPlayback namespace to play MP3 files to provide a soundtrack for the game. Then we are going to focus on some non DirectX parts of game development - Artificial Intelligence (AI). Until then: Happy coding.
Derek Pierson is a software developer with 12 years experience designing and developing enterprise applications using a variety of programming languages. He is an enthusiastic teacher of the art of programming and believes that elegance and simplicity are defining features of good software.
0 comments:
Post a Comment