Skip to main content

rill_telemetry/
probe.rs

1use std::sync::Arc;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use rill_core::math::Transcendental;
5use rill_core::prelude::{
6    SignalNode, NodeCategory, NodeId, NodeMetadata, NodeState, ParamValue, ParameterId, Port,
7    ProcessResult,
8};
9use rill_core::queues::spsc::SpscQueue;
10use rill_core::queues::TelemetryBlock;
11use rill_core::time::ClockTick;
12use rill_core::traits::Processor;
13
14/// Passive telemetry probe that passes audio through while periodically
15/// capturing block data + metrics into a shared ring buffer.
16///
17/// # Type Parameters
18/// - `T` — sample type (f32/f64)
19/// - `BUF_SIZE` — signal block size
20/// - `QUEUE_CAP` — capacity of the telemetry ring buffer (must be power of two)
21///
22/// # Design
23/// - 1 audio input → 1 audio output (passthrough, zero-latency)
24/// - Every `interval` blocks, computes peak/RMS/DC and pushes into the queue
25/// - Queue is `SpscQueue` with overwrite-oldest policy (default)
26/// - Non-blocking: if the queue is full, old entries are overwritten
27pub struct TelemetryProbe<T: Transcendental, const BUF_SIZE: usize, const QUEUE_CAP: usize> {
28    id: NodeId,
29    inputs: Vec<Port<T, BUF_SIZE>>,
30    outputs: Vec<Port<T, BUF_SIZE>>,
31    state: NodeState<T, BUF_SIZE>,
32
33    /// Shared telemetry ring buffer (Arc for shared ownership)
34    queue: Arc<SpscQueue<TelemetryBlock<T, BUF_SIZE>, QUEUE_CAP>>,
35
36    /// Send telemetry every N blocks
37    interval: u32,
38    /// Block counter
39    counter: u32,
40    /// Monotonic block index (for telemetry frames)
41    block_index: u64,
42    /// Audio channel index reported in telemetry
43    channel: u32,
44    /// Node name reported in telemetry
45    node_name: String,
46}
47
48impl<T: Transcendental, const BUF_SIZE: usize, const QUEUE_CAP: usize>
49    TelemetryProbe<T, BUF_SIZE, QUEUE_CAP>
50{
51    /// Create a new telemetry probe.
52    ///
53    /// # Arguments
54    /// * `queue` — shared `SpscQueue` for telemetry output
55    /// * `interval` — send telemetry every N blocks (1 = every block)
56    /// * `channel` — channel index to report in telemetry frames
57    /// * `node_name` — human-readable node name for metadata
58    pub fn new(
59        queue: Arc<SpscQueue<TelemetryBlock<T, BUF_SIZE>, QUEUE_CAP>>,
60        interval: u32,
61        channel: u32,
62        node_name: &str,
63    ) -> Self {
64        assert!(interval > 0, "interval must be positive");
65
66        let id = NodeId(0);
67        let inputs = vec![Port::input(id, 0, "signal_in")];
68
69        let outputs = vec![Port::output(id, 0, "signal_out")];
70
71        Self {
72            id,
73            inputs,
74            outputs,
75            state: NodeState::new(44100.0),
76            queue,
77            interval,
78            counter: 0,
79            block_index: 0,
80            channel,
81            node_name: node_name.to_string(),
82        }
83    }
84
85    /// Get a reference to the shared telemetry queue.
86    pub fn queue(&self) -> &Arc<SpscQueue<TelemetryBlock<T, BUF_SIZE>, QUEUE_CAP>> {
87        &self.queue
88    }
89}
90
91// ── SignalNode ──────────────────────────────────────────────────────────────
92
93impl<T: Transcendental, const BUF_SIZE: usize, const QUEUE_CAP: usize> SignalNode<T, BUF_SIZE>
94    for TelemetryProbe<T, BUF_SIZE, QUEUE_CAP>
95{
96    fn metadata(&self) -> NodeMetadata {
97        let mut meta = NodeMetadata::new(&self.node_name, NodeCategory::Analyzer);
98        meta.description = "Pass-through telemetry probe".to_string();
99        meta.author = "Rill".to_string();
100        meta.version = env!("CARGO_PKG_VERSION").to_string();
101        meta.signal_inputs = self.inputs.len();
102        meta.signal_outputs = self.outputs.len();
103        meta
104    }
105
106    fn init(&mut self, sample_rate: f32) {
107        self.state = NodeState::new(sample_rate);
108    }
109
110    fn reset(&mut self) {
111        self.state.reset();
112        self.counter = 0;
113        self.block_index = 0;
114    }
115
116    fn get_parameter(&self, _id: &ParameterId) -> Option<ParamValue> {
117        None
118    }
119
120    fn set_parameter(&mut self, _id: &ParameterId, _value: ParamValue) -> ProcessResult<()> {
121        Ok(())
122    }
123
124    fn id(&self) -> NodeId {
125        self.id
126    }
127
128    fn set_id(&mut self, id: NodeId) {
129        self.id = id;
130    }
131
132    fn input_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
133        self.inputs.get(index)
134    }
135
136    fn input_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
137        self.inputs.get_mut(index)
138    }
139
140    fn output_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
141        self.outputs.get(index)
142    }
143
144    fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
145        self.outputs.get_mut(index)
146    }
147
148    fn control_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
149        None
150    }
151
152    fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
153        None
154    }
155
156    fn num_control_inputs(&self) -> usize {
157        0
158    }
159
160    fn num_control_outputs(&self) -> usize {
161        0
162    }
163
164    fn state(&self) -> &NodeState<T, BUF_SIZE> {
165        &self.state
166    }
167
168    fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
169        &mut self.state
170    }
171}
172
173// ── Processor ──────────────────────────────────────────────────────────────
174
175impl<T: Transcendental, const BUF_SIZE: usize, const QUEUE_CAP: usize> Processor<T, BUF_SIZE>
176    for TelemetryProbe<T, BUF_SIZE, QUEUE_CAP>
177{
178    fn process(
179        &mut self,
180        _clock: &ClockTick,
181        signal_inputs: &[&[T; BUF_SIZE]],
182        _control_inputs: &[T],
183        _clock_inputs: &[ClockTick],
184        _feedback_inputs: &[&[T; BUF_SIZE]],
185    ) -> ProcessResult<()> {
186        // ── Passthrough: copy input[0] → output[0] ──────────────────────
187        let silence = [T::ZERO; BUF_SIZE];
188        let input = signal_inputs.first().copied().unwrap_or(&silence);
189        if let Some(port) = self.outputs.first_mut() {
190            port.buffer.as_mut_array().copy_from_slice(input);
191        }
192
193        // ── Telemetry capture (every N blocks) ──────────────────────────
194        self.counter += 1;
195        if self.counter >= self.interval {
196            self.counter = 0;
197
198            let timestamp = SystemTime::now()
199                .duration_since(UNIX_EPOCH)
200                .unwrap_or_default()
201                .as_micros() as u64;
202
203            let mut frame = TelemetryBlock {
204                node_id: self.id,
205                channel: self.channel,
206                sample_rate: self.state.sample_rate,
207                block_index: self.block_index,
208                timestamp,
209                ..Default::default()
210            };
211            frame.data.copy_from_slice(input);
212            frame.compute_metrics();
213
214            self.block_index += 1;
215
216            // Non-blocking push with overwrite-oldest semantics
217            let _ = self.queue.push(frame);
218        }
219
220        self.state.advance();
221        Ok(())
222    }
223
224    fn latency(&self) -> usize {
225        0
226    }
227}