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}
14
15/// Linear resampler to interleaved stereo (same algorithm as gasm-host.js `Resampler`).
16pub struct Resampler {
17    dst_rate: u32,
18    src_rate: u32,
19    channels: u32,
20    t: f64,
21    prev: [f32; 2],
22}
23
24impl Resampler {
25    pub fn new(dst_rate: u32) -> Resampler {
26        Resampler { dst_rate, src_rate: 44100, channels: 2, t: 0.0, prev: [0.0; 2] }
27    }
28
29    pub fn configure(&mut self, rate: u32, channels: u32) {
30        self.src_rate = rate;
31        self.channels = channels;
32    }
33
34    /// Resample `bytes` (LE f32, configured format), appending stereo samples to `out`.
35    pub fn process(&mut self, bytes: &[u8], out: &mut Vec<f32>) {
36        let step = self.src_rate as f64 / self.dst_rate as f64;
37        let ch = self.channels as usize;
38        out.reserve(((bytes.len() / 4 / ch) as f64 / step) as usize * 2 + 4);
39        for frame in bytes.chunks_exact(4 * ch) {
40            let s = |i: usize| f32::from_le_bytes([frame[i * 4], frame[i * 4 + 1], frame[i * 4 + 2], frame[i * 4 + 3]]);
41            let cur = if ch == 1 { [s(0), s(0)] } else { [s(0), s(1)] };
42            while self.t < 1.0 {
43                let t = self.t as f32;
44                out.push(self.prev[0] + (cur[0] - self.prev[0]) * t);
45                out.push(self.prev[1] + (cur[1] - self.prev[1]) * t);
46                self.t += step;
47            }
48            self.t -= 1.0;
49            self.prev = cur;
50        }
51    }
52}
53
54#[cfg(feature = "window")]
55pub use sink::{AudioSink, AudioStream};
56
57#[cfg(feature = "window")]
58mod sink {
59    use std::sync::Arc;
60    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
61
62    use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
63    use cpal::{FromSample, SizedSample};
64
65    use super::{AudioOut, Resampler};
66
67    /// Buffer this much audio before playback starts / after an underrun.
68    const TARGET_LATENCY_S: f32 = 0.06;
69    /// Drop the oldest samples when the queue grows past this (guest runs fast).
70    const MAX_LATENCY_S: f32 = 0.20;
71
72    /// Single-producer single-consumer ring of interleaved stereo f32 samples.
73    /// The guest thread pushes, the device callback pops; neither ever blocks.
74    struct Ring {
75        slots: Box<[AtomicU32]>,
76        /// total samples written / read (monotonic; index = count % len)
77        head: AtomicUsize,
78        tail: AtomicUsize,
79        /// set by the producer when the queue is too long: the consumer drops
80        /// down to the target latency
81        trim: AtomicBool,
82    }
83
84    impl Ring {
85        fn new(capacity: usize) -> Ring {
86            Ring {
87                slots: (0..capacity).map(|_| AtomicU32::new(0)).collect(),
88                head: AtomicUsize::new(0),
89                tail: AtomicUsize::new(0),
90                trim: AtomicBool::new(false),
91            }
92        }
93
94        fn len(&self) -> usize {
95            self.head.load(Ordering::Acquire) - self.tail.load(Ordering::Acquire)
96        }
97
98        /// Producer: append what fits.
99        fn push(&self, samples: &[f32]) {
100            let head = self.head.load(Ordering::Relaxed);
101            let free = self.slots.len() - (head - self.tail.load(Ordering::Acquire));
102            let n = samples.len().min(free) & !1;
103            for (i, s) in samples[..n].iter().enumerate() {
104                self.slots[(head + i) % self.slots.len()].store(s.to_bits(), Ordering::Relaxed);
105            }
106            self.head.store(head + n, Ordering::Release);
107        }
108
109        /// Consumer: one stereo frame, if there is one.
110        fn pop(&self) -> Option<(f32, f32)> {
111            let tail = self.tail.load(Ordering::Relaxed);
112            if self.head.load(Ordering::Acquire) - tail < 2 {
113                return None;
114            }
115            let get = |i: usize| f32::from_bits(self.slots[i % self.slots.len()].load(Ordering::Relaxed));
116            let frame = (get(tail), get(tail + 1));
117            self.tail.store(tail + 2, Ordering::Release);
118            Some(frame)
119        }
120
121        /// Consumer: drop the oldest samples down to `keep`.
122        fn skip_to(&self, keep: usize) {
123            let tail = self.tail.load(Ordering::Relaxed);
124            let len = self.head.load(Ordering::Acquire) - tail;
125            if len > keep {
126                self.tail.store(tail + ((len - keep) & !1), Ordering::Release);
127            }
128        }
129    }
130
131    pub struct AudioSink {
132        ring: Arc<Ring>,
133        device_rate: u32,
134        resampler: Resampler,
135        scratch: Vec<f32>,
136        max: usize,
137    }
138
139    /// The device stream feeding from an [`AudioSink`]: keep it alive while
140    /// playing. Separate because cpal streams aren't `Send` (the sink goes into
141    /// the host, see [`AudioOut`]).
142    pub struct AudioStream {
143        _stream: cpal::Stream,
144    }
145
146    fn build<T: SizedSample + FromSample<f32>>(
147        device: &cpal::Device,
148        config: &cpal::StreamConfig,
149        ring: Arc<Ring>,
150        target: usize,
151    ) -> Result<cpal::Stream, cpal::Error> {
152        let channels = config.channels as usize;
153        let mut primed = false;
154        device.build_output_stream::<T, _, _>(
155            *config,
156            move |out: &mut [T], _| {
157                if ring.trim.swap(false, Ordering::AcqRel) {
158                    ring.skip_to(target);
159                }
160                if !primed && ring.len() >= target {
161                    primed = true;
162                }
163                for frame in out.chunks_mut(channels) {
164                    let (l, r) = match ring.pop().filter(|_| primed) {
165                        Some(s) => s,
166                        None => {
167                            primed = false; // underrun: re-buffer
168                            (0.0, 0.0)
169                        }
170                    };
171                    match frame.len() {
172                        1 => frame[0] = T::from_sample(0.5 * (l + r)),
173                        _ => {
174                            frame[0] = T::from_sample(l);
175                            frame[1] = T::from_sample(r);
176                            for s in &mut frame[2..] {
177                                *s = T::from_sample(0.0);
178                            }
179                        }
180                    }
181                }
182            },
183            |e| eprintln!("[audio] stream error: {e}"),
184            None,
185        )
186    }
187
188    impl AudioSink {
189        pub fn open() -> Result<(AudioSink, AudioStream), String> {
190            let host = cpal::default_host();
191            let device = host.default_output_device().ok_or("no audio output device")?;
192            let supported = device.default_output_config().map_err(|e| e.to_string())?;
193            let format = supported.sample_format();
194            let config = supported.config();
195            let device_rate = config.sample_rate;
196            let target = (device_rate as f32 * TARGET_LATENCY_S) as usize * 2;
197            let max = (device_rate as f32 * MAX_LATENCY_S) as usize * 2;
198            let ring = Arc::new(Ring::new(max * 2));
199            let r = ring.clone();
200            use cpal::SampleFormat as F;
201            let stream = match format {
202                F::F32 => build::<f32>(&device, &config, r, target),
203                F::F64 => build::<f64>(&device, &config, r, target),
204                F::I16 => build::<i16>(&device, &config, r, target),
205                F::U16 => build::<u16>(&device, &config, r, target),
206                F::I32 => build::<i32>(&device, &config, r, target),
207                F::U32 => build::<u32>(&device, &config, r, target),
208                F::I8 => build::<i8>(&device, &config, r, target),
209                F::U8 => build::<u8>(&device, &config, r, target),
210                other => return Err(format!("unsupported device sample format {other:?}")),
211            }
212            .map_err(|e| e.to_string())?;
213            stream.play().map_err(|e| e.to_string())?;
214            let sink = AudioSink { ring, device_rate, resampler: Resampler::new(device_rate), scratch: Vec::new(), max };
215            Ok((sink, AudioStream { _stream: stream }))
216        }
217
218        pub fn device_rate(&self) -> u32 {
219            self.device_rate
220        }
221    }
222
223    impl AudioOut for AudioSink {
224        fn configure(&mut self, rate: u32, channels: u32) {
225            self.resampler.configure(rate, channels);
226        }
227
228        fn push_le_f32(&mut self, bytes: &[u8]) {
229            self.scratch.clear();
230            self.resampler.process(bytes, &mut self.scratch);
231            self.ring.push(&self.scratch);
232            if self.ring.len() > self.max {
233                self.ring.trim.store(true, Ordering::Release);
234            }
235        }
236    }
237}