Chapter 9: Game Audio - Let s Make Some Noise


All games use audio to set their tone and to provide cues that an event has occurred. Music often sets the mood in a game. Most of us expect to hear something when a collision occurs or a motor is running. Without these aural cues, something is missing and everything feels less real. Microsoft included the DirectSound and Audio Video namespaces within DirectX to address this part of the game experience.

Making Beautiful Music Together

We will begin with the musical portion of our program. Prior to DirectX 9, this capability was part of the DirectShow interface. This interface was not included in the Managed DirectX available to C#. Instead, Microsoft provided the Audio Video namespace, which furnishes some of the functionality that was lost when DirectShow became unavailable. The Video class in this namespace includes the ability to stream video to either the screen or a texture. The Audio class provides the ability to play music files and supports the audio portion of a video file. We will use this ability within the game engine to play music.

The music files that are used within a game setting normally rely on one or both of two file formats. The first format is the MIDI format. This format holds a musical score in the form of the notes required to play the music. The files are typically created using composing software or a MIDI-capable keyboard attached to a computer. MIDI music was the original format used within games, and it is supported within the Audio class.

A more popular format these days is MP3. This format allows for quality music with near CD-quality sound at a decent compression to save disk space. This is another of the many formats supported by the Audio class.

Controlling the Music

The Audio class is very capable but does have a few limitations. We will address these limitations by creating the Music class that inherits from Audio. Listing 9 “1 shows the attributes and properties that we will use to extend the class. The first of these limitations is the ability for a song to repeat. This allows a properly written MIDI song to repeat over and over in the background. If the beginning and end of the MIDI song are written to flow together, we can make the transition without it being noticeable. To support looping, we will have a Boolean attribute that signals that looping is required and a property to expose this flag to the user .

