Optional override for global spatial scale (#10419)

# Objective

Fixes #10414.

That issue and its comments do a great job of laying out the case for
this.

## Solution

Added an optional `spatial_scale` field to `PlaybackSettings`, which
overrides the default value set on `AudioPlugin`.

## Changelog

- `AudioPlugin::spatial_scale` has been renamed to
`default_spatial_scale`.
- `SpatialScale` is no longer a resource and is wrapped by
`DefaultSpatialScale`.
- Added an optional `spatial_scale` to `PlaybackSettings`.

## Migration Guide

`AudioPlugin::spatial_scale` has been renamed to `default_spatial_scale`
and the default spatial scale can now be overridden on individual audio
sources with `PlaybackSettings::spatial_scale`.

If you were modifying or reading `SpatialScale` at run time, use
`DefaultSpatialScale` instead.

```rust
// before
app.add_plugins(DefaultPlugins.set(AudioPlugin {
    spatial_scale: SpatialScale::new(AUDIO_SCALE),
    ..default()
}));

// after
app.add_plugins(DefaultPlugins.set(AudioPlugin {
    default_spatial_scale: SpatialScale::new(AUDIO_SCALE),
    ..default()
}));
```
This commit is contained in:
Rob Parrett 2024-01-25 09:29:35 -07:00 committed by GitHub
parent 2ebf5a303e
commit 29224768e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 33 deletions

View File

@ -67,6 +67,9 @@ pub struct PlaybackSettings {
/// Note: Bevy does not currently support HRTF or any other high-quality 3D sound rendering /// Note: Bevy does not currently support HRTF or any other high-quality 3D sound rendering
/// features. Spatial audio is implemented via simple left-right stereo panning. /// features. Spatial audio is implemented via simple left-right stereo panning.
pub spatial: bool, pub spatial: bool,
/// Optional scale factor applied to the positions of this audio source and the listener,
/// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale).
pub spatial_scale: Option<SpatialScale>,
} }
impl Default for PlaybackSettings { impl Default for PlaybackSettings {
@ -84,6 +87,7 @@ impl PlaybackSettings {
speed: 1.0, speed: 1.0,
paused: false, paused: false,
spatial: false, spatial: false,
spatial_scale: None,
}; };
/// Will play the associated audio source in a loop. /// Will play the associated audio source in a loop.
@ -127,6 +131,12 @@ impl PlaybackSettings {
self.spatial = spatial; self.spatial = spatial;
self self
} }
/// Helper to use a custom spatial scale.
pub const fn with_spatial_scale(mut self, spatial_scale: SpatialScale) -> Self {
self.spatial_scale = Some(spatial_scale);
self
}
} }
/// Settings for the listener for spatial audio sources. /// Settings for the listener for spatial audio sources.
@ -180,25 +190,22 @@ impl GlobalVolume {
} }
} }
/// The scale factor applied to the positions of audio sources and listeners for /// A scale factor applied to the positions of audio sources and listeners for
/// spatial audio. /// spatial audio.
/// ///
/// You may need to adjust this scale to fit your world's units.
///
/// Default is `Vec3::ONE`. /// Default is `Vec3::ONE`.
#[derive(Resource, Clone, Copy, Reflect)] #[derive(Clone, Copy, Debug, Reflect)]
#[reflect(Resource)]
pub struct SpatialScale(pub Vec3); pub struct SpatialScale(pub Vec3);
impl SpatialScale { impl SpatialScale {
/// Create a new `SpatialScale` with the same value for all 3 dimensions. /// Create a new `SpatialScale` with the same value for all 3 dimensions.
pub fn new(scale: f32) -> Self { pub const fn new(scale: f32) -> Self {
Self(Vec3::splat(scale)) Self(Vec3::splat(scale))
} }
/// Create a new `SpatialScale` with the same value for `x` and `y`, and `0.0` /// Create a new `SpatialScale` with the same value for `x` and `y`, and `0.0`
/// for `z`. /// for `z`.
pub fn new_2d(scale: f32) -> Self { pub const fn new_2d(scale: f32) -> Self {
Self(Vec3::new(scale, scale, 0.0)) Self(Vec3::new(scale, scale, 0.0))
} }
} }
@ -209,6 +216,16 @@ impl Default for SpatialScale {
} }
} }
/// The default scale factor applied to the positions of audio sources and listeners for
/// spatial audio. Can be overridden for individual sounds in [`PlaybackSettings`].
///
/// You may need to adjust this scale to fit your world's units.
///
/// Default is `Vec3::ONE`.
#[derive(Resource, Default, Clone, Copy, Reflect)]
#[reflect(Resource)]
pub struct DefaultSpatialScale(pub SpatialScale);
/// Bundle for playing a standard bevy audio asset /// Bundle for playing a standard bevy audio asset
pub type AudioBundle = AudioSourceBundle<AudioSource>; pub type AudioBundle = AudioSourceBundle<AudioSource>;

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
AudioSourceBundle, Decodable, GlobalVolume, PlaybackMode, PlaybackSettings, SpatialAudioSink, AudioSourceBundle, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode,
SpatialListener, SpatialScale, PlaybackSettings, SpatialAudioSink, SpatialListener,
}; };
use bevy_asset::{Asset, Assets, Handle}; use bevy_asset::{Asset, Assets, Handle};
use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_ecs::{prelude::*, system::SystemParam};
@ -56,10 +56,9 @@ pub struct PlaybackRemoveMarker;
#[derive(SystemParam)] #[derive(SystemParam)]
pub(crate) struct EarPositions<'w, 's> { pub(crate) struct EarPositions<'w, 's> {
pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>, pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>,
pub(crate) scale: Res<'w, SpatialScale>,
} }
impl<'w, 's> EarPositions<'w, 's> { impl<'w, 's> EarPositions<'w, 's> {
/// Gets a set of transformed and scaled ear positions. /// Gets a set of transformed ear positions.
/// ///
/// If there are no listeners, use the default values. If a user has added multiple /// If there are no listeners, use the default values. If a user has added multiple
/// listeners for whatever reason, we will return the first value. /// listeners for whatever reason, we will return the first value.
@ -70,16 +69,13 @@ impl<'w, 's> EarPositions<'w, 's> {
.next() .next()
.map(|(_, transform, settings)| { .map(|(_, transform, settings)| {
( (
transform.transform_point(settings.left_ear_offset) * self.scale.0, transform.transform_point(settings.left_ear_offset),
transform.transform_point(settings.right_ear_offset) * self.scale.0, transform.transform_point(settings.right_ear_offset),
) )
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
let settings = SpatialListener::default(); let settings = SpatialListener::default();
( (settings.left_ear_offset, settings.right_ear_offset)
settings.left_ear_offset * self.scale.0,
settings.right_ear_offset * self.scale.0,
)
}); });
(left_ear, right_ear) (left_ear, right_ear)
@ -112,6 +108,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
(Without<AudioSink>, Without<SpatialAudioSink>), (Without<AudioSink>, Without<SpatialAudioSink>),
>, >,
ear_positions: EarPositions, ear_positions: EarPositions,
default_spatial_scale: Res<DefaultSpatialScale>,
mut commands: Commands, mut commands: Commands,
) where ) where
f32: rodio::cpal::FromSample<Source::DecoderItem>, f32: rodio::cpal::FromSample<Source::DecoderItem>,
@ -138,8 +135,10 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
); );
} }
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform { let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform {
(emitter_transform.translation() * ear_positions.scale.0).into() (emitter_transform.translation() * scale).into()
} else { } else {
warn!("Spatial AudioBundle with no GlobalTransform component. Using zero."); warn!("Spatial AudioBundle with no GlobalTransform component. Using zero.");
Vec3::ZERO.into() Vec3::ZERO.into()
@ -148,8 +147,8 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
let sink = match SpatialSink::try_new( let sink = match SpatialSink::try_new(
stream_handle, stream_handle,
emitter_translation, emitter_translation,
left_ear.into(), (left_ear * scale).into(),
right_ear.into(), (right_ear * scale).into(),
) { ) {
Ok(sink) => sink, Ok(sink) => sink,
Err(err) => { Err(err) => {
@ -285,34 +284,46 @@ pub(crate) fn audio_output_available(audio_output: Res<AudioOutput>) -> bool {
/// Updates spatial audio sinks when emitter positions change. /// Updates spatial audio sinks when emitter positions change.
pub(crate) fn update_emitter_positions( pub(crate) fn update_emitter_positions(
mut emitters: Query<(&GlobalTransform, &SpatialAudioSink), Changed<GlobalTransform>>, mut emitters: Query<
spatial_scale: Res<SpatialScale>, (&GlobalTransform, &SpatialAudioSink, &PlaybackSettings),
Or<(Changed<GlobalTransform>, Changed<PlaybackSettings>)>,
>,
default_spatial_scale: Res<DefaultSpatialScale>,
) { ) {
for (transform, sink) in emitters.iter_mut() { for (transform, sink, settings) in emitters.iter_mut() {
let translation = transform.translation() * spatial_scale.0; let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
let translation = transform.translation() * scale;
sink.set_emitter_position(translation); sink.set_emitter_position(translation);
} }
} }
/// Updates spatial audio sink ear positions when spatial listeners change. /// Updates spatial audio sink ear positions when spatial listeners change.
pub(crate) fn update_listener_positions( pub(crate) fn update_listener_positions(
mut emitters: Query<&SpatialAudioSink>, mut emitters: Query<(&SpatialAudioSink, &PlaybackSettings)>,
changed_listener: Query< changed_listener: Query<
(), (),
( (
Or<(Changed<SpatialListener>, Changed<GlobalTransform>)>, Or<(
Changed<SpatialListener>,
Changed<GlobalTransform>,
Changed<PlaybackSettings>,
)>,
With<SpatialListener>, With<SpatialListener>,
), ),
>, >,
ear_positions: EarPositions, ear_positions: EarPositions,
default_spatial_scale: Res<DefaultSpatialScale>,
) { ) {
if !ear_positions.scale.is_changed() && changed_listener.is_empty() { if !default_spatial_scale.is_changed() && changed_listener.is_empty() {
return; return;
} }
let (left_ear, right_ear) = ear_positions.get(); let (left_ear, right_ear) = ear_positions.get();
for sink in emitters.iter_mut() { for (sink, settings) in emitters.iter_mut() {
sink.set_ears_position(left_ear, right_ear); let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
sink.set_ears_position(left_ear * scale, right_ear * scale);
} }
} }

View File

@ -67,7 +67,7 @@ pub struct AudioPlugin {
pub global_volume: GlobalVolume, pub global_volume: GlobalVolume,
/// The scale factor applied to the positions of audio sources and listeners for /// The scale factor applied to the positions of audio sources and listeners for
/// spatial audio. /// spatial audio.
pub spatial_scale: SpatialScale, pub default_spatial_scale: SpatialScale,
} }
impl Plugin for AudioPlugin { impl Plugin for AudioPlugin {
@ -75,11 +75,11 @@ impl Plugin for AudioPlugin {
app.register_type::<Volume>() app.register_type::<Volume>()
.register_type::<GlobalVolume>() .register_type::<GlobalVolume>()
.register_type::<SpatialListener>() .register_type::<SpatialListener>()
.register_type::<SpatialScale>() .register_type::<DefaultSpatialScale>()
.register_type::<PlaybackMode>() .register_type::<PlaybackMode>()
.register_type::<PlaybackSettings>() .register_type::<PlaybackSettings>()
.insert_resource(self.global_volume) .insert_resource(self.global_volume)
.insert_resource(self.spatial_scale) .insert_resource(DefaultSpatialScale(self.default_spatial_scale))
.configure_sets( .configure_sets(
PostUpdate, PostUpdate,
AudioPlaySet AudioPlaySet

View File

@ -13,7 +13,7 @@ const AUDIO_SCALE: f32 = 1. / 100.0;
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(AudioPlugin { .add_plugins(DefaultPlugins.set(AudioPlugin {
spatial_scale: SpatialScale::new_2d(AUDIO_SCALE), default_spatial_scale: SpatialScale::new_2d(AUDIO_SCALE),
..default() ..default()
})) }))
.add_systems(Startup, setup) .add_systems(Startup, setup)