Hearing in Three Dimensions


The Music class has the ability to play the WAV files that are typically used for sound effects within a game. Unfortunately, there is limited control over the audio that can be played this way. The Audio class that we used for the music has the ability to manually change the volume and the balance through method calls. Using this capability to control sound effects would be incredibly painful. Microsoft provides a better solution using the classes within the DirectSound namespace.

DirectSound provides the concept of three-dimensional sound. By defining the position of the object listening for the sounds, and the positions of the objects generating sounds, the DirectSound classes have the ability to mix multiple sounds into a single buffer with the balance and volume of each sound appropriate for the relative position. The DirectSound documentation gives a detailed explanation on how three-dimensional sound works. We will concentrate on how we can take advantage of the capabilities that it gives us.

Listening for Noise

The first half of the equation is the ability to listen for the sounds that we will be making. All listening will take place wherever the player is within the game. It is important that there only be one active listener at any point in time. We will provide a Listener class that encapsulates all of the required functionality. The game application will create and hold one copy of this class.

The attributes and properties that define our Listener class are shown in Listing 9 “12. This class has three attributes that hold the required information for the class. The first is an instance of Listener3DSettings that holds the parameters, defining the way in which the class will be able to listen. The second is an instance of Listener3D that is the actual buffer within which the sounds will be mixed. The third is a reference to Object3D that defines the position and orientation of the listener at any point in time. There is also a static attribute holding the DirectSound device that will be shared with all of the sound effects. This ties the entire system together. A static property ( Device ) provides the access to this attribute by other classes.

