1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Support for spatial audio through `kira`'s spatial features.
use bevy::diagnostic::{Diagnostic, DiagnosticPath, RegisterDiagnostic};
use bevy::prelude::*;

use kira::spatial::emitter::{EmitterDistances, EmitterHandle, EmitterSettings};
use kira::spatial::listener::ListenerHandle;
use kira::spatial::scene::{SpatialSceneHandle, SpatialSceneSettings};
use kira::tween::{Easing, Tween};

use crate::{AudioPlaybackSet, AudioSourceSetup, AudioWorld, InternalAudioMarker};

#[doc(hidden)]
#[allow(missing_docs)]
pub mod prelude {
    pub use super::{AudioListener, SpatialEmitter, SpatialWorld};
}

/// Spatial audio plugin. This is an internal plugin, useful for some separation of concerns.
///
/// It is automatically added by the main [`AudioPlugin`].
pub(crate) struct SpatialAudioPlugin;

impl Plugin for SpatialAudioPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<SpatialWorld>()
            .add_plugins(SpatialDiagnosticsPlugin)
            .add_systems(
                PreUpdate,
                (add_listeners, add_emitters)
                    .in_set(AudioPlaybackSet::Setup)
                    .before(AudioSourceSetup),
            )
            .add_systems(
                PostUpdate,
                (update_listeners, update_emitters).in_set(AudioPlaybackSet::Update),
            );
    }
}

/// Marker component setting this entity as an audio listener. It must have a [`GlobalTransform`]
/// attached for the spatial systems to pick it up.
#[derive(Component)]
pub struct AudioListener;

/// Internal handle to a Kira listener. Used to update the audio listener position.
#[derive(Component)]
pub(crate) struct SpatialListenerHandle(ListenerHandle);

/// Marker component setting this entity as a spatial emitter. It must have a [`GlobalTransform`]
/// attached for the spatial systems to pick it up.
///
/// Note that these settings are only used in the setup of the spatial emitter, and not kept in
/// sync afterwards.
#[derive(Component)]
pub struct SpatialEmitter {
    /// Function describing the attenuation in volume depending on the distance of this emitter
    /// to the listener.
    pub attenuation: Option<Easing>,
    /// Enables the panning effect that depends on the orientation of the listener.
    pub enable_spatialization: bool,
    /// Range of distances describing the distance at which the sound will be playing at full
    /// volume, and the maximum distance at which the sound will be able to be heard.
    pub distances: EmitterDistances,
}

impl Default for SpatialEmitter {
    fn default() -> Self {
        Self {
            attenuation: Some(Easing::OutPowi(2)),
            enable_spatialization: true,
            distances: EmitterDistances::default(),
        }
    }
}

/// Internal Kira handle emitter. Used to update the spatial emitter position.
#[derive(Component)]
pub(crate) struct SpatialEmitterHandle(pub(crate) EmitterHandle);

/// Global data related to spatial handling in the audio engine.
#[derive(Resource)]
pub struct SpatialWorld {
    pub(crate) spatial_handle: SpatialSceneHandle,
}

impl FromWorld for SpatialWorld {
    fn from_world(world: &mut World) -> Self {
        let settings = world
            .remove_non_send_resource::<SpatialSceneSettings>()
            .unwrap_or_default();
        let mut audio_world = world.resource_mut::<AudioWorld>();
        let spatial_handle = audio_world
            .audio_manager
            .add_spatial_scene(settings)
            .expect("Cannot create audio spatial world");
        Self { spatial_handle }
    }
}

fn add_listeners(
    mut commands: Commands,
    mut spatial_world: ResMut<SpatialWorld>,
    q: Query<(Entity, &GlobalTransform), Added<AudioListener>>,
) {
    for (entity, global_transform) in &q {
        let (_, quat, position) = global_transform.to_scale_rotation_translation();
        let listener = spatial_world
            .spatial_handle
            .add_listener(position, quat, default())
            .unwrap();
        debug!("Add listener to {entity:?}");
        commands
            .entity(entity)
            .insert(SpatialListenerHandle(listener));
    }
}

fn add_emitters(
    mut commands: Commands,
    mut spatial_world: ResMut<SpatialWorld>,
    q: Query<(Entity, &GlobalTransform, &SpatialEmitter), Added<InternalAudioMarker>>,
) {
    for (entity, global_transform, spatial_emitter) in &q {
        let result = spatial_world.spatial_handle.add_emitter(
            global_transform.translation(),
            EmitterSettings::default()
                .attenuation_function(spatial_emitter.attenuation)
                .enable_spatialization(spatial_emitter.enable_spatialization)
                .distances(spatial_emitter.distances)
                .persist_until_sounds_finish(true),
        );
        debug!("Add emitter to {entity:?}");
        match result {
            Ok(emitter) => {
                commands
                    .entity(entity)
                    .insert(SpatialEmitterHandle(emitter));
            }
            Err(err) => {
                error!("Cannot create spatial audio emitter for entity {entity:?}: {err}");
            }
        }
    }
}

fn update_listeners(mut q: Query<(&mut SpatialListenerHandle, &GlobalTransform)>) {
    for (mut listener, global_transform) in &mut q {
        let (_, quat, position) = global_transform.to_scale_rotation_translation();
        listener.0.set_position(position, Tween::default());
        listener.0.set_orientation(quat, Tween::default());
    }
}

fn update_emitters(mut q: Query<(&mut SpatialEmitterHandle, &GlobalTransform)>) {
    for (mut emitter, global_transform) in &mut q {
        let position = global_transform.translation();
        emitter.0.set_position(position, Tween::default());
    }
}

/// Bevy diagnostic path recording the number of emitters present.
pub const SPATIAL_EMITTERS: DiagnosticPath = DiagnosticPath::const_new("kira::spatial::emitters");
/// Bevy diagnostic path recording the number of listeners present.
pub const SPATIAL_LISTENERS: DiagnosticPath = DiagnosticPath::const_new("kira::spatial::listeners");

struct SpatialDiagnosticsPlugin;

impl Plugin for SpatialDiagnosticsPlugin {
    fn build(&self, app: &mut App) {
        app.register_diagnostic(Diagnostic::new(SPATIAL_EMITTERS).with_suffix(" emitters"))
            .register_diagnostic(Diagnostic::new(SPATIAL_LISTENERS).with_suffix(" listeners"));
    }
}