Friday, 31 January 2014

Beginning Game Development Part X –Direct Sound Part III

Beginning Game Development Part X –Direct Sound Part III

Why more sound

You may be wondering why we even need to add longer sound files to the game. As I mentioned in article 8, most games include fully featured soundtracks in addition to the regular game sounds. These sound tracks are often produced specifically for the game much like a movie soundtrack is produced for a movie. A well made sound track can add additional drama to the game and provide a memorable experience. Sometimes these game tracks are also available as separate music CDs or downloads to bring more players to the game. There is really no way that anyone is going to be able to produce a nice sounding soundtrack as a WAV file. Most music uses the MP3 codec or can be converted to this codec.

Audio Playback

Using the DirectSound namespace we were only able to play WAV files. Longer music is normally encoded in more efficient formats such as MP3 or WMA. To play these files we have to access the API in the AudioVideoPlayback namespace. This namespace is the smallest of the namespaces and contains a single class called Audio for playing audio files. (It also contains a Video class which I will not cover)
Using the Audio class to play an audio file is very easy.
1. Create a new Audio class and pass the path to the Audio file to the constructor.
Audio audio = new Audio("AudioFileName");
Or instantiate a new Audio class and then call the Open method.
 
Audio audio = new Audio();
audio.Open("AudioFileName");
2. Call the Play method.
 
audio.Play();
3. While playing an audio file you can pause it and then resume it by calling pause and then play.
 
audio.Pause();
4. When you are done playing the file just call the Stop method.
 
audio.Stop();
5. Finally remember to dispose the Audio Object.
 
audio.Dispose();
There is no looping built into the Audio class, but the class exposes an Ending Event that we can hook into the loop the audio.
 
private void _audio_Ending(object sender, EventArgs e)
{
    audio.Play();
}
Another neat feature of the Audio class is the ability to open an audio file from an URI rather than a file name. This allows you play audio files from the Internet or URL addressable location.
 
audio.Open("//www.audiofile.com/music.mp3");
While playing the audio file you can control the volume and balance of the file.
Volume: Volume is expressed as an integer. Somewhat counterintuitive is that a setting of 0 is full volume. Smaller values decrease the volume up to a setting of -10,000 which is silent.
audio.Volume = 0; // Maximum
audio.Volume = -10000; // Silent
Balance: Balance is expressed as an integer in the range of 10,000 to -10,000. A value of zero means that the sound is perfectly balanced, while -10,000 plays only the left channel and, you guessed it, 10,000 plays on the right.
 
audio.Balance = -10000; // Only Left channel
audio.Balance = 10000; // only Right channel
audio.Balance = 0; // balanced
For each of the methods representing the major states of the class the Audio class raises events to indicate these states and exposes properties to check what state the class is in. You already saw the Ending event. This matrix shows the major states, methods to get the class into the state, event raised when entering the state and properties that can be used to detect the state.
StateMethodEventProperty (Boolean)
StartedPlayStartingPlaying*
PausedPausePausingPaused
StoppedStop and StopWhenReadyStoppingStopped**
EndedN/AEnding
* While playing the audio class indicates the current position of the stream with theCurrentPosition property.
** The audio class also provides a property, StopPosition, which indicates the position in the audio stream where the file is stopped
You can also use the State property of the Audio class to determine its state. This property returns a StateFlags enumeration with the possible values of: Running,Paused and Stopped.
Also of interest is the SeekingCaps property of the Audio class. This read only property indicates the seeking capabilities of the playback stream. It is represented in the form of the SeekingCaps structure. This structure indicates whether the stream supports the following seek actions:
ActionSeekingCaps property (Boolean)
Current positionCanGetCurrentPostion
DurationCanGetDuration
Stop positionCanGetStopPostition
AbsoluteCanSeekAbsolute
Seek ForwardsCanSeekForward
Seek BackwardCanSeekBackward
You should check the capabilities of the stream before calling SeekCurrentPosition, SeekStopPosition or checking the Duration property.
Ok, let's package this simple class into a Jukebox. I want the Jukebox class to pull files from a predetermined location so I can just point it at a media folder within the games directory structure. I also want the Jukebox to either play all the files it finds in order or play a single file in a looping fashion.
Finally I want this class to play a specific file on demand, for example, when I am on the splash screen I want to play the intro tune, while on the main game screen I want some other background music.
NOTE: I am not providing any audio files with the source code. By default the SoundTrack class reads from C:\Documents and Settings\All Users\Documents\My Music\Sample Music where you should have two WMA files on a standard XP machine. If you do not have those files you need to change the directory and point it at a location that has sound files.

Creating the SoundTrack class

The first step is to add a reference to the Microsoft.DirectX.AudioVideoPlaybackassembly.
Next we add a new class and call it Soundtrack. At the top of the class we need add the using statement for the AudioVideoPlayback namespace.
using Microsoft.DirectX.AudioVideoPlayback;
Like all the other classes we are going to implement the IDisposable interface to ensure proper resource management.
 
public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}
 
protected virtual void Dispose(bool disposing)
{
    if (!this._disposed)
    {
        if (disposing)
        {
            if (_audio != null)
            _audio.Dispose();
        }
    }
    _disposed = true;
}
 
// Use C# destructor syntax for finalization code.
~SoundTrack()
{
    // Simply call Dispose(false).
    Dispose(false);
}
 
private bool _disposed;
The constructor of the class will receive an array of file names and an indicator if we want to play the files in a loop.
 
public SoundTrack ( string[] songCollection, bool repeat )
{
    _songList = songCollection;
    _isRepeat = repeat;
    
    if ( _songList.Length == 0 )
        _enabled = false;
    else
        _audio = new Audio ( _songList[ _songCounter ].ToString ( ) );
}
Next we declare a number of private properties for the class to hold the songs, and counter information.
 
private Audio _audio;
private string[] _songList;
private int _songCounter = 0;
private bool _isRepeat;
private bool _enabled = true;
Now we add the Play method to the class.
 
public void Play()
{
    if ( !_enabled )
        return;
    
    _audio.Play();
    _audio.Ending += new EventHandler(_audio_Ending);
}
All we have to do is to call the Play method of the Audio class. Finally we need to hook the Ending event to implement the looping capabilities.
 
private void _audio_Ending(object sender, EventArgs e)
{
    if ( _isRepeat )
    {
        Stop ( );
        Play ( );
    }
    else
        NextSong ( );
}
This method simply calls the NextSong method if we are not repeating the song, or stops and plays the same song again.
 
public void NextSong ( )
{
    if ( !_enabled )
        return;
    
    _songCounter++;
    
    if ( _songCounter == _songList.Length )
        _songCounter = 0;
    
    if ( _audio.Playing == true )
        _audio.Stop ( );
    
    _audio.Open ( _songList[ _songCounter ].ToString ( ) );
    _audio.Play ( );
}
Here we simply increment the counter to the next song, pass that song to the Audio class and play the song.
The only remaining method we now need is to stop the player
public void Stop ( )
{
    if ( !_enabled )
        return;
    
    if ( _audio != null )
        _audio.Stop ( );
}
To allow us to control the balance and volume we simply add two properties to the SoundTrack class that expose the identical properties of the Audio class. Since we know the upper and lower bounds of the values, we add a simple check to the property setter to ensure we only pass valid values. This is more efficient than passing an invalid value and having to check for exceptions. In general you should enforce boundaries whenever possible.
   1: public int Volume
   2: {
   3:     get { return _audio.Volume; }
   4:     set 
   5:     {
   6:         if ( value > 0 | value < -10000 )
   7:             return;
   8:         
   9:         _audio.Volume = value; 
  10:     }
  11: }
  12:  
  13: public int Balance
  14: {
  15:     get { return _audio.Balance; }
  16:     set 
  17:     {
  18:         if ( value > 10000 | value < -10000 )
  19:             return;
  20:         
  21:         _audio.Balance = value; 
  22:     }
  23: }
Now we can integrate the new class into the game. First we add a reference to the new class to the GameEngine class.
private SoundTrack _mainSoundTrack;
Now we initialize the new class in the ConfigureSounds method of the GameEngineclass.
 
_mainSoundTrack = new SoundTrack ( Directory.GetFiles ( @"C:\Documents" + 
    "and Settings\All Users\Documents\My Music\Sample Music" ), true );
To control the sounds for experimental purpose only, we are going to associate a number of keyboard keys with the Play, Stop and Next methods of the class. In a real game we would only expose a mute function, while the sound settings for volume and balance would be part of the game options screen. The actual playing of the various sounds would be controlled by the state of the game.
 
// Control the Sound
if ( _keyboard.State[ Key.P ] )
    _mainSoundTrack.Play ( );
 
if ( _keyboard.State[ Key.M ] )
    _mainSoundTrack.Stop ( );
 
if ( _keyboard.State[ Key.N ] )
    _mainSoundTrack.NextSong ( );
 
if ( _keyboard.State[ Key.UpArrow ] )
    _mainSoundTrack.Volume += 10;
 
if ( _keyboard.State[ Key.DownArrow ] )
    _mainSoundTrack.Volume -= 10;
 
if ( _keyboard.State[ Key.RightArrow ] )
    _mainSoundTrack.Balance += 10;
 
if ( _keyboard.State[ Key.LeftArrow ] )
    _mainSoundTrack.Balance -= 10; 
That's it. Start the game and press P to hear the first tune, you can use the cursor keys to change the volume (up/down) and the balance (left/right). To skip to the next song just press N, and to stop the soundtrack press M.

Summary

Now we have pretty much completed the pass through the basic functionality of the all the DirectX namespaces. There are many more advanced topics that could cover additional graphics classes such as HLSL (High Level Shader Language), but in the next articles we are going to focus on two non DirectX parts of game development - Artificial Intelligence (AI) and Physics.
Without good AI and Physics, even the best designed games are no fun to play. Nothing shatters the illusion of reality more than predictable enemies or rocks floating in mid air.
Together with game Physics, AI represents a major portion of the game that has no support in DirectX and limited support in other (free) libraries. We are not going to be able to fill that gap and write a fully featured AI engine, but we can cover most of the principles and techniques behind some of the more basic AI problems, such as chasing and evading and path finding.
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