Listing 9.12: Listener Class Attributes and Properties
start example
 using System;  using System.Drawing;  using Microsoft.DirectX;  using Microsoft.DirectX.DirectSound;  using Sound = Microsoft.DirectX.DirectSound;  using Buffer = Microsoft.DirectX.Direct Sound.Buffer;  namespace GameEngine  {     /// <summary>     /// Summary description for Listener     /// </summary>     public class Listener : IDisposable     {        #region Attributes        private Sound.Listener3DSettings listenerParameters =                     new Sound.Listener3DSettings();        private Sound.Listener3D applicationListener = null;        private Object3D m_listener = null;        private static Sound.Device applicationDevice = new Sound.Device();        #endregion        #region Properties        public static Sound.Device Device { get { return applicationDevice; } }        #endregion 
end example
 

The constructor for the Listener class (shown in Listing 9 “13) is a vital part of the class. This class is largely passive. It defines the properties and capabilities of the listener and collects and mixes the sounds as they occur. The arguments of the constructor are the reference to a Windows Form and to the object that we will be listening from. We need the Windows Form in order to create the DirectSound device. All of the DirectX devices need knowledge of the main application s form so that they can receive Windows messages and have knowledge of the current state of the application.

Listing 9.13: Listener Class Constructor
start example
 public Listener(System.Windows.Forms.Form form, Object3D object_listening)  {     m_listener = object_listening;     Sound.BufferDescription description = new Sound.BufferDescription();     Sound.WaveFormat fmt = new Sound.WaveFormat();     description.PrimaryBuffer = true;     description.Control3D = true;     Sound.Buffer buff = null;     fmt.FormatTag = Sound.WaveFormatTag.Pcm;     fmt.Channels = 2;     fmt.SamplesPerSecond = 22050;     fmt.BitsPerSample = 16;     fmt.BlockAlign = (short)(fmt.BitsPerSample / 8 * fmt.Channels);     fmt.AverageBytesPerSecond = fmt.SamplesPerSecond * fmt.BlockAlign;     applicationDevice.SetCooperativeLevel(form,               Sound.CooperativeLevel.Priority);     // Get the primary buffer and set the format,     buff = new Buffer(description, Device);     buff.Format = fmt;     applicationListener = new Listener3D(buff);     listenerParameters = applicationListener.AllParameters;  } 
end example
 

The BufferDescription and WaveFormat classes provide the means of configuring the DirectSound device. The description defines how the sound buffer will be used, and the format defines how the data in the buffer will be organized. It is important that the format defined for this buffer matches the formats used in the sound effect files. DirectSound will be mixing the active sounds into this buffer. If the formats differ between the buffer in the Listener object and the buffer in the SoundEffect object, the mixing software is forced to perform additional manipulation of the source data before it can be mixed into the Listener object s buffer. This will adversely affect our game s performance.

We will designate the Listener object s buffer as the primary buffer on the sound card and flag that we will be using the buffer for three-dimensional sound. If performance is a problem or three-dimensional sound is not appropriate for the games that the engine will be used for, we could set this to false. The format settings shown in the example code are common for WAV files and produce reasonable quality.

We will set the cooperative level for the sound at the priority level. This gives our device priority for the sound hardware whenever our game application has the focus. If the application loses the focus, it will surrender the device to the application that has gained focus. The actual Listener object s buffer is created using the DirectSound device and the description that we set up earlier. The buffer is supplied with the format description and is passed to the Listener3D class for final construction. The final step is to get a reference to the configuration parameters for the Listener3D class. These parameters provide the means of controlling the position and orientation of the listener.

The dynamic portion of the Listener class is held within the Update method shown in Listing 9 “14. Assuming that an Object3D instance was supplied, we will be able to update the position and orientation of our listener. The position is easy. We can just set the listener position to that of the object that we are attached to. The orientation is a bit more complicated. The listener requires a pair of vectors that define the orientation. One vector is the front vector that defines the direction in which the listener is looking. The second is the top vector that defines which way is up. Although these vectors exist for the camera that is usually attached to the player, we will not assume that this is always the case. To calculate the vectors that we need, we start with two unit vectors. One vector is in the Z direction that is the untransformed front vector. The other is a unit vector in the Y direction for the untransformed top vector. Rotating these vectors for the actual orientation of the object will require a rotation matrix. We can create this using the object s attitude and the RotationYawPitchRoll method of the Matrix class. The TransformCoordinate method of the Vector3 class takes care of multiplying the vector and the matrix and producing the new vectors that are passed to the listener. The application of the new listener parameters is held until we flag that we have finished changing parameters. The CommitDeferredSettings method notifies the listener that we are done.

Listing 9.14: Listener Update Method
start example
 public void Update()  {     if (m_listener != null)     {        listenerParameters.Position = m_listener.Position;              Vector3 front = new Vector3(0.0f, 0.0f, 1.0f);        Vector3 top = new Vector3(0.0f, 1.0f, 0.0f);        Matrix transform = Matrix.RotationYawPitchRoll(m_listener.Attitude.Heading,           m_listener.Attitude.Pitch,           m_listener.Attitude.Roll);        listenerParameters.OrientFront =           Vector3.TransformCoordinate(front, transform);        listenerParameters.OrientTop           Vector3.TransformCoordinate(top, transform);     }     applicationListener.CommitDeferredSettings();  } 
end example
 

That is everything we need for the Listener class aside from cleaning up after ourselves . The Dispose method (shown in Listing 9 “15) handles this cleanup by disposing of both the listener and the DirectSound device.

Listing 9.15: Listener Dispose Method
start example
 public void Dispose()        {           applicationListener.Dispose();           applicationDevice.Dispose();        }     }  } 
end example
 

Making Noise in Three Dimensions

So far, we have prepared to listen to three-dimensional sound. Now it is time to make noise. For some reason Microsoft decided to refer to the structure holding the sounds that will be played as SecondaryBuffer. While this may hold some reasoning somewhere in the history of DirectX, it is not a very descriptive name . We will encapsulate all of the pieces required to make three-dimensional sound into the SoundEffect class.

Note

In order for three-dimensional sound to work, all WAV flies used must be mono rather than stereo. The three-dimensional algorithms built into the Buffer3D class will determine how loud this sound will be in each speaker. Chapter 11 will identify utilities to convert stereo sound files into the mono format.

The SecondaryBuffer s soundBuffer holds the WAV file that we will be using. In order to be three dimensional, we need to wrap this buffer with a Buffer3D object. This adds the three-dimensional capability. Just like in our listener, all sound effects will be tied to an object to provide the source position for each effect. The class will also remember the file that the sound was loaded from in case we need to reload the sound.