Listing 9.1: Music Class Attributes and Properties
start example
 using System;  using System.Drawing;  using Microsoft.DirectX;  using Microsoft.DirectX.AudioVideoPlayback;  namespace GameEngine  {     /// <summary>     /// Summary description for music     /// </summary>     public class Music : Microsoft.DirectX.AudioVideoPlayback.Audio     {        #region Attributes        private bool loop = false;        #endregion        #region Properties        public bool Loop { get { return loop; } set { loop = value; } }        public float MusicVolume {           set { base.Volume = (int)(   4000 * (1.0f   value)); } }        #endregion 
end example
 

The second limitation is the way Microsoft designed the control of the music s volume. They provided a Volume property for the Audio class that is a bit misleading. Although they named the property Volume, it is really an attenuation factor. A value of 0 in the property represents full volume. A value of “10,000 is stated to be complete silence. This attenuation is in decibels, though, which makes it nonlinear. A value of “4000 is silent for all intents and purposes. To address this, we will provide a MusicVolume property that contains a floatingpoint value, where a 0 represents silence and a 1 represents full volume. Since there is no reason to query the volume, this is defined as a read-only property.

The constructor for the Music class (shown in Listing 9 “2) is responsible for loading the song file. It does this by passing the supplied filename on to the base class constructor. Since we want our class to be able to loop and repeat the song, we will need to know when the song is finished. The Audio class provides the Ending event that is triggered when the song completes. In order to be notified when the event has occurred, we need to register an event handler with the event. The handler will be the ClipEnded method of our class. We will provide a Try/Catch block around this code, but this will not detect the problem of trying to load a file that can t be found. Unfortunately, the base class will throw an exception before our constructor code has a chance to execute. To protect against this occurrence, any class that instantiates copies of the Music class must safeguard against failing to load the file.

Listing 9.2: Music Constructor
start example
 /// <summary>  /// Music constructor  /// </summary>  public Music(string filename) : base(filename)  {     try     {        Ending += new System.EventHandler(this.ClipEnded);     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to create music ");        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to create music ");        Console.AddLine(e.Message);     }  } 
end example
 

The ClipEnded event handler (shown in Listing 9 “3) is used to repeat songs that are selected for looping. Remember, we registered to have this method called when the song finishes. When this method is called, we check to see if the looping attribute has been set. If it has, we call the Audio class s Stop method to put the class into the stopped state. We then call the Play method to start the song playing again.

Listing 9.3: Music Class ClipEnded Event Handler
start example
 private void ClipEnded(object sender, System.EventArgs e)        {           // The clip has ended, stop and restart it.           if (loop)           {              Stop();              Play();           }       }    } } 
end example
 

Playing Multiple Songs

It would quickly become repetitious and boring if the same song were played over and over for the duration of the game. We need the means to play a number of songs one after another as an ongoing soundtrack for our game engine. To accomplish this, we will create the Jukebox class. This class will hold a collection of songs contained within instances of our Music class. Listing 9 “4 shows the attributes and properties that will be needed for the class.

Listing 9.4: Jukebox Attributes and Properties
start example
 using System;  using System.Drawing;  using System.Collections;  using Microsoft.DirectX;  using Microsoft.DirectX.AudioVideoPlayback;  namespace GameEngine  {     /// <summary>     /// Summary description for Jukebox     /// </summary>     public class Jukebox : IDisposable     {        #region Attributes        private ArrayList playlist = null;        private int        current_song = 0;        private int        volume = 0;        #endregion        #region Properties        public float Volume { set { volume = (int)(   4000 * (1.0f   value)); } }        #endregion 
end example
 

We will use an ArrayList as our playlist of songs. We will add songs to the list as they are added to the jukebox. An index to the collection provides the selection of the song being played. We will also include an attribute and property for setting the master volume for the jukebox. This volume will work the same as the volume we used for the Music class. As each song is played, this volume value will be passed on to that song.

The constructor for the Jukebox class (shown in Listing 9 “5) is quite simple. All the constructor needs to do is prepare the playlist collection to hold the music.

Listing 9.5: Jukebox Constructor
start example
 /// <summary>  /// Jukebox constructor  /// </summary>  public Jukebox()  {     playlist = new ArrayList();  } 
end example
 

Music is added to our jukebox through the AddSong method shown in Listing 9 “6. A string with the full path and filename for the song file is passed into the method. Remember that this can be any combination of MIDI, MP3, or even WAV files that we wish to play during the game. When we defined the Music class, I stated that the Try/Catch block in the constructor would not catch cases where the song could not be loaded because the file was not found or was not of an acceptable format. The Try/Catch block in this method is to safeguard against these types of errors.

Listing 9.6: Jukebox AddSong Method
start example
 public void AddSong(string filename)  {     try     {        Music song = new Music(filename);        song.Ending += new System. EventHandler(this.ClipEnded);        playlist.Add(song);     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to add " + filename +               " to the jukebox playlist ");        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to add " + filename +               " to the jukebox playlist ");        Console.AddLine(e.Message);     }  } 
end example
 

A new instance of the Music object is created for the requested file. Assuming that an exception was not thrown and the song was loaded, we will need to register a handler for the Ending event so that we will be notified when this song finishes. All songs will notify the same event handler when they finish. Finally, we will add the song to our playlist collection. If any exceptions occur along the way, we will post an error to the console. If we are running in debug mode, the message will also appear in the debug output window.

The constructor and the AddSong method take care of setting up the jukebox. Now it is time to put the jukebox to work. We will need several methods to control the playing of the songs stored within the jukebox. The Play method (shown in Listing 9 “7) will start the current song playing. The current song is initially the first song stored in the playlist. Before we try to play the song, we need to make sure that the current song index is a valid value. The only time it might not be is if no songs have been loaded into the jukebox.

Listing 9.7: Jukebox Play Method
start example
 public void Play()  {     if (current_song < playlist. Count)     {        Music song = (Music) (playlist [current_song]);        song.Volume = volume;        song.Play();     }  } 
end example
 

Assuming that the current song index is valid, we need to get a copy of the class s reference from the collection. The collections hold all objects as instances of the general Object base class. This requires that we cast the object back to the proper type before we do anything with it. Once we have the song, we will set its volume to the jukebox volume. This allows the game to set the master volume for all songs in one place. Typically this would be an option screen that is accessed before the game is started or possibly even while the game is in play. Since it is possible for the volume to change at any time, we will reset the song s volume each time the song is started. We will then command the song to start playing itself.

There may be times when the person writing the game does not want a song playing. The Stop method (shown in Listing 9 “8) provides the means for stopping whatever song is currently playing. This method is just an alias for the Next method shown in Listing 9 “9.

Listing 9.8: Jukebox Stop Method
start example
 public void Stop()  {     Next();  } 
end example
 
Listing 9.9: Jukebox Next Method
start example
 public void Next()  {     Music song = (Audio)(playlist[current_song]);     song.Stop();     song.SeekCurrentPosition(0.0, SeekPositionFlags.AbsolutePositioning);     current_song++;     if (current_song >= playlist.Count)     {        current_song = 0;     }  } 
end example
 

The Next method obtains a reference to the song that is currently playing. It stops that song and resets the current playing position within the song back to the beginning. This is to ensure that the song is ready to play again next time it comes around in the playlist. The current song index is incremented and checked to see if it has been advanced past the end of the playlist. If the method has passed the end of the collection, it is reset back to the first song in the list. When this method is used as the Stop method, it not only stops the current song, but also prepares to play the next song in the list the next time the Play command is issued.

The ClipEnded event handler (shown in Listing 9 “10) is called each time the current song finishes. The handler calls the Next method to reset the song that just completed. It then calls the Play method to start the next song in the playlist. Since all songs in the playlist have registered for this handler, calling Play on the first song is all that is required to keep music playing for the entire time that the game is being played.

Listing 9.10: Jukebox ClipEnded Event Handler
start example
 private void ClipEnded(object sender, System.EventArgs e)  {     Next();     Play();  } 
end example
 

The final method in the Jukebox class is the Dispose method shown in Listing 9 “11. Since each instance of the Music class is disposable, we need to call the Dispose method for each one. We will use the foreach command to iterate through the collection and call each song s Dispose method.

Listing 9.11: Jukebox Dispose Method
start example
 public void Dispose()        {           foreach (Music song in playlist)           {              song.Dispose();           }        }     }  } 
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