Table of Contents

Chapter 14: SoundEffects and Music

Learn how to load and play sound effects and background music in MonoGame including managing audio volume, looping, and handling multiple sound effects at once.

In Chapter 12, we implemented collision detection to enable interactions between game objects; the slime can now "eat" the bat, which respawns in a random location, while the bat bounces off walls of the dungeon. While these mechanics work visually, our game lacks an important element of player feedback: audio.

Audio plays a crucial role in game development by providing immediate feedback for player actions and creating atmosphere. Sound effects alert players when events occur (like collisions or collecting items), while background music helps establish mood and atmosphere.

In this chapter, you will:

  • Learn how MonoGame handles different types of audio content.
  • Learn how to load and play sound effects and music using the content pipeline.
  • Implement sound effects for collision events.
  • Add background music to enhance atmosphere.

Let's start by understanding how MonoGame approaches audio content.

Understanding Audio in MonoGame

Recall from Chapter 01 that MonoGame is an implementation of the XNA API. With XNA, there were two methods for implementing audio in your game: the Microsoft Cross-Platform Audio Creation Tool (XACT) and the simplified sound API.

Important

XACT is a mini audio engineering studio where you can easily edit the audio for your game like editing volume, pitch, looping, applying effects, and other properties without having to do it in code. At that time, XACT for XNA games was akin to what FMOD Studio is today for game audio.

Figure 14-1: Microsoft Cross-Platform Audio Creation Tool
Figure 14-1: Microsoft Cross-Platform Audio Creation Tool

While XACT projects are still fully supported in MonoGame, it remains a Windows-only tool that has not been updated since Microsoft discontinued the original XNA, nor has its source code been made open source. Though it is possible to install XACT on modern Windows, the process can be complex.

For these reasons, this tutorial will focus on the simplified sound API, which provides all the core functionality needed for most games while remaining cross-platform compatible.

The simplified sound API approaches audio management through two distinct paths, each optimized for different use cases in games. When adding audio to your game, you need to consider how different types of sounds should be handled:

  • Sound Effects: Short audio clips that need to play immediately and often simultaneously, like the bounce of a ball or feedback for picking up a collectable.
  • Music: Longer audio pieces that play continuously in the background, like level themes.

MonoGame addresses these different needs through two main classes:

Sound Effects

The SoundEffect class handles short audio clips like:

  • Collision sounds.
  • Player action feedback (jumping, shooting, etc.).
  • UI interactions (button clicks, menu navigation).
  • Environmental effects (footsteps, ambient sounds).

The key characteristics of sound effects are:

  • Loaded entirely into memory for quick access
  • Can play multiple instances simultaneously:
    • Mobile platforms can have a maximum of 32 sounds playing simultaneously.
    • Desktop platforms have a maximum of 256 sounds playing simultaneously.
    • Consoles and other platforms have their own constraints, and you would need to refer to the SDK documentation for that platform.
  • Lower latency playback (ideal for immediate feedback)
  • Individual volume control per instance.

Music

The Song class handles longer audio pieces like background music. The key characteristics of songs are:

  • Streamed from storage rather than loaded into memory.
  • Only one song can be played at a time.
  • Higher latency, but lower memory usage.

Throughout this chapter, we will use both classes to add audio feedback to our game; sound effects for the bat bouncing and being eaten by the slime, and background music to create atmosphere.

Loading Audio Content

Just like textures, audio content in MonoGame can be loaded through the content pipeline, optimizing the format for your target platform.

Supported Audio Formats

MonoGame supports several audio file formats for both sound effects and music:

  • .wav: Uncompressed audio, ideal for short sound effects
  • .mp3: Compressed audio, better for music and longer sounds
  • .ogg: Open source compressed format, supported on all platforms
  • .wma: Windows Media Audio format (not recommended for cross-platform games)
Tip

For sound effects, .wav files provide the best loading and playback performance since they do not need to be decompressed. For music, .mp3 or .ogg files are better choices as they reduce file size while maintaining good quality.

Adding Audio Files

Adding audio files can be done through the content pipeline, just like we did for image files, using the MGCB Editor. When you add an audio file to the content project, the MGCB Editor will automatically select the appropriate importer and processor for the audio file based on the file extension.