Some of the advantages of using these DirectSound classes for sound effects over the Audio class include some powerful low-level control over the sounds. Rather than having to catch a completion event and restart a sound to have it loop, we can provide a looping flag when a sound is played. When this flag is set, the sound will continue to play until explicitly stopped . For some sounds, we may also wish to modify the frequency of a sound while it is playing. The sound our engine makes, for example, will change in frequency as a function of the engine speed. To facilitate this capability, we will provide a minimum and maximum valid frequency as well as a floating-point current frequency value where zero represents the minimum frequency and a one represents the maximum frequency. Values in between zero and one will be linearly interpolated between these extremes. The final parameter is a static integer that is shared among all sound effects. This is the master volume for sound effects. Just like how the volume within the jukebox works across all songs, this volume can be used to control all sound effects.

The attributes for the SoundEffect class are shown in Listing 9 “16. These attributes include the buffers used in playing the sound as well the attributes we will need to adjust the volume and frequency of the sound.

Listing 9.16: SoundEffect Attributes
start example
 using System;  using System.Drawing;  using Microsoft.DirectX;  using Microsoft.DirectX.DirectSound;  using Sound = Microsoft.DirectX.DirectSound;  using Buffer = Microsoft.DirectX.DirectSound.Buffer;  namespace GameEngine  {     /// <summary>     /// Summary description for SoundEffect     /// </summary>     public class SoundEffect : IDisposable     {        #region Attributes        private SecondaryBuffer soundBuffer = null;        private Buffer3D soundBuffer3D = null;        private Object3D m_source = null;        private string m_FileName;        private bool looping = false;        private int min_freq = 22050;        private int max_freq = 22050;        private float current_freq = 0.0f;        private static int master_volume = 0;        #endregion 
end example
 

The SoundEffect class will expose a number of properties (shown in Listing 9 “17) to provide programmatic control of the effects. The first three are read/write properties to provide access to the looping flag and the frequency range for the sound. The property for the current frequency is also read/write but includes logic in the set function to ensure that the value is clamped within the proper range of values. The DirectSound classes have the concept of a minimum and maximum range. Any sound within the minimum range between the sound source and the listener will be at its maximum value. Any sound that is further than the maximum range from the listener will not be heard at all. These ranges default to a minimum range of 1 and a maximum range of 100,000,000. Since the default value for maximum range is so large, it is a good idea to set this for each sound as it is loaded to improve performance.

Listing 9.17: SoundEffect Properties
start example
 #region Properties  public bool Looping { set { looping = value; } get { return looping; } }  public int MinFreq { set { min_freq = value; } get { return min_freq; } }  public int MaxFreq { set { max_freq = value; } get { return max_freq; } }  public float Frequency {     set {        current_freq = value;        if (current_freq > 1.0) current_freq = 1.0f;        if (current_freq < 0.0) current_freq = 0.0f;     } get { return current_freq; } }  public float MinDistance { set { soundBuffer3D.MinDistance = value; }                                 get { return soundBuffer3D.MinDistance; } }  public float MaxDistance { set { soundBuffer3D.MaxDistance = value; }                                 get { return soundBuffer3D.MaxDistance; } }  public static float Volume {     set { master_volume = (int)(   4000 * (1.0f   value)); } }  private static int MasterVolume { get { return master_volume; } }  #endregion 
end example
 

We will provide two static properties for the volume. The first property is a write-only property that allows the user to set the master volume to a floatingpoint value between zero and one as we did for the music and jukebox. The second property is private, which limits its use to within this class. This is the property that will be used to retrieve the integer attenuation value that actually controls sound volume.

The constructor for the SoundEffect class (shown in Listing 9 “18) is fairly simple. It saves the sound file s path to the m_FileName attribute and calls the LoadSoundFile to actually perform the file loading. Since we may need to reload the sound at a later point, it is better to encapsulate this functionality in a separate private method.

Listing 9.18: SoundEffect Constructor
start example
 public SoundEffect(string FileName)  {     m_FileName = FileName;     LoadSoundFile();  } 
end example
 

The implementation of the LoadSoundFile method is shown in Listing 9 “19. Just as the Listener class has a description class to define its properties, SoundEffect has such a class as well. We can use a number of different algorithms to process sounds for three-dimensional representation. The four choices for the algorithms can be found in the DirectSound documentation. We will use Guid3DalgorithmHrtfLight , which provides a good middle of the road between performance and sound quality. We will also set flags within the description to indicate that we want threedimensional processing and that we may also be modifying both frequency and volume.

Listing 9.19: SoundEffect LoadSoundFile Method
start example
 private void LoadSoundFile()  {     BufferDescription description = new BufferDescription();     description.Guid3DAlgorithm = DSoundHelper.Guid3 DAlgorithmHrtfLight;     description.Control3D = true;     description.ControlFrequency = true;     description.ControlVolume = true;     if (null != soundBuffer)     {        soundBuffer.Stop();        soundBuffer.SetCurrentPosition(0);     }     // Load the wave file into a DirectSound buffer.     try     {        soundBuffer = new SecondaryBuffer(m_FileName, description,               Listener.Device);        soundBuffer3D = new Buffer3D(soundBuffer);     }     catch (Exception e)     {        GameEngine.Console.AddLine("Exception on loading " + m_FileName +              ". Ensure file is Mono");        GameEngine.Console.AddLine(e.Message);     }     if (WaveFormatTag.Pcm != (WaveFormatTag.Pcm &               description.Format.FormatTag))     {        GameEngine.Console.AddLine("Wave file must be PCM for 3D control.");        if (null != soundBuffer)           soundBuffer.Dispose();        soundBuffer = null;     }  } 
end example
 

It is possible that this method may be called after the class has been created. If a sound buffer already exists, we will ensure that the sound is not playing before we continue. To actually load the sound from the file, we will create a new secondary buffer using the filename, description, and the DirectSound device created for the Listener . The Buffer3D is generated using this buffer. If the loading fails for any reason (such as the method being unable to find the file or an incorrect file type), an exception will be thrown and a message displayed on the console.

If the file was successfully loaded, we must check the format of the sound data. We can t control the sound in three dimensions unless it is in PCM format. This won t tend to be a problem, since this is the most common WAV file format. If the data is not in PCM format, we will post a message to the console. If we have a sound buffer, we will dispose of it and set the reference to null.

It is possible that during the course of the game, the game application may lose focus and the sound buffer may be lost. The private RestoreBuffer method (shown in Listing 9 “20) will check to see if this has happened . If the buffer has not been lost, it will simply return a false to flag the calling method that everything is fine. If the buffer was lost, though, we need to restore the buffer before proceeding. We will loop and call the Restore method of the buffer until the BufferLost flag is cleared. We then return a true so that the calling method will know that the file needs to be reloaded.

Listing 9.20: SoundEffect RestoreBuffer Method
start example
 private bool RestoreBuffer()  {     if (false == soundBuffer.Status.BufferLost)        return false;     while(true == soundBuffer.Status.BufferLost)     {        soundBuffer.Restore();     }     return true;  } 
end example
 

The sounds are activated using the PlaySound method shown in Listing 9 “21. The BufferPlayFlags enumeration defines the choices that may be made when playing a sound. We will be concerned with two of the values of the enumeration. If we are not looping, we will take the default settings for playing the sound. Otherwise, we will use the Looping flag in the enumeration. Before playing the sound, we need to ensure that the buffer is still valid. We will call the RestoreBuffer method that we described earlier. If it returns a true value, then we must reload the sound file. We will also ensure that the position within the buffer is set back to the beginning in case there is leftover data for the buffer position. The Play method of the buffer starts the sound playing. If an exception occurs at any point in this procedure, we will post a message to the console.

Listing 9.21: SoundEffect PlaySound Method
start example
 public void PlaySound()  {     try     {        BufferPlayFlags flags;        if (looping)        {           flags = BufferPlayFlags.Looping;        }        else        {           flags = BufferPlayFlags.Default;        }        if (RestoreBuffer())        {           LoadSoundFile();           soundBuffer.SetCurrentPosition(0);        }        soundBuffer.Play(0, flags);     }     catch (Exception e)     {        GameEngine.Console.AddLine("Exception on playing " + m_FileName);        GameEngine.Console.Add Line(e.Message);     }  } 
end example
 

Normally, we will let sounds continue until they have finished on their own. This only works, of course, if we are not looping the sound. For sounds that we might wish to terminate early or are looping, we include a method to stop the sound. The StopSound method (shown in Listing 9 “22) commands the buffer to stop playing and resets the position within the buffer back to the beginning. This prepares the sound for reactivation in the future.

Listing 9.22: SoundEffect StopSound Method
start example
 public void StopSound()  {     soundBuffer.Stop();     soundBuffer.SetCurrentPosition(0);  } 
end example
 

Just as the Listener class has an Update method to refresh the position of the listener, the sound effect needs a similar method (shown in Listing 9 “23). The code within the method will be wrapped in a Try/Catch block to protected against exceptions during the update procedure. We will begin by calculating the range of frequencies between the minimum and maximum values. If the range is nonzero, then this sound has a varying frequency. We will calculate the value for this point in time by interpolating between the values in the range based on the current_freq value. We will also copy the master volume into the buffers volume. Finally, if a source object has been defined, we will set the buffer s position to that of the object.

Listing 9.23: SoundEffect Update Method
start example
 public void Update()  {     try     {        int freq_range = max_freq   min_freq;        int freq_now;        if (freq_range > 0)        {           soundBuffer.Frequency =              min_freq + (int)(freq_range * current_freq);        }        soundBuffer.Volume = MasterVolume;        if (m_source != null)        {           soundBuffer3D.Position = m_source.Position;        }        }        catch (Exception e)        {           GameEngine.Console.AddLine("Exception while updating " + m_FileName);           GameEngine.Console.AddLine(e.Message);        }     } 
end example
 

The Dispose method for the SoundEffect class (shown in Listing 9 “24) has two attributes that need to be disposed. One is the sound buffer and the other is the Buffer3D .

Listing 9.24: SoundEffect Dispose Method
start example
 public void Dispose()        {           soundBuffer.Dispose();           soundBuffer3D.Dispose();        }     }  } 
