Tutorial 15: Playing Background Music
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
Songcan 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
MediaStateChangedEventArgscallback 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()inUnloadContent()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().