Skip to main content

aether_nodes/
record.rs

1//! RecordNode — taps audio into a lock-free SPSC ring buffer.
2//!
3//! Passes input through to output unchanged (unity gain).
4//! If the ring is full, samples are dropped silently.
5//! No allocation, no locks in the hot path.
6
7use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
8use ringbuf::{traits::Producer, HeapProd};
9
10pub struct RecordNode {
11    producer: HeapProd<f32>,
12}
13
14impl RecordNode {
15    pub fn new(producer: HeapProd<f32>) -> Self {
16        Self { producer }
17    }
18}
19
20impl DspNode for RecordNode {
21    fn process(
22        &mut self,
23        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
24        output: &mut [f32; BUFFER_SIZE],
25        _params: &mut ParamBlock,
26        _sample_rate: f32,
27    ) {
28        let silence = [0.0f32; BUFFER_SIZE];
29        let input = inputs[0].unwrap_or(&silence);
30
31        // Attempt to push; silently drop if ring is full.
32        let _ = self.producer.push_slice(input);
33
34        // Pass-through: copy input to output.
35        output.copy_from_slice(input);
36    }
37
38    fn type_name(&self) -> &'static str {
39        "RecordNode"
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use proptest::prelude::*;
47    use ringbuf::{
48        traits::{Consumer, Split},
49        HeapRb,
50    };
51
52    /// Generate a [f32; BUFFER_SIZE] array via proptest (BUFFER_SIZE = 64).
53    fn audio_buffer() -> impl Strategy<Value = [f32; BUFFER_SIZE]> {
54        prop::collection::vec(-1.0f32..=1.0f32, BUFFER_SIZE).prop_map(|v| v.try_into().unwrap())
55    }
56
57    // Property 3
58    proptest! {
59        /// **Validates: Requirements 2.11**
60        ///
61        /// Property 3: RecordNode pass-through.
62        ///
63        /// For any input buffer passed to `RecordNode::process()`, the output buffer
64        /// SHALL be identical to the input buffer (unity gain pass-through).
65        #[test]
66        fn prop_record_node_pass_through(
67            input_samples in audio_buffer(),
68        ) {
69            let ring = HeapRb::<f32>::new(BUFFER_SIZE * 2);
70            let (producer, _consumer) = ring.split();
71            let mut node = RecordNode::new(producer);
72            let input_buffer: [f32; BUFFER_SIZE] = input_samples;
73            let mut output_buffer = [0.0f32; BUFFER_SIZE];
74            let mut params = ParamBlock::default();
75            let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
76                let mut arr: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = [None; MAX_INPUTS];
77                arr[0] = Some(&input_buffer);
78                arr
79            };
80            node.process(&inputs, &mut output_buffer, &mut params, 48000.0);
81            prop_assert_eq!(output_buffer, input_buffer);
82        }
83
84        // Property 4
85        /// **Validates: Requirements 2.2**
86        ///
87        /// Property 4: RecordNode ring buffer round-trip.
88        ///
89        /// For any input buffer passed to `RecordNode::process()` when the ring buffer
90        /// has capacity, draining the ring buffer SHALL yield the same 64 samples.
91        #[test]
92        fn prop_record_node_ring_buffer_round_trip(
93            input_samples in audio_buffer(),
94        ) {
95            let ring = HeapRb::<f32>::new(BUFFER_SIZE * 2);
96            let (producer, mut consumer) = ring.split();
97            let mut node = RecordNode::new(producer);
98            let input_buffer: [f32; BUFFER_SIZE] = input_samples;
99            let mut output_buffer = [0.0f32; BUFFER_SIZE];
100            let mut params = ParamBlock::default();
101            let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
102                let mut arr: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = [None; MAX_INPUTS];
103                arr[0] = Some(&input_buffer);
104                arr
105            };
106            node.process(&inputs, &mut output_buffer, &mut params, 48000.0);
107            let mut drained_samples = [0.0f32; BUFFER_SIZE];
108            let drained_count = consumer.pop_slice(&mut drained_samples);
109            prop_assert_eq!(drained_count, BUFFER_SIZE);
110            prop_assert_eq!(drained_samples, input_buffer);
111        }
112    }
113}