Audio System
Live Demo: Audio
Audio in nightshade is Kira behind an ECS facade. Sounds are decoded once and stored in a name-keyed cache on world.resources.audio. Playback happens through the AudioSource component, which holds a reference to the cached sound by name plus per-source state like volume, looping, and spatial flag. An AudioListener component on the camera (or wherever else) gives spatial sources a reference point.
Enabling Audio
The audio feature pulls in Kira and the audio components.
[dependencies]
nightshade = { git = "...", features = ["engine", "audio"] }
Loading Sounds
Sound loading is two steps. Decode the bytes into a StaticSoundData, then cache that data by name. The cache is shared, so any number of AudioSource components can reference the same sound without duplicating the decoded samples.
#![allow(unused)] fn main() { use nightshade::ecs::audio::*; const EXPLOSION_WAV: &[u8] = include_bytes!("../assets/sounds/explosion.wav"); const MUSIC_OGG: &[u8] = include_bytes!("../assets/sounds/music.ogg"); fn initialize(&mut self, world: &mut World) { if let Ok(data) = load_sound_from_bytes(EXPLOSION_WAV) { world.resources.audio.load_sound("explosion", data); } if let Ok(data) = load_sound_from_bytes(MUSIC_OGG) { world.resources.audio.load_sound("music", data); } } }
load_sound_from_bytes is the decoder. It accepts WAV, OGG, MP3, and FLAC and returns the same StaticSoundData regardless of source format.
Playing Sounds
Entity-Based Playback
Playback is driven by AudioSource. The component references a cached sound by name and the audio system polls for active sources each frame.
#![allow(unused)] fn main() { let entity = world.spawn_entities(AUDIO_SOURCE | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0]; world.core.set_audio_source(entity, AudioSource::new("explosion").playing()); }
Looping Music
#![allow(unused)] fn main() { let music = world.spawn_entities(AUDIO_SOURCE, 1)[0]; world.core.set_audio_source(music, AudioSource::new("music") .with_looping(true) .playing(), ); }
Music does not need a transform. The audio system treats a source with no transform as non-spatial and routes it to the master bus directly.
Audio Source Component
#![allow(unused)] fn main() { pub struct AudioSource { pub audio_ref: Option<String>, pub volume: f32, pub looping: bool, pub playing: bool, pub spatial: bool, pub reverb: bool, } }
audio_ref is the lookup key into the sound cache. volume is a per-source multiplier. looping, playing, spatial, and reverb are flags that gate playback behavior.
Builder methods chain the same fields.
#![allow(unused)] fn main() { AudioSource::new("name") .with_volume(0.8) .with_looping(true) .with_spatial(true) .with_reverb(true) .playing() }
Audio Listener
A spatial audio source needs to know where the listener is. Mark the listener with the AUDIO_LISTENER component, usually on the active camera.
#![allow(unused)] fn main() { world.core.add_components(camera_entity, AUDIO_LISTENER); world.core.set_audio_listener(camera_entity, AudioListener); }
The audio system reads the listener's global transform each frame and uses it as the reference point for distance attenuation and panning.
Spatial Audio Sources
A spatial source attenuates by distance and pans by direction relative to the listener. The flag is spatial = true.
#![allow(unused)] fn main() { const ENGINE_LOOP: &[u8] = include_bytes!("../assets/sounds/engine_loop.wav"); fn initialize(&mut self, world: &mut World) { if let Ok(data) = load_sound_from_bytes(ENGINE_LOOP) { world.resources.audio.load_sound("engine_loop", data); } let entity = world.spawn_entities( AUDIO_SOURCE | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1, )[0]; world.core.set_audio_source(entity, AudioSource::new("engine_loop") .with_spatial(true) .with_looping(true) .playing(), ); } }
The source's transform is the position in world space. Move the transform and the sound moves.
Sound Variations
Repeating the same audio sample every time a footstep fires is audibly mechanical. Pick from a small pool of variations to make the result feel less canned.
#![allow(unused)] fn main() { const FOOTSTEP_1: &[u8] = include_bytes!("../assets/sounds/footstep_1.wav"); const FOOTSTEP_2: &[u8] = include_bytes!("../assets/sounds/footstep_2.wav"); const FOOTSTEP_3: &[u8] = include_bytes!("../assets/sounds/footstep_3.wav"); const FOOTSTEP_4: &[u8] = include_bytes!("../assets/sounds/footstep_4.wav"); fn initialize(&mut self, world: &mut World) { for (name, bytes) in [ ("footstep_1", FOOTSTEP_1), ("footstep_2", FOOTSTEP_2), ("footstep_3", FOOTSTEP_3), ("footstep_4", FOOTSTEP_4), ] { if let Ok(data) = load_sound_from_bytes(bytes) { world.resources.audio.load_sound(name, data); } } } fn play_footstep(world: &mut World, source_entity: Entity) { let sounds = ["footstep_1", "footstep_2", "footstep_3", "footstep_4"]; let index = rand::random::<usize>() % sounds.len(); if let Some(audio) = world.core.get_audio_source_mut(source_entity) { audio.audio_ref = Some(sounds[index].to_string()); audio.playing = true; } } }
Four variations is the practical minimum to break the loop perception. Eight is better.
Triggering Sounds on Events
Physics collision events are a natural source of impact sounds. Read the event queue and flip the appropriate source's playing flag.
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { for event in world.resources.physics.collision_events() { if matches!(event.kind, CollisionEventKind::Started) { if let Some(audio) = world.core.get_audio_source_mut(self.impact_source) { audio.audio_ref = Some("impact".to_string()); audio.playing = true; } } } } }
Stopping Sounds
Two ways to stop a sound. Through the audio resource, or by setting the flag.
#![allow(unused)] fn main() { world.resources.audio.stop_sound(entity); }
#![allow(unused)] fn main() { if let Some(audio) = world.core.get_audio_source_mut(entity) { audio.playing = false; } }
The resource call stops Kira's playback handle immediately. The flag flips on the next frame when the audio system processes sources. Use the flag for game-logic-driven stops and the resource for clean shutdown.
Supported Formats
| Format | Extension | Notes |
|---|---|---|
| WAV | .wav | Uncompressed, fast loading |
| OGG | .ogg | Compressed, good for music |
| MP3 | .mp3 | Compressed, widely supported |
| FLAC | .flac | Lossless compression |
WAV is the right format for short effects where decode time matters. OGG and MP3 are right for music where file size matters. FLAC sits in between with lossless compression at a moderate decode cost.