Audio in MonoGame
Audio in MonoGame
MonoGame exposes two distinct audio APIs: a low-level SoundEffect / SoundEffectInstance stack for sound effects, and a high-level Song / MediaPlayer stack for background music. This article covers both, adds 3D positional audio, and highlights common pitfalls that trip up newcomers.
💡 MonoGame wraps OpenAL on Linux and macOS, and XAudio2 on Windows. The public API is identical across platforms; the only difference you will notice is which audio file formats are supported by the Content Pipeline on each OS.
Adding Audio via the Content Pipeline
All audio assets should be added through the Content Pipeline so they are compiled to a compressed, platform-optimised format. Open the MGCB Editor and add your audio files:
- Sound effects — use
.wav(PCM, uncompressed). The pipeline imports them asSoundEffect. - Music — use
.mp3or.ogg. The pipeline imports them asSong. On Windows,.wmais also supported.
A minimal Content.mgcb entry for a sound effect and a music track looks like this:
#begin Sounds/explosion.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:Sounds/explosion.wav
#begin Music/theme.mp3
/importer:Mp3Importer
/processor:SongProcessor
/build:Music/theme.mp3
Load these in LoadContent() using the asset names (path relative to Content/, no extension):
private SoundEffect _explosionSfx;
private Song _themeMusic;
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_explosionSfx = Content.Load<SoundEffect>("Sounds/explosion");
_themeMusic = Content.Load<Song>("Music/theme");
}
One-Shot Sound Effects with SoundEffect
For sounds you simply want to fire and forget, call SoundEffect.Play(). It creates and discards a temporary SoundEffectInstance internally:
// Simplest — plays at full volume, no pitch shift, centred
_explosionSfx.Play();
// With volume, pitch, and pan
// volume: 0.0f (silent) to 1.0f (full)
// pitch: -1.0f (one octave down) to 1.0f (one octave up), 0 = no change
// pan: -1.0f (full left) to 1.0f (full right), 0 = centre
_explosionSfx.Play(volume: 0.8f, pitch: 0.1f, pan: 0.0f);
// SoundEffect.MasterVolume scales ALL sound effects globally (0–1)
SoundEffect.MasterVolume = 0.5f;
⚠️ SoundEffect.Play() has a hard limit on the number of simultaneous instances (typically 64). If this limit is reached it returns false silently. For high-frequency sounds (e.g. bullet impacts) this can cause dropped audio.
SoundEffectInstance — Looping, Volume, and Pitch Control
When you need to control a sound after it starts — loop it, pause it, change its volume, or stop it explicitly — create a SoundEffectInstance from the SoundEffect:
private SoundEffect _engineSfx;
private SoundEffectInstance _engineInstance;
protected override void LoadContent()
{
_engineSfx = Content.Load<SoundEffect>("Sounds/engine");
_engineInstance = _engineSfx.CreateInstance();
_engineInstance.IsLooped = true;
_engineInstance.Volume = 0.6f;
_engineInstance.Pitch = 0f;
}
protected override void Update(GameTime gameTime)
{
bool engineRunning = /* game logic */ true;
if (engineRunning && _engineInstance.State != SoundState.Playing)
_engineInstance.Play();
if (!engineRunning && _engineInstance.State == SoundState.Playing)
_engineInstance.Stop();
// Pitch up as speed increases — 0 to +1 octave
float speed = player.Speed; // 0–500 units/s
_engineInstance.Pitch = MathHelper.Clamp(speed / 500f, 0f, 1f);
base.Update(gameTime);
}
protected override void UnloadContent()
{
// Always dispose instances you created manually
_engineInstance?.Dispose();
base.UnloadContent();
}
The SoundState enum has three values: Playing, Paused, and Stopped. Use Pause() and Resume() (Play() again after pause) to suspend and resume a looping sound without restarting it.
⚠️ Always call Dispose() on any SoundEffectInstance you created via CreateInstance(). Failing to dispose leaks the underlying audio buffer on some platforms.
Background Music with Song and MediaPlayer
The MediaPlayer class is a static singleton that plays Song assets. It supports play, pause, resume, stop, and volume control, and can loop the current track automatically.
private Song _themeMusic;
private Song _battleMusic;
protected override void LoadContent()
{
_themeMusic = Content.Load<Song>("Music/theme");
_battleMusic = Content.Load<Song>("Music/battle");
// Start playing immediately
MediaPlayer.Volume = 0.4f; // 0–1
MediaPlayer.IsRepeating = true;
MediaPlayer.Play(_themeMusic);
}
// Call this when a battle begins
public void StartBattle()
{
// Stop current track and start the battle theme
MediaPlayer.Stop();
MediaPlayer.Play(_battleMusic);
}
// Pause music when the game is paused
protected override void OnDeactivated(object sender, EventArgs args)
{
if (MediaPlayer.State == MediaState.Playing)
MediaPlayer.Pause();
base.OnDeactivated(sender, args);
}
// Resume when the game regains focus
protected override void OnActivated(object sender, EventArgs args)
{
if (MediaPlayer.State == MediaState.Paused)
MediaPlayer.Resume();
base.OnActivated(sender, args);
}
You can cross-fade between two tracks by gradually lowering the volume, swapping the track, then raising it again:
public class MusicManager
{
private Song _nextSong;
private float _targetVolume = 0.4f;
private float _currentVolume = 0.4f;
private bool _fadingOut = false;
private const float FadeSpeed = 0.8f; // volume units per second
public void RequestSong(Song song)
{
if (MediaPlayer.Queue.ActiveSong == song) return;
_nextSong = song;
_fadingOut = true;
}
public void Update(GameTime gameTime)
{
float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (_fadingOut)
{
_currentVolume -= FadeSpeed * delta;
if (_currentVolume <= 0f)
{
_currentVolume = 0f;
_fadingOut = false;
MediaPlayer.Play(_nextSong);
_nextSong = null;
}
}
else
{
_currentVolume = MathHelper.Min(_currentVolume + FadeSpeed * delta, _targetVolume);
}
MediaPlayer.Volume = _currentVolume;
}
}
💡 MediaPlayer is a global singleton — there is only one active Song at a time. Do not attempt to play two Song objects simultaneously; use two SoundEffectInstances instead if you need overlapping streams.
3D Positional Audio
MonoGame supports 3D audio via the AudioEmitter and AudioListener classes. Apply 3D settings to a SoundEffectInstance via Apply3D():
private SoundEffect _footstepSfx;
private SoundEffectInstance _footstepInstance;
private AudioListener _listener;
private AudioEmitter _emitter;
protected override void LoadContent()
{
_footstepSfx = Content.Load<SoundEffect>("Sounds/footstep");
_footstepInstance = _footstepSfx.CreateInstance();
_listener = new AudioListener();
_emitter = new AudioEmitter();
}
protected override void Update(GameTime gameTime)
{
// Update the listener to match the camera / player
_listener.Position = new Vector3(camera.Position, 0f);
_listener.Forward = Vector3.Forward;
_listener.Up = Vector3.Up;
// Update the emitter to match the sound source (e.g. an NPC)
_emitter.Position = new Vector3(npc.Position, 0f);
_emitter.Velocity = new Vector3(npc.Velocity, 0f); // for Doppler effect
// Apply 3D spatialisation — this recalculates pan and volume falloff
_footstepInstance.Apply3D(_listener, _emitter);
// Play when the NPC takes a step
if (npc.IsSteppingThisFrame && _footstepInstance.State != SoundState.Playing)
_footstepInstance.Play();
base.Update(gameTime);
}
💡 Apply3D() modifies the instance’s Pan and Volume internally based on the relative positions. For a 2D game, set the Z component to 0 for both the listener and emitter as shown above.
Common Pitfalls
MediaPlayer.State after Play()
On some platforms (notably Android), MediaPlayer.State is not synchronously MediaState.Playing immediately after calling Play(). Guard state checks with a small delay or use the MediaPlayer.MediaStateChanged event instead.
Disposing Content-Loaded Assets
Do not call Dispose() on SoundEffect or Song objects loaded via Content.Load — the ContentManager owns and disposes them automatically when Content.Unload() is called. Only dispose SoundEffectInstance objects you created via CreateInstance().
Maximum Simultaneous Instances
The XAudio2 / OpenAL backends have a practical limit of around 64 concurrent SoundEffectInstance objects. Recycle instances from a pool for high-frequency effects such as bullets or particle impacts:
using System.Collections.Generic;
using Microsoft.Xna.Framework.Audio;
public class SoundPool
{
private readonly SoundEffect _effect;
private readonly Queue<SoundEffectInstance> _pool = new();
private readonly int _maxInstances;
public SoundPool(SoundEffect effect, int maxInstances = 8)
{
_effect = effect;
_maxInstances = maxInstances;
// Pre-warm the pool
for (int i = 0; i < maxInstances; i++)
_pool.Enqueue(effect.CreateInstance());
}
/// <summary>
/// Plays the effect if a free instance is available.
/// If all instances are playing, the oldest one is restarted.
/// </summary>
public void Play(float volume = 1f, float pitch = 0f)
{
SoundEffectInstance instance;
// Prefer a stopped instance
foreach (var inst in _pool)
{
if (inst.State == SoundState.Stopped)
{
instance = inst;
instance.Volume = volume;
instance.Pitch = pitch;
instance.Play();
return;
}
}
// All playing — steal the oldest (first in queue) and restart it
instance = _pool.Dequeue();
instance.Stop();
instance.Volume = volume;
instance.Pitch = pitch;
instance.Play();
_pool.Enqueue(instance);
}
public void Dispose()
{
foreach (var inst in _pool) inst.Dispose();
_pool.Clear();
}
}
Next Steps
- Explore the XACT audio engine for advanced mixing, categories, and cue-based audio — it is fully supported in MonoGame
- Add an audio settings screen that persists
SoundEffect.MasterVolumeandMediaPlayer.Volumeto a settings file - Combine 3D audio with the Input Handling article to let players pan a top-down camera while audio follows correctly
- Experiment with dynamic pitch on
SoundEffectInstanceto create engine sounds that scale with movement speed