The processor that are available for audio files file:

  • Sound Effects: Processes the audio file as a SoundEffect. This is automatically selected for .wav files.
  • Song: Processes the audio file as a Song. This is automatically selected for .mp3, .ogg, and .wma files.
Figure 14-2: MGCB Editor properties panel showing Sound Effect content processor settings for .wav files Figure 14-3: MGCB Editor properties panel showing Song content processor settings for .mp3 files
Figure 14-2: MGCB Editor properties panel showing Sound Effect content processor settings for .wav files Figure 14-3: MGCB Editor properties panel showing Song content processor settings for .mp3 files
Note

While you typically will not need to change the processor it automatically selects, there may be times where you add files, such as .mp3 files that are meant to be sound effects and not songs. Always double check that the processor selected is for the intended type.

Loading Sound Effects

To load a sound effect, we use ContentManager.Load with the SoundEffect type:

// Loading a SoundEffect using the content pipeline
SoundEffect soundEffect = Content.Load<SoundEffect>("soundEffect");

Loading Music

Loading music is similar, only we specify the Song type instead.

// Loading a Song using the content pipeline
Song song = Content.Load<Song>("song");

Playing Sound Effects

Sound effects are played using the SoundEffect class. This class provides two ways to play sounds:

  1. Direct playback using SoundEffect.Play:

    // Loading a SoundEffect using the content pipeline
    SoundEffect soundEffect = Content.Load<SoundEffect>("soundEffect");
    
    // Play the sound effect with default settings
    soundEffect.Play();
    
  2. Creating an instance using SoundEffect.CreateInstance:

    // Loading a SoundEffect using the content pipeline
    SoundEffect soundEffect = Content.Load<SoundEffect>("soundEffect");
    
    // Create an instance we can control
    SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
    
    // Adjust the properties of the instance as needed
    soundEffectInstance.IsLooped = true;    // Make it loop
    soundEffectInstance.Volume = 0.5f;      // Set half volume.
    
    // Play the sound effect using the instance.
    soundEffectInstance.Play();
    
  • Use SoundEffect.Play for simple sound effects that you just want to play once.
  • Use SoundEffect.CreateInstance when you need more control over the sound effect, like adjusting volume, looping, or managing multiple instances of the same sound.

SoundEffectInstance contains several properties that can be used to control how the sound effect is played:

Property Type Description
IsLooped bool Whether the sound should loop when it reaches the end.
Pan float Stereo panning between -1.0f (full left) and 1.0f (full right).
Pitch float Pitch adjustment between -1.0f (down one octave) and 1.0f (up one octave).
State SoundState Current playback state (Playing, Paused, or Stopped).
Volume float Volume level between 0.0f (silent) and 1.0f (full volume).

Playing Music

Unlike sound effects, music is played through the MediaPlayer class. This static class manages playback of Song instances and provides global control over music playback:

// Loading a Song using the content pipeline
Song song = Content.Load<Song>("song");

// Set whether the song should repeat when finished
MediaPlayer.IsRepeating = true;

// Adjust the volume (0.0f to 1.0f)
MediaPlayer.Volume = 0.5f;

// Check if the media player is already playing, if so, stop it
if(MediaPlayer.State == MediaState.Playing)
{
    MediaPlayer.Stop();
}

// Start playing the background music
MediaPlayer.Play(song);
Important

While SoundEffect instances can be played simultaneously, trying to play a new Song while another is playing will stop the current song in the best case, and in the worst case cause a crash on some platforms. In the example above, the state of the media player is checked first before we tell it to play a song. Checking the state first and stopping it manually if it is playing is best practice to prevent potential crashes.

Adding Audio To Our Game

Before we can add audio to our game, we need some sound files to work with. Download the following audio files:

Note

Add these files to your content project using the MGCB Editor:

  1. Open the Content.mgcb file in the MGCB Editor.
  2. Create a new directory called audio (right-click Content > Add > New Folder).
  3. Right-click the new audio directory and choose Add > Existing Item....
  4. Navigate to and select the audio files you downloaded.
  5. For each file that is added, check its properties in the Properties panel:
    • For .wav files, ensure the Processor is set to Sound Effect.
    • For .mp3 files, ensure the Processor is set to Song.

Next, open the Game1.cs file and update it to the following:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;

namespace DungeonSlime;

public class Game1 : Core
{
    // Defines the slime animated sprite.
    private AnimatedSprite _slime;

