Tutorial 15: Playing Background Music

Audio  ·  MediaPlayer  ·  Song  ·  MediaState

Background music in XNA is handled by a dedicated static class called MediaPlayer together with the Song asset type. Unlike SoundEffect, music is streamed from disk rather than fully decoded into memory — essential for multi-minute tracks that would otherwise consume hundreds of megabytes of RAM.

MediaPlayer overview

MediaPlayer is a static singleton that manages exactly one active music track at a time. It mirrors the original XNA design where background music was treated as a global resource. CNA backs it with SDL3_mixer's music channel, which also streams audio.

Key characteristics:

  • Only one Song can play at a time — starting a new one stops the current one.
  • Volume, repeat, and state are global to the single channel.
  • Pause/Resume operate on the whole music stream, not individual songs.
  • The MediaStateChangedEventArgs callback fires on every state transition.

The Song class

Song is a lightweight asset handle that holds a path to the audio file and metadata (name, duration). The actual audio data is not decoded until MediaPlayer::Play() is called. This means loading a Song is cheap and you can safely pre-load a playlist of songs without memory pressure.

#include "Microsoft/Xna/Framework/Media/Song.hpp"

using namespace Microsoft::Xna::Framework::Media;

// Song is a thin wrapper — cheap to store
std::shared_ptr<Song> menuTheme;
std::shared_ptr<Song> battleTheme;

ContentManager::Load<Song>

Songs are loaded through ContentManager just like textures and sounds:

// In LoadContent():
menuTheme   = Content.Load<Song>("Audio/menu_theme");
battleTheme = Content.Load<Song>("Audio/battle_theme");

ContentManager resolves the file extension automatically: it tries .ogg, .mp3, .flac, and .wav in that order. OGG Vorbis is recommended for background music — it offers good quality at low bitrates and is patent-free.

MediaPlayer::Play, Pause, Stop, Resume

#include "Microsoft/Xna/Framework/Media/MediaPlayer.hpp"

using namespace Microsoft::Xna::Framework::Media;

// Start playing (restarts from the beginning)
MediaPlayer::Play(menuTheme);

// Pause — preserves position
MediaPlayer::Pause();

// Resume from where it was paused
MediaPlayer::Resume();

// Stop — resets position to beginning
MediaPlayer::Stop();

Calling Play() with a different song while one is already playing implicitly stops the current track and starts the new one with no gap.

Volume

Music volume is a separate scalar from sound-effect volume. It ranges 0.0 (silent) to 1.0 (full):

// Set music volume to 70%
MediaPlayer::setVolume(0.7f);

// Read current volume
Single vol = MediaPlayer::getVolume();

Changing the volume while a song is playing takes effect immediately on the next audio buffer callback.

IsRepeating

Set IsRepeating to true before or after calling Play() to loop the track continuously:

MediaPlayer::setIsRepeating(true);
MediaPlayer::Play(battleTheme);

// Query
bool loops = MediaPlayer::getIsRepeating();

MediaPlayer.State — the MediaState enum

You can poll the current playback state with MediaPlayer::getState():

MediaState state = MediaPlayer::getState();

switch (state) {
    case MediaState::Playing: /* music is audible  */ break;
    case MediaState::Paused:  /* position preserved */ break;
    case MediaState::Stopped: /* at beginning       */ break;
}

Use this to, for example, show a "Music: PAUSED" indicator in your HUD or to restart the music if it finished naturally (when not repeating).

XACT — real, ~97% functional

XACT (Cross-platform Audio Creation Tool) was XNA's high-level audio engine for complex cue-based audio. CNA implements a real XACT runtime (Microsoft::Xna::Framework::Audio::AudioEngine, WaveBank, SoundBank, Cue) with a genuine .xgs/.xsb/.xwb parser and SDL3_mixer-backed playback — category/lifecycle/3D positional audio/instance-limit enforcement (with fade in/out) and continuous RPC volume/pitch curves are all real. A couple of narrow, documented deviations remain (no HRTF/elevation, no AttackTime/ReleaseTime envelope tracking). For simple one-off sounds or streaming music, SoundEffect and MediaPlayer are still the more lightweight choice.

Complete example: auto-play with M key toggle

#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Media/Song.hpp"
#include "Microsoft/Xna/Framework/Media/MediaPlayer.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Media;
using namespace Microsoft::Xna::Framework::Input;

class MusicDemo final : public Game {
public:
    MusicDemo() : graphics_(this) {}

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());

        // Load two tracks — cheap, no decoding yet
        menuTheme_   = Content.Load<Song>("Audio/menu_theme");
        battleTheme_ = Content.Load<Song>("Audio/battle_theme");

        // Start playing menu theme on loop immediately
        MediaPlayer::setIsRepeating(true);
        MediaPlayer::setVolume(0.8f);
        MediaPlayer::Play(menuTheme_);
    }

    void Update(GameTime& gt) override {
        auto kb = Keyboard::GetState();

        // Toggle pause/resume with M key
        if (kb.IsKeyDown(Keys::M) && !prevKb_.IsKeyDown(Keys::M)) {
            if (MediaPlayer::getState() == MediaState::Playing)
                MediaPlayer::Pause();
            else
                MediaPlayer::Resume();
        }

        // Switch to battle theme with B key
        if (kb.IsKeyDown(Keys::B) && !prevKb_.IsKeyDown(Keys::B)) {
            MediaPlayer::Play(battleTheme_);
        }

        // Switch back to menu theme with N key
        if (kb.IsKeyDown(Keys::N) && !prevKb_.IsKeyDown(Keys::N)) {
            MediaPlayer::Play(menuTheme_);
        }

        // Volume up/down with +/- keys
        if (kb.IsKeyDown(Keys::OemPlus)) {
            MediaPlayer::setVolume(
                std::min(1.0f, MediaPlayer::getVolume() + 0.01f));
        }
        if (kb.IsKeyDown(Keys::OemMinus)) {
            MediaPlayer::setVolume(
                std::max(0.0f, MediaPlayer::getVolume() - 0.01f));
        }

        prevKb_ = kb;
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);
        spriteBatch_->Begin();
        // Draw state / volume text (Tutorial 7 covers SpriteFont)
        spriteBatch_->End();
        gd.Present();
    }

    void UnloadContent() override {
        // Good practice: stop music before unloading Song assets
        MediaPlayer::Stop();
    }

private:
    GraphicsDeviceManager         graphics_;
    std::unique_ptr<SpriteBatch>  spriteBatch_;
    std::shared_ptr<Song>         menuTheme_;
    std::shared_ptr<Song>         battleTheme_;
    KeyboardState                 prevKb_;
};

int main() { MusicDemo game; game.Run(); }

Cross-fading between tracks

XNA does not have a built-in cross-fade. In CNA you can approximate it by adjusting MediaPlayer::setVolume() over several frames and switching songs at zero volume:

// Fade out then switch — call once per frame during transition
void FadeToSong(std::shared_ptr<Song> next, Single& fadeTimer, Single duration) {
    fadeTimer -= (Single)gameTime.getElapsedGameTime().TotalSeconds();
    Single t = fadeTimer / duration;               // 1.0 → 0.0
    MediaPlayer::setVolume(std::max(0.0f, t));

    if (fadeTimer <= 0.0f) {
        MediaPlayer::Play(next);
        MediaPlayer::setVolume(0.0f);
        // Then fade back up in subsequent frames
    }
}

Tips

  • OGG files at 128 kbps are a good default for background music.
  • Always call MediaPlayer::Stop() in UnloadContent() or before loading a new level — leaving music playing while assets are unloaded can cause SDL3_mixer to read freed memory on some platforms.
  • On web (Emscripten) the browser may mute audio until the first user interaction. CNA does not work around this automatically; you should start music from a button click or keypress rather than from LoadContent().