Skip to main content

gasm_host/
audio.rs

1//! Audio output: guest samples -> linear resampler -> ring buffer -> cpal device.
2
3use std::collections::VecDeque;
4use std::sync::{Arc, Mutex};
5
6use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
7
8/// Buffer this much audio before playback starts / after an underrun.
9const TARGET_LATENCY_S: f32 = 0.06;
10/// Drop the oldest samples when the queue grows past this (guest runs fast).
11const MAX_LATENCY_S: f32 = 0.20;
12
13struct Queue {
14    samples: VecDeque<f32>, // interleaved stereo at device rate
15    primed: bool,
16}
17
18pub struct AudioSink {
19    queue: Arc<Mutex<Queue>>,
20    device_rate: u32,
21    src_rate: u32,
22    src_channels: u32,
23    t: f64,
24    prev: [f32; 2],
25    _stream: cpal::Stream,
26}
27
28impl AudioSink {
29    pub fn open() -> Result<AudioSink, String> {
30        let host = cpal::default_host();
31        let device = host.default_output_device().ok_or("no audio output device")?;
32        let supported = device.default_output_config().map_err(|e| e.to_string())?;
33        if supported.sample_format() != cpal::SampleFormat::F32 {
34            return Err(format!("unsupported device sample format {:?}", supported.sample_format()));
35        }
36        let config = supported.config();
37        let channels = config.channels as usize;
38        let device_rate = config.sample_rate;
39
40        let queue = Arc::new(Mutex::new(Queue { samples: VecDeque::new(), primed: false }));
41        let q = queue.clone();
42        let target = (device_rate as f32 * TARGET_LATENCY_S) as usize * 2;
43        let stream = device
44            .build_output_stream::<f32, _, _>(
45                config,
46                move |out: &mut [f32], _| {
47                    let mut q = q.lock().unwrap();
48                    if !q.primed && q.samples.len() >= target {
49                        q.primed = true;
50                    }
51                    for frame in out.chunks_mut(channels) {
52                        let (l, r) = if q.primed && q.samples.len() >= 2 {
53                            (q.samples.pop_front().unwrap(), q.samples.pop_front().unwrap())
54                        } else {
55                            q.primed = false; // underrun: re-buffer
56                            (0.0, 0.0)
57                        };
58                        match frame.len() {
59                            1 => frame[0] = 0.5 * (l + r),
60                            _ => {
61                                frame[0] = l;
62                                frame[1] = r;
63                                frame[2..].fill(0.0);
64                            }
65                        }
66                    }
67                },
68                |e| eprintln!("[audio] stream error: {e}"),
69                None,
70            )
71            .map_err(|e| e.to_string())?;
72        stream.play().map_err(|e| e.to_string())?;
73        Ok(AudioSink {
74            queue,
75            device_rate,
76            src_rate: 44100,
77            src_channels: 2,
78            t: 0.0,
79            prev: [0.0; 2],
80            _stream: stream,
81        })
82    }
83
84    pub fn device_rate(&self) -> u32 {
85        self.device_rate
86    }
87
88    pub fn configure(&mut self, rate: u32, channels: u32) {
89        self.src_rate = rate;
90        self.src_channels = channels;
91    }
92
93    /// `bytes` is interleaved little-endian f32 in the configured source format.
94    pub fn push_le_f32(&mut self, bytes: &[u8]) {
95        let step = self.src_rate as f64 / self.device_rate as f64;
96        let ch = self.src_channels as usize;
97        let mut out = Vec::with_capacity((bytes.len() / 4 / ch) * 2 * 2);
98        for frame in bytes.chunks_exact(4 * ch) {
99            let s = |i: usize| f32::from_le_bytes(frame[i * 4..i * 4 + 4].try_into().unwrap());
100            let cur = if ch == 1 { [s(0), s(0)] } else { [s(0), s(1)] };
101            while self.t < 1.0 {
102                let t = self.t as f32;
103                out.push(self.prev[0] + (cur[0] - self.prev[0]) * t);
104                out.push(self.prev[1] + (cur[1] - self.prev[1]) * t);
105                self.t += step;
106            }
107            self.t -= 1.0;
108            self.prev = cur;
109        }
110        let max = (self.device_rate as f32 * MAX_LATENCY_S) as usize * 2;
111        let target = (self.device_rate as f32 * TARGET_LATENCY_S) as usize * 2;
112        let mut q = self.queue.lock().unwrap();
113        q.samples.extend(out);
114        if q.samples.len() > max {
115            let excess = q.samples.len() - target;
116            q.samples.drain(..excess & !1);
117        }
118    }
119}