Skip to main content

gasm_host/
audio.rs

1//! Audio output: guest samples -> linear resampler -> lock-free ring -> device.
2//!
3//! [`AudioOut`] is what the host needs; [`AudioSink`] (feature `window`) plays it
4//! on the default cpal device in any of its sample formats.
5
6/// Where the host sends `audio_push` samples. `Send`: the host lives in the
7/// wasmtime store, which async (`gasm_run`) calls require to be `Send`.
8pub trait AudioOut: Send {
9    /// Source format of the following pushes (validated by the host).
10    fn configure(&mut self, rate: u32, channels: u32);
11    /// Interleaved little-endian f32 samples in the configured format.
12    fn push_le_f32(&mut self, bytes: &[u8]);
13    /// The bus gasm:media streams play into (added after these samples), if this output has one.
14    fn media_bus(&self) -> Option<std::sync::Arc<crate::media::MediaBus>> {
15        None
16    }
17}
18
19/// Linear resampler to interleaved stereo (same algorithm as gasm-host.js `Resampler`).
20pub struct Resampler {
21    dst_rate: u32,
22    src_rate: u32,
23    channels: u32,
24    t: f64,
25    prev: [f32; 2],
26}
27
28impl Resampler {
29    pub fn new(dst_rate: u32) -> Resampler {
30        Resampler { dst_rate, src_rate: 44100, channels: 2, t: 0.0, prev: [0.0; 2] }
31    }
32
33    pub fn configure(&mut self, rate: u32, channels: u32) {
34        self.src_rate = rate;
35        self.channels = channels;
36    }
37
38    /// Resample `bytes` (LE f32, configured format), appending stereo samples to `out`.
39    pub fn process(&mut self, bytes: &[u8], out: &mut Vec<f32>) {
40        let step = self.src_rate as f64 / self.dst_rate as f64;
41        let ch = self.channels as usize;
42        out.reserve(((bytes.len() / 4 / ch) as f64 / step) as usize * 2 + 4);
43        for frame in bytes.chunks_exact(4 * ch) {
44            let s = |i: usize| f32::from_le_bytes([frame[i * 4], frame[i * 4 + 1], frame[i * 4 + 2], frame[i * 4 + 3]]);
45            let cur = if ch == 1 { [s(0), s(0)] } else { [s(0), s(1)] };
46            while self.t < 1.0 {
47                let t = self.t as f32;
48                out.push(self.prev[0] + (cur[0] - self.prev[0]) * t);
49                out.push(self.prev[1] + (cur[1] - self.prev[1]) * t);
50                self.t += step;
51            }
52            self.t -= 1.0;
53            self.prev = cur;
54        }
55    }
56}
57
58#[cfg(feature = "window")]
59pub use sink::{AudioSink, AudioStream};
60
61#[cfg(feature = "window")]
62mod sink {
63    use std::sync::Arc;
64    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
65    use std::time::{Duration, Instant};
66
67    use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
68    use cpal::{FromSample, SizedSample};
69
70    use super::{AudioOut, Resampler};
71    use crate::media::MediaBus;
72
73    /// Buffer this much audio before playback starts / after an underrun...
74    const TARGET_LATENCY_S: f32 = 0.06;
75    /// ... more after repeated underruns (a game below its frame rate, a busy system),
76    /// up to this: some latency is better than gaps
77    const TARGET_STEP_S: f32 = 0.03;
78    const TARGET_MAX_S: f32 = 0.15;
79    /// Drop the oldest samples when the queue grows past the target plus this (guest runs fast).
80    const HEADROOM_S: f32 = 0.14;
81
82    /// Single-producer single-consumer ring of interleaved stereo f32 samples.
83    /// The guest thread pushes, the device callback pops; neither ever blocks.
84    struct Ring {
85        slots: Box<[AtomicU32]>,
86        /// total samples written / read (monotonic; index = count % len)
87        head: AtomicUsize,
88        tail: AtomicUsize,
89        /// set by the producer when the queue is too long: the consumer drops
90        /// down to the target latency
91        trim: AtomicBool,
92        /// samples to buffer before playing (grows after repeated underruns)
93        target: AtomicUsize,
94        /// underruns so far (the consumer counts, the producer adapts the target)
95        underruns: AtomicUsize,
96    }
97
98    impl Ring {
99        fn new(capacity: usize) -> Ring {
100            Ring {
101                slots: (0..capacity).map(|_| AtomicU32::new(0)).collect(),
102                head: AtomicUsize::new(0),
103                tail: AtomicUsize::new(0),
104                trim: AtomicBool::new(false),
105                target: AtomicUsize::new(0),
106                underruns: AtomicUsize::new(0),
107            }
108        }
109
110        fn len(&self) -> usize {
111            self.head.load(Ordering::Acquire) - self.tail.load(Ordering::Acquire)
112        }
113
114        /// Producer: append what fits.
115        fn push(&self, samples: &[f32]) {
116            let head = self.head.load(Ordering::Relaxed);
117            let free = self.slots.len() - (head - self.tail.load(Ordering::Acquire));
118            let n = samples.len().min(free) & !1;
119            for (i, s) in samples[..n].iter().enumerate() {
120                self.slots[(head + i) % self.slots.len()].store(s.to_bits(), Ordering::Relaxed);
121            }
122            self.head.store(head + n, Ordering::Release);
123        }
124
125        /// Consumer: one stereo frame, if there is one.
126        fn pop(&self) -> Option<(f32, f32)> {
127            let tail = self.tail.load(Ordering::Relaxed);
128            if self.head.load(Ordering::Acquire) - tail < 2 {
129                return None;
130            }
131            let get = |i: usize| f32::from_bits(self.slots[i % self.slots.len()].load(Ordering::Relaxed));
132            let frame = (get(tail), get(tail + 1));
133            self.tail.store(tail + 2, Ordering::Release);
134            Some(frame)
135        }
136
137        /// Consumer: drop the oldest samples down to `keep`.
138        fn skip_to(&self, keep: usize) {
139            let tail = self.tail.load(Ordering::Relaxed);
140            let len = self.head.load(Ordering::Acquire) - tail;
141            if len > keep {
142                self.tail.store(tail + ((len - keep) & !1), Ordering::Release);
143            }
144        }
145    }
146
147    pub struct AudioSink {
148        ring: Arc<Ring>,
149        /// gasm:media's streams, added after the game's samples
150        bus: Arc<MediaBus>,
151        device_rate: u32,
152        resampler: Resampler,
153        scratch: Vec<f32>,
154        /// samples per second (interleaved stereo)
155        per_second: usize,
156        /// underruns seen at the last check, and when
157        underruns_seen: usize,
158        last_underrun: Option<Instant>,
159    }
160
161    /// The device stream feeding from an [`AudioSink`]: keep it alive while
162    /// playing. Separate because cpal streams aren't `Send` (the sink goes into
163    /// the host, see [`AudioOut`]). [`AudioStream::check`] rebuilds it when it broke or
164    /// the default output device changed.
165    pub struct AudioStream {
166        stream: cpal::Stream,
167        ring: Arc<Ring>,
168        bus: Arc<MediaBus>,
169        device_name: Option<String>,
170        device_rate: u32,
171        broken: Arc<AtomicBool>,
172        last_check: Instant,
173    }
174
175    fn build<T: SizedSample + FromSample<f32>>(
176        device: &cpal::Device,
177        config: &cpal::StreamConfig,
178        ring: Arc<Ring>,
179        broken: Arc<AtomicBool>,
180        bus: Arc<MediaBus>,
181    ) -> Result<cpal::Stream, cpal::Error> {
182        let channels = config.channels as usize;
183        let mut primed = false;
184        // the block in stereo f32: the game's samples, then gasm:media's streams on top
185        let mut mix: Vec<f32> = Vec::new();
186        device.build_output_stream::<T, _, _>(
187            *config,
188            move |out: &mut [T], _| {
189                let target = ring.target.load(Ordering::Relaxed);
190                if ring.trim.swap(false, Ordering::AcqRel) {
191                    ring.skip_to(target);
192                }
193                if !primed && ring.len() >= target {
194                    primed = true;
195                }
196                let frames = out.len() / channels.max(1);
197                mix.clear();
198                mix.resize(frames * 2, 0.0);
199                for f in 0..frames {
200                    let (l, r) = match ring.pop().filter(|_| primed) {
201                        Some(s) => s,
202                        None => {
203                            if primed {
204                                ring.underruns.fetch_add(1, Ordering::Relaxed);
205                            }
206                            primed = false; // underrun: re-buffer
207                            (0.0, 0.0)
208                        }
209                    };
210                    (mix[f * 2], mix[f * 2 + 1]) = (l, r);
211                }
212                bus.mix(&mut mix);
213                for (f, frame) in out.chunks_mut(channels).enumerate() {
214                    let (l, r) = (mix[f * 2].clamp(-1.0, 1.0), mix[f * 2 + 1].clamp(-1.0, 1.0));
215                    match frame.len() {
216                        1 => frame[0] = T::from_sample(0.5 * (l + r)),
217                        _ => {
218                            frame[0] = T::from_sample(l);
219                            frame[1] = T::from_sample(r);
220                            for s in &mut frame[2..] {
221                                *s = T::from_sample(0.0);
222                            }
223                        }
224                    }
225                }
226            },
227            move |e| {
228                // e.g. the device went away: the runner rebuilds the stream (AudioStream::check)
229                eprintln!("[audio] stream error: {e}");
230                broken.store(true, Ordering::Release);
231            },
232            None,
233        )
234    }
235
236    /// The default output device's stream on `ring`, and the device's name and rate.
237    fn open_stream(ring: &Arc<Ring>, broken: &Arc<AtomicBool>, bus: &Arc<MediaBus>) -> Result<(cpal::Stream, Option<String>, u32), String> {
238        let host = cpal::default_host();
239        let device = host.default_output_device().ok_or("no audio output device")?;
240        // the device's id tells a change of device; its name goes to the log
241        let name = device.id().ok().map(|i| i.to_string());
242        if let Ok(d) = device.description() {
243            eprintln!("[audio] output: {}", d.name());
244        }
245        let supported = device.default_output_config().map_err(|e| e.to_string())?;
246        let format = supported.sample_format();
247        let config = supported.config();
248        let (r, b, m) = (ring.clone(), broken.clone(), bus.clone());
249        use cpal::SampleFormat as F;
250        let stream = match format {
251            F::F32 => build::<f32>(&device, &config, r, b, m),
252            F::F64 => build::<f64>(&device, &config, r, b, m),
253            F::I16 => build::<i16>(&device, &config, r, b, m),
254            F::U16 => build::<u16>(&device, &config, r, b, m),
255            F::I32 => build::<i32>(&device, &config, r, b, m),
256            F::U32 => build::<u32>(&device, &config, r, b, m),
257            F::I8 => build::<i8>(&device, &config, r, b, m),
258            F::U8 => build::<u8>(&device, &config, r, b, m),
259            other => return Err(format!("unsupported device sample format {other:?}")),
260        }
261        .map_err(|e| e.to_string())?;
262        stream.play().map_err(|e| e.to_string())?;
263        Ok((stream, name, config.sample_rate))
264    }
265
266    impl AudioStream {
267        /// Call now and then (the window runner: every frame; it checks once a second):
268        /// rebuilds the stream if it broke or the default output device changed. A device
269        /// with another rate than the first is kept as is (the sink resamples to that one).
270        pub fn check(&mut self) {
271            if self.last_check.elapsed() < Duration::from_secs(1) {
272                return;
273            }
274            self.last_check = Instant::now();
275            let now = cpal::default_host().default_output_device().and_then(|d| d.id().ok().map(|i| i.to_string()));
276            let changed = now.is_some() && now != self.device_name;
277            if !self.broken.load(Ordering::Acquire) && !changed {
278                return;
279            }
280            self.broken.store(false, Ordering::Release);
281            match open_stream(&self.ring, &self.broken, &self.bus) {
282                Ok((stream, name, rate)) if rate == self.device_rate => (self.stream, self.device_name) = (stream, name),
283                Ok((_, name, rate)) => {
284                    eprintln!("[audio] the new output runs at {rate} Hz, not {} Hz: staying on the old one", self.device_rate);
285                    self.device_name = name;
286                }
287                Err(e) => eprintln!("[audio] couldn't reopen the output: {e}"),
288            }
289        }
290    }
291
292    impl AudioSink {
293        pub fn open() -> Result<(AudioSink, AudioStream), String> {
294            let device_rate = cpal::default_host()
295                .default_output_device()
296                .ok_or("no audio output device")?
297                .default_output_config()
298                .map_err(|e| e.to_string())?
299                .config()
300                .sample_rate;
301            let per_second = device_rate as usize * 2;
302            let max = (per_second as f32 * (TARGET_MAX_S + HEADROOM_S)) as usize;
303            let ring = Arc::new(Ring::new(max * 2));
304            ring.target.store((per_second as f32 * TARGET_LATENCY_S) as usize & !1, Ordering::Relaxed);
305            let broken = Arc::new(AtomicBool::new(false));
306            let bus = MediaBus::new(device_rate);
307            let (stream, device_name, device_rate) = open_stream(&ring, &broken, &bus)?;
308            let sink = AudioSink {
309                ring: ring.clone(), bus: bus.clone(), device_rate, resampler: Resampler::new(device_rate), scratch: Vec::new(),
310                per_second, underruns_seen: 0, last_underrun: None,
311            };
312            Ok((sink, AudioStream { stream, ring, bus, device_name, device_rate, broken, last_check: Instant::now() }))
313        }
314
315        pub fn device_rate(&self) -> u32 {
316            self.device_rate
317        }
318    }
319
320    impl AudioOut for AudioSink {
321        fn configure(&mut self, rate: u32, channels: u32) {
322            self.resampler.configure(rate, channels);
323        }
324
325        fn media_bus(&self) -> Option<Arc<MediaBus>> {
326            Some(self.bus.clone())
327        }
328
329        fn push_le_f32(&mut self, bytes: &[u8]) {
330            self.scratch.clear();
331            self.resampler.process(bytes, &mut self.scratch);
332            self.ring.push(&self.scratch);
333            // two underruns within 5 s: buffer more (30 ms at a time, up to 150 ms)
334            let underruns = self.ring.underruns.load(Ordering::Relaxed);
335            if underruns != self.underruns_seen {
336                self.underruns_seen = underruns;
337                let again = self.last_underrun.is_some_and(|t| t.elapsed() < Duration::from_secs(5));
338                self.last_underrun = Some(Instant::now());
339                let (target, max) = (self.ring.target.load(Ordering::Relaxed), (self.per_second as f32 * TARGET_MAX_S) as usize);
340                if again && target < max {
341                    let more = (target + (self.per_second as f32 * TARGET_STEP_S) as usize).min(max) & !1;
342                    self.ring.target.store(more, Ordering::Relaxed);
343                    eprintln!("[audio] underruns: buffering {} ms", more * 1000 / self.per_second);
344                }
345            }
346            let target = self.ring.target.load(Ordering::Relaxed);
347            if self.ring.len() > target + (self.per_second as f32 * HEADROOM_S) as usize {
348                self.ring.trim.store(true, Ordering::Release);
349            }
350        }
351    }
352}