Audio Getting Started¶
Learn how to add sound effects, music, and audio to your Brine2D games.
Overview¶
Brine2D's audio system provides:
- Sound Effects — Short audio clips (explosions, jumps, shots)
- Music — Long-form background audio (looping tracks, crossfade)
- Volume Control — Master, music, and sound volume channels
- Track Management — Control individual playing sounds (pause, resume, stop, volume, pan, pitch)
- Bus System — Group tracks for batch operations (pause/stop/volume by bus)
- Spatial Audio — ECS-based positional sound via
SoundEffectSourceComponent - Priority Eviction — Lowest-priority tracks evicted when all tracks are in use
Powered by: SDL3_mixer (high-quality audio mixing)
Supported formats:
- WAV, MP3, OGG, FLAC (sound effects)
- MP3, OGG (music streaming)
Audio Architecture¶
graph TB
A[Your Game] --> B[IAudioService]
B --> C[AudioService / HeadlessAudioService]
C --> D[Sound Effects]
C --> E[Music]
C --> F[Buses]
D --> D1[ISoundEffect]
D --> D2[PlaySound / StopTrack / PauseTrack]
E --> E1[IMusic]
E --> E2[PlayMusic / StopMusic / Crossfade]
F --> F1[PauseBus / ResumeBus / StopBus]
C --> G[SDL3_mixer]
G --> H[Audio Output]
style B fill:#2d5016,stroke:#4ec9b0,stroke-width:2px,color:#fff
style C fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,color:#fff
style G fill:#4a3d1f,stroke:#ce9178,stroke-width:2px,color:#fff
style H fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
Interface hierarchy:
| Interface | Responsibility |
|---|---|
ISoundLoader |
GetOrLoadSoundAsync |
IMusicLoader |
GetOrLoadMusicAsync |
IAudioPlayer |
Playback, volume, track control, buses |
IAudioService |
Composite: all of the above |
The Audio property on Scene is IAudioService. Prefer the narrower interfaces when injecting into your own services.
Setup¶
Step 1: No Extra Registration Needed¶
Audio is included automatically when you call GameApplication.CreateBuilder(). No separate registration step is required.
var builder = GameApplication.CreateBuilder(args);
builder.Configure(options =>
{
options.Window.Title = "Audio Demo";
options.Window.Width = 800;
options.Window.Height = 600;
});
builder.AddScene<GameScene>();
await using var game = builder.Build();
await game.RunAsync<GameScene>();
Step 2: Use the Audio Property or Inject Loaders¶
In your scene, use the built-in Audio property (no constructor injection needed), or inject ISoundLoader / IMusicLoader for loading:
using Brine2D.Audio;
using Brine2D.Core;
using Brine2D.Engine;
public class GameScene : Scene
{
private readonly ISoundLoader _soundLoader;
private readonly IMusicLoader _musicLoader;
public GameScene(ISoundLoader soundLoader, IMusicLoader musicLoader)
{
_soundLoader = soundLoader;
_musicLoader = musicLoader;
}
}
Loading Audio¶
Load Sound Effects¶
private ISoundEffect? _jumpSound;
private ISoundEffect? _shootSound;
private ISoundEffect? _explosionSound;
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_jumpSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/jump.wav", ct);
_shootSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/shoot.wav", ct);
_explosionSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/explosion.wav", ct);
Logger.LogInformation("Sound effects loaded");
}
All calls share a thread-safe cache — loading the same path twice returns the cached instance.
When to use: Short duration (< 5 seconds), played frequently, may play simultaneously.
Load Music¶
private IMusic? _backgroundMusic;
private IMusic? _bossMusic;
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_backgroundMusic = await _musicLoader.GetOrLoadMusicAsync("assets/music/background.ogg", ct);
_bossMusic = await _musicLoader.GetOrLoadMusicAsync("assets/music/boss.ogg", ct);
}
When to use: Long duration (> 30 seconds), background music, only one track at a time, streamed from disk.
Playing Audio¶
Play Sound Effects¶
Simple playback:
protected override void OnUpdate(GameTime gameTime)
{
if (Input.IsKeyPressed(Key.Space))
Audio.PlaySound(_jumpSound!);
if (Input.IsKeyPressed(Key.X))
Audio.PlaySound(_shootSound!, volume: 0.5f);
}
With full options:
// Volume, loops, pan, pitch, priority, bus
nint track = Audio.PlaySound(_explosionSound!,
volume: 0.7f,
loops: 0,
pan: -0.5f,
pitch: 1.2f,
priority: 5,
bus: "sfx");
Play Music¶
// Loop infinitely
Audio.PlayMusic(_backgroundMusic!, loops: -1);
// Play once
Audio.PlayMusic(_backgroundMusic!, loops: 0);
// With loop start point (intro then loop)
Audio.PlayMusic(_backgroundMusic!, loops: -1, loopStartMs: 5000);
// Pause / resume / stop
Audio.PauseMusic();
Audio.ResumeMusic();
Audio.StopMusic();
// Stop with fade-out
Audio.StopMusic(fadeDuration: 2.0f);
Crossfade Music¶
// Crossfade from current track to boss music over 1.5 seconds
Audio.CrossfadeMusic(_bossMusic!, fadeDuration: 1.5f, loops: -1);
Audio Tracks¶
PlaySound always returns a track handle (nint) for lifecycle control:
nint track = Audio.PlaySound(_engineSound!, volume: 0.5f, loops: -1);
// Control the track
Audio.SetTrackVolume(track, 0.3f);
Audio.SetTrackPan(track, 0.5f);
Audio.SetTrackPitch(track, 0.8f);
Audio.PauseTrack(track);
Audio.ResumeTrack(track);
Audio.StopTrack(track);
// Check if still playing
if (Audio.IsTrackAlive(track))
Logger.LogInformation("Still playing");
Returns nint.Zero if the sound could not be played (all tracks full and priority too low).
Managing Multiple Sounds¶
public class AudioController
{
private readonly IAudioPlayer _audio;
private readonly List<nint> _activeTracks = new();
public AudioController(IAudioPlayer audio) => _audio = audio;
public void PlayAndTrack(ISoundEffect sound, float volume = 1.0f)
{
nint track = _audio.PlaySound(sound, volume: volume);
if (track != nint.Zero)
_activeTracks.Add(track);
}
public void CleanupFinished()
{
_activeTracks.RemoveAll(t => !_audio.IsTrackAlive(t));
}
public void StopAllTracked()
{
foreach (var track in _activeTracks)
_audio.StopTrack(track);
_activeTracks.Clear();
}
}
Looping Sounds¶
public class EngineSound
{
private readonly IAudioPlayer _audio;
private readonly ISoundEffect _engineSound;
private nint _engineTrack;
public EngineSound(IAudioPlayer audio, ISoundEffect engineSound)
{
_audio = audio;
_engineSound = engineSound;
}
public void StartEngine()
{
_engineTrack = _audio.PlaySound(_engineSound, volume: 0.5f, loops: -1);
}
public void StopEngine()
{
if (_engineTrack != nint.Zero)
{
_audio.StopTrack(_engineTrack);
_engineTrack = nint.Zero;
}
}
public void UpdateEngineVolume(float volume)
{
if (_audio.IsTrackAlive(_engineTrack))
_audio.SetTrackVolume(_engineTrack, volume);
}
}
Volume Control¶
Master Volume¶
Controls all audio:
Audio.MasterVolume = 0.8f;
Audio.MasterVolume = 0.0f; // Mute all
Music Volume¶
Controls only music:
Audio.MusicVolume = 0.6f;
Sound Volume¶
Controls only sound effects:
Audio.SoundVolume = 0.7f;
Per-Track Music Volume¶
// Reduce the current music track's volume independently
Audio.SetMusicTrackVolume(0.5f);
// Final gain = MusicVolume x track volume
Volume Hierarchy¶
graph TB
A[MasterVolume<br/>0.8]
A --> B[MusicVolume<br/>0.6]
A --> C[SoundVolume<br/>0.7]
B --> D[Music Track<br/>0.8 x 0.6 = 0.48]
C --> E[Sound Effect<br/>0.8 x 0.7 x per-sound = final]
style A fill:#2d5016,stroke:#4ec9b0,stroke-width:2px,color:#fff
style B fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,color:#fff
style C fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,color:#fff
Formula: Final = MasterVolume x (MusicVolume or SoundVolume) x per-track volume
Bus System¶
Group tracks by purpose for batch control:
// Play with bus tag
nint track = Audio.PlaySound(_uiClick!, bus: "ui");
// Or tag after creation
Audio.TagTrack(track, "dialog");
// Batch operations
Audio.PauseBus("sfx");
Audio.ResumeBus("sfx");
Audio.StopBus("ui");
Audio.SetBusVolume("sfx", 0.5f);
Default bus names: "sfx" for sound effects, "music" for music components.
Complete Example¶
using Brine2D.Audio;
using Brine2D.Core;
using Brine2D.Engine;
using Brine2D.Input;
public class AudioDemoScene : Scene
{
private readonly ISoundLoader _soundLoader;
private readonly IMusicLoader _musicLoader;
private ISoundEffect? _jumpSound;
private ISoundEffect? _hitSound;
private IMusic? _backgroundMusic;
private nint _loopingTrack;
public AudioDemoScene(ISoundLoader soundLoader, IMusicLoader musicLoader)
{
_soundLoader = soundLoader;
_musicLoader = musicLoader;
}
protected override async Task OnLoadAsync(CancellationToken ct, IProgress<float>? progress = null)
{
_jumpSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/jump.wav", ct);
_hitSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/hit.wav", ct);
_backgroundMusic = await _musicLoader.GetOrLoadMusicAsync("assets/music/background.ogg", ct);
Audio.MasterVolume = 0.8f;
Audio.MusicVolume = 0.6f;
Audio.SoundVolume = 0.7f;
Audio.PlayMusic(_backgroundMusic, loops: -1);
}
protected override void OnUpdate(GameTime gameTime)
{
if (Input.IsKeyPressed(Key.Space))
Audio.PlaySound(_jumpSound!);
if (Input.IsKeyPressed(Key.X))
Audio.PlaySound(_hitSound!, volume: 0.5f);
if (Input.IsKeyPressed(Key.L))
{
if (_loopingTrack == nint.Zero || !Audio.IsTrackAlive(_loopingTrack))
{
_loopingTrack = Audio.PlaySound(_hitSound!, loops: -1, volume: 0.3f);
}
else
{
Audio.StopTrack(_loopingTrack);
_loopingTrack = nint.Zero;
}
}
if (Input.IsKeyPressed(Key.M))
{
if (Audio.IsMusicPaused)
Audio.ResumeMusic();
else
Audio.PauseMusic();
}
if (Input.IsKeyDown(Key.Up))
Audio.MasterVolume = Math.Min(Audio.MasterVolume + 0.01f, 1.0f);
if (Input.IsKeyDown(Key.Down))
Audio.MasterVolume = Math.Max(Audio.MasterVolume - 0.01f, 0.0f);
}
protected override void OnRender(GameTime gameTime)
{
Renderer.DrawText($"Master: {Audio.MasterVolume:P0}", 10, 10, Color.White);
Renderer.DrawText($"Music: {Audio.MusicVolume:P0}", 10, 30, Color.White);
Renderer.DrawText($"Sound: {Audio.SoundVolume:P0}", 10, 50, Color.White);
Renderer.DrawText($"Active tracks: {Audio.ActiveSoundTrackCount}", 10, 70, Color.White);
}
protected override void OnExit()
{
Audio.StopMusic();
Audio.StopAllSounds();
}
}
State-Based Music¶
Switch music based on game state:
private void ChangeGameState(GameState newState)
{
_currentState = newState;
switch (newState)
{
case GameState.Normal:
Audio.CrossfadeMusic(_normalMusic!, fadeDuration: 1.0f, loops: -1);
break;
case GameState.Boss:
Audio.CrossfadeMusic(_bossMusic!, fadeDuration: 0.5f, loops: -1);
break;
case GameState.Victory:
Audio.StopMusic(fadeDuration: 1.0f);
Audio.PlayMusic(_victoryMusic!, loops: 0);
break;
}
}
Music Seek and Position¶
// Get current position and duration
double posMs = Audio.MusicPositionMs;
double durMs = Audio.MusicDurationMs;
// Seek to a position
Audio.SeekMusic(30_000); // Jump to 30 seconds
// Set music pitch
Audio.SetMusicPitch(1.2f); // Speed up
Asset Organization¶
assets/
+-- sounds/
| +-- player/
| | +-- jump.wav
| | +-- land.wav
| | +-- hurt.wav
| +-- weapons/
| | +-- pistol.wav
| | +-- rifle.wav
| +-- ui/
| | +-- button_click.wav
| | +-- menu_open.wav
| +-- ambient/
| +-- wind.wav
| +-- water.wav
+-- music/
+-- menu.ogg
+-- level1.ogg
+-- boss.ogg
+-- victory.ogg
Best Practices¶
DO¶
- Load audio in OnLoadAsync
- Use WAV for SFX, OGG for music
- Use
IsTrackAliveto check tracks before controlling them - Stop music on scene exit in
OnExit - Use buses for group control (pause all SFX during menus)
- Use priority for important sounds that shouldn't be evicted
DON'T¶
- Don't load in OnUpdate — causes lag
- Don't play too many sounds — use priority eviction or
MaxConcurrentInstances - Don't max out volume — causes clipping
- Don't forget to copy assets to output
Troubleshooting¶
No audio plays¶
- Check volume:
Audio.MasterVolume,Audio.SoundVolume,Audio.MusicVolume - Verify files exist and are copied to output
- Test with a known-good WAV file
Music doesn't loop¶
// Use loops: -1 for infinite
Audio.PlayMusic(_music!, loops: -1);
Track handle is zero¶
The sound couldn't be played — all tracks are in use and the new sound's priority was too low. Increase priority or reduce concurrent sounds.
Summary¶
Volume hierarchy:
| Volume | Controls | Range |
|---|---|---|
| MasterVolume | All audio | 0.0–1.0 |
| MusicVolume | Music only | 0.0–1.0 |
| SoundVolume | Sound effects only | 0.0–1.0 |
Key methods:
| Method | Purpose |
|---|---|
GetOrLoadSoundAsync() |
Load sound effect |
GetOrLoadMusicAsync() |
Load music track |
PlaySound() |
Play sound, returns track handle |
StopTrack() / PauseTrack() / ResumeTrack() |
Track lifecycle |
SetTrackVolume() / SetTrackPan() / SetTrackPitch() |
Track properties |
IsTrackAlive() |
Check if track is still playing |
StopAllSounds() / PauseAllSounds() / ResumeAllSounds() |
Batch sound control |
PlayMusic() / CrossfadeMusic() |
Play or crossfade music |
StopMusic(fadeDuration) |
Stop music with optional fade |
PauseMusic() / ResumeMusic() |
Music lifecycle |
SeekMusic() / SetMusicPitch() |
Music position and speed |
PauseBus() / ResumeBus() / StopBus() |
Bus operations |
Quick Reference¶
// Load audio (in OnLoadAsync)
_jumpSound = await _soundLoader.GetOrLoadSoundAsync("assets/sounds/jump.wav", ct);
_music = await _musicLoader.GetOrLoadMusicAsync("assets/music/background.ogg", ct);
// Play audio
Audio.PlaySound(_jumpSound!);
Audio.PlaySound(_explosionSound!, volume: 0.5f, pan: -0.5f, pitch: 1.1f);
// Play with track handle
nint track = Audio.PlaySound(_engineSound!, volume: 0.8f, loops: -1);
Audio.SetTrackVolume(track, 0.6f);
Audio.SetTrackPan(track, 0.5f);
Audio.StopTrack(track);
// Music
Audio.PlayMusic(_music!, loops: -1);
Audio.CrossfadeMusic(_bossMusic!, fadeDuration: 1.5f, loops: -1);
Audio.StopMusic(fadeDuration: 2.0f);
// Volume control
Audio.MasterVolume = 0.8f;
Audio.MusicVolume = 0.6f;
Audio.SoundVolume = 0.7f;
// Music control
Audio.PauseMusic();
Audio.ResumeMusic();
Audio.SeekMusic(15_000);
// Bus control
Audio.PauseBus("sfx");
Audio.ResumeBus("sfx");
Audio.SetBusVolume("ui", 0.5f);
// Cleanup
Audio.StopMusic();
Audio.StopAllSounds();
Next: Sound Effects | Music Playback | Spatial Audio