Skip to main content

aether_nodes/
scope.rs

1//! ScopeNode — feeds audio into a lock-free SPSC ring buffer for visualisation.
2//!
3//! The ring is sized to 512 samples (8 × 64-sample frames).
4//! Passes input through to output unchanged (unity gain).
5//! If the ring is full, samples are dropped silently.
6//! No allocation, no locks in the hot path.
7
8use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
9use ringbuf::{traits::Producer, HeapProd};
10
11pub struct ScopeNode {
12    producer: HeapProd<f32>,
13}
14
15impl ScopeNode {
16    pub fn new(producer: HeapProd<f32>) -> Self {
17        Self { producer }
18    }
19}
20
21impl DspNode for ScopeNode {
22    fn process(
23        &mut self,
24        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
25        output: &mut [f32; BUFFER_SIZE],
26        _params: &mut ParamBlock,
27        _sample_rate: f32,
28    ) {
29        let silence = [0.0f32; BUFFER_SIZE];
30        let input = inputs[0].unwrap_or(&silence);
31
32        // Attempt to push; silently drop if ring is full.
33        let _ = self.producer.push_slice(input);
34
35        // Pass-through: copy input to output.
36        output.copy_from_slice(input);
37    }
38
39    fn type_name(&self) -> &'static str {
40        "ScopeNode"
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use proptest::prelude::*;
48    use ringbuf::HeapRb;
49
50    // Property 9
51    proptest! {
52        /// **Validates: Requirements 4.4**
53        ///
54        /// Feature: aether-engine-upgrades, Property 9: ScopeNode pass-through
55        ///
56        /// Property 9: ScopeNode pass-through.
57        ///
58        /// For any input buffer passed to `ScopeNode::process()`, the output buffer
59        /// SHALL be identical to the input buffer (unity gain pass-through).
60        #[test]
61        fn prop_scope_node_pass_through(
62            input_samples in prop::array::uniform64(-1.0f32..=1.0f32),
63        ) {
64            // Create a ring buffer with sufficient capacity
65            let ring = HeapRb::<f32>::new(BUFFER_SIZE * 2);
66            let (producer, _consumer) = ring.split();
67
68            // Create ScopeNode
69            let mut node = ScopeNode::new(producer);
70
71            // Prepare input and output buffers
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
76            // Create inputs array with our test input
77            let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
78                let mut arr: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = [None; MAX_INPUTS];
79                arr[0] = Some(&input_buffer);
80                arr
81            };
82
83            // Process
84            node.process(&inputs, &mut output_buffer, &mut params, 48000.0);
85
86            // Assert: output buffer equals input buffer exactly
87            prop_assert_eq!(output_buffer, input_buffer);
88        }
89    }
90
91    // Property 10
92    proptest! {
93        /// **Validates: Requirements 4.7**
94        ///
95        /// Feature: aether-engine-upgrades, Property 10: Scope frame serialization
96        ///
97        /// Property 10: Scope frame serialization.
98        ///
99        /// For any array of 64 f32 samples serialized as a binary scope frame,
100        /// the resulting byte slice SHALL be exactly 256 bytes, and decoding it
101        /// as [f32; 64] little-endian SHALL recover the original values exactly.
102        #[test]
103        fn prop_scope_frame_serialization(
104            samples in prop::array::uniform64(-1.0f32..=1.0f32),
105        ) {
106            // Serialize: 64 f32 values to 256 bytes (64 × 4 bytes each)
107            let mut serialized = Vec::with_capacity(256);
108            for &sample in &samples {
109                serialized.extend_from_slice(&sample.to_le_bytes());
110            }
111
112            // Assert: byte length is exactly 256
113            prop_assert_eq!(serialized.len(), 256);
114
115            // Deserialize: 256 bytes back to [f32; 64]
116            let mut deserialized = [0.0f32; 64];
117            for (i, chunk) in serialized.chunks_exact(4).enumerate() {
118                let bytes: [u8; 4] = chunk.try_into().unwrap();
119                deserialized[i] = f32::from_le_bytes(bytes);
120            }
121
122            // Assert: round-trip equality
123            prop_assert_eq!(deserialized, samples);
124        }
125    }
126}