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 new(rx: Receiver<AudioEvent>, sample_rate: f32, seed: u32) -> Self {
160 AudioState {
161 sources: Vec::with_capacity(128),
162 rx,
163 master_volume: 1.0,
164 music_volume: 1.0,
165 music_vibe: MusicVibe::Silence,
166 sample_rate,
167 listener: Vec3::ZERO,
168 time: 0.0,
169 seed,
170 reverb: Reverb::new(0.62, 0.45, 1.0, 0.0, 12.0, 0.8),
173 duck: 0.0,
174 scratch: [0.0],
175 last_send: -10.0,
176 }
177 }
178
179 fn process_events(&mut self) {
180 while let Ok(event) = self.rx.try_recv() {
181 match event {
182 AudioEvent::SpawnSource { source, position } => {
183 let mut src = source;
184 if position != Vec3::ZERO {
185 src.position = position;
186 }
187 self.seed = self.seed.wrapping_mul(1664525).wrapping_add(1013904223);
188 self.sources.push(ActiveSource::new(src, self.seed));
189 }
190 AudioEvent::StopTag(tag) => {
191 for s in &mut self.sources {
192 if s.src.tag.as_deref() == Some(&tag) && s.note_off.is_none() {
193 s.note_off = Some(s.age);
194 }
195 }
196 }
197 AudioEvent::SetMasterVolume(v) => {
198 self.master_volume = v.clamp(0.0, 1.0);
199 }
200 AudioEvent::SetMusicVolume(v) => {
201 self.music_volume = v.clamp(0.0, 1.0);
202 }
203 AudioEvent::PlaySfx { name: _, position, volume } => {
204 use crate::math::MathFunction;
207 let src = MathAudioSource {
208 function: MathFunction::Constant(0.0),
209 frequency_range: (520.0, 520.0),
210 amplitude: volume.clamp(0.0, 1.0) * 0.5,
211 waveform: MsWaveform::Triangle,
212 position,
213 tag: Some("sfx".to_string()),
214 lifetime: 0.14,
215 fade_out: 0.12,
216 pitch_env: (2.5, 0.05),
217 ..Default::default()
218 };
219 self.seed = self.seed.wrapping_mul(1664525).wrapping_add(1013904223);
220 self.sources.push(ActiveSource::new(src, self.seed));
221 }
222 AudioEvent::SetMusicVibe(vibe) => {
223 self.music_vibe = vibe;
224 }
225 }
226 }
227 if self.sources.len() > 96 {
229 self.sources.sort_by(|a, b| b.src.amplitude.total_cmp(&a.src.amplitude));
230 self.sources.truncate(96);
231 }
232 }
233
234 fn next_sample(&mut self) -> (f32, f32) {
236 let dt = 1.0 / self.sample_rate;
237 self.time += dt;
238
239 let mut sfx_l = 0.0f32;
240 let mut sfx_r = 0.0f32;
241 let mut mus_l = 0.0f32;
242 let mut mus_r = 0.0f32;
243 let mut send = 0.0f32;
244 let mut sfx_peak = 0.0f32;
245 let listener = self.listener;
246
247 let mut i = 0;
248 while i < self.sources.len() {
249 let a = &mut self.sources[i];
250 let src = &a.src;
251
252 let t = a.age - src.start_delay;
254 if t < 0.0 {
255 a.age += dt;
256 i += 1;
257 continue;
258 }
259 if src.lifetime >= 0.0 && t >= src.lifetime {
261 self.sources.swap_remove(i);
262 continue;
263 }
264 let release = match a.note_off {
265 Some(off) => {
266 let gone = (a.age - off) / RELEASE_SECS;
267 if gone >= 1.0 {
268 self.sources.swap_remove(i);
269 continue;
270 }
271 1.0 - gone
272 }
273 None => 1.0,
274 };
275
276 let fn_out = src.function.evaluate(t, 0.0);
279 let mut freq = src.map_to_frequency(fn_out);
280 if src.detune_cents != 0.0 {
281 freq *= (2.0f32).powf(src.detune_cents / 1200.0);
282 }
283 let (env_mult, env_secs) = src.pitch_env;
284 if env_secs > 0.0 && env_mult != 1.0 {
285 freq *= 1.0 + (env_mult - 1.0) * (-t / (env_secs * 0.25)).exp();
286 }
287 freq = freq.clamp(1.0, self.sample_rate * 0.45);
288
289 a.phase = (a.phase + freq * dt).fract();
290 let mut raw = oscillator(ms_to_synth_waveform(src.waveform), a.phase);
291
292 let (ratio, mix) = src.partial;
293 if mix > 0.0 && ratio > 0.0 {
294 a.phase2 = (a.phase2 + freq * ratio * dt).fract();
295 raw = raw * (1.0 - mix) + oscillator(SynthWaveform::Sine, a.phase2) * mix;
296 }
297 if src.noise_mix > 0.0 {
298 let n = a.noise.next();
299 raw = raw * (1.0 - src.noise_mix) + n * src.noise_mix;
300 }
301 if let Some(s) = a.stage1.as_mut() {
302 raw = s.tick(raw);
303 }
304 if let Some(s) = a.stage2.as_mut() {
305 raw = s.tick(raw);
306 }
307 if src.drive > 0.0 {
308 let g = 1.0 + src.drive * 4.0;
309 raw = (raw * g).tanh() / g.tanh();
310 }
311
312 let mut env = src.envelope(t) * release;
315 if src.fade_in <= 0.0 && t < DECLICK_SECS {
316 env *= t / DECLICK_SECS;
317 }
318 let sample = raw * env;
319 if !sample.is_finite() {
320 self.sources.swap_remove(i);
321 continue;
322 }
323
324 let (pan_l, pan_r, weight) = if src.spatial && src.position != Vec3::ZERO {
325 let w = spatial_weight(listener, src.position, src.max_distance.max(1.0));
326 let (l, r) = stereo_pan(listener, src.position);
327 (l, r, w)
328 } else {
329 (0.7071, 0.7071, 1.0)
330 };
331 let l = sample * pan_l * weight;
332 let r = sample * pan_r * weight;
333 if a.music {
334 mus_l += l;
335 mus_r += r;
336 } else {
337 sfx_l += l;
338 sfx_r += r;
339 sfx_peak = sfx_peak.max(sample.abs() * weight);
340 }
341 send += sample * weight * src.reverb_send;
342
343 a.age += dt;
344 i += 1;
345 }
346
347 let target = (sfx_peak * 2.0).clamp(0.0, 1.0) * DUCK_DEPTH;
349 if target > self.duck {
350 self.duck = target;
351 } else {
352 self.duck -= (self.duck - target) * (DUCK_RELEASE_PER_SEC * dt).min(1.0);
353 }
354 let music_gain = self.music_volume * (1.0 - self.duck);
355
356 let mut left = sfx_l + mus_l * music_gain;
357 let mut right = sfx_r + mus_r * music_gain;
358
359 if send.abs() > 1e-6 {
360 self.last_send = self.time;
361 }
362 if self.time - self.last_send < 3.0 {
363 self.scratch[0] = send;
364 self.reverb.process_block(&mut self.scratch, self.sample_rate);
365 let wet = self.scratch[0];
366 left += wet;
367 right += wet;
368 }
369
370 let mv = self.master_volume;
371 (soft_limit(left * mv), soft_limit(right * mv))
372 }
373
374}
375
376#[inline]
379fn soft_limit(x: f32) -> f32 {
380 const CEIL: f32 = 0.98;
381 if x.abs() < 0.6 {
382 x
383 } else {
384 let s = x.signum();
385 let e = (x.abs() - 0.6) / (CEIL - 0.6);
386 s * (0.6 + (CEIL - 0.6) * (1.0 - (-e).exp()))
387 }
388}
389
390pub struct AudioOutput {
394 pub sample_rate: u32,
395 pub channels: u16,
396 _stream: Stream,
397}
398
399impl AudioOutput {
400 pub fn try_new(rx: Receiver<AudioEvent>) -> Option<Self> {
403 let host = cpal::default_host();
404 let device = host.default_output_device()?;
405
406 let supported = device.default_output_config().ok()?;
407 let channels = supported.channels();
408 let rate = supported.sample_rate().0;
409
410 let config = StreamConfig {
411 channels,
412 sample_rate: supported.sample_rate(),
413 buffer_size: cpal::BufferSize::Default,
414 };
415
416 let state = AudioState::new(rx, rate as f32, 0x9E37_79B9);
417
418 let stream = match supported.sample_format() {
419 SampleFormat::F32 => build_stream_f32(&device, &config, state),
420 fmt => {
421 log::warn!("AudioOutput: unsupported sample format {:?}, defaulting to f32", fmt);
422 build_stream_f32(&device, &config, state)
423 }
424 }?;
425
426 stream.play().ok()?;
427
428 log::info!("AudioOutput: {} Hz, {} ch", rate, channels);
429 Some(Self { sample_rate: rate, channels, _stream: stream })
430 }
431}
432
433pub struct OfflineRenderer {
456 state: AudioState,
457 tx: std::sync::mpsc::Sender<AudioEvent>,
458 sample_rate: u32,
459}
460
461impl OfflineRenderer {
462 pub fn new(sample_rate: u32) -> Self {
464 Self::with_seed(sample_rate, 0x9E37_79B9)
465 }
466
467 pub fn with_seed(sample_rate: u32, seed: u32) -> Self {
470 let (tx, rx) = std::sync::mpsc::channel();
471 let sample_rate = sample_rate.max(1);
472 Self { state: AudioState::new(rx, sample_rate as f32, seed), tx, sample_rate }
473 }
474
475 pub fn emit(&mut self, event: AudioEvent) {
479 let _ = self.tx.send(event);
480 }
481
482 pub fn render(&mut self, secs: f32) -> Vec<f32> {
485 let frames = (secs.max(0.0) * self.sample_rate as f32).round() as usize;
486 self.render_frames(frames)
487 }
488
489 pub fn render_frames(&mut self, frames: usize) -> Vec<f32> {
491 self.state.process_events();
492 let mut out = Vec::with_capacity(frames * 2);
493 for _ in 0..frames {
494 let (l, r) = self.state.next_sample();
495 out.push(l.clamp(-1.0, 1.0));
496 out.push(r.clamp(-1.0, 1.0));
497 }
498 out
499 }
500
501 pub fn sample_rate(&self) -> u32 {
503 self.sample_rate
504 }
505
506 pub fn time(&self) -> f32 {
508 self.state.time
509 }
510
511 pub fn active_sources(&self) -> usize {
513 self.state.sources.len()
514 }
515}
516
517fn build_stream_f32(
518 device: &cpal::Device,
519 config: &StreamConfig,
520 mut state: AudioState,
521) -> Option<Stream> {
522 let ch = config.channels as usize;
523 let stream = device
524 .build_output_stream(
525 config,
526 move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
527 state.process_events();
528 for frame in data.chunks_mut(ch) {
529 let (l, r) = state.next_sample();
530 frame[0] = l.clamp(-1.0, 1.0);
531 if ch > 1 {
532 frame[1] = r.clamp(-1.0, 1.0);
533 }
534 }
535 },
536 |err| log::error!("AudioOutput stream error: {err}"),
537 None,
538 )
539 .ok()?;
540 Some(stream)
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn the_limiter_is_linear_low_and_never_exceeds_the_ceiling() {
549 assert_eq!(soft_limit(0.3), 0.3);
550 assert_eq!(soft_limit(-0.3), -0.3);
551 for x in [0.7f32, 1.0, 2.0, 10.0, 100.0] {
552 assert!(soft_limit(x) < 0.99, "{x} -> {}", soft_limit(x));
553 assert!(soft_limit(-x) > -0.99);
554 assert!(soft_limit(x) >= soft_limit(x * 0.9), "should never fall");
555 }
556 assert!(soft_limit(1.0) > soft_limit(0.7));
558 assert!(soft_limit(2.0) > soft_limit(1.0));
559 }
560
561 fn offline_state() -> (AudioState, std::sync::mpsc::SyncSender<AudioEvent>) {
563 let (tx, rx) = std::sync::mpsc::sync_channel(64);
564 let state = AudioState::new(rx, 44100.0, 12345);
565 (state, tx)
566 }
567
568 fn render(state: &mut AudioState, secs: f32) -> Vec<(f32, f32)> {
569 state.process_events();
570 (0..(secs * 44100.0) as usize).map(|_| state.next_sample()).collect()
571 }
572
573 #[test]
574 fn a_layered_blow_renders_finite_bounded_and_audible() {
575 use crate::math::MathFunction;
576 let (mut state, tx) = offline_state();
577 let crack = MathAudioSource {
579 function: MathFunction::Constant(0.0),
580 frequency_range: (2600.0, 2600.0),
581 amplitude: 0.5,
582 waveform: MsWaveform::Noise,
583 filter: Some(AudioFilter::HighPass { cutoff_hz: 2600.0, resonance: 0.8 }),
584 lifetime: 0.035,
585 fade_out: 0.02,
586 spatial: false,
587 ..Default::default()
588 };
589 let thud = MathAudioSource {
590 function: MathFunction::Constant(0.0),
591 frequency_range: (170.0, 170.0),
592 amplitude: 0.5,
593 waveform: MsWaveform::Sine,
594 pitch_env: (3.4, 0.07),
595 drive: 0.45,
596 noise_mix: 0.05,
597 filter: Some(AudioFilter::LowPass { cutoff_hz: 700.0, resonance: 0.8 }),
598 lifetime: 0.15,
599 fade_out: 0.12,
600 reverb_send: 0.15,
601 spatial: true,
602 position: Vec3::new(-0.3, 0.0, 0.85),
603 ..Default::default()
604 };
605 let ring = MathAudioSource {
606 function: MathFunction::Constant(0.0),
607 frequency_range: (1900.0, 1900.0),
608 amplitude: 0.2,
609 waveform: MsWaveform::Triangle,
610 partial: (2.76, 0.45),
611 filter: Some(AudioFilter::Comb { delay_ms: 1000.0 / 1900.0, feedback: 0.55 }),
612 lifetime: 0.24,
613 fade_in: 0.002,
614 fade_out: 0.2,
615 start_delay: 0.012,
616 reverb_send: 0.3,
617 spatial: false,
618 ..Default::default()
619 };
620 for s in [crack, thud, ring] {
621 tx.send(AudioEvent::SpawnSource { source: s, position: Vec3::ZERO }).unwrap();
622 }
623 let out = render(&mut state, 0.6);
624 let mut peak = 0.0f32;
625 let mut energy = 0.0f32;
626 for (l, r) in &out {
627 assert!(l.is_finite() && r.is_finite(), "NaN in the output");
628 assert!(l.abs() <= 1.0 && r.abs() <= 1.0, "clipped: {l} {r}");
629 peak = peak.max(l.abs()).max(r.abs());
630 energy += l * l + r * r;
631 }
632 assert!(peak > 0.05, "the blow is inaudible: peak {peak}");
633 assert!(energy > 1.0, "the blow has no body: energy {energy}");
634 let left: f32 = out.iter().map(|(l, _)| l * l).sum();
636 let right: f32 = out.iter().map(|(_, r)| r * r).sum();
637 assert!(left > right, "a left-panned blow should favour the left: {left} vs {right}");
638 let tail: f32 = out[out.len() - 4410..].iter().map(|(l, r)| l.abs().max(r.abs())).fold(0.0, f32::max);
641 assert!(tail < 0.2, "the blow never ends: tail peak {tail}");
642 assert!(state.sources.is_empty(), "sources were not retired");
643 }
644
645 #[test]
646 fn music_ducks_under_effects_and_comes_back() {
647 use crate::math::MathFunction;
648 let (mut state, tx) = offline_state();
649 let music = MathAudioSource {
650 function: MathFunction::Constant(0.0),
651 frequency_range: (220.0, 220.0),
652 amplitude: 0.3,
653 waveform: MsWaveform::Sine,
654 tag: Some("music".to_string()),
655 lifetime: 2.0,
656 spatial: false,
657 ..Default::default()
658 };
659 tx.send(AudioEvent::SpawnSource { source: music, position: Vec3::ZERO }).unwrap();
660 let before = render(&mut state, 0.3);
661 let hit = MathAudioSource {
662 function: MathFunction::Constant(0.0),
663 frequency_range: (100.0, 100.0),
664 amplitude: 0.8,
665 waveform: MsWaveform::Sine,
666 lifetime: 0.1,
667 spatial: false,
668 ..Default::default()
669 };
670 tx.send(AudioEvent::SpawnSource { source: hit, position: Vec3::ZERO }).unwrap();
671 let _during = render(&mut state, 0.12);
672 assert!(state.duck > 0.1, "the hit did not duck the music: {}", state.duck);
673 let _after = render(&mut state, 1.0);
674 assert!(state.duck < 0.05, "the duck never released: {}", state.duck);
675 let rms = |v: &[(f32, f32)]| (v.iter().map(|(l, _)| l * l).sum::<f32>() / v.len() as f32).sqrt();
676 assert!(rms(&before) > 0.1, "music is inaudible");
677 }
678
679 #[test]
680 fn the_offline_renderer_matches_the_real_time_path() {
681 use crate::math::MathFunction;
682 let tone = MathAudioSource {
683 function: MathFunction::Constant(0.0),
684 frequency_range: (440.0, 440.0),
685 amplitude: 0.4,
686 waveform: MsWaveform::Sine,
687 lifetime: 0.2,
688 spatial: false,
689 ..Default::default()
690 };
691 let (mut state, tx) = offline_state();
694 tx.send(AudioEvent::SpawnSource { source: tone.clone(), position: Vec3::ZERO }).unwrap();
695 let reference = render(&mut state, 0.3);
696 let mut off = OfflineRenderer::with_seed(44100, 12345);
697 off.emit(AudioEvent::SpawnSource { source: tone, position: Vec3::ZERO });
698 let out = off.render(0.3);
699 assert_eq!(out.len(), reference.len() * 2);
700 for (i, (l, r)) in reference.iter().enumerate() {
701 assert_eq!((out[2 * i], out[2 * i + 1]), (l.clamp(-1.0, 1.0), r.clamp(-1.0, 1.0)));
702 }
703 assert!((off.time() - 0.3).abs() < 1e-3);
704 assert_eq!(off.active_sources(), 0, "the tone ended");
705 let left: Vec<f32> = out.iter().step_by(2).copied().take(4410).collect();
707 let crossings = left.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
708 assert!((40..=48).contains(&crossings), "{crossings} upward crossings in 0.1 s");
709 }
710
711 #[test]
712 fn noise_is_not_constant() {
713 let mut n = Noise(7);
714 let a = n.next();
715 let b = n.next();
716 assert_ne!(a, b);
717 assert!(a.abs() <= 1.0 && b.abs() <= 1.0);
718 }
719}