1use std::sync::mpsc::Receiver;
21
22use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
23use cpal::{SampleFormat, Stream, StreamConfig};
24use glam::Vec3;
25
26use crate::audio::{AudioEvent, MusicVibe};
27use crate::audio::effects::{AudioEffect, Reverb};
28use crate::audio::math_source::{AudioFilter, MathAudioSource, Waveform as MsWaveform};
29use crate::audio::mixer::{spatial_weight, stereo_pan};
30use crate::audio::synth::{oscillator, BiquadFilter, DelayLine, Waveform as SynthWaveform};
31
32fn ms_to_synth_waveform(w: MsWaveform) -> SynthWaveform {
33 match w {
34 MsWaveform::Sine => SynthWaveform::Sine,
35 MsWaveform::Triangle => SynthWaveform::Triangle,
36 MsWaveform::Square => SynthWaveform::Square,
37 MsWaveform::Sawtooth => SynthWaveform::Sawtooth,
38 MsWaveform::ReverseSaw => SynthWaveform::ReverseSaw,
39 MsWaveform::Pulse(d) => SynthWaveform::Pulse(d),
40 MsWaveform::Noise => SynthWaveform::Noise,
41 }
42}
43
44enum Stage {
46 Biquad(BiquadFilter),
47 Comb { delay: DelayLine, feedback: f32, last: f32 },
49}
50
51impl Stage {
52 fn from_filter(f: &AudioFilter) -> Stage {
53 match *f {
54 AudioFilter::LowPass { cutoff_hz, resonance } =>
55 Stage::Biquad(BiquadFilter::low_pass(cutoff_hz.max(20.0), resonance.max(0.5))),
56 AudioFilter::HighPass { cutoff_hz, resonance } =>
57 Stage::Biquad(BiquadFilter::high_pass(cutoff_hz.max(20.0), resonance.max(0.5))),
58 AudioFilter::BandPass { center_hz, bandwidth } =>
59 Stage::Biquad(BiquadFilter::band_pass(center_hz.max(20.0), (center_hz / bandwidth.max(1.0)).clamp(0.3, 20.0))),
60 AudioFilter::Notch { center_hz, bandwidth } =>
61 Stage::Biquad(BiquadFilter::notch(center_hz.max(20.0), (center_hz / bandwidth.max(1.0)).clamp(0.3, 20.0))),
62 AudioFilter::Formant { f1_hz, .. } =>
63 Stage::Biquad(BiquadFilter::band_pass(f1_hz.max(20.0), 4.0)),
64 AudioFilter::Comb { delay_ms, feedback } => {
65 let mut delay = DelayLine::new(delay_ms.max(0.2) + 1.0);
66 delay.set_delay_ms(delay_ms.max(0.2));
67 Stage::Comb { delay, feedback: feedback.clamp(-0.98, 0.98), last: 0.0 }
68 }
69 }
70 }
71
72 fn tick(&mut self, x: f32) -> f32 {
73 match self {
74 Stage::Biquad(b) => b.tick(x),
75 Stage::Comb { delay, feedback, last } => {
76 let y = x + *feedback * *last;
77 *last = delay.tick(y);
78 y
79 }
80 }
81 }
82}
83
84struct Noise(u32);
87
88impl Noise {
89 fn next(&mut self) -> f32 {
90 self.0 ^= self.0 << 13;
91 self.0 ^= self.0 >> 17;
92 self.0 ^= self.0 << 5;
93 (self.0 as f32 / u32::MAX as f32) * 2.0 - 1.0
94 }
95}
96
97struct ActiveSource {
99 src: MathAudioSource,
100 phase: f32,
101 phase2: f32,
102 age: f32,
103 note_off: Option<f32>,
104 stage1: Option<Stage>,
105 stage2: Option<Stage>,
106 noise: Noise,
107 music: bool,
108}
109
110impl ActiveSource {
111 fn new(src: MathAudioSource, seed: u32) -> Self {
112 let stage1 = src.filter.as_ref().map(Stage::from_filter);
113 let stage2 = src.filter2.as_ref().map(Stage::from_filter);
114 let music = src.tag.as_deref() == Some("music");
115 Self {
116 src,
117 phase: 0.0,
118 phase2: 0.0,
119 age: 0.0,
120 note_off: None,
121 stage1,
122 stage2,
123 noise: Noise(seed | 1),
124 music,
125 }
126 }
127}
128
129const RELEASE_SECS: f32 = 0.25;
131const DECLICK_SECS: f32 = 0.003;
133const DUCK_DEPTH: f32 = 0.45;
135const DUCK_RELEASE_PER_SEC: f32 = 4.0;
136
137struct AudioState {
139 sources: Vec<ActiveSource>,
140 rx: Receiver<AudioEvent>,
141 master_volume: f32,
142 music_volume: f32,
143 #[allow(dead_code)]
144 music_vibe: MusicVibe,
145 sample_rate: f32,
146 listener: Vec3,
147 time: f32,
148 seed: u32,
149 reverb: Reverb,
150 duck: f32,
152 scratch: [f32; 1],
154 last_send: f32,
156}
157
158impl AudioState {
159 fn process_events(&mut self) {
160 while let Ok(event) = self.rx.try_recv() {
161 match event {
162 AudioEvent::SpawnSource { source, position } => {
163 let mut src = source;
164 if position != Vec3::ZERO {
165 src.position = position;
166 }
167 self.seed = self.seed.wrapping_mul(1664525).wrapping_add(1013904223);
168 self.sources.push(ActiveSource::new(src, self.seed));
169 }
170 AudioEvent::StopTag(tag) => {
171 for s in &mut self.sources {
172 if s.src.tag.as_deref() == Some(&tag) && s.note_off.is_none() {
173 s.note_off = Some(s.age);
174 }
175 }
176 }
177 AudioEvent::SetMasterVolume(v) => {
178 self.master_volume = v.clamp(0.0, 1.0);
179 }
180 AudioEvent::SetMusicVolume(v) => {
181 self.music_volume = v.clamp(0.0, 1.0);
182 }
183 AudioEvent::PlaySfx { name: _, position, volume } => {
184 use crate::math::MathFunction;
187 let src = MathAudioSource {
188 function: MathFunction::Constant(0.0),
189 frequency_range: (520.0, 520.0),
190 amplitude: volume.clamp(0.0, 1.0) * 0.5,
191 waveform: MsWaveform::Triangle,
192 position,
193 tag: Some("sfx".to_string()),
194 lifetime: 0.14,
195 fade_out: 0.12,
196 pitch_env: (2.5, 0.05),
197 ..Default::default()
198 };
199 self.seed = self.seed.wrapping_mul(1664525).wrapping_add(1013904223);
200 self.sources.push(ActiveSource::new(src, self.seed));
201 }
202 AudioEvent::SetMusicVibe(vibe) => {
203 self.music_vibe = vibe;
204 }
205 }
206 }
207 if self.sources.len() > 96 {
209 self.sources.sort_by(|a, b| b.src.amplitude.total_cmp(&a.src.amplitude));
210 self.sources.truncate(96);
211 }
212 }
213
214 fn next_sample(&mut self) -> (f32, f32) {
216 let dt = 1.0 / self.sample_rate;
217 self.time += dt;
218
219 let mut sfx_l = 0.0f32;
220 let mut sfx_r = 0.0f32;
221 let mut mus_l = 0.0f32;
222 let mut mus_r = 0.0f32;
223 let mut send = 0.0f32;
224 let mut sfx_peak = 0.0f32;
225 let listener = self.listener;
226
227 let mut i = 0;
228 while i < self.sources.len() {
229 let a = &mut self.sources[i];
230 let src = &a.src;
231
232 let t = a.age - src.start_delay;
234 if t < 0.0 {
235 a.age += dt;
236 i += 1;
237 continue;
238 }
239 if src.lifetime >= 0.0 && t >= src.lifetime {
241 self.sources.swap_remove(i);
242 continue;
243 }
244 let release = match a.note_off {
245 Some(off) => {
246 let gone = (a.age - off) / RELEASE_SECS;
247 if gone >= 1.0 {
248 self.sources.swap_remove(i);
249 continue;
250 }
251 1.0 - gone
252 }
253 None => 1.0,
254 };
255
256 let fn_out = src.function.evaluate(t, 0.0);
259 let mut freq = src.map_to_frequency(fn_out);
260 if src.detune_cents != 0.0 {
261 freq *= (2.0f32).powf(src.detune_cents / 1200.0);
262 }
263 let (env_mult, env_secs) = src.pitch_env;
264 if env_secs > 0.0 && env_mult != 1.0 {
265 freq *= 1.0 + (env_mult - 1.0) * (-t / (env_secs * 0.25)).exp();
266 }
267 freq = freq.clamp(1.0, self.sample_rate * 0.45);
268
269 a.phase = (a.phase + freq * dt).fract();
270 let mut raw = oscillator(ms_to_synth_waveform(src.waveform), a.phase);
271
272 let (ratio, mix) = src.partial;
273 if mix > 0.0 && ratio > 0.0 {
274 a.phase2 = (a.phase2 + freq * ratio * dt).fract();
275 raw = raw * (1.0 - mix) + oscillator(SynthWaveform::Sine, a.phase2) * mix;
276 }
277 if src.noise_mix > 0.0 {
278 let n = a.noise.next();
279 raw = raw * (1.0 - src.noise_mix) + n * src.noise_mix;
280 }
281 if let Some(s) = a.stage1.as_mut() {
282 raw = s.tick(raw);
283 }
284 if let Some(s) = a.stage2.as_mut() {
285 raw = s.tick(raw);
286 }
287 if src.drive > 0.0 {
288 let g = 1.0 + src.drive * 4.0;
289 raw = (raw * g).tanh() / g.tanh();
290 }
291
292 let mut env = src.envelope(t) * release;
295 if src.fade_in <= 0.0 && t < DECLICK_SECS {
296 env *= t / DECLICK_SECS;
297 }
298 let sample = raw * env;
299 if !sample.is_finite() {
300 self.sources.swap_remove(i);
301 continue;
302 }
303
304 let (pan_l, pan_r, weight) = if src.spatial && src.position != Vec3::ZERO {
305 let w = spatial_weight(listener, src.position, src.max_distance.max(1.0));
306 let (l, r) = stereo_pan(listener, src.position);
307 (l, r, w)
308 } else {
309 (0.7071, 0.7071, 1.0)
310 };
311 let l = sample * pan_l * weight;
312 let r = sample * pan_r * weight;
313 if a.music {
314 mus_l += l;
315 mus_r += r;
316 } else {
317 sfx_l += l;
318 sfx_r += r;
319 sfx_peak = sfx_peak.max(sample.abs() * weight);
320 }
321 send += sample * weight * src.reverb_send;
322
323 a.age += dt;
324 i += 1;
325 }
326
327 let target = (sfx_peak * 2.0).clamp(0.0, 1.0) * DUCK_DEPTH;
329 if target > self.duck {
330 self.duck = target;
331 } else {
332 self.duck -= (self.duck - target) * (DUCK_RELEASE_PER_SEC * dt).min(1.0);
333 }
334 let music_gain = self.music_volume * (1.0 - self.duck);
335
336 let mut left = sfx_l + mus_l * music_gain;
337 let mut right = sfx_r + mus_r * music_gain;
338
339 if send.abs() > 1e-6 {
340 self.last_send = self.time;
341 }
342 if self.time - self.last_send < 3.0 {
343 self.scratch[0] = send;
344 self.reverb.process_block(&mut self.scratch, self.sample_rate);
345 let wet = self.scratch[0];
346 left += wet;
347 right += wet;
348 }
349
350 let mv = self.master_volume;
351 (soft_limit(left * mv), soft_limit(right * mv))
352 }
353
354}
355
356#[inline]
359fn soft_limit(x: f32) -> f32 {
360 const CEIL: f32 = 0.98;
361 if x.abs() < 0.6 {
362 x
363 } else {
364 let s = x.signum();
365 let e = (x.abs() - 0.6) / (CEIL - 0.6);
366 s * (0.6 + (CEIL - 0.6) * (1.0 - (-e).exp()))
367 }
368}
369
370pub struct AudioOutput {
374 pub sample_rate: u32,
375 pub channels: u16,
376 _stream: Stream,
377}
378
379impl AudioOutput {
380 pub fn try_new(rx: Receiver<AudioEvent>) -> Option<Self> {
383 let host = cpal::default_host();
384 let device = host.default_output_device()?;
385
386 let supported = device.default_output_config().ok()?;
387 let channels = supported.channels();
388 let rate = supported.sample_rate().0;
389
390 let config = StreamConfig {
391 channels,
392 sample_rate: supported.sample_rate(),
393 buffer_size: cpal::BufferSize::Default,
394 };
395
396 let state = AudioState {
397 sources: Vec::with_capacity(128),
398 rx,
399 master_volume: 1.0,
400 music_volume: 1.0,
401 music_vibe: MusicVibe::Silence,
402 sample_rate: rate as f32,
403 listener: Vec3::ZERO,
404 time: 0.0,
405 seed: 0x9E37_79B9,
406 reverb: Reverb::new(0.62, 0.45, 1.0, 0.0, 12.0, 0.8),
409 duck: 0.0,
410 scratch: [0.0],
411 last_send: -10.0,
412 };
413
414 let stream = match supported.sample_format() {
415 SampleFormat::F32 => build_stream_f32(&device, &config, state),
416 fmt => {
417 log::warn!("AudioOutput: unsupported sample format {:?}, defaulting to f32", fmt);
418 build_stream_f32(&device, &config, state)
419 }
420 }?;
421
422 stream.play().ok()?;
423
424 log::info!("AudioOutput: {} Hz, {} ch", rate, channels);
425 Some(Self { sample_rate: rate, channels, _stream: stream })
426 }
427}
428
429fn build_stream_f32(
430 device: &cpal::Device,
431 config: &StreamConfig,
432 mut state: AudioState,
433) -> Option<Stream> {
434 let ch = config.channels as usize;
435 let stream = device
436 .build_output_stream(
437 config,
438 move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
439 state.process_events();
440 for frame in data.chunks_mut(ch) {
441 let (l, r) = state.next_sample();
442 frame[0] = l.clamp(-1.0, 1.0);
443 if ch > 1 {
444 frame[1] = r.clamp(-1.0, 1.0);
445 }
446 }
447 },
448 |err| log::error!("AudioOutput stream error: {err}"),
449 None,
450 )
451 .ok()?;
452 Some(stream)
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn the_limiter_is_linear_low_and_never_exceeds_the_ceiling() {
461 assert_eq!(soft_limit(0.3), 0.3);
462 assert_eq!(soft_limit(-0.3), -0.3);
463 for x in [0.7f32, 1.0, 2.0, 10.0, 100.0] {
464 assert!(soft_limit(x) < 0.99, "{x} -> {}", soft_limit(x));
465 assert!(soft_limit(-x) > -0.99);
466 assert!(soft_limit(x) >= soft_limit(x * 0.9), "should never fall");
467 }
468 assert!(soft_limit(1.0) > soft_limit(0.7));
470 assert!(soft_limit(2.0) > soft_limit(1.0));
471 }
472
473 fn offline_state() -> (AudioState, std::sync::mpsc::SyncSender<AudioEvent>) {
475 let (tx, rx) = std::sync::mpsc::sync_channel(64);
476 let state = AudioState {
477 sources: Vec::new(),
478 rx,
479 master_volume: 1.0,
480 music_volume: 1.0,
481 music_vibe: MusicVibe::Silence,
482 sample_rate: 44100.0,
483 listener: Vec3::ZERO,
484 time: 0.0,
485 seed: 12345,
486 reverb: Reverb::new(0.62, 0.45, 1.0, 0.0, 12.0, 0.8),
487 duck: 0.0,
488 scratch: [0.0],
489 last_send: -10.0,
490 };
491 (state, tx)
492 }
493
494 fn render(state: &mut AudioState, secs: f32) -> Vec<(f32, f32)> {
495 state.process_events();
496 (0..(secs * 44100.0) as usize).map(|_| state.next_sample()).collect()
497 }
498
499 #[test]
500 fn a_layered_blow_renders_finite_bounded_and_audible() {
501 use crate::math::MathFunction;
502 let (mut state, tx) = offline_state();
503 let crack = MathAudioSource {
505 function: MathFunction::Constant(0.0),
506 frequency_range: (2600.0, 2600.0),
507 amplitude: 0.5,
508 waveform: MsWaveform::Noise,
509 filter: Some(AudioFilter::HighPass { cutoff_hz: 2600.0, resonance: 0.8 }),
510 lifetime: 0.035,
511 fade_out: 0.02,
512 spatial: false,
513 ..Default::default()
514 };
515 let thud = MathAudioSource {
516 function: MathFunction::Constant(0.0),
517 frequency_range: (170.0, 170.0),
518 amplitude: 0.5,
519 waveform: MsWaveform::Sine,
520 pitch_env: (3.4, 0.07),
521 drive: 0.45,
522 noise_mix: 0.05,
523 filter: Some(AudioFilter::LowPass { cutoff_hz: 700.0, resonance: 0.8 }),
524 lifetime: 0.15,
525 fade_out: 0.12,
526 reverb_send: 0.15,
527 spatial: true,
528 position: Vec3::new(-0.3, 0.0, 0.85),
529 ..Default::default()
530 };
531 let ring = MathAudioSource {
532 function: MathFunction::Constant(0.0),
533 frequency_range: (1900.0, 1900.0),
534 amplitude: 0.2,
535 waveform: MsWaveform::Triangle,
536 partial: (2.76, 0.45),
537 filter: Some(AudioFilter::Comb { delay_ms: 1000.0 / 1900.0, feedback: 0.55 }),
538 lifetime: 0.24,
539 fade_in: 0.002,
540 fade_out: 0.2,
541 start_delay: 0.012,
542 reverb_send: 0.3,
543 spatial: false,
544 ..Default::default()
545 };
546 for s in [crack, thud, ring] {
547 tx.send(AudioEvent::SpawnSource { source: s, position: Vec3::ZERO }).unwrap();
548 }
549 let out = render(&mut state, 0.6);
550 let mut peak = 0.0f32;
551 let mut energy = 0.0f32;
552 for (l, r) in &out {
553 assert!(l.is_finite() && r.is_finite(), "NaN in the output");
554 assert!(l.abs() <= 1.0 && r.abs() <= 1.0, "clipped: {l} {r}");
555 peak = peak.max(l.abs()).max(r.abs());
556 energy += l * l + r * r;
557 }
558 assert!(peak > 0.05, "the blow is inaudible: peak {peak}");
559 assert!(energy > 1.0, "the blow has no body: energy {energy}");
560 let left: f32 = out.iter().map(|(l, _)| l * l).sum();
562 let right: f32 = out.iter().map(|(_, r)| r * r).sum();
563 assert!(left > right, "a left-panned blow should favour the left: {left} vs {right}");
564 let tail: f32 = out[out.len() - 4410..].iter().map(|(l, r)| l.abs().max(r.abs())).fold(0.0, f32::max);
567 assert!(tail < 0.2, "the blow never ends: tail peak {tail}");
568 assert!(state.sources.is_empty(), "sources were not retired");
569 }
570
571 #[test]
572 fn music_ducks_under_effects_and_comes_back() {
573 use crate::math::MathFunction;
574 let (mut state, tx) = offline_state();
575 let music = MathAudioSource {
576 function: MathFunction::Constant(0.0),
577 frequency_range: (220.0, 220.0),
578 amplitude: 0.3,
579 waveform: MsWaveform::Sine,
580 tag: Some("music".to_string()),
581 lifetime: 2.0,
582 spatial: false,
583 ..Default::default()
584 };
585 tx.send(AudioEvent::SpawnSource { source: music, position: Vec3::ZERO }).unwrap();
586 let before = render(&mut state, 0.3);
587 let hit = MathAudioSource {
588 function: MathFunction::Constant(0.0),
589 frequency_range: (100.0, 100.0),
590 amplitude: 0.8,
591 waveform: MsWaveform::Sine,
592 lifetime: 0.1,
593 spatial: false,
594 ..Default::default()
595 };
596 tx.send(AudioEvent::SpawnSource { source: hit, position: Vec3::ZERO }).unwrap();
597 let _during = render(&mut state, 0.12);
598 assert!(state.duck > 0.1, "the hit did not duck the music: {}", state.duck);
599 let _after = render(&mut state, 1.0);
600 assert!(state.duck < 0.05, "the duck never released: {}", state.duck);
601 let rms = |v: &[(f32, f32)]| (v.iter().map(|(l, _)| l * l).sum::<f32>() / v.len() as f32).sqrt();
602 assert!(rms(&before) > 0.1, "music is inaudible");
603 }
604
605 #[test]
606 fn noise_is_not_constant() {
607 let mut n = Noise(7);
608 let a = n.next();
609 let b = n.next();
610 assert_ne!(a, b);
611 assert!(a.abs() <= 1.0 && b.abs() <= 1.0);
612 }
613}