Instruction file imported from 50C4L/Vortex (
.cursor/rules/audio-module.mdc). Copyright stays with the author.
Audio Module
Purpose
Thin RAII wrapper over miniaudio. AudioMixer owns the engine and creates SoundInstance objects from file paths. The ECS AudioSystem manages SoundInstance pools and drives playback via AudioEventComponent (SFX) and BgmComponent (scene music).
Key Classes
| Class | Responsibility |
|---|---|
AudioMixer |
Owns ma_engine; creates SoundInstance; RAII cleanup via unique_ptr custom deleters |
SoundInstance |
Wraps ma_sound; movable, not copyable; supports play / stop / restart |
SoundPool (in AudioSystem) |
Vector of SoundInstance; SFX keyed by path in mSoundPathToId, BGM in mBgmPathToId so pause policy cannot collide |
BgmComponent |
Scene music: sound_id + desired playing state; reconciled even while the game is paused |
API
// Mixer lifetime is managed by AudioSystem -- rarely construct directly
AudioMixer mixer;
// Create a sound (loaded asynchronously by miniaudio)
SoundInstance sfx = mixer.CreateSound( "resources/sounds/laser.wav", /*looping=*/false );
SoundInstance music = mixer.CreateSound( "resources/sounds/bgm.ogg", /*looping=*/true );
// Playback control
sfx.Play();
sfx.Stop();
sfx.Restart(); // seek to 0 + Play -- used for overlapping one-shot SFX
Key Patterns
- RAII: Both
ma_engineandma_soundare held inunique_ptrwith custom deleters that call miniaudio teardown. Never manually uninit. - Move semantics:
SoundInstanceis movable for storage instd::vector/ResourceStore. Copy is deleted. - SoundPool (in AudioSystem): For SFX that can overlap (e.g. bullet fire),
AudioSystemkeeps a pool ofSoundInstanceobjects and round-robinsRestart()calls instead of creating new instances each time. Pools withignore_pause = trueare skipped byOnPauseChanged-- BGM uses this; looping SFX (thruster) do not. - BGM:
LoadBgmloads a 1-instanceignore_pausepool and returns aResourceId. The scene creates aBgmComponententity and parents it to the scene root.ProcessBgm()runs before the pause gate and reconcilesplayingvspool.is_playing. Destroying the entity stops the pool viaOnEntityDestroying. - Async loading: Miniaudio loads audio data on a background thread.
Play()called immediately afterCreateSound()may produce silence for a frame -- this is expected.
DOs
- Use
Restart()for one-shot SFX that can overlap -- cheaper than creating a newSoundInstanceeach time - Store
SoundInstanceby value in pools (it's movable) -- don't heap-allocate unnecessarily - Control SFX from game code via
AudioEventComponentqueue +AudioSystem, not by callingSoundInstancemethods directly - Control scene music via
BgmComponent::playing(or destroy the entity); do not route BGM throughAudioSourceComponent
DON'Ts
- Don't copy
SoundInstance-- it's deleted; move it instead - Don't call
ma_engine_uninit/ma_sound_uninitmanually -- RAII handles cleanup - Don't treat
loopingas ignore-pause -- thruster loops and must still mute with the game - Don't construct
AudioMixermore than once --ma_engineis a singleton-like resource