end example
 

Putting 3D Sound to Work in a Game

We have completely defined the two classes that we will need for three-dimensional sound. To use these classes, we will employ two excerpts from the Ownship class in the game application. The first excerpt (shown in Listing 9 “25) is from the Ownship class constructor. We instantiate a copy of the Listener class attached to our Ownship vehicle. There will be three sound effects related to this vehicle. The first will be our engine sound. This will be a variable frequency effect that loops . You will see that we create the sound with our base idle sound and set the looping flag and the frequency range. Two different sounds are created for two different types of crash events. The thump sound will represent hitting minor objects that do not damage the vehicle. The crash sound is for collisions that will cause damage.

Listing 9.25: Ownship Class Constructor Excerpt
start example
 ears = new Listener(form, this);  engine_sound = new SoundEffect("car_idle.wav");  engine_sound.Looping = true;  engine_sound.MinFreq = 9700;  engine_sound.MaxFreq = 13500;  thump = new SoundEffect("thump.wav");  crash = new SoundEf feet ("crash.wav"); 
end example
 

The second excerpt (shown in Listing 9 “26) is from the Update method of the Ownship class. This is where we will update the listener and active effects. If this is the first time the Update method has been called, we start the engine sound. The listener s Update method is called and the engine sound s frequency is updated based on the current throttle setting, and its Update method is called as well. Later in the Update method, there are checks to see if the vehicle has collided with anything. If the vehicle has collided with something, we will check the object s name to see what it was that we hit. If we hit one of the red or blue marker posts or the small cactus, we will trigger the thump sound. Collision with any other objects (vehicles or palm trees) will trigger the crash sound.

Listing 9.26: Ownship Class Update Method Excerpt
start example
 if (first_pass)  {     first_pass=false;     engine_sound.PlaySound ();  }  ears.Update();  engine_sound. Frequency=Gas;  engine_sound.Update();  if (Collide (test_obj))  {     if (test_obj.Name.Substring(0,3) == "red"            test_obj.Name.Substring(0,4) == "blue"            test_obj.Name.Substring(0,6) == "cactus")     {        thump.PlaySound();     }     else     {        crash.PlaySound();     }  } 
end example
 



Introduction to 3D Game Engine Design Using DirectX 9 and C#
Introduction to 3D Game Engine Design Using DirectX 9 and C#
ISBN: 1590590813
EAN: 2147483647
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net