    // Defines the bat animated sprite.
    private AnimatedSprite _bat;

    // Tracks the position of the slime.
    private Vector2 _slimePosition;

    // Speed multiplier when moving.
    private const float MOVEMENT_SPEED = 5.0f;

    // Tracks the position of the bat.
    private Vector2 _batPosition;

    // Tracks the velocity of the bat.
    private Vector2 _batVelocity;

    // Defines the tilemap to draw.
    private Tilemap _tilemap;

    // Defines the bounds of the room that the slime and bat are contained within.
    private Rectangle _roomBounds;

    // The sound effect to play when the bat bounces off the edge of the screen.
    private SoundEffect _bounceSoundEffect;

    // The sound effect to play when the slime eats a bat.
    private SoundEffect _collectSoundEffect;

    public Game1() : base("Dungeon Slime", 1280, 720, false)
    {

    }

    protected override void Initialize()
    {
        base.Initialize();

        Rectangle screenBounds = GraphicsDevice.PresentationParameters.Bounds;

        _roomBounds = new Rectangle(
            _tilemap.TileSize,
            _tilemap.TileSize,
            screenBounds.Width - _tilemap.TileSize * 2,
            screenBounds.Height - _tilemap.TileSize * 2
        );

        // Initial slime position will be the center tile of the tile map.
        int centerRow = _tilemap.Rows / 2;
        int centerColumn = _tilemap.Columns / 2;
        _slimePosition = new Vector2(centerColumn, centerRow) * _tilemap.TileSize;

        // Initial bat position will the in the top left corner of the room
        _batPosition = new Vector2(_roomBounds.Left, _roomBounds.Top);

        // Assign the initial random velocity to the bat.
        AssignRandomBatVelocity();
    }

    protected override void LoadContent()
    {
        // Create the texture atlas from the XML configuration file
        TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml");

        // Create the slime animated sprite from the atlas.
        _slime = atlas.CreateAnimatedSprite("slime-animation");

        // Create the bat animated sprite from the atlas.
        _bat = atlas.CreateAnimatedSprite("bat-animation");

        // Load the tilemap from the XML configuration file.
        _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");

        // Load the bounce sound effect
        _bounceSoundEffect = Content.Load<SoundEffect>("audio/bounce");

        // Load the collect sound effect
        _collectSoundEffect = Content.Load<SoundEffect>("audio/collect");

        // Load the background theme music
        Song theme = Content.Load<Song>("audio/theme");

        // Ensure media player isn't already playing on device, if so, stop it
        if (MediaPlayer.State == MediaState.Playing)
        {
            MediaPlayer.Stop();
        }

        // Play the background theme music.
        MediaPlayer.Play(theme);

        // Set the theme music to repeat.
        MediaPlayer.IsRepeating = true;

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        // Update the slime animated sprite.
        _slime.Update(gameTime);

        // Update the bat animated sprite.
        _bat.Update(gameTime);

        // Check for keyboard input and handle it.
        CheckKeyboardInput();

        // Check for gamepad input and handle it.
        CheckGamePadInput();

        // Creating a bounding circle for the slime
        Circle slimeBounds = new Circle(
            (int)(_slimePosition.X + (_slime.Width * 0.5f)),
            (int)(_slimePosition.Y + (_slime.Height * 0.5f)),
            (int)(_slime.Width * 0.5f)
        );

        // Use distance based checks to determine if the slime is within the
        // bounds of the game screen, and if it's outside that screen edge,
        // move it back inside.
        if (slimeBounds.Left < _roomBounds.Left)
        {
            _slimePosition.X = _roomBounds.Left;
        }
        else if (slimeBounds.Right > _roomBounds.Right)
        {
            _slimePosition.X = _roomBounds.Right - _slime.Width;
        }

        if (slimeBounds.Top < _roomBounds.Top)
        {
            _slimePosition.Y = _roomBounds.Top;
        }
        else if (slimeBounds.Bottom > _roomBounds.Bottom)
        {
            _slimePosition.Y = _roomBounds.Bottom - _slime.Height;
        }

        // Calculate the new position of the bat based on the velocity
        Vector2 newBatPosition = _batPosition + _batVelocity;

        // Create a bounding circle for the bat
        Circle batBounds = new Circle(
            (int)(newBatPosition.X + (_bat.Width * 0.5f)),
            (int)(newBatPosition.Y + (_bat.Height * 0.5f)),
            (int)(_bat.Width * 0.5f)
        );

        Vector2 normal = Vector2.Zero;

        // Use distance based checks to determine if the bat is within the
        // bounds of the game screen, and if it's outside that screen edge,
        // reflect it about the screen edge normal
        if (batBounds.Left < _roomBounds.Left)
        {
            normal.X = Vector2.UnitX.X;
            newBatPosition.X = _roomBounds.Left;
        }
        else if (batBounds.Right > _roomBounds.Right)
        {
            normal.X = -Vector2.UnitX.X;
            newBatPosition.X = _roomBounds.Right - _bat.Width;
        }

        if (batBounds.Top < _roomBounds.Top)
        {
            normal.Y = Vector2.UnitY.Y;
            newBatPosition.Y = _roomBounds.Top;
        }
        else if (batBounds.Bottom > _roomBounds.Bottom)
        {
            normal.Y = -Vector2.UnitY.Y;
            newBatPosition.Y = _roomBounds.Bottom - _bat.Height;
        }

        // If the normal is anything but Vector2.Zero, this means the bat had
        // moved outside the screen edge so we should reflect it about the
        // normal.
        if (normal != Vector2.Zero)
        {
            _batVelocity = Vector2.Reflect(_batVelocity, normal);

            // Play the bounce sound effect
            _bounceSoundEffect.Play();
        }

        _batPosition = newBatPosition;

        if (slimeBounds.Intersects(batBounds))
        {
            // Choose a random row and column based on the total number of each
            int column = Random.Shared.Next(1, _tilemap.Columns - 1);
            int row = Random.Shared.Next(1, _tilemap.Rows - 1);

            // Change the bat position by setting the x and y values equal to
            // the column and row multiplied by the width and height.
            _batPosition = new Vector2(column * _bat.Width, row * _bat.Height);

            // Assign a new random velocity to the bat
            AssignRandomBatVelocity();

            // Play the collect sound effect
            _collectSoundEffect.Play();
        }

        base.Update(gameTime);
    }

