Advanced Sound Manager For XNA GS 3.1
From XNAWiki
(Note: This class should be 100% compatible with XNA Game Studio 4.0.)
The Basic Sound Manager is really handy if you just need a simple audio manager to use in your game, but if you need a component with a little more oomph, this should serve your needs better. As with the Basic Sound Manager, this class uses the Song/Sound Effect APIs, though this can easily be converted to use XACT instead.
Here is how you use this class:
- Create a new AudioManager instance in your Game's constructor and add it to the Game's Components collection.
- In your LoadContent method, call LoadSong and LoadSound for each song and sound you want to load.
- [Optional] Call AudioManager.UnloadContent in your UnloadContent method. This is not automatically called, since GameComponent does not provide an UnloadContent method.
- Use the PlaySong/PauseSong/ResumeSong/StopSong methods for songs, and the PlaySound/StopAllSounds methods for playing sounds. Sounds do not have the same granular control over playback as songs do, but most games don't really need this kind of control.
- Use FadeSong to smoothly transition between two music volumes for the currently playing song. Warning: This will cause the AudioManager to set MediaPlayer.Volume on every frame, which I've heard generates garbage.
- Use CancelFade to stop the current fade transition. You can choose to set the post-fade volume to the fade source volume, fade target volume, or just leave it at the current volume.
- You can set volumes directly using the MusicVolume and SoundVolume properties, which are just dummy members for MediaPlayer.Volume and SoundEffect.MasterVolume, respectively.
- Use the Enabled property of the AudioManager to globally pause all sounds and music currently playing. This is useful, for example, when you want to stop all sounds while the game is paused. You can continue to play more songs and sounds while the component is disabled, but nothing will start playing until Enabled is set true again.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Media;
/// <summary>
/// Manages playback of sounds and music.
/// </summary>
public class AudioManager : GameComponent
{
#region Private fields
private ContentManager _content;
private Dictionary<string, Song> _songs = new Dictionary<string, Song>();
private Dictionary<string, SoundEffect> _sounds = new Dictionary<string, SoundEffect>();
private Song _currentSong = null;
private SoundEffectInstance[] _playingSounds = new SoundEffectInstance[MaxSounds];
private bool _isMusicPaused = false;
private bool _isFading = false;
private MusicFadeEffect _fadeEffect;
#endregion
// Change MaxSounds to set the maximum number of simultaneous sounds that can be playing.
private const int MaxSounds = 32;
/// <summary>
/// Gets the name of the currently playing song, or null if no song is playing.
/// </summary>
public string CurrentSong { get; private set; }
/// <summary>
/// Gets or sets the volume to play songs. 1.0f is max volume.
/// </summary>
public float MusicVolume
{
get { return MediaPlayer.Volume; }
set { MediaPlayer.Volume = value; }
}
/// <summary>
/// Gets or sets the master volume for all sounds. 1.0f is max volume.
/// </summary>
public float SoundVolume
{
get { return SoundEffect.MasterVolume; }
set { SoundEffect.MasterVolume = value; }
}
/// <summary>
/// Gets whether a song is playing or paused (i.e. not stopped).
/// </summary>
public bool IsSongActive { get { return _currentSong != null && MediaPlayer.State != MediaState.Stopped; } }
/// <summary>
/// Gets whether the current song is paused.
/// </summary>
public bool IsSongPaused { get { return _currentSong != null && _isMusicPaused; } }
/// <summary>
/// Creates a new Audio Manager. Add this to the Components collection of your Game.
/// </summary>
/// <param name="game">The Game</param>
public AudioManager(Game game)
: base(game)
{
_content = new ContentManager(game.Content.ServiceProvider, game.Content.RootDirectory);
}
/// <summary>
/// Creates a new Audio Manager. Add this to the Components collection of your Game.
/// </summary>
/// <param name="game">The Game</param>
/// <param name="contentFolder">Root folder to load audio content from</param>
public AudioManager(Game game, string contentFolder)
: base(game)
{
_content = new ContentManager(game.Content.ServiceProvider, contentFolder);
}
/// <summary>
/// Loads a Song into the AudioManager.
/// </summary>
/// <param name="songName">Name of the song to load</param>
public void LoadSong(string songName)
{
LoadSong(songName, songName);
}
/// <summary>
/// Loads a Song into the AudioManager.
/// </summary>
/// <param name="songName">Name of the song to load</param>
/// <param name="songPath">Path to the song asset file</param>
public void LoadSong(string songName, string songPath)
{
if (_songs.ContainsKey(songName))
{
throw new InvalidOperationException(string.Format("Song '{0}' has already been loaded", songName));
}
_songs.Add(songName, _content.Load<Song>(songPath));
}
/// <summary>
/// Loads a SoundEffect into the AudioManager.
/// </summary>
/// <param name="soundName">Name of the sound to load</param>
public void LoadSound(string soundName)
{
LoadSound(soundName, soundName);
}
/// <summary>
/// Loads a SoundEffect into the AudioManager.
/// </summary>
/// <param name="soundName">Name of the sound to load</param>
/// <param name="soundPath">Path to the song asset file</param>
public void LoadSound(string soundName, string soundPath)
{
if (_sounds.ContainsKey(soundName))
{
throw new InvalidOperationException(string.Format("Sound '{0}' has already been loaded", soundName));
}
_sounds.Add(soundName, _content.Load<SoundEffect>(soundPath));
}
/// <summary>
/// Unloads all loaded songs and sounds.
/// </summary>
public void UnloadContent()
{
_content.Unload();
}
/// <summary>
/// Starts playing the song with the given name. If it is already playing, this method
/// does nothing. If another song is currently playing, it is stopped first.
/// </summary>
/// <param name="songName">Name of the song to play</param>
public void PlaySong(string songName)
{
PlaySong(songName, false);
}
/// <summary>
/// Starts playing the song with the given name. If it is already playing, this method
/// does nothing. If another song is currently playing, it is stopped first.
/// </summary>
/// <param name="songName">Name of the song to play</param>
/// <param name="loop">True if song should loop, false otherwise</param>
public void PlaySong(string songName, bool loop)
{
if (CurrentSong != songName)
{
if (_currentSong != null)
{
MediaPlayer.Stop();
}
if (!_songs.TryGetValue(songName, out _currentSong))
{
throw new ArgumentException(string.Format("Song '{0}' not found", songName));
}
CurrentSong = songName;
_isMusicPaused = false;
MediaPlayer.IsRepeating = loop;
MediaPlayer.Play(_currentSong);
if (!Enabled)
{
MediaPlayer.Pause();
}
}
}
/// <summary>
/// Pauses the currently playing song. This is a no-op if the song is already paused,
/// or if no song is currently playing.
/// </summary>
public void PauseSong()
{
if (_currentSong != null && !_isMusicPaused)
{
if (Enabled) MediaPlayer.Pause();
_isMusicPaused = true;
}
}
/// <summary>
/// Resumes the currently paused song. This is a no-op if the song is not paused,
/// or if no song is currently playing.
/// </summary>
public void ResumeSong()
{
if (_currentSong != null && _isMusicPaused)
{
if (Enabled) MediaPlayer.Resume();
_isMusicPaused = false;
}
}
/// <summary>
/// Stops the currently playing song. This is a no-op if no song is currently playing.
/// </summary>
public void StopSong()
{
if (_currentSong != null && MediaPlayer.State != MediaState.Stopped)
{
MediaPlayer.Stop();
_isMusicPaused = false;
}
}
/// <summary>
/// Smoothly transition between two volumes.
/// </summary>
/// <param name="targetVolume">Target volume, 0.0f to 1.0f</param>
/// <param name="duration">Length of volume transition</param>
public void FadeSong(float targetVolume, TimeSpan duration)
{
if (duration <= TimeSpan.Zero)
{
throw new ArgumentException("Duration must be a positive value");
}
_fadeEffect = new MusicFadeEffect(MediaPlayer.Volume, targetVolume, duration);
_isFading = true;
}
/// <summary>
/// Stop the current fade.
/// </summary>
/// <param name="option">Options for setting the music volume</param>
public void CancelFade(FadeCancelOptions option)
{
if (_isFading)
{
switch (option)
{
case FadeCancelOptions.Source: MediaPlayer.Volume = _fadeEffect.SourceVolume; break;
case FadeCancelOptions.Target: MediaPlayer.Volume = _fadeEffect.TargetVolume; break;
}
_isFading = false;
}
}
/// <summary>
/// Plays the sound of the given name.
/// </summary>
/// <param name="soundName">Name of the sound</param>
public void PlaySound(string soundName)
{
PlaySound(soundName, 1.0f, 0.0f, 0.0f);
}
/// <summary>
/// Plays the sound of the given name at the given volume.
/// </summary>
/// <param name="soundName">Name of the sound</param>
/// <param name="volume">Volume, 0.0f to 1.0f</param>
public void PlaySound(string soundName, float volume)
{
PlaySound(soundName, volume, 0.0f, 0.0f);
}
/// <summary>
/// Plays the sound of the given name with the given parameters.
/// </summary>
/// <param name="soundName">Name of the sound</param>
/// <param name="volume">Volume, 0.0f to 1.0f</param>
/// <param name="pitch">Pitch, -1.0f (down one octave) to 1.0f (up one octave)</param>
/// <param name="pan">Pan, -1.0f (full left) to 1.0f (full right)</param>
public void PlaySound(string soundName, float volume, float pitch, float pan)
{
SoundEffect sound;
if (!_sounds.TryGetValue(soundName, out sound))
{
throw new ArgumentException(string.Format("Sound '{0}' not found", soundName));
}
int index = GetAvailableSoundIndex();
if (index != -1)
{
_playingSounds[index] = sound.CreateInstance();
_playingSounds[index].Volume = volume;
_playingSounds[index].Pitch = pitch;
_playingSounds[index].Pan = pan;
_playingSounds[index].Play();
if (!Enabled)
{
_playingSounds[index].Pause();
}
}
}
/// <summary>
/// Stops all currently playing sounds.
/// </summary>
public void StopAllSounds()
{
for (int i = 0; i < _playingSounds.Length; ++i)
{
if (_playingSounds[i] != null)
{
_playingSounds[i].Stop();
_playingSounds[i].Dispose();
_playingSounds[i] = null;
}
}
}
/// <summary>
/// Called per loop unless Enabled is set to false.
/// </summary>
/// <param name="gameTime">Time elapsed since last frame</param>
public override void Update(GameTime gameTime)
{
for (int i = 0; i < _playingSounds.Length; ++i)
{
if (_playingSounds[i] != null && _playingSounds[i].State == SoundState.Stopped)
{
_playingSounds[i].Dispose();
_playingSounds[i] = null;
}
}
if (_currentSong != null && MediaPlayer.State == MediaState.Stopped)
{
_currentSong = null;
CurrentSong = null;
_isMusicPaused = false;
}
if (_isFading && !_isMusicPaused)
{
if (_currentSong != null && MediaPlayer.State == MediaState.Playing)
{
if (_fadeEffect.Update(gameTime.ElapsedGameTime))
{
_isFading = false;
}
MediaPlayer.Volume = _fadeEffect.GetVolume();
}
else
{
_isFading = false;
}
}
base.Update(gameTime);
}
// Pauses all music and sound if disabled, resumes if enabled.
protected override void OnEnabledChanged(object sender, EventArgs args)
{
if (Enabled)
{
for (int i = 0; i < _playingSounds.Length; ++i)
{
if (_playingSounds[i] != null && _playingSounds[i].State == SoundState.Paused)
{
_playingSounds[i].Resume();
}
}
if (!_isMusicPaused)
{
MediaPlayer.Resume();
}
}
else
{
for (int i = 0; i < _playingSounds.Length; ++i)
{
if (_playingSounds[i] != null && _playingSounds[i].State == SoundState.Playing)
{
_playingSounds[i].Pause();
}
}
MediaPlayer.Pause();
}
base.OnEnabledChanged(sender, args);
}
// Acquires an open sound slot.
private int GetAvailableSoundIndex()
{
for (int i = 0; i < _playingSounds.Length; ++i)
{
if (_playingSounds[i] == null)
{
return i;
}
}
return -1;
}
#region MusicFadeEffect
private struct MusicFadeEffect
{
public float SourceVolume;
public float TargetVolume;
private TimeSpan _time;
private TimeSpan _duration;
public MusicFadeEffect(float sourceVolume, float targetVolume, TimeSpan duration)
{
SourceVolume = sourceVolume;
TargetVolume = targetVolume;
_time = TimeSpan.Zero;
_duration = duration;
}
public bool Update(TimeSpan time)
{
_time += time;
if (_time >= _duration)
{
_time = _duration;
return true;
}
return false;
}
public float GetVolume()
{
return MathHelper.Lerp(SourceVolume, TargetVolume, (float)_time.Ticks / _duration.Ticks);
}
}
#endregion
}
/// <summary>
/// Options for AudioManager.CancelFade
/// </summary>
public enum FadeCancelOptions
{
/// <summary>
/// Return to pre-fade volume
/// </summary>
Source,
/// <summary>
/// Snap to fade target volume
/// </summary>
Target,
/// <summary>
/// Keep current volume
/// </summary>
Current
}