use crate::ecs::world::{Entity, World};
use kira::{
Decibels, Easing, Mapping, Tween, Value, effect::filter::FilterBuilder,
sound::static_sound::StaticSoundData, track::SpatialTrackBuilder,
};
use std::io::Cursor;
type KiraVec3 = mint::Vector3<f32>;
type KiraQuat = mint::Quaternion<f32>;
pub fn initialize_audio_system(world: &mut World) {
let _span = tracing::info_span!("audio_init").entered();
if !world.resources.audio.is_initialized() {
#[cfg(not(target_arch = "wasm32"))]
{
if let Err(error) = world.resources.audio.initialize() {
tracing::error!("Failed to initialize audio: {}", error);
}
}
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn lazy_initialize_audio_system(world: &mut World) {
if !world.resources.audio.is_initialized()
&& let Err(error) = world.resources.audio.initialize()
{
tracing::error!("Failed to initialize audio (WASM): {}", error);
}
}
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 !world.resources.audio.is_initialized() {
return;
}
#[cfg(feature = "openxr")]
let xr_input = world.resources.xr.input.clone();
#[cfg(not(feature = "openxr"))]
let xr_input: Option<()> = None;
let listener_entity = world
.core
.query_entities(crate::ecs::world::AUDIO_LISTENER)
.next();
let has_listener_entity = listener_entity.is_some() || xr_input.is_some();
if has_listener_entity {
if world.resources.audio.listener.is_none() {
#[cfg(feature = "openxr")]
let position = if let Some(ref xr) = xr_input {
KiraVec3 {
x: xr.head_position.x,
y: xr.head_position.y,
z: xr.head_position.z,
}
} else 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,
}
};
#[cfg(not(feature = "openxr"))]
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);
}
}
}
}
#[cfg(feature = "openxr")]
let (position, kira_orientation) = if let Some(ref xr) = xr_input {
let pos = KiraVec3 {
x: xr.head_position.x,
y: xr.head_position.y,
z: xr.head_position.z,
};
let quat = KiraQuat {
v: mint::Vector3 {
x: xr.head_orientation.coords.x,
y: xr.head_orientation.coords.y,
z: xr.head_orientation.coords.z,
},
s: xr.head_orientation.coords.w,
};
(Some(pos), Some(quat))
} else 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)
};
#[cfg(not(feature = "openxr"))]
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();
let mut sounds_to_play: Vec<(Entity, StaticSoundData, bool, bool)> = 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_reverb = source.reverb;
let has_handle = world.resources.audio.has_handle(*entity);
let is_playing = source.playing;
let audio_ref = source.audio_ref.clone();
if is_playing
&& !has_handle
&& let Some(ref audio_ref) = audio_ref
&& let Some(sound_data) = world.resources.audio.get_sound(audio_ref)
{
let mut sound_data = sound_data.clone();
if source.looping {
sound_data = sound_data.loop_region(..);
}
sound_data = sound_data.volume(source.volume);
sounds_to_play.push((*entity, sound_data, is_spatial, has_reverb));
} 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));
}
}
let mut entity_positions: Vec<(Entity, KiraVec3)> = Vec::new();
for (entity, _, is_spatial, _) in &sounds_to_play {
if *is_spatial && 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)],
};
entity_positions.push((*entity, position));
}
}
let mut play_results = Vec::new();
{
let manager = match &mut world.resources.audio.manager {
Some(manager) => manager,
None => return,
};
let listener = &world.resources.audio.listener;
let reverb_send = &world.resources.audio.reverb_send;
for (entity, sound_data, is_spatial, has_reverb) in sounds_to_play {
if is_spatial {
if let Some(listener_handle) = listener {
let position = entity_positions
.iter()
.find(|(e, _)| *e == entity)
.map(|(_, pos)| *pos)
.unwrap_or(KiraVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
});
let mut track_builder = SpatialTrackBuilder::new()
.distances((1.0, 50.0))
.with_effect(FilterBuilder::new().cutoff(Value::FromListenerDistance(
Mapping {
input_range: (0.0, 100.0),
output_range: (20000.0, 500.0),
easing: Easing::Linear,
},
)));
if has_reverb && let Some(reverb) = reverb_send {
track_builder = track_builder.with_send(
reverb,
Value::FromListenerDistance(Mapping {
input_range: (0.0, 100.0),
output_range: (Decibels(-12.0), Decibels(6.0)),
easing: Easing::Linear,
}),
);
}
let spatial_track_result =
manager.add_spatial_sub_track(listener_handle, position, track_builder);
match spatial_track_result {
Ok(mut spatial_track) => {
let play_result = spatial_track.play(sound_data);
if let Err(ref error) = play_result {
tracing::error!(
"Failed to play sound on spatial track for entity {:?}: {}",
entity,
error
);
}
play_results.push((
entity,
Err(()),
Some(spatial_track),
play_result.err(),
));
}
Err(error) => {
tracing::error!(
"Failed to create spatial track for entity {:?}: {}",
entity,
error
);
play_results.push((entity, Err(()), None, None));
}
}
} else {
tracing::warn!("Spatial audio requested but no listener exists");
let result = manager.play(sound_data);
if let Err(ref error) = result {
tracing::error!("Failed to play sound for entity {:?}: {}", entity, error);
}
play_results.push((entity, result.map_err(|_| ()), None, None));
}
} else {
let result = manager.play(sound_data);
if let Err(ref error) = result {
tracing::error!("Failed to play sound for entity {:?}: {}", entity, error);
}
play_results.push((entity, result.map_err(|_| ()), None, None));
}
}
}
for (entity, handle_result, spatial_track, _spatial_error) in play_results {
if let Some(spatial_track) = spatial_track {
world
.resources
.audio
.spatial_tracks
.insert(entity, spatial_track);
tracing::debug!("Started playing spatial audio for entity {:?}", entity);
} else {
match handle_result {
Ok(handle) => {
world.resources.audio.sound_handles.insert(entity, handle);
tracing::debug!("Started playing audio for entity {:?}", entity);
}
Err(_) => {
if let Some(source) = world.core.get_audio_source_mut(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 {
world.resources.audio.stop_sound(entity);
}
}
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)
}