Skip to content

Music Playback

This guide covers music streaming — loading, playback, crossfading, seeking, and the ECS-driven MusicSourceComponent.

For a quick introduction to audio in general, see Getting Started.


Loading Music

Music is loaded through IMusicLoader (part of IAudioService). Like sound effects, loaded music is cached.

public class GameScene : Scene
{
    private readonly IAssetLoader _assetLoader;

    public GameScene(IAssetLoader assetLoader) => _assetLoader = assetLoader;

    protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
    {
        var theme = await _assetLoader.GetOrLoadMusicAsync("assets/audio/music/theme.ogg", ct);
    }
}

With an asset manifest:

public class LevelAssets : AssetManifest
{
    public readonly AssetRef<IMusic> Theme   = Music("assets/audio/music/theme.ogg");
    public readonly AssetRef<IMusic> Boss    = Music("assets/audio/music/boss.ogg");
    public readonly AssetRef<IMusic> Victory = Music("assets/audio/music/victory.ogg");
}

Supported Formats

Format Extension Notes
OGG Vorbis .ogg Recommended — good compression, seamless looping
MP3 .mp3 Widely supported
WAV .wav Uncompressed, large files
FLAC .flac Lossless compression

.ogg is the best choice for most game music — it supports gapless looping and has good compression.

One Music Track

SDL3_mixer supports a single music stream at a time. Playing new music replaces the current track. Use CrossfadeMusic for smooth transitions between tracks.


Playing Music

Basic Playback

Audio.PlayMusic(_assets.Theme);

This starts the music immediately, playing once with no loop.

Playback Parameters

Audio.PlayMusic(
    _assets.Theme,
    loops: -1,            // -1 = loop forever, 0 = play once, n = play n+1 times
    loopStartMs: 5000,    // loop restarts at 5 seconds instead of the beginning
    bus: "music"          // optional bus name
);

Loop Points

Many game music tracks have an intro that should only play once. Use loopStartMs to set where the loop restarts:

// 4-second intro, then loops from the 4-second mark onward
Audio.PlayMusic(_assets.Theme, loops: -1, loopStartMs: 4000);

Crossfading

Smoothly transition between two music tracks:

// Fade from current track to boss music over 2 seconds
Audio.CrossfadeMusic(
    _assets.BossTheme,
    fadeDuration: 2.0f,
    loops: -1
);

The current music fades out while the new music fades in simultaneously. All PlayMusic parameters are supported:

Audio.CrossfadeMusic(
    _assets.BossTheme,
    fadeDuration: 1.5f,
    loops: -1,
    loopStartMs: 3000,
    bus: "music"
);

Stopping Music

Immediate Stop

Audio.StopMusic();

Fade Out

// Fade out over 2 seconds
Audio.StopMusic(fadeDuration: 2.0f);

Pause and Resume

Audio.PauseMusic();
Audio.ResumeMusic();

Seeking

Jump to a specific position in the current track:

Audio.SeekMusic(positionMs: 30000);  // jump to 30 seconds

Query the current position and total duration:

double currentMs = Audio.MusicPositionMs;
double totalMs   = Audio.MusicDurationMs;

double progress = currentMs / totalMs;  // 0.0 – 1.0

Music State

bool playing  = Audio.IsMusicPlaying;
bool paused   = Audio.IsMusicPaused;
bool fading   = Audio.IsMusicFadingOut;

Pitch and Volume

Adjust the playing music track in real time:

Audio.SetMusicPitch(0.8f);         // slow down (0.5 – 2.0)
Audio.SetMusicTrackVolume(0.5f);   // per-track volume (0.0 – 1.0)

Volume Hierarchy

Music volume flows through the same hierarchy as sound effects:

Final volume = MasterVolume × MusicVolume × per-track volume
Audio.MasterVolume = 0.8f;          // affects everything
Audio.MusicVolume  = 0.7f;          // affects all music
Audio.SetMusicTrackVolume(0.5f);    // affects the current track

// Effective volume: 0.8 × 0.7 × 0.5 = 0.28

Bus Control

Music can be assigned to a bus for group control alongside sound effects:

Audio.PlayMusic(_assets.Theme, loops: -1, bus: "music");

// Later: mute all audio in the "music" bus
Audio.PauseBus("music");
Audio.ResumeBus("music");
Audio.SetBusVolume("music", 0.5f);

ECS: MusicSourceComponent

For entity-driven music, add a MusicSourceComponent. The AudioSystem processes it automatically.

World.CreateEntity("BGM")
    .AddComponent<MusicSourceComponent>(m =>
    {
        m.Music       = _assets.Theme;
        m.LoopCount   = -1;
        m.PlayOnEnable = true;
        m.Bus         = "music";
    });

Inherited Properties (from AudioSourceComponent)

Property Type Default Description
Volume float 1.0 Base volume (0.0 – 1.0)
Pitch float 1.0 Playback pitch (0.5 – 2.0)
Priority int 0 Track priority
Bus string? "music" Bus name (defaults to "music")
PlayOnEnable bool false Auto-play when entity is enabled
LoopCount int 0 0 = once, -1 = forever, n = n+1 times

Music-Specific Properties

Property Type Default Description
Music IMusic? null The music track to play
CrossfadeDuration float 0.0 Crossfade time in seconds (0 = instant switch)
FadeOutDuration float 0.0 Fade-out time when stopping
LoopStartMs double 0.0 Position in ms where loops restart
TriggerSeek bool false Seek to SeekPositionMs next frame
SeekPositionMs double 0.0 Target position for seek

Trigger Properties

Same as SoundEffectSourceComponent — the AudioSystem reads and resets these each frame:

var music = entity.GetComponent<MusicSourceComponent>()!;

music.TriggerPlay   = true;   // start playback
music.TriggerStop   = true;   // stop (with optional fade-out)
music.TriggerPause  = true;   // pause
music.TriggerResume = true;   // resume

Read-Only State

Property Type Description
IsPlaying bool Currently playing
IsPaused bool Currently paused
PlaybackEnded bool Finished playing

Example: Crossfade Between Zones

// Zone trigger changes the music with a crossfade
var bgm = entity.GetComponent<MusicSourceComponent>()!;
bgm.Music             = _assets.DungeonTheme;
bgm.CrossfadeDuration = 2.0f;
bgm.LoopCount         = -1;
bgm.TriggerPlay       = true;

Example: Music with Loop Point

entity.AddComponent<MusicSourceComponent>(m =>
{
    m.Music        = _assets.BossTheme;
    m.LoopCount    = -1;
    m.LoopStartMs  = 4500;     // intro plays once, loops from 4.5s
    m.PlayOnEnable = true;
});

Example: Fade Out on Scene Exit

protected override void OnExit()
{
    var bgm = _bgmEntity.GetComponent<MusicSourceComponent>()!;
    bgm.FadeOutDuration = 1.5f;
    bgm.TriggerStop     = true;
}

Example: Seeking

var bgm = entity.GetComponent<MusicSourceComponent>()!;
bgm.SeekPositionMs = 30000;   // jump to 30 seconds
bgm.TriggerSeek    = true;

What's Next?