    private void AssignRandomBatVelocity()
    {
        // Generate a random angle
        float angle = (float)(Random.Shared.NextDouble() * Math.PI * 2);

        // Convert angle to a direction vector
        float x = (float)Math.Cos(angle);
        float y = (float)Math.Sin(angle);
        Vector2 direction = new Vector2(x, y);

        // Multiply the direction vector by the movement speed
        _batVelocity = direction * MOVEMENT_SPEED;
    }

    private void CheckKeyboardInput()
    {
        // If the space key is held down, the movement speed increases by 1.5
        float speed = MOVEMENT_SPEED;
        if (Input.Keyboard.IsKeyDown(Keys.Space))
        {
            speed *= 1.5f;
        }

        // If the W or Up keys are down, move the slime up on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.W) || Input.Keyboard.IsKeyDown(Keys.Up))
        {
            _slimePosition.Y -= speed;
        }

        // if the S or Down keys are down, move the slime down on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.S) || Input.Keyboard.IsKeyDown(Keys.Down))
        {
            _slimePosition.Y += speed;
        }

        // If the A or Left keys are down, move the slime left on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.A) || Input.Keyboard.IsKeyDown(Keys.Left))
        {
            _slimePosition.X -= speed;
        }

        // If the D or Right keys are down, move the slime right on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.D) || Input.Keyboard.IsKeyDown(Keys.Right))
        {
            _slimePosition.X += speed;
        }
    }

    private void CheckGamePadInput()
    {
        GamePadInfo gamePadOne = Input.GamePads[(int)PlayerIndex.One];

        // If the A button is held down, the movement speed increases by 1.5
        // and the gamepad vibrates as feedback to the player.
        float speed = MOVEMENT_SPEED;
        if (gamePadOne.IsButtonDown(Buttons.A))
        {
            speed *= 1.5f;
            GamePad.SetVibration(PlayerIndex.One, 1.0f, 1.0f);
        }
        else
        {
            GamePad.SetVibration(PlayerIndex.One, 0.0f, 0.0f);
        }

        // Check thumbstick first since it has priority over which gamepad input
        // is movement.  It has priority since the thumbstick values provide a
        // more granular analog value that can be used for movement.
        if (gamePadOne.LeftThumbStick != Vector2.Zero)
        {
            _slimePosition.X += gamePadOne.LeftThumbStick.X * speed;
            _slimePosition.Y -= gamePadOne.LeftThumbStick.Y * speed;
        }
        else
        {
            // If DPadUp is down, move the slime up on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadUp))
            {
                _slimePosition.Y -= speed;
            }

            // If DPadDown is down, move the slime down on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadDown))
            {
                _slimePosition.Y += speed;
            }

            // If DPapLeft is down, move the slime left on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadLeft))
            {
                _slimePosition.X -= speed;
            }

            // If DPadRight is down, move the slime right on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadRight))
            {
                _slimePosition.X += speed;
            }
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        // Clear the back buffer.
        GraphicsDevice.Clear(Color.CornflowerBlue);

        // Begin the sprite batch to prepare for rendering.
        SpriteBatch.Begin(samplerState: SamplerState.PointClamp);

        // Draw the tilemap
        _tilemap.Draw(SpriteBatch);

        // Draw the slime sprite.
        _slime.Draw(SpriteBatch, _slimePosition);

        // Draw the bat sprite.
        _bat.Draw(SpriteBatch, _batPosition);

        // Always end the sprite batch when finished.
        SpriteBatch.End();

        base.Draw(gameTime);
    }
}

The key changes here are:

  1. Added the using Microsoft.Xna.Framework.Audio; and using Microsoft.Xna.Framework.Media; directories to access the Song and SoundEffect classes.
  2. Added the _boundSoundEffect and _collectSoundEffect fields to store those sound effects when loaded and use them for playback.
  3. In LoadContent
    1. The bounce and collect sound effects are loaded using the content manager.
    2. The background theme music is loaded using the content manager.
    3. The background music is played using the media player, checking its state first.
    4. The MediaPlayer.IsRepeating is set to true so the background music loops.
  4. In Update:
    1. The bounce sound effect is played when the bat bounces off the edge of the screen.
    2. The collect sound effect is played when the slime eats the bat.

Running the game now, the theme music plays in the background, you can hear the bat bounce off the edge of the screen, and if you move the slime to eat the bat, you hear that as well.

Figure 14-4: Gameplay with audio.

Conclusion

Let's review what you accomplished in this chapter:

  • Learned about MonoGame's audio system including sound effects and music.
  • Explored the key differences between:
    • Sound effects (short, multiple simultaneous playback).
    • Music (longer, streamed, single playback).
  • Added audio content to your game project through the content pipeline.
  • Loaded audio files using the ContentManager.
  • Implemented audio feedback in your game:
    • Background music to set atmosphere.
    • Sound effects for bat bouncing and collection events.
  • Learned best practices for handling audio playback across different platforms.

In the next chapter, we'll explore additional ways to manage audio by creating an audio controller module that will help with common tasks such as volume control, muting, and state management.

Test Your Knowledge

  1. What are the two main classes MonoGame provides for audio playback and how do they differ?

    MonoGame provides:

    • SoundEffect for short audio clips (loaded entirely into memory, multiple can play at once) and
    • Song for longer audio like music (streamed from storage, only one can play at a time).
  2. Why is it important to check if MediaPlayer is already playing before starting a new song?

    Checking if MediaPlayer is already playing and stopping it if necessary helps prevent crashes on some platforms. Since only one song can play at a time, properly stopping the current song before starting a new one ensures reliable behavior across different platforms.

  3. What file formats are best suited for sound effects and music, respectively, and why?

    For sound effects, .wav files are generally best because they're uncompressed and load quickly into memory for immediate playback. For music, compressed formats like .mp3 or .ogg are better suited because they greatly reduce file size while maintaining good audio quality, which is important for longer audio that's streamed rather than fully loaded.

  4. What's the difference between using SoundEffect.Play directly and creating a SoundEffectInstance?

    • SoundEffect.Play is simpler but provides limited control - it plays the sound once with basic volume/pitch/pan settings.
    • Creating a SoundEffectInstance gives more control including the ability to pause, resume, loop, and change properties during playback, as well as track the sound's state.
  5. How many sound effects can play simultaneously on different platforms?

    The number of simultaneous sound effects varies by platform:

    • Mobile platforms: maximum of 32 sounds.
    • Desktop platforms: maximum of 256 sounds.
    • Consoles and other platforms have their own constraints specified in their respective SDK documentation.