use crate::ecs::audio::components::{AudioBus, AudioSource};
use crate::ecs::audio::resources::{
audio_engine_get_sound, audio_engine_has_handle, audio_engine_initialize,
audio_engine_is_initialized, audio_engine_stop_sound,
};
use crate::ecs::world::{Entity, World};
use kira::{
Decibels, Easing, Mapping, Mix, Tween, Value,
effect::{filter::FilterBuilder, reverb::ReverbBuilder, volume_control::VolumeControlBuilder},
modulator::tweener::TweenerBuilder,
sound::static_sound::StaticSoundData,
track::{SendTrackBuilder, SpatialTrackBuilder, TrackBuilder, TrackHandle},
};
use std::io::Cursor;
use std::time::Duration;
type KiraVec3 = mint::Vector3<f32>;
type KiraQuat = mint::Quaternion<f32>;
const VOICE_DUCKING_DEPTH_DB: f32 = -18.0;
pub fn initialize_audio_system(world: &mut World) {
let _span = tracing::info_span!("audio_init").entered();
if !audio_engine_is_initialized(&world.resources.audio) {
#[cfg(not(target_arch = "wasm32"))]
{
if let Err(error) = audio_engine_initialize(&mut world.resources.audio) {
tracing::error!("Failed to initialize audio: {}", error);
}
}
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn lazy_initialize_audio_system(world: &mut World) {
if !audio_engine_is_initialized(&world.resources.audio)
&& let Err(error) = audio_engine_initialize(&mut world.resources.audio)
{
tracing::error!("Failed to initialize audio (WASM): {}", error);
}
}
pub fn build_audio_buses_system(world: &mut World) {
if world.resources.audio.buses_built {
return;
}
let audio = &mut world.resources.audio;
let Some(manager) = audio.manager.as_mut() else {
return;
};
let voice_ducking = match manager.add_modulator(TweenerBuilder { initial_value: 0.0 }) {
Ok(handle) => handle,
Err(error) => {
tracing::error!("Failed to create voice ducking tweener: {}", error);
return;
}
};
let voice_ducking_id = voice_ducking.id();
let ducking_mapping = Mapping {
input_range: (0.0, 1.0),
output_range: (Decibels::IDENTITY, Decibels(VOICE_DUCKING_DEPTH_DB)),
easing: Easing::Linear,
};
let default_reverb = match manager.add_send_track(
SendTrackBuilder::new().with_effect(ReverbBuilder::new().mix(Mix::WET).damping(0.5)),
) {
Ok(handle) => handle,
Err(error) => {
tracing::error!("Failed to create default reverb send: {}", error);
return;
}
};
let mut master = match manager.add_sub_track(TrackBuilder::new()) {
Ok(handle) => handle,
Err(error) => {
tracing::error!("Failed to create master bus: {}", error);
return;
}
};
let music = master.add_sub_track(TrackBuilder::new().with_effect(VolumeControlBuilder::new(
Value::FromModulator {
id: voice_ducking_id,
mapping: ducking_mapping,
},
)));
let ambient = master.add_sub_track(TrackBuilder::new().with_effect(VolumeControlBuilder::new(
Value::FromModulator {
id: voice_ducking_id,
mapping: ducking_mapping,
},
)));
let sfx = master.add_sub_track(TrackBuilder::new());
let voice = master.add_sub_track(TrackBuilder::new());
let ui = master.add_sub_track(TrackBuilder::new());
let (music, ambient, sfx, voice, ui) = match (music, ambient, sfx, voice, ui) {
(Ok(music), Ok(ambient), Ok(sfx), Ok(voice), Ok(ui)) => (music, ambient, sfx, voice, ui),
_ => {
tracing::error!("Failed to create one or more audio buses");
return;
}
};
audio.buses.master = Some(master);
audio.buses.music = Some(music);
audio.buses.ambient = Some(ambient);
audio.buses.sfx = Some(sfx);
audio.buses.voice = Some(voice);
audio.buses.ui = Some(ui);
audio.voice_ducking = Some(voice_ducking);
audio
.reverb_sends
.insert("default".to_string(), default_reverb);
audio.buses_built = true;
tracing::info!("Audio buses built");
}
fn bus_handle_mut(
audio: &mut crate::ecs::audio::resources::AudioEngine,
bus: AudioBus,
) -> Option<&mut TrackHandle> {
match bus {
AudioBus::Master => audio.buses.master.as_mut(),
AudioBus::Music => audio.buses.music.as_mut(),
AudioBus::Sfx => audio.buses.sfx.as_mut(),
AudioBus::Ambient => audio.buses.ambient.as_mut(),
AudioBus::Voice => audio.buses.voice.as_mut(),
AudioBus::Ui => audio.buses.ui.as_mut(),
}
}
pub fn set_audio_bus_volume(world: &mut World, bus: AudioBus, decibels: f32, fade_seconds: f32) {
if let Some(handle) = bus_handle_mut(&mut world.resources.audio, bus) {
handle.set_volume(
Decibels(decibels),
Tween {
duration: Duration::from_secs_f32(fade_seconds.max(0.0)),
..Default::default()
},
);
}
}
pub fn set_voice_ducking(world: &mut World, ducking: f32, fade_seconds: f32) {
if let Some(tweener) = world.resources.audio.voice_ducking.as_mut() {
tweener.set(
ducking.clamp(0.0, 1.0) as f64,
Tween {
duration: Duration::from_secs_f32(fade_seconds.max(0.0)),
..Default::default()
},
);
}
}
pub fn update_audio_system(world: &mut World) {
let source_count = world
.core
.query_entities(crate::ecs::world::AUDIO_SOURCE)
.count();
let _span = tracing::info_span!("audio", sources = source_count).entered();
if !audio_engine_is_initialized(&world.resources.audio) {
return;
}
if !world.resources.audio.buses_built {
return;
}
let listener_entity = world
.core
.query_entities(crate::ecs::world::AUDIO_LISTENER)
.next();
let has_listener_entity = listener_entity.is_some();
if has_listener_entity {
if world.resources.audio.listener.is_none() {
let position = if let Some(entity) = listener_entity
&& let Some(transform) = world.core.get_global_transform(entity)
{
KiraVec3 {
x: transform.0[(0, 3)],
y: transform.0[(1, 3)],
z: transform.0[(2, 3)],
}
} else {
KiraVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
}
};
let orientation = nalgebra_glm::Quat::identity();
let kira_orientation = KiraQuat {
v: mint::Vector3 {
x: orientation.coords.x,
y: orientation.coords.y,
z: orientation.coords.z,
},
s: orientation.coords.w,
};
if let Some(manager) = &mut world.resources.audio.manager {
match manager.add_listener(position, kira_orientation) {
Ok(listener) => {
world.resources.audio.listener = Some(listener);
tracing::info!("Created audio listener");
}
Err(error) => {
tracing::error!("Failed to create audio listener: {}", error);
}
}
}
}
let (position, kira_orientation) = if let Some(entity) = listener_entity
&& let Some(transform) = world.core.get_global_transform(entity)
{
let pos = KiraVec3 {
x: transform.0[(0, 3)],
y: transform.0[(1, 3)],
z: transform.0[(2, 3)],
};
let rotation_matrix = nalgebra_glm::mat4_to_mat3(&transform.0);
let orientation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
let quat = KiraQuat {
v: mint::Vector3 {
x: orientation.coords.x,
y: orientation.coords.y,
z: orientation.coords.z,
},
s: orientation.coords.w,
};
(Some(pos), Some(quat))
} else {
(None, None)
};
if let (Some(pos), Some(quat)) = (position, kira_orientation)
&& let Some(listener_handle) = &mut world.resources.audio.listener
{
listener_handle.set_position(pos, Tween::default());
listener_handle.set_orientation(quat, Tween::default());
}
}
let audio_entities: Vec<_> = world
.core
.query_entities(crate::ecs::world::AUDIO_SOURCE)
.collect();
struct PlayIntent {
entity: Entity,
sound_data: StaticSoundData,
is_spatial: bool,
bus: AudioBus,
min_distance: f32,
max_distance: f32,
reverb_zones: Vec<(String, f32)>,
position: KiraVec3,
}
let mut sounds_to_play: Vec<PlayIntent> = Vec::new();
let mut sounds_to_stop = Vec::new();
let mut spatial_tracks_to_update = Vec::new();
for entity in &audio_entities {
let Some(source) = world.core.get_audio_source(*entity) else {
continue;
};
let is_spatial = source.spatial;
let has_handle = audio_engine_has_handle(&world.resources.audio, *entity);
let is_playing = source.playing;
let audio_ref = pick_active_clip(
source,
*entity,
world.resources.window.timing.uptime_milliseconds,
);
let bus = source.bus;
let min_distance = source.min_distance;
let max_distance = source.max_distance;
let reverb_zones = source.reverb_zones.clone();
let looping = source.looping;
let volume = source.volume;
let playback_rate = source.playback_rate;
if is_playing
&& !has_handle
&& let Some(ref audio_ref) = audio_ref
&& let Some(sound_data) = audio_engine_get_sound(&world.resources.audio, audio_ref)
{
let mut sound_data = sound_data.clone();
if looping {
sound_data = sound_data.loop_region(..);
}
sound_data = sound_data.volume(Decibels::from(volume));
if (playback_rate - 1.0).abs() > f64::EPSILON {
sound_data = sound_data.playback_rate(playback_rate);
}
let position = if is_spatial {
world
.core
.get_global_transform(*entity)
.map(|transform| KiraVec3 {
x: transform.0[(0, 3)],
y: transform.0[(1, 3)],
z: transform.0[(2, 3)],
})
.unwrap_or(KiraVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
})
} else {
KiraVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
}
};
sounds_to_play.push(PlayIntent {
entity: *entity,
sound_data,
is_spatial,
bus,
min_distance,
max_distance,
reverb_zones,
position,
});
} else if !is_playing && has_handle {
sounds_to_stop.push(*entity);
}
if is_spatial
&& world.resources.audio.spatial_tracks.contains_key(entity)
&& let Some(transform) = world.core.get_global_transform(*entity)
{
let position = KiraVec3 {
x: transform.0[(0, 3)],
y: transform.0[(1, 3)],
z: transform.0[(2, 3)],
};
spatial_tracks_to_update.push((*entity, position));
}
}
for intent in sounds_to_play {
let audio = &mut world.resources.audio;
if intent.is_spatial && audio.listener.is_none() {
tracing::warn!("Spatial audio requested but no listener exists");
continue;
}
let bus_slot: &mut Option<TrackHandle> = match intent.bus {
AudioBus::Master => &mut audio.buses.master,
AudioBus::Music => &mut audio.buses.music,
AudioBus::Sfx => &mut audio.buses.sfx,
AudioBus::Ambient => &mut audio.buses.ambient,
AudioBus::Voice => &mut audio.buses.voice,
AudioBus::Ui => &mut audio.buses.ui,
};
let Some(bus) = bus_slot.as_mut() else {
tracing::warn!("Audio bus not built for entity {:?}", intent.entity);
continue;
};
if intent.is_spatial {
let listener = audio.listener.as_ref().unwrap();
let reverb_sends = &audio.reverb_sends;
let distance_input = (intent.min_distance as f64, intent.max_distance as f64);
let mut track_builder = SpatialTrackBuilder::new()
.distances((intent.min_distance, intent.max_distance))
.attenuation_function(Easing::OutPowf(2.0))
.with_effect(
FilterBuilder::new().cutoff(Value::FromListenerDistance(Mapping {
input_range: distance_input,
output_range: (20000.0, 500.0),
easing: Easing::OutPowf(2.0),
})),
);
for (zone_name, send_decibels) in &intent.reverb_zones {
if let Some(send) = reverb_sends.get(zone_name) {
track_builder = track_builder.with_send(
send,
Value::FromListenerDistance(Mapping {
input_range: distance_input,
output_range: (Decibels(send_decibels - 6.0), Decibels(*send_decibels)),
easing: Easing::OutPowf(2.0),
}),
);
} else {
tracing::warn!("Unknown reverb zone '{}'", zone_name);
}
}
let track_result = bus.add_spatial_sub_track(listener, intent.position, track_builder);
match track_result {
Ok(mut spatial_track) => {
if let Err(error) = spatial_track.play(intent.sound_data) {
tracing::error!(
"Failed to play sound on spatial track for entity {:?}: {}",
intent.entity,
error
);
} else {
audio.spatial_tracks.insert(intent.entity, spatial_track);
}
}
Err(error) => {
tracing::error!(
"Failed to create spatial track for entity {:?}: {}",
intent.entity,
error
);
}
}
} else {
let play_result = bus.play(intent.sound_data);
match play_result {
Ok(handle) => {
audio.sound_handles.insert(intent.entity, handle);
}
Err(error) => {
tracing::error!(
"Failed to play sound for entity {:?}: {}",
intent.entity,
error
);
if let Some(source) = world.core.get_audio_source_mut(intent.entity) {
source.playing = false;
}
}
}
}
}
for (entity, position) in spatial_tracks_to_update {
if let Some(spatial_track) = world.resources.audio.spatial_tracks.get_mut(&entity) {
spatial_track.set_position(position, Tween::default());
}
}
for entity in sounds_to_stop {
audio_engine_stop_sound(&mut world.resources.audio, entity);
}
}
fn pick_active_clip(
source: &AudioSource,
entity: Entity,
uptime_milliseconds: u64,
) -> Option<String> {
if !source.random_pick || source.random_clips.is_empty() {
return source.audio_ref.clone();
}
let mut pool: Vec<&String> = Vec::with_capacity(source.random_clips.len() + 1);
if let Some(reference) = source.audio_ref.as_ref() {
pool.push(reference);
}
for clip in &source.random_clips {
if !clip.is_empty() {
pool.push(clip);
}
}
if pool.is_empty() {
return None;
}
let mut seed = uptime_milliseconds.wrapping_mul(0x9E3779B97F4A7C15);
seed ^= (entity.id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
seed = seed.wrapping_mul(0x94D049BB133111EB);
let index = ((seed >> 32) as usize) % pool.len();
Some(pool[index].clone())
}
pub fn load_sound_from_bytes(
bytes: &'static [u8],
) -> Result<StaticSoundData, Box<dyn std::error::Error>> {
let cursor = Cursor::new(bytes);
let sound_data = StaticSoundData::from_cursor(cursor)?;
Ok(sound_data)
}
pub fn load_sound_from_cursor<T: AsRef<[u8]> + Send + Sync + 'static>(
data: T,
) -> Result<StaticSoundData, Box<dyn std::error::Error>> {
let cursor = Cursor::new(data);
let sound_data = StaticSoundData::from_cursor(cursor)?;
Ok(sound_data)
}