Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Per Entity SpatialRadius for controlling spatial audio ranges #128

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

- 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`

Expand Down
5 changes: 3 additions & 2 deletions examples/spatial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use bevy_kira_audio::prelude::*;

fn main() {
App::new()
.insert_resource(SpatialAudio { max_distance: 25. })
.insert_resource(GlobalSpatialRadius { radius: 25. })
.add_plugins((DefaultPlugins, AudioPlugin, CameraPlugin))
.add_systems(Startup, setup)
.run();
Expand All @@ -30,7 +30,8 @@ fn setup(
))
.insert(AudioEmitter {
instances: vec![cooking],
});
})
.insert(SpatialRadius { radius: 10.0 });
// Emitter Nr. 2
let elevator_music = audio
.play(asset_server.load("sounds/loop.ogg"))
Expand Down
21 changes: 5 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ use bevy::app::{PostUpdate, PreUpdate};
use bevy::asset::AssetApp;
pub use channel::AudioControl;
pub use source::AudioSource;
use spatial::cleanup_stopped_spatial_instances;

/// Most commonly used types
pub mod prelude {
Expand Down Expand Up @@ -88,7 +87,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::source::AudioSource;
#[doc(hidden)]
pub use crate::spatial::{AudioEmitter, AudioReceiver, SpatialAudio};
pub use crate::spatial::{AudioEmitter, AudioReceiver, GlobalSpatialRadius, SpatialRadius};
#[doc(hidden)]
pub use crate::{Audio, AudioPlugin, MainTrack};
pub use kira::{
Expand All @@ -113,8 +112,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;
Expand Down Expand Up @@ -170,7 +168,8 @@ impl Plugin for AudioPlugin {
#[cfg(feature = "settings_loader")]
app.init_asset_loader::<SettingsLoader>();

app.init_resource::<DynamicAudioChannels>()
app.add_plugins(spatial::SpatialAudioPlugin)
.init_resource::<DynamicAudioChannels>()
.add_systems(
PostUpdate,
play_dynamic_channels.in_set(AudioSystemSet::PlayDynamicChannels),
Expand All @@ -179,17 +178,7 @@ impl Plugin for AudioPlugin {
PreUpdate,
cleanup_stopped_instances.in_set(AudioSystemSet::InstanceCleanup),
)
.add_audio_channel::<MainTrack>()
.add_systems(
PreUpdate,
cleanup_stopped_spatial_instances
.in_set(AudioSystemSet::InstanceCleanup)
.run_if(resource_exists::<SpatialAudio>),
)
.add_systems(
PostUpdate,
run_spatial_audio.run_if(resource_exists::<SpatialAudio>),
);
.add_audio_channel::<MainTrack>();
}
}

Expand Down
100 changes: 58 additions & 42 deletions src/spatial.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
use crate::{AudioInstance, AudioTween};
use bevy::asset::{Assets, Handle};
use bevy::ecs::component::Component;
use bevy::prelude::{GlobalTransform, Query, Res, ResMut, Resource, With};
use crate::{AudioInstance, AudioSystemSet, AudioTween};
use bevy::prelude::*;

pub(crate) struct SpatialAudioPlugin;

impl Plugin for SpatialAudioPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PreUpdate,
cleanup_stopped_spatial_instances
.in_set(AudioSystemSet::InstanceCleanup)
.run_if(spatial_audio_enabled),
)
.add_systems(PostUpdate, run_spatial_audio.run_if(spatial_audio_enabled));
}
}

/// Component for audio emitters
///
Expand All @@ -23,27 +35,39 @@ pub struct AudioEmitter {
#[derive(Component)]
pub struct AudioReceiver;

/// 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.
/// If neither this resource or the [`SpatialRadius`] component for entity exists in the ECS,
/// spatial audio is not applied.
#[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 GlobalSpatialRadius {
/// The volume will change from `1` at distance `0` to `0` at distance `radius`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resource would make more sense as a field on SpatialAudioPlugin. E.g. default_spatial_audio_radius.

pub radius: f32,
}

/// Component for per-entity spatial audio radius
///
/// If neither this component or the [`GlobalSpatialRadius`] resource exists in the ECS, spatial
/// audio is not applied.
#[derive(Component)]
pub struct SpatialRadius {
/// The volume will change from `1` at distance `0` to `0` at distance `radius`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe SpatialRadius could be a required component of AudioEmitter? If so, the default value could not be changed...

pub radius: f32,
}

impl SpatialAudio {
pub(crate) fn update(
&self,
receiver_transform: &GlobalTransform,
emitters: &Query<(&GlobalTransform, &AudioEmitter)>,
audio_instances: &mut Assets<AudioInstance>,
) {
for (emitter_transform, emitter) in emitters {
fn run_spatial_audio(
spatial_audio: Res<GlobalSpatialRadius>,
receiver: Query<&GlobalTransform, With<AudioReceiver>>,
emitters: Query<(&GlobalTransform, &AudioEmitter, Option<&SpatialRadius>)>,
mut audio_instances: ResMut<Assets<AudioInstance>>,
) {
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 = receiver_transform.right().angle_between(sound_path);
let panning = (right_ear_angle.cos() + 1.) / 2.;
Expand All @@ -58,30 +82,22 @@ impl SpatialAudio {
}
}

pub(crate) fn run_spatial_audio(
spatial_audio: Res<SpatialAudio>,
receiver: Query<&GlobalTransform, With<AudioReceiver>>,
emitters: Query<(&GlobalTransform, &AudioEmitter)>,
mut audio_instances: ResMut<Assets<AudioInstance>>,
) {
if let Ok(receiver_transform) = receiver.get_single() {
spatial_audio.update(receiver_transform, &emitters, &mut audio_instances);
}
}

pub(crate) fn cleanup_stopped_spatial_instances(
fn cleanup_stopped_spatial_instances(
mut emitters: Query<&mut AudioEmitter>,
instances: ResMut<Assets<AudioInstance>>,
) {
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)
})
});
}
});
}

fn spatial_audio_enabled(
global: Option<Res<GlobalSpatialRadius>>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means the system won't run without the resource, even if all emitters have their own SpacialRadius components.
I think it would be more idiomatic to just let users add the SpatialAudioPlugin.

local: Query<(), With<SpatialRadius>>,
) -> bool {
global.is_some() || !local.is_empty()
}
Loading