Skip to main content

audio_code/audio/
mixer.rs

1//! The real-time mixer running on the audio thread.
2//!
3//! The game thread never touches mixer state directly. It sends [`Command`]s through a
4//! channel which the mixer drains at the start of every audio block. Per-voice status flows
5//! back through atomics in [`VoiceShared`].
6
7use std::f32::consts::FRAC_PI_4;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::mpsc::Receiver;
11
12use glam::Vec3;
13
14use super::data::SoundSlot;
15use crate::math;
16
17/// Upper limit of simultaneously mixed voices. Older voices are dropped beyond this.
18const MAX_VOICES: usize = 512;
19
20/// Volume group a voice belongs to. Each group has its own volume control.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Group {
23    /// Sound effects (the default).
24    Sound,
25    /// Background music.
26    Music,
27    /// Menu and interface sounds.
28    Ui,
29}
30
31impl Group {
32    fn index(self) -> usize {
33        match self {
34            Group::Sound => 0,
35            Group::Music => 1,
36            Group::Ui => 2,
37        }
38    }
39}
40
41/// Distance model of a positioned sound.
42#[derive(Debug, Clone, Copy)]
43pub(crate) struct Spatial {
44    pub position: Vec3,
45    pub min_distance: f32,
46    pub max_distance: f32,
47    pub rolloff: f32,
48}
49
50/// Status shared between a voice in the mixer and its [`Sound`](super::Sound) handles.
51#[derive(Debug, Default)]
52pub(crate) struct VoiceShared {
53    pub finished: AtomicBool,
54    /// Playback position in seconds, stored as `f64` bits.
55    pub position: AtomicU64,
56}
57
58pub(crate) enum Change {
59    Volume { target: f32, fade: f32 },
60    Stop { fade: f32 },
61    Pitch(f32),
62    Pan(f32),
63    Position(Vec3),
64    Paused(bool),
65    Looping(bool),
66    Distance { min: f32, max: f32 },
67    Seek(f32),
68}
69
70pub(crate) enum Command {
71    Play(Box<Voice>),
72    Change(u64, Change),
73    StopAll { group: Option<Group>, fade: f32 },
74    Listener { position: Vec3, yaw: f32, pitch: f32 },
75    GroupVolume(Group, f32),
76    MasterVolume(f32),
77}
78
79pub(crate) struct Voice {
80    pub id: u64,
81    pub slot: Arc<SoundSlot>,
82    pub shared: Arc<VoiceShared>,
83    pub group: Group,
84    pub looping: bool,
85    pub paused: bool,
86    pub pitch: f32,
87    pub pan: f32,
88    pub spatial: Option<Spatial>,
89    /// Current volume and where it is fading to.
90    pub volume: f32,
91    pub target_volume: f32,
92    pub fade_step: f32,
93    pub stop_after_fade: bool,
94
95    pos: f64,
96    gains: [f32; 2],
97    lowpass: f32,
98    lp_state: [f32; 2],
99    fresh: bool,
100}
101
102impl Voice {
103    pub(crate) fn new(id: u64, slot: Arc<SoundSlot>, shared: Arc<VoiceShared>, group: Group) -> Self {
104        Self {
105            id,
106            slot,
107            shared,
108            group,
109            looping: false,
110            paused: false,
111            pitch: 1.0,
112            pan: 0.0,
113            spatial: None,
114            volume: 1.0,
115            target_volume: 1.0,
116            fade_step: 0.0,
117            stop_after_fade: false,
118            pos: 0.0,
119            gains: [1.0, 1.0],
120            lowpass: 1.0,
121            lp_state: [0.0; 2],
122            fresh: true,
123        }
124    }
125
126    /// Starts the voice silent and fades it in.
127    pub(crate) fn fade_in(&mut self, seconds: f32, rate: f32) {
128        self.target_volume = self.volume;
129        self.volume = 0.0;
130        self.fade_step = fade_step(self.target_volume, seconds, rate);
131    }
132
133    fn finish(&mut self) {
134        self.shared.finished.store(true, Ordering::Release);
135    }
136
137    fn is_finished(&self) -> bool {
138        self.shared.finished.load(Ordering::Acquire)
139    }
140
141    fn apply(&mut self, change: Change, rate: f32) {
142        match change {
143            Change::Volume { target, fade } => {
144                self.target_volume = target.max(0.0);
145                self.stop_after_fade = false;
146                self.fade_step = fade_step((self.target_volume - self.volume).abs(), fade, rate);
147            }
148            Change::Stop { fade } => {
149                if fade <= 0.0 {
150                    self.finish();
151                } else {
152                    self.target_volume = 0.0;
153                    self.stop_after_fade = true;
154                    self.fade_step = fade_step(self.volume, fade, rate);
155                }
156            }
157            Change::Pitch(p) => self.pitch = p.clamp(0.01, 16.0),
158            Change::Pan(p) => self.pan = p.clamp(-1.0, 1.0),
159            Change::Position(p) => {
160                if let Some(s) = &mut self.spatial {
161                    s.position = p;
162                }
163            }
164            Change::Paused(p) => self.paused = p,
165            Change::Looping(l) => self.looping = l,
166            Change::Distance { min, max } => {
167                if let Some(s) = &mut self.spatial {
168                    s.min_distance = min.max(0.01);
169                    s.max_distance = max.max(s.min_distance + 0.01);
170                }
171            }
172            Change::Seek(seconds) => {
173                if let Some(data) = self.slot.get() {
174                    self.pos = (seconds.max(0.0) as f64 * data.sample_rate as f64)
175                        .min(data.frames().saturating_sub(1) as f64);
176                }
177            }
178        }
179    }
180
181    /// Computes the left/right gains and low-pass coefficient for the current state.
182    fn targets(&self, listener: &Listener, rate: f32, source_channels: usize) -> ([f32; 2], f32) {
183        let Some(s) = &self.spatial else {
184            // Plain stereo balance; centered sounds play at full volume in both ears.
185            let p = self.pan;
186            let gains = if source_channels == 1 && p == 0.0 {
187                [1.0, 1.0]
188            } else {
189                [(1.0 - p).min(1.0), (1.0 + p).min(1.0)]
190            };
191            return (gains, 1.0);
192        };
193
194        // The offset in the listener's own frame: right, ahead and above the head.
195        let rel = s.position - listener.position;
196        let local_x = rel.dot(math::right(listener.yaw));
197        let local_y = rel.dot(math::forward_3d(listener.yaw, listener.pitch));
198        let local_z = rel.dot(math::up_3d(listener.yaw, listener.pitch));
199        let distance = rel.length();
200
201        if distance >= s.max_distance {
202            return ([0.0, 0.0], 1.0);
203        }
204        let near = s.min_distance / distance.max(s.min_distance);
205        let t = ((distance - s.min_distance) / (s.max_distance - s.min_distance)).clamp(0.0, 1.0);
206        let mut gain = near.powf(s.rolloff) * (1.0 - t * t);
207
208        let (pan, behind, below) = if distance > 1e-4 {
209            ((local_x / distance) * 0.9, (-local_y / distance).max(0.0), (-local_z / distance).max(0.0))
210        } else {
211            (0.0, 0.0, 0.0)
212        };
213        // Sounds behind the listener are a bit quieter and duller, which helps telling
214        // front from back on headphones. Sounds below are dulled less, so above (clear),
215        // below (slightly dull) and behind (dull) can be told apart. Sounds straight above or
216        // below are centered, because the pan only uses the sideways part of the offset.
217        gain *= 1.0 - 0.3 * behind - 0.15 * below * (1.0 - behind);
218        let muffle = behind.max(0.6 * below);
219        let cutoff = math::lerp(20_000.0, 2_500.0, muffle);
220        let lowpass = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
221
222        let angle = (pan + 1.0) * FRAC_PI_4;
223        ([angle.cos() * gain, angle.sin() * gain], lowpass)
224    }
225
226    /// Mixes this voice into `out` (stereo interleaved). Returns `false` once finished.
227    fn render(&mut self, out: &mut [f32], listener: &Listener, group_gain: f32, rate: f32) -> bool {
228        if self.is_finished() {
229            return false;
230        }
231        let slot = self.slot.clone();
232        let Some(data) = slot.get() else {
233            if slot.has_failed() {
234                self.finish();
235                return false;
236            }
237            return true; // Still loading.
238        };
239        if self.paused {
240            return true;
241        }
242
243        let frames = out.len() / 2;
244        let total = data.frames();
245        if total == 0 {
246            self.finish();
247            return false;
248        }
249        let ch = data.channels;
250        let (target_gains, lowpass) = self.targets(listener, rate, ch);
251        if self.fresh {
252            self.gains = target_gains;
253            self.lowpass = lowpass;
254            self.fresh = false;
255        }
256        let start_gains = self.gains;
257        let gain_step = [
258            (target_gains[0] - start_gains[0]) / frames as f32,
259            (target_gains[1] - start_gains[1]) / frames as f32,
260        ];
261        let lp_start = self.lowpass;
262        let lp_step = (lowpass - lp_start) / frames as f32;
263        let mono = self.spatial.is_some() || ch == 1;
264        let step = data.sample_rate as f64 / rate as f64 * self.pitch as f64;
265        let s = &data.samples;
266
267        let read = |frame: usize, c: usize| s[frame * ch + c.min(ch - 1)];
268
269        for i in 0..frames {
270            // Volume fades.
271            if self.volume != self.target_volume {
272                if self.volume < self.target_volume {
273                    self.volume = (self.volume + self.fade_step).min(self.target_volume);
274                } else {
275                    self.volume = (self.volume - self.fade_step).max(self.target_volume);
276                }
277            } else if self.stop_after_fade {
278                self.finish();
279                break;
280            }
281
282            let idx = self.pos as usize;
283            let frac = (self.pos - idx as f64) as f32;
284            let next = if idx + 1 < total {
285                Some(idx + 1)
286            } else if self.looping {
287                Some(0)
288            } else {
289                None
290            };
291            let sample = |c: usize| {
292                let a = read(idx, c);
293                let b = next.map_or(0.0, |n| read(n, c));
294                a + (b - a) * frac
295            };
296            let (mut l, mut r) = if mono {
297                let m = if ch == 1 {
298                    sample(0)
299                } else {
300                    (0..ch).map(sample).sum::<f32>() / ch as f32
301                };
302                (m, m)
303            } else {
304                (sample(0), sample(1))
305            };
306
307            let fi = i as f32;
308            let lp = lp_start + lp_step * fi;
309            if lp < 0.999 {
310                self.lp_state[0] += lp * (l - self.lp_state[0]);
311                self.lp_state[1] += lp * (r - self.lp_state[1]);
312                l = self.lp_state[0];
313                r = self.lp_state[1];
314            } else {
315                self.lp_state = [l, r];
316            }
317
318            let v = self.volume * group_gain;
319            out[i * 2] += l * (start_gains[0] + gain_step[0] * fi) * v;
320            out[i * 2 + 1] += r * (start_gains[1] + gain_step[1] * fi) * v;
321
322            self.pos += step;
323            if self.pos >= total as f64 {
324                if self.looping {
325                    self.pos %= total as f64;
326                } else {
327                    self.finish();
328                    break;
329                }
330            }
331        }
332        self.gains = target_gains;
333        self.lowpass = lowpass;
334        let seconds = self.pos / data.sample_rate as f64;
335        self.shared.position.store(seconds.to_bits(), Ordering::Relaxed);
336        !self.is_finished()
337    }
338}
339
340fn fade_step(distance: f32, seconds: f32, rate: f32) -> f32 {
341    if seconds <= 0.0 { f32::INFINITY } else { distance / (seconds * rate) }
342}
343
344#[derive(Debug, Clone, Copy)]
345pub(crate) struct Listener {
346    pub position: Vec3,
347    pub yaw: f32,
348    pub pitch: f32,
349}
350
351pub(crate) struct Mixer {
352    rx: Receiver<Command>,
353    voices: Vec<Voice>,
354    listener: Listener,
355    groups: [f32; 3],
356    master: f32,
357    rate: f32,
358}
359
360impl Mixer {
361    pub(crate) fn new(rx: Receiver<Command>, rate: u32) -> Self {
362        Self {
363            rx,
364            voices: Vec::with_capacity(64),
365            listener: Listener { position: Vec3::ZERO, yaw: 0.0, pitch: 0.0 },
366            groups: [1.0; 3],
367            master: 1.0,
368            rate: rate as f32,
369        }
370    }
371
372    fn handle_commands(&mut self) {
373        while let Ok(cmd) = self.rx.try_recv() {
374            match cmd {
375                Command::Play(mut voice) => {
376                    if self.voices.len() >= MAX_VOICES {
377                        let oldest = self.voices.remove(0);
378                        oldest.shared.finished.store(true, Ordering::Release);
379                    }
380                    if voice.fade_step == 0.0 {
381                        voice.fade_step = f32::INFINITY;
382                    }
383                    self.voices.push(*voice);
384                }
385                Command::Change(id, change) => {
386                    let rate = self.rate;
387                    if let Some(v) = self.voices.iter_mut().find(|v| v.id == id) {
388                        v.apply(change, rate);
389                    }
390                }
391                Command::StopAll { group, fade } => {
392                    let rate = self.rate;
393                    for v in self.voices.iter_mut().filter(|v| group.is_none_or(|g| v.group == g)) {
394                        v.apply(Change::Stop { fade }, rate);
395                    }
396                }
397                Command::Listener { position, yaw, pitch } => self.listener = Listener { position, yaw, pitch },
398                Command::GroupVolume(group, volume) => self.groups[group.index()] = volume.max(0.0),
399                Command::MasterVolume(volume) => self.master = volume.max(0.0),
400            }
401        }
402    }
403
404    /// Renders stereo interleaved audio into `out`.
405    pub(crate) fn render(&mut self, out: &mut [f32]) {
406        self.handle_commands();
407        out.fill(0.0);
408        let (listener, rate, master, groups) = (self.listener, self.rate, self.master, self.groups);
409        self.voices.retain_mut(|v| v.render(out, &listener, master * groups[v.group.index()], rate));
410        for s in out.iter_mut() {
411            *s = s.clamp(-1.0, 1.0);
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::super::data::SoundData;
419    use super::*;
420    use std::sync::mpsc::channel;
421
422    fn voice(id: u64, data: SoundData) -> (Box<Voice>, Arc<VoiceShared>) {
423        let shared = Arc::new(VoiceShared::default());
424        let v = Voice::new(id, Arc::new(SoundSlot::ready(data)), shared.clone(), Group::Sound);
425        (Box::new(v), shared)
426    }
427
428    #[test]
429    fn plays_and_finishes() {
430        let (tx, rx) = channel();
431        let mut mixer = Mixer::new(rx, 44_100);
432        let (v, shared) = voice(1, SoundData::from_samples(vec![0.5; 100], 1, 44_100));
433        tx.send(Command::Play(v)).unwrap();
434        let mut out = vec![0.0; 128];
435        mixer.render(&mut out);
436        assert!((out[0] - 0.5).abs() < 1e-4 && (out[1] - 0.5).abs() < 1e-4);
437        assert!(!shared.finished.load(Ordering::Acquire));
438        mixer.render(&mut out);
439        assert!(shared.finished.load(Ordering::Acquire));
440        assert!(mixer.voices.is_empty());
441    }
442
443    #[test]
444    fn spatial_left_is_louder_in_left_ear() {
445        let (tx, rx) = channel();
446        let mut mixer = Mixer::new(rx, 44_100);
447        let (mut v, _) = voice(1, SoundData::from_samples(vec![0.5; 1000], 1, 44_100));
448        v.spatial = Some(Spatial { position: Vec3::new(-3.0, 0.0, 0.0), min_distance: 1.0, max_distance: 30.0, rolloff: 1.0 });
449        tx.send(Command::Play(v)).unwrap();
450        let mut out = vec![0.0; 64];
451        mixer.render(&mut out);
452        assert!(out[0] > out[1] * 3.0);
453    }
454
455    #[test]
456    fn height_and_pitch_change_the_sound() {
457        let (_, rx) = channel();
458        let mixer = Mixer::new(rx, 44_100);
459        let at = |position: Vec3, pitch: f32| {
460            let (mut v, _) = voice(1, SoundData::from_samples(vec![0.5; 10], 1, 44_100));
461            v.spatial = Some(Spatial { position, min_distance: 1.0, max_distance: 30.0, rolloff: 1.0 });
462            let listener = Listener { position: Vec3::ZERO, yaw: 0.0, pitch };
463            let ([l, r], lowpass) = v.targets(&listener, mixer.rate, 1);
464            (l + r, lowpass)
465        };
466        let (above, above_lp) = at(Vec3::new(0.0, 0.0, 3.0), 0.0);
467        let (below, below_lp) = at(Vec3::new(0.0, 0.0, -3.0), 0.0);
468        let (behind, behind_lp) = at(Vec3::new(0.0, -3.0, 0.0), 0.0);
469        assert!(above > below && below > behind);
470        assert!(above_lp > below_lp && below_lp > behind_lp);
471        // Looking straight down, a sound above is behind the head.
472        let (_, looking_down_lp) = at(Vec3::new(0.0, 0.0, 3.0), -90.0);
473        assert!((looking_down_lp - behind_lp).abs() < 1e-4);
474    }
475}