bevy_kira_components/
spatial.rs

1//! Support for spatial audio through `kira`'s spatial features.
2use bevy::diagnostic::{Diagnostic, DiagnosticPath, RegisterDiagnostic};
3use bevy::prelude::*;
4
5use kira::spatial::emitter::{EmitterDistances, EmitterHandle, EmitterSettings};
6use kira::spatial::listener::ListenerHandle;
7use kira::spatial::scene::{SpatialSceneHandle, SpatialSceneSettings};
8use kira::tween::{Easing, Tween};
9
10use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker};
11
12#[doc(hidden)]
13#[allow(missing_docs)]
14pub mod prelude {
15    pub use super::{AudioListener, SpatialEmitter, SpatialWorld};
16}
17
18/// Spatial audio plugin. This is an internal plugin, useful for some separation of concerns.
19///
20/// It is automatically added by the main [`AudioPlugin`].
21pub(crate) struct SpatialAudioPlugin;
22
23impl Plugin for SpatialAudioPlugin {
24    fn build(&self, app: &mut App) {
25        app.init_resource::<SpatialWorld>()
26            .add_plugins(SpatialDiagnosticsPlugin)
27            .add_systems(
28                PreUpdate,
29                (add_listeners, add_emitters)
30                    .in_set(AudioPlaybackSet::Setup)
31                    .before(AudioSourceSetup),
32            )
33            .add_systems(
34                PostUpdate,
35                (update_listeners, update_emitters).in_set(AudioPlaybackSet::Update),
36            );
37    }
38}
39
40/// Marker component setting this entity as an audio listener. It must have a [`GlobalTransform`]
41/// attached for the spatial systems to pick it up.
42#[derive(Component)]
43pub struct AudioListener;
44
45/// Internal handle to a Kira listener. Used to update the audio listener position.
46#[derive(Component)]
47pub(crate) struct SpatialListenerHandle(ListenerHandle);
48
49/// Marker component setting this entity as a spatial emitter. It must have a [`GlobalTransform`]
50/// attached for the spatial systems to pick it up.
51///
52/// Note that these settings are only used in the setup of the spatial emitter, and not kept in
53/// sync afterwards.
54#[derive(Component)]
55pub struct SpatialEmitter {
56    /// Function describing the attenuation in volume depending on the distance of this emitter
57    /// to the listener.
58    pub attenuation: Option<Easing>,
59    /// Enables the panning effect that depends on the orientation of the listener.
60    pub enable_spatialization: bool,
61    /// Range of distances describing the distance at which the sound will be playing at full
62    /// volume, and the maximum distance at which the sound will be able to be heard.
63    pub distances: EmitterDistances,
64}
65
66impl Default for SpatialEmitter {
67    fn default() -> Self {
68        Self {
69            attenuation: None,
70            enable_spatialization: true,
71            distances: EmitterDistances::default(),
72        }
73    }
74}
75
76/// Internal Kira handle emitter. Used to update the spatial emitter position.
77#[derive(Component)]
78pub(crate) struct SpatialEmitterHandle(pub(crate) EmitterHandle);
79
80/// Global data related to spatial handling in the audio engine.
81#[derive(Resource)]
82pub struct SpatialWorld {
83    pub(crate) spatial_handle: SpatialSceneHandle,
84}
85
86impl FromWorld for SpatialWorld {
87    fn from_world(world: &mut World) -> Self {
88        let settings = world
89            .remove_non_send_resource::<SpatialSceneSettings>()
90            .unwrap_or_default();
91        let mut audio_world = world.resource_mut::<AudioWorld>();
92        let spatial_handle = audio_world
93            .audio_manager
94            .add_spatial_scene(settings)
95            .expect("Cannot create audio spatial world");
96        Self { spatial_handle }
97    }
98}
99
100fn add_listeners(
101    mut commands: Commands,
102    mut spatial_world: ResMut<SpatialWorld>,
103    q: Query<(Entity, &GlobalTransform), Added<AudioListener>>,
104) {
105    for (entity, global_transform) in &q {
106        let (_, quat, position) = global_transform.to_scale_rotation_translation();
107        let listener = spatial_world
108            .spatial_handle
109            .add_listener(position, quat, default())
110            .unwrap();
111        debug!("Add listener to {entity:?}");
112        commands
113            .entity(entity)
114            .insert(SpatialListenerHandle(listener));
115    }
116}
117
118fn add_emitters(
119    mut commands: Commands,
120    mut spatial_world: ResMut<SpatialWorld>,
121    q: Query<(Entity, &GlobalTransform, &SpatialEmitter), Added<InternalAudioMarker>>,
122) {
123    for (entity, global_transform, spatial_emitter) in &q {
124        let result = spatial_world.spatial_handle.add_emitter(
125            global_transform.translation(),
126            EmitterSettings::default()
127                .attenuation_function(spatial_emitter.attenuation)
128                .enable_spatialization(spatial_emitter.enable_spatialization)
129                .distances(spatial_emitter.distances)
130                .persist_until_sounds_finish(true),
131        );
132        debug!("Add emitter to {entity:?}");
133        match result {
134            Ok(emitter) => {
135                commands
136                    .entity(entity)
137                    .insert(SpatialEmitterHandle(emitter));
138            }
139            Err(err) => {
140                error!("Cannot create spatial audio emitter for entity {entity:?}: {err}");
141            }
142        }
143    }
144}
145
146fn update_listeners(mut q: Query<(&mut SpatialListenerHandle, &GlobalTransform)>) {
147    for (mut listener, global_transform) in &mut q {
148        let (_, quat, position) = global_transform.to_scale_rotation_translation();
149        listener.0.set_position(position, Tween::default()).unwrap();
150        listener.0.set_orientation(quat, Tween::default()).unwrap();
151    }
152}
153
154fn update_emitters(mut q: Query<(Entity, &mut SpatialEmitterHandle, &GlobalTransform)>) {
155    for (entity, mut emitter, global_transform) in &mut q {
156        let position = global_transform.translation();
157        match emitter.0.set_position(position, Tween::default()) {
158            Ok(_) => {}
159            Err(err) => {
160                error!("Cannot set spatial audio position for entity {entity:?}: {err}");
161            }
162        }
163    }
164}
165
166/// Bevy diagnostic path recording the number of emitters present.
167pub const SPATIAL_EMITTERS: DiagnosticPath = DiagnosticPath::const_new("kira::spatial::emitters");
168/// Bevy diagnostic path recording the number of listeners present.
169pub const SPATIAL_LISTENERS: DiagnosticPath = DiagnosticPath::const_new("kira::spatial::listeners");
170
171struct SpatialDiagnosticsPlugin;
172
173impl Plugin for SpatialDiagnosticsPlugin {
174    fn build(&self, app: &mut App) {
175        app.register_diagnostic(Diagnostic::new(SPATIAL_EMITTERS).with_suffix(" emitters"))
176            .register_diagnostic(Diagnostic::new(SPATIAL_LISTENERS).with_suffix(" listeners"));
177    }
178}