Skip to main content

phosphor_core/
audio.rs

1//! Audio I/O backend abstraction.
2//!
3//! The real backend uses cpal for hardware audio. The test backend captures
4//! output to a `Vec<f32>` so tests run without a sound card.
5
6use anyhow::Result;
7
8/// A processed audio buffer returned by the test backend.
9#[derive(Debug, Clone)]
10pub struct AudioBuffer {
11    pub samples: Vec<f32>,
12    pub channels: u16,
13    pub sample_rate: u32,
14}
15
16/// Audio callback type alias.
17pub type AudioCallback = Box<dyn FnMut(&mut [f32]) + Send>;
18
19/// Trait for audio output backends. Allows swapping real hardware for
20/// an in-memory test backend.
21pub trait AudioBackend: Send {
22    /// Start the audio stream, calling `callback` for each buffer.
23    fn start(&mut self, callback: AudioCallback) -> Result<()>;
24    /// Stop the audio stream.
25    fn stop(&mut self) -> Result<()>;
26    /// Sample rate the backend is running at.
27    fn sample_rate(&self) -> u32;
28    /// Buffer size in samples per channel.
29    fn buffer_size(&self) -> u32;
30    /// Number of output channels.
31    fn channels(&self) -> u16;
32}
33
34/// In-memory audio backend for testing. No sound card required.
35pub struct TestBackend {
36    sample_rate: u32,
37    buffer_size: u32,
38    channels: u16,
39    captured: Vec<f32>,
40}
41
42impl TestBackend {
43    pub fn new(sample_rate: u32, buffer_size: u32, channels: u16) -> Self {
44        Self {
45            sample_rate,
46            buffer_size,
47            channels,
48            captured: Vec::new(),
49        }
50    }
51
52    /// Run the callback for `num_buffers` cycles and capture the output.
53    pub fn process_blocks(
54        &mut self,
55        num_buffers: usize,
56        mut callback: impl FnMut(&mut [f32]),
57    ) -> AudioBuffer {
58        let block_size = self.buffer_size as usize * self.channels as usize;
59        self.captured.clear();
60        self.captured.reserve(block_size * num_buffers);
61
62        for _ in 0..num_buffers {
63            let start = self.captured.len();
64            self.captured.resize(start + block_size, 0.0);
65            callback(&mut self.captured[start..]);
66        }
67
68        AudioBuffer {
69            samples: self.captured.clone(),
70            channels: self.channels,
71            sample_rate: self.sample_rate,
72        }
73    }
74}
75
76impl AudioBackend for TestBackend {
77    fn start(&mut self, _callback: Box<dyn FnMut(&mut [f32]) + Send>) -> Result<()> {
78        // TestBackend uses process_blocks() directly instead
79        Ok(())
80    }
81
82    fn stop(&mut self) -> Result<()> {
83        Ok(())
84    }
85
86    fn sample_rate(&self) -> u32 {
87        self.sample_rate
88    }
89
90    fn buffer_size(&self) -> u32 {
91        self.buffer_size
92    }
93
94    fn channels(&self) -> u16 {
95        self.channels
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_backend_captures_silence() {
105        let mut backend = TestBackend::new(44100, 64, 2);
106        let result = backend.process_blocks(10, |_buf| {
107            // callback does nothing — buffer stays zeroed
108        });
109        assert_eq!(result.samples.len(), 64 * 2 * 10);
110        assert!(result.samples.iter().all(|&s| s == 0.0));
111    }
112
113    #[test]
114    fn test_backend_captures_signal() {
115        let mut backend = TestBackend::new(44100, 64, 1);
116        let mut phase = 0.0f32;
117        let result = backend.process_blocks(10, |buf| {
118            for sample in buf.iter_mut() {
119                *sample = phase.sin();
120                phase += 440.0 * std::f32::consts::TAU / 44100.0;
121            }
122        });
123        assert_eq!(result.samples.len(), 64 * 10);
124        // Should contain non-zero samples
125        assert!(result.samples.iter().any(|&s| s.abs() > 0.1));
126        // All samples must be finite
127        assert!(result.samples.iter().all(|s| s.is_finite()));
128    }
129
130    #[test]
131    fn test_backend_correct_metadata() {
132        let backend = TestBackend::new(48000, 128, 2);
133        assert_eq!(backend.sample_rate(), 48000);
134        assert_eq!(backend.buffer_size(), 128);
135        assert_eq!(backend.channels(), 2);
136    }
137}