In my last post I talked about varying the sounds played for events in your game. I also said I would write about a simple sound effect cue system that would work on Windows Phone 7 and all of the other platforms XNA Game Studio 4.0 supports.

Below is a simple class that will manage the playback of the several sound effects. The class will handle two types of playback.

/// <summary>
/// Determines how the next sound effect in the cue will be selected
/// </summary>
public enum SelectionMode
{
    RandomNoRepeats,
    Shuffle,
}

The first selection mode RandomNoRepeats does exactly what the name suggests and plays a random sound effect without playing the same sound effect twice in a row. The second Shuffle plays the list of sound effects in order. Once the end is reached the list is shuffled.

Now lets create the class that will manage our cue of sound effects.

 

/// <summary>
/// Simple class to play a cue of sound effects
/// </summary>
public class SoundEffectCue
{
    /// <summary>
    /// We hold a list of sound effect instances for each type of sound effect in the cue
    /// </summary>
    List<List<SoundEffectInstance>> soundEffectInstances;
    /// <summary>
    /// The current selection mode
    /// </summary>
    SelectionMode mode;
    /// <summary>
    /// The index into the soundEffectInstances list of the last sound we played
    /// </summary>
    int lastPlayedIndex;

    Random random;

    // ...
}

The class has a few simple members. The first is a list that holds lists of sound effect instances. There is one list of sound effect instances per sound effect. Having multiple instances allows playback of the same sound effect even if a previous playback has not completed. The other variables hold the selection mode and the index of the last sound effect we played.

The constructor for the class is the following.

/// <summary>
/// Constructor used to create a new instance of a SoundEffectCue. 
/// The cue contains the sound effects that are passed into the constructor via the soundEffects parameter.
/// </summary>
public SoundEffectCue(IEnumerable<SoundEffect> soundEffects, int instanceCount, SelectionMode mode)
{
    // Create the list that will hold the list of sound effect instances
    soundEffectInstances = new List<List<SoundEffectInstance>>();

    // Loop over the passed in sound effect instances
    foreach (SoundEffect soundEffect in soundEffects)
    {
        // Create a new list of sound effect instances for each sound effect
        List<SoundEffectInstance> instances = new List<SoundEffectInstance>();
        for (int i = 0; i < instanceCount; i++)
        {
            instances.Add(soundEffect.CreateInstance());
        }
        soundEffectInstances.Add(instances);
    }

    this.mode = mode;
    random = new Random();
    lastPlayedIndex = -1;

    // If we are using the shuffle selection mode then shuffle for the first time
    if (mode == SelectionMode.Shuffle)
    {
        Shuffle();
    }
}

The constructor creates the list of sound effect instances from the list of sound effects passed into the constructor. If the selection mode was set to shuffle the list is shuffled for the first time.

The only other public method in the class is the Play method which will play a sound effect in the cue.

/// <summary>
/// Called from the game when you want to play a sound from the cue
/// </summary>
public void Play()
{
    // Call the appropriate play method based on the selection mode
    switch (mode)
    {
        case SelectionMode.RandomNoRepeats:
            PlayRandomNoRepeats();
            break;
        case SelectionMode.Shuffle:
            PlayShuffle();
            break;
        default:
            Debug.Assert(false, "Invalid playback mode");
            break;
    };
}

The Play method calls out to different play methods depending on the selection mode.

The first play method PlayRandomNoRepeats will play a random sound effect from the cue without playing the same sound effect twice in a row.

/// <summary>
/// Plays a random sound effect from the cue and prevents repeating sound effects from playing.
/// </summary>
private void PlayRandomNoRepeats()
{
    // Select a random index
    int playIndex = random.Next(soundEffectInstances.Count);

    // Did we just play this sound effect?
    if (playIndex == lastPlayedIndex)
    {
        // Just play the next index
        playIndex++;
        // Bounds check
        if (playIndex >= soundEffectInstances.Count)
        {
            playIndex = 0;
        }
    }

    // Loop over all of the instances looking for one that can be played
    for (int i = 0; i < soundEffectInstances[playIndex].Count; i++)
    {
        // If the instance is not playing already we can play the sound
        if (soundEffectInstances[playIndex][i].State != SoundState.Playing)
        {
            lastPlayedIndex = playIndex;
            soundEffectInstances[playIndex][i].Play();
            return;
        }
    }

    // Could not find a sound effect instance to play
    Debug.Assert(false, "All sound effect instances were already playing. Increase the number of sound effect instances.");
}

If all of the sound effect instance for the selected sound effect index are already playing the assert will be hit and lets you know that you should increate the number of instances in the cue.

The other playback method PlayShuffle is similar except it plays the sound effects in the list order until it reaches the end of the list where it then shuffles the list.

/// <summary>
/// Plays sound effects in order until reaching the end of the list then shuffle the list
/// </summary>
private void PlayShuffle()
{
    // Increment to the next sound effect in the list
    lastPlayedIndex++;
    // Are we at the end of the list?
    if (lastPlayedIndex >= soundEffectInstances.Count)
    {
        Shuffle();
    }

    // Loop over all of the instances looking for one that can be played
    for (int i = 0; i < soundEffectInstances[lastPlayedIndex].Count; i++)
    {
        // If the instance is not playing already we can play the sound
        if (soundEffectInstances[lastPlayedIndex][i].State != SoundState.Playing)
        {
            soundEffectInstances[lastPlayedIndex][i].Play();
            return;
        }
    }
            
    // Could not find a sound effect instance to play
    Debug.Assert(false, "All sound effect instances were already playing. Increase the number of sound effect instances.");
}

The PlayShuffle method utilizes the last method in the class Shuffle which performs a Fischer-Yates shuffle on the list of sound effect instances.

/// <summary>
/// An implementation of the Fischer-Yates shuffle that does not allow for the last item in the list to become the 
/// first item in the list. This prevents duplicate sounds after the shuffle.
/// </summary>
private void Shuffle()
{
    // Save the last sound effect played
    List<SoundEffectInstance> lastSoundPlayed = soundEffectInstances[soundEffectInstances.Count - 1];

    // Fischer-Yates shuffle
    for (int i = soundEffectInstances.Count; i > 1; i--)
    {
        int swapIndex = random.Next(i);
        List<SoundEffectInstance> temp = soundEffectInstances[swapIndex];
        soundEffectInstances[swapIndex] = soundEffectInstances[i - 1];
        soundEffectInstances[i - 1] = temp;
    }

    // Check to see if we just played this
    if (lastSoundPlayed == soundEffectInstances[0])
    {
        // Swap for some other random index
        int swapIndex = random.Next(1, soundEffectInstances.Count);
        soundEffectInstances[0] = soundEffectInstances[swapIndex];
        soundEffectInstances[swapIndex] = lastSoundPlayed;
    }

    // Reset the play index
    lastPlayedIndex = 0;
}

So that's a simple sound effect cue class that will allow you to have multiple sound effects in a single cue that can be used for a sound event in your game and allow for sound variation.