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::{traits::Consumer, HeapRb};
48
49    // Property 3
50    proptest! {
51        /// **Validates: Requirements 2.11**
52        ///
53        /// Feature: aether-engine-upgrades, Property 3: RecordNode pass-through
54        ///
55        /// Property 3: RecordNode pass-through.
56        ///
57        /// For any input buffer passed to `RecordNode::process()`, the output buffer
58        /// SHALL be identical to the input buffer (unity gain pass-through).
59        #[test]
60        fn prop_record_node_pass_through(
61            input_samples in prop::array::uniform64(-1.0f32..=1.0f32),
62        ) {
63            // Create a ring buffer with sufficient capacity
64            let ring = HeapRb::<f32>::new(BUFFER_SIZE * 2);
65            let (producer, _consumer) = ring.split();
66
67            // Create RecordNode
68            let mut node = RecordNode::new(producer);
69
70            // Prepare input and output buffers
71            let input_buffer: [f32; BUFFER_SIZE] = input_samples;
72            let mut output_buffer = [0.0f32; BUFFER_SIZE];
73            let mut params = ParamBlock::default();
74
75            // Create inputs array with our test input
76            let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
77                let mut arr: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = [None; MAX_INPUTS];
78                arr[0] = Some(&input_buffer);
79                arr
80            };
81
82            // Process
83            node.process(&inputs, &mut output_buffer, &mut params, 48000.0);
84
85            // Assert: output buffer equals input buffer exactly
86            prop_assert_eq!(output_buffer, input_buffer);
87        }
88
89        // Property 4
90        /// **Validates: Requirements 2.2**
91        ///
92        /// Feature: aether-engine-upgrades, Property 4: RecordNode ring buffer round-trip
93        ///
94        /// Property 4: RecordNode ring buffer round-trip.
95        ///
96        /// For any input buffer passed to `RecordNode::process()` when the ring buffer
97        /// has capacity, draining the ring buffer SHALL yield the same 64 samples that
98        /// were in the input buffer.
99        #[test]
100        fn prop_record_node_ring_buffer_round_trip(
101            input_samples in prop::array::uniform64(-1.0f32..=1.0f32),
102        ) {
103            // Create a ring buffer with sufficient capacity
104            let ring = HeapRb::<f32>::new(BUFFER_SIZE * 2);
105            let (producer, mut consumer) = ring.split();
106
107            // Create RecordNode
108            let mut node = RecordNode::new(producer);
109
110            // Prepare input and output buffers
111            let input_buffer: [f32; BUFFER_SIZE] = input_samples;
112            let mut output_buffer = [0.0f32; BUFFER_SIZE];
113            let mut params = ParamBlock::default();
114
115            // Create inputs array with our test input
116            let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
117                let mut arr: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = [None; MAX_INPUTS];
118                arr[0] = Some(&input_buffer);
119                arr
120            };
121
122            // Process - this should write to the ring buffer
123            node.process(&inputs, &mut output_buffer, &mut params, 48000.0);
124
125            // Drain the ring buffer
126            let mut drained_samples = [0.0f32; BUFFER_SIZE];
127            let drained_count = consumer.pop_slice(&mut drained_samples);
128
129            // Assert: we drained exactly 64 samples
130            prop_assert_eq!(drained_count, BUFFER_SIZE);
131
132            // Assert: drained samples equal input samples exactly
133            prop_assert_eq!(drained_samples, input_buffer);
134        }
135    }
136}