1use super::{AudioBus, AudioEffects, Ctx, Playing, Sound, SoundParams};
3use crate::{Entity, Quat, Transform3D, Vec3};
4
5#[derive(Clone, Copy, Debug, Default)]
12pub enum AudioRolloff {
13 #[default]
15 Logarithmic,
16 Linear,
18 Keyed(&'static [(f32, f32)]),
20}
21
22#[derive(Clone, Copy, Debug)]
29pub struct AudioSettings3D {
30 pub sound: SoundParams,
32 pub bus: AudioBus,
38 pub effects: AudioEffects,
45 pub min_distance: f32,
47 pub max_distance: f32,
49 pub rolloff: AudioRolloff,
51 pub doppler: f32,
53}
54impl Default for AudioSettings3D {
55 fn default() -> Self {
56 Self {
57 sound: SoundParams::default(),
58 bus: AudioBus::default(),
59 effects: AudioEffects::default(),
60 min_distance: 1.0,
61 max_distance: 100.0,
62 rolloff: AudioRolloff::default(),
63 doppler: 1.0,
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, Default)]
76pub struct AudioListener3D {
77 pub position: Vec3,
79 pub rotation: Quat,
81}
82
83#[derive(Debug)]
93pub struct AudioSource3D {
94 pub sound: Sound,
96 pub settings: AudioSettings3D,
98 playing: Option<Playing>,
99}
100impl AudioSource3D {
101 pub fn new(sound: Sound, settings: AudioSettings3D) -> Self {
109 Self {
110 sound,
111 settings,
112 playing: None,
113 }
114 }
115}
116
117struct Voice {
118 id: Playing,
119 entity: Option<Entity>,
120 position: Vec3,
121 settings: AudioSettings3D,
122 fresh: bool,
123 remaining: f32,
124}
125#[derive(Default)]
126pub(super) struct SpatialState {
127 listener: Option<AudioListener3D>,
129 custom: Option<AudioListener3D>,
131 voices: Vec<Voice>,
132 next_silent: u64,
133}
134
135impl SpatialState {
136 pub(super) fn remove(&mut self, id: Playing) {
137 self.voices.retain(|voice| voice.id != id);
138 }
139}
140
141fn gain(distance: f32, settings: AudioSettings3D) -> f32 {
142 let min = settings.min_distance.max(0.001);
143 let max = settings.max_distance.max(min + 0.001);
144 if distance >= max {
145 return 0.0;
146 }
147 if distance <= min {
148 return 1.0;
149 }
150 let t = (distance - min) / (max - min);
151 match settings.rolloff {
152 AudioRolloff::Linear => 1.0 - t,
153 AudioRolloff::Logarithmic => min / distance,
154 AudioRolloff::Keyed(keys) => {
155 let Some(&(mut x, mut y)) = keys.first() else {
156 return 0.0;
157 };
158 for &(next_x, next_y) in &keys[1..] {
159 if t <= next_x {
160 return (y + (next_y - y) * ((t - x) / (next_x - x).max(f32::EPSILON)).clamp(0.0, 1.0)).max(0.0);
161 }
162 (x, y) = (next_x, next_y);
163 }
164 y.max(0.0)
165 }
166 }
167}
168fn pan(offset: Vec3, rotation: Quat) -> f32 {
169 offset.normalize_or_zero().dot(rotation * Vec3::X).clamp(-1.0, 1.0)
170}
171fn doppler(direction: Vec3, emitter: Vec3, listener: Vec3, factor: f32) -> f32 {
172 let factor = factor.max(0.0);
173 let radial = |velocity: Vec3| (velocity.dot(direction) * factor).clamp(-300.0, 300.0);
174 ((343.0 + radial(listener)) / (343.0 + radial(emitter))).clamp(0.25, 4.0)
175}
176
177impl Ctx {
178 pub fn play_sound_at(&mut self, sound: &Sound, position: Vec3, settings: AudioSettings3D) -> Playing {
188 let listener = self.audio_listener();
189 let offset = position - listener.position;
190 let mut params = settings.sound;
191 params.volume *= gain(offset.length(), settings);
192 let id = if self.headless {
193 self.audio.spatial.next_silent += 1;
194 Playing(self.audio.spatial.next_silent)
195 } else {
196 self.play_sound_on(sound, params, settings.bus, settings.effects)
197 };
198 self.audio.spatialize(
199 id.0,
200 params.volume,
201 pan(offset, listener.rotation),
202 params.speed.max(0.01),
203 );
204 self.audio.spatial.voices.push(Voice {
205 id,
206 entity: None,
207 position,
208 settings,
209 fresh: true,
210 remaining: sound.seconds(),
211 });
212 id
213 }
214}
215
216impl Ctx {
217 pub fn set_audio_listener(&mut self, listener: Option<AudioListener3D>) {
226 self.audio.spatial.custom = listener;
227 }
228
229 pub fn audio_listener(&self) -> AudioListener3D {
238 self.audio.spatial.custom.unwrap_or(AudioListener3D {
239 position: self.camera3d.position,
240 rotation: self.camera3d.rotation,
241 })
242 }
243}
244
245pub(crate) fn update_audio3d(ctx: &mut Ctx) {
248 let listener = ctx.audio_listener();
249 let dt = ctx.dt();
250 let inverse_dt = if dt > 0.0 { dt.recip() } else { 0.0 };
251 let velocity = ctx
252 .audio
253 .spatial
254 .listener
255 .map_or(Vec3::ZERO, |old| (listener.position - old.position) * inverse_dt);
256 ctx.audio.spatial.listener = Some(listener);
257 for (entity, transform, source) in ctx.world.query::<(Entity, &Transform3D, &mut AudioSource3D)>().iter() {
259 if source.playing.is_none() {
260 let offset = transform.position - listener.position;
261 let mut params = source.settings.sound;
262 params.volume *= gain(offset.length(), source.settings);
263 let id = Playing(if ctx.headless {
264 ctx.audio.spatial.next_silent += 1;
265 ctx.audio.spatial.next_silent
266 } else {
267 ctx.audio
268 .play_on(source.sound.id, params, source.settings.bus, source.settings.effects)
269 });
270 source.playing = Some(id);
271 ctx.audio.spatial.voices.push(Voice {
272 id,
273 entity: Some(entity),
274 position: transform.position,
275 settings: source.settings,
276 fresh: true,
277 remaining: source.sound.seconds(),
278 });
279 }
280 }
281 let mut i = 0;
282 while i < ctx.audio.spatial.voices.len() {
283 let voice = &mut ctx.audio.spatial.voices[i];
284 let mut position = voice.position;
285 let mut changed_effects = None;
286 if let Some(entity) = voice.entity {
287 match (
288 ctx.world.get::<Transform3D>(entity),
289 ctx.world.get::<AudioSource3D>(entity),
290 ) {
291 (Some(transform), Some(source)) if source.playing == Some(voice.id) => {
292 position = transform.position;
293 let looping = voice.settings.sound.looping;
294 if source.settings.effects != voice.settings.effects {
295 changed_effects = Some(source.settings.effects);
296 }
297 voice.settings = source.settings;
298 voice.settings.sound.looping = looping;
299 }
300 _ => {
301 let id = voice.id;
302 ctx.audio.stop(id.0);
303 ctx.audio.spatial.voices.swap_remove(i);
304 continue;
305 }
306 }
307 }
308 let emitter_velocity = if voice.fresh {
309 Vec3::ZERO
310 } else {
311 (position - voice.position) * inverse_dt
312 };
313 let offset = position - listener.position;
314 let settings = voice.settings;
315 let speed = settings.sound.speed.max(0.01)
316 * doppler(offset.normalize_or_zero(), emitter_velocity, velocity, settings.doppler);
317 voice.remaining -= dt.max(0.0) * speed;
318 voice.position = position;
319 voice.fresh = false;
320 let (id, volume, panning) = (
321 voice.id,
322 settings.sound.volume * gain(offset.length(), settings),
323 pan(offset, listener.rotation),
324 );
325 let finished = !settings.sound.looping && voice.remaining <= 0.0;
326 if let Some(effects) = changed_effects {
327 ctx.audio.effects(id.0, effects);
330 }
331 ctx.audio.spatialize(id.0, volume, panning, speed);
332 if finished {
333 ctx.audio.spatial.voices.swap_remove(i);
334 } else {
335 i += 1;
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 #[test]
344 fn audio_distance_curves_and_boundaries() {
345 let mut settings = AudioSettings3D {
346 min_distance: 1.0,
347 max_distance: 11.0,
348 rolloff: AudioRolloff::Linear,
349 ..Default::default()
350 };
351 assert_eq!(gain(0.0, settings), 1.0);
352 assert_eq!(gain(6.0, settings), 0.5);
353 assert_eq!(gain(11.0, settings), 0.0);
354 settings.rolloff = AudioRolloff::Logarithmic;
355 assert_eq!(gain(2.0, settings), 0.5);
356 settings.rolloff = AudioRolloff::Keyed(&[(0.0, 1.0), (1.0, 0.2)]);
357 assert!((gain(6.0, settings) - 0.6).abs() < 1e-6);
358 }
359 #[test]
360 fn audio_pan_and_doppler_follow_listener_orientation_and_velocity() {
361 assert_eq!(pan(Vec3::X, Quat::IDENTITY), 1.0);
362 assert_eq!(pan(-Vec3::X, Quat::IDENTITY), -1.0);
363 assert_eq!(pan(Vec3::ZERO, Quat::IDENTITY), 0.0);
364 assert!(doppler(Vec3::X, -Vec3::X * 20.0, Vec3::ZERO, 1.0) > 1.0);
365 assert!(doppler(Vec3::X, Vec3::X * 20.0, Vec3::ZERO, 1.0) < 1.0);
366 assert_eq!(doppler(Vec3::X, Vec3::X, Vec3::ZERO, 0.0), 1.0);
367 }
368 #[test]
369 fn audio_headless_sources_start_update_and_cleanup_without_device() {
370 crate::Game::new("audio test", 1, 1)
371 .system(|ctx| {
372 update_audio3d(ctx);
373 assert!(ctx.world.query::<&AudioSource3D>().iter().all(|s| s.playing.is_some()));
374 })
375 .run_headless(2, |ctx| {
376 let sound = ctx.create_sound(100, &[0.0; 100]);
377 let entity = ctx.world.spawn((
378 Transform3D::default(),
379 AudioSource3D::new(sound, AudioSettings3D::default()),
380 ));
381 update_audio3d(ctx);
382 assert_eq!(ctx.audio.spatial.voices.len(), 1);
383 ctx.world.get_mut::<Transform3D>(entity).unwrap().position = Vec3::X * 5.0;
384 update_audio3d(ctx);
385 assert_eq!(ctx.audio.spatial.voices[0].position, Vec3::X * 5.0);
386 ctx.world.despawn(entity);
387 update_audio3d(ctx);
388 assert!(ctx.audio.spatial.voices.is_empty());
389 });
390 }
391}