nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
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)
}