Skip to main content

pipecrab_audio/
mock.rs

1//! Hardware-free [`AudioSource`] / [`AudioSink`] implementations for tests.
2//!
3//! [`MockSource`] replays a fixed script of chunks — build one straight from a
4//! ramp with [`MockSource::ramp`]. [`MockSink`] records everything it is asked
5//! to play so a test can assert on the samples that came out. No devices, no
6//! threads.
7
8use std::collections::VecDeque;
9use std::sync::Arc;
10
11use pipecrab_runtime::maybe_async_trait;
12
13use crate::{AudioChunk, AudioError, AudioFormat, AudioSink, AudioSource};
14
15/// An [`AudioSource`] that yields a predetermined list of chunks, then `None`.
16pub struct MockSource {
17    format: AudioFormat,
18    queue: VecDeque<AudioChunk>,
19}
20
21impl MockSource {
22    /// A source that yields each of `chunks` in order (restamped with `format`),
23    /// then `None`.
24    pub fn new(format: AudioFormat, chunks: impl IntoIterator<Item = Arc<[f32]>>) -> Self {
25        let queue = chunks
26            .into_iter()
27            .map(|samples| AudioChunk::new(samples, format))
28            .collect();
29        Self { format, queue }
30    }
31
32    /// A source whose entire output is the ramp `0.0, 1.0, 2.0, …` — one value
33    /// per sample — split into `chunks` chunks of `chunk_frames` samples each.
34    ///
35    /// The values are exact and monotonic, so a passthrough test can flatten the
36    /// sink's output and compare it to the same ramp.
37    pub fn ramp(format: AudioFormat, chunk_frames: usize, chunks: usize) -> Self {
38        let mut queue = VecDeque::with_capacity(chunks);
39        for c in 0..chunks {
40            let start = (c * chunk_frames) as u32;
41            let samples: Arc<[f32]> = (0..chunk_frames)
42                .map(|i| (start + i as u32) as f32)
43                .collect();
44            queue.push_back(AudioChunk::new(samples, format));
45        }
46        Self { format, queue }
47    }
48}
49
50maybe_async_trait! {
51    impl AudioSource for MockSource {
52        fn format(&self) -> AudioFormat {
53            self.format
54        }
55
56        async fn next_chunk(&mut self) -> Result<Option<AudioChunk>, AudioError> {
57            // A mock never fails: an empty queue is a graceful end of stream.
58            Ok(self.queue.pop_front())
59        }
60    }
61}
62
63/// An [`AudioSink`] that records every chunk it is asked to play.
64pub struct MockSink {
65    format: AudioFormat,
66    received: Vec<AudioChunk>,
67}
68
69impl MockSink {
70    /// A sink expecting chunks in `format`, with nothing recorded yet.
71    pub fn new(format: AudioFormat) -> Self {
72        Self {
73            format,
74            received: Vec::new(),
75        }
76    }
77
78    /// The chunks played so far, in arrival order.
79    pub fn chunks(&self) -> &[AudioChunk] {
80        &self.received
81    }
82
83    /// Every received sample, flattened into one buffer in arrival order.
84    pub fn samples(&self) -> Vec<f32> {
85        self.received
86            .iter()
87            .flat_map(|c| c.samples.iter().copied())
88            .collect()
89    }
90}
91
92maybe_async_trait! {
93    impl AudioSink for MockSink {
94        fn format(&self) -> AudioFormat {
95            self.format
96        }
97
98        async fn play(&mut self, chunk: AudioChunk) -> Result<(), AudioError> {
99            self.received.push(chunk);
100            Ok(())
101        }
102    }
103}