diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf639b..d82f99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - removed `playback_region` from `SoundSettings` - Add `android_shared_stdcxx` feature for Android Builds - fix spatial audio when position of receiver and emitter are the same ([#135](https://github.com/NiklasEi/bevy_kira_audio/issues/135)) +- add `SpatialRadius` to control spatial audio distance range per entity +- rename `SpatialAudio` to `GlobalSpatialRadius` and rename it's field `max_distance` to `radius` ## v0.21.0 - 30.11.2024 - Update to Bevy `0.15` diff --git a/examples/spatial.rs b/examples/spatial.rs index 09375f8..2f8da11 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -3,10 +3,17 @@ use bevy::prelude::*; use bevy::window::{CursorGrabMode, PrimaryWindow}; use bevy_kira_audio::prelude::*; +/// This example demonstrates the basic spatial audio support in `bevy_kira_audio`. +/// It adds `SpatialAudioPlugin` then spawns entities with `SpatialAudioEmitter` +/// and a receiver with the `SpatialAudioReceiver` component. fn main() { App::new() - .insert_resource(SpatialAudio { max_distance: 25. }) - .add_plugins((DefaultPlugins, AudioPlugin, CameraPlugin)) + .add_plugins(( + DefaultPlugins, + AudioPlugin, + SpatialAudioPlugin, + CameraPlugin, + )) .add_systems(Startup, setup) .run(); } @@ -23,31 +30,33 @@ fn setup( .play(asset_server.load("sounds/cooking.ogg")) .looped() .handle(); - commands - .spawn(( - SceneRoot(asset_server.load("models/panStew.glb#Scene0")), - Transform::from_xyz(-5.0, 0., 0.), - )) - .insert(AudioEmitter { + commands.spawn(( + SceneRoot(asset_server.load("models/panStew.glb#Scene0")), + Transform::from_xyz(-5.0, 0., 0.), + SpatialAudioEmitter { instances: vec![cooking], - }); + }, + // at a distance of more than 10, we will not hear this emitter anymore + SpatialRadius { radius: 10.0 }, + )); // Emitter Nr. 2 let elevator_music = audio .play(asset_server.load("sounds/loop.ogg")) .looped() .handle(); - commands - .spawn(( - SceneRoot(asset_server.load("models/boxOpen.glb#Scene0")), - Transform::from_xyz(10., 0., 0.), - )) - .insert(AudioEmitter { + commands.spawn(( + SceneRoot(asset_server.load("models/boxOpen.glb#Scene0")), + Transform::from_xyz(10., 0., 0.), + SpatialAudioEmitter { instances: vec![elevator_music], - }); - // Our camera will be the receiver + }, + )); + // If an emitter has no SpatialRadius, the resource DefaultSpatialRadius is used instead. + // It defaults to a spatial radius of 25. + // Our camera will be the receiver. commands .spawn((Camera3d::default(), Transform::from_xyz(0.0, 0.5, 10.0))) - .insert(AudioReceiver) + .insert(SpatialAudioReceiver) .insert(FlyCam); // Other scene setup... diff --git a/src/lib.rs b/src/lib.rs index 288569b..b2d441a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,10 @@ use bevy::app::{PostUpdate, PreUpdate}; use bevy::asset::AssetApp; pub use channel::AudioControl; pub use source::AudioSource; -use spatial::cleanup_stopped_spatial_instances; +pub use spatial::{ + DefaultSpatialRadius, SpatialAudioEmitter, SpatialAudioPlugin, SpatialAudioReceiver, + SpatialRadius, +}; /// Most commonly used types pub mod prelude { @@ -88,7 +91,10 @@ pub mod prelude { #[doc(hidden)] pub use crate::source::AudioSource; #[doc(hidden)] - pub use crate::spatial::{AudioEmitter, AudioReceiver, SpatialAudio}; + pub use crate::spatial::{ + DefaultSpatialRadius, SpatialAudioEmitter, SpatialAudioPlugin, SpatialAudioReceiver, + SpatialRadius, + }; #[doc(hidden)] pub use crate::{Audio, AudioPlugin, MainTrack}; pub use kira::{ @@ -112,8 +118,7 @@ use crate::source::ogg_loader::OggLoader; use crate::source::settings_loader::SettingsLoader; #[cfg(feature = "wav")] use crate::source::wav_loader::WavLoader; -use crate::spatial::{run_spatial_audio, SpatialAudio}; -use bevy::prelude::{resource_exists, App, IntoSystemConfigs, Plugin, Resource, SystemSet}; +use bevy::prelude::{App, IntoSystemConfigs, Plugin, Resource, SystemSet}; pub use channel::dynamic::DynamicAudioChannel; pub use channel::dynamic::DynamicAudioChannels; pub use channel::typed::AudioChannel; @@ -178,17 +183,7 @@ impl Plugin for AudioPlugin { PreUpdate, cleanup_stopped_instances.in_set(AudioSystemSet::InstanceCleanup), ) - .add_audio_channel::() - .add_systems( - PreUpdate, - cleanup_stopped_spatial_instances - .in_set(AudioSystemSet::InstanceCleanup) - .run_if(resource_exists::), - ) - .add_systems( - PostUpdate, - run_spatial_audio.run_if(resource_exists::), - ); + .add_audio_channel::(); } } diff --git a/src/spatial.rs b/src/spatial.rs index 6aa9c67..afe41f5 100644 --- a/src/spatial.rs +++ b/src/spatial.rs @@ -1,51 +1,99 @@ -use crate::{AudioInstance, AudioTween}; +use crate::{AudioInstance, AudioSystemSet, AudioTween}; +use bevy::app::{App, Plugin, PostUpdate, PreUpdate}; use bevy::asset::{Assets, Handle}; use bevy::ecs::component::Component; +use bevy::ecs::{ + change_detection::{Res, ResMut}, + query::With, + schedule::IntoSystemConfigs, + system::{Query, Resource}, +}; use bevy::math::Vec3; -use bevy::prelude::{GlobalTransform, Query, Res, ResMut, Resource, With}; +use bevy::transform::components::{GlobalTransform, Transform}; use std::f32::consts::PI; +/// This plugin adds basic spatial audio. +/// +/// Add `SpatialAudioEmitter` components to entities that emit spacial audio. +/// One entity, usually the "Player" or the Camera should get the `SpatialAudioReceiver` component. +/// +/// See the `spacial` example of `bevy_kira_audio`. +pub struct SpatialAudioPlugin; + +impl Plugin for SpatialAudioPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems( + PreUpdate, + cleanup_stopped_spatial_instances.in_set(AudioSystemSet::InstanceCleanup), + ) + .add_systems(PostUpdate, run_spatial_audio); + } +} + /// Component for audio emitters /// /// Add [`Handle`]s to control their pan and volume based on emitter /// and receiver positions. -#[derive(Component, Default)] -pub struct AudioEmitter { +#[derive(Component)] +#[require(Transform)] +pub struct SpatialAudioEmitter { /// Audio instances that are played by this emitter /// /// The same instance should only be on one emitter. pub instances: Vec>, } -/// Component for the audio receiver +/// Component for the spatial audio receiver. /// /// Most likely you will want to add this component to your player or you camera. -/// The entity needs a [`Transform`] and [`GlobalTransform`]. The view direction of the [`GlobalTransform`] -/// will +/// There can only ever be one entity with this component at a given time! #[derive(Component)] -pub struct AudioReceiver; +#[require(Transform)] +pub struct SpatialAudioReceiver; -/// Configuration resource for spatial audio +/// Configuration resource for global spatial audio radius /// -/// If this resource is not added to the ECS, spatial audio is not applied. +/// This resource has to exist for spatial audio and will be initialized by the `SpatialAudioPlugin`. +/// If an emitter does not have a `SpatialRadius`, the `GlobalSpatialRadius` is used. #[derive(Resource)] -pub struct SpatialAudio { - /// The volume will change from `1` at distance `0` to `0` at distance `max_distance` - pub max_distance: f32, +pub struct DefaultSpatialRadius { + /// The volume will change from `1` at distance `0` to `0` at distance `radius` + pub radius: f32, +} + +impl Default for DefaultSpatialRadius { + fn default() -> Self { + Self { radius: 25.0 } + } +} + +/// Component for per-entity spatial audio radius +/// +/// If an emitter does not have this component, the [`DefaultSpatialRadius`] is used instead. +#[derive(Component)] +pub struct SpatialRadius { + /// The volume will change from `1` at distance `0` to `0` at distance `radius` + pub radius: f32, } -impl SpatialAudio { - pub(crate) fn update( - &self, - receiver_transform: &GlobalTransform, - emitters: &Query<(&GlobalTransform, &AudioEmitter)>, - audio_instances: &mut Assets, - ) { - for (emitter_transform, emitter) in emitters { +fn run_spatial_audio( + spatial_audio: Res, + receiver: Query<&GlobalTransform, With>, + emitters: Query<( + &GlobalTransform, + &SpatialAudioEmitter, + Option<&SpatialRadius>, + )>, + mut audio_instances: ResMut>, +) { + if let Ok(receiver_transform) = receiver.get_single() { + for (emitter_transform, emitter, range) in emitters.iter() { let sound_path = emitter_transform.translation() - receiver_transform.translation(); - let volume = (1. - sound_path.length() / self.max_distance) - .clamp(0., 1.) - .powi(2); + let volume = (1. + - sound_path.length() / range.map_or(spatial_audio.radius, |r| r.radius)) + .clamp(0., 1.) + .powi(2); let right_ear_angle = if sound_path == Vec3::ZERO { PI / 2. @@ -64,30 +112,15 @@ impl SpatialAudio { } } -pub(crate) fn run_spatial_audio( - spatial_audio: Res, - receiver: Query<&GlobalTransform, With>, - emitters: Query<(&GlobalTransform, &AudioEmitter)>, - mut audio_instances: ResMut>, -) { - if let Ok(receiver_transform) = receiver.get_single() { - spatial_audio.update(receiver_transform, &emitters, &mut audio_instances); - } -} - -pub(crate) fn cleanup_stopped_spatial_instances( - mut emitters: Query<&mut AudioEmitter>, +fn cleanup_stopped_spatial_instances( + mut emitters: Query<&mut SpatialAudioEmitter>, instances: ResMut>, ) { - for mut emitter in emitters.iter_mut() { - let handles = &mut emitter.instances; - - handles.retain(|handle| { - if let Some(instance) = instances.get(handle) { - instance.handle.state() != kira::sound::PlaybackState::Stopped - } else { - true - } + emitters.iter_mut().for_each(|mut emitter| { + emitter.instances.retain(|handle| { + instances.get(handle).map_or(true, |instance| { + !matches!(instance.handle.state(), kira::sound::PlaybackState::Stopped) + }) }); - } + }); }