1use wavecraft_dsp::{Processor, Transport};
6use wavecraft_protocol::{OscilloscopeFrame, OscilloscopeTriggerMode};
7
8pub const OSCILLOSCOPE_FRAME_POINTS: usize = 1024;
10const OSCILLOSCOPE_HISTORY_FRAMES: usize = 3;
11const OSCILLOSCOPE_HISTORY_POINTS: usize = OSCILLOSCOPE_FRAME_POINTS * OSCILLOSCOPE_HISTORY_FRAMES;
12const OSCILLOSCOPE_HISTORY_TAIL_START: usize =
13 OSCILLOSCOPE_FRAME_POINTS * (OSCILLOSCOPE_HISTORY_FRAMES - 1);
14const DEFAULT_NO_SIGNAL_THRESHOLD: f32 = 1e-4;
15
16#[derive(Clone)]
18pub struct OscilloscopeFrameSnapshot {
19 pub points_l: [f32; OSCILLOSCOPE_FRAME_POINTS],
20 pub points_r: [f32; OSCILLOSCOPE_FRAME_POINTS],
21 pub sample_rate: f32,
22 pub timestamp: u64,
23 pub no_signal: bool,
24 pub trigger_mode: OscilloscopeTriggerMode,
25}
26
27impl OscilloscopeFrameSnapshot {
28 pub fn to_protocol_frame(&self) -> OscilloscopeFrame {
30 OscilloscopeFrame {
31 points_l: self.points_l.to_vec(),
32 points_r: self.points_r.to_vec(),
33 sample_rate: self.sample_rate,
34 timestamp: self.timestamp,
35 no_signal: self.no_signal,
36 trigger_mode: self.trigger_mode,
37 }
38 }
39}
40
41pub struct OscilloscopeFrameProducer {
43 producer: rtrb::Producer<OscilloscopeFrameSnapshot>,
44}
45
46impl OscilloscopeFrameProducer {
47 pub fn push(&mut self, frame: OscilloscopeFrameSnapshot) {
49 let _ = self.producer.push(frame);
50 }
51}
52
53pub struct OscilloscopeFrameConsumer {
55 consumer: rtrb::Consumer<OscilloscopeFrameSnapshot>,
56}
57
58impl OscilloscopeFrameConsumer {
59 pub fn read_latest(&mut self) -> Option<OscilloscopeFrameSnapshot> {
61 let mut latest = None;
62 while let Ok(frame) = self.consumer.pop() {
63 latest = Some(frame);
64 }
65 latest
66 }
67}
68
69pub fn create_oscilloscope_channel(
71 capacity: usize,
72) -> (OscilloscopeFrameProducer, OscilloscopeFrameConsumer) {
73 let (producer, consumer) = rtrb::RingBuffer::new(capacity);
74 (
75 OscilloscopeFrameProducer { producer },
76 OscilloscopeFrameConsumer { consumer },
77 )
78}
79
80pub struct OscilloscopeTap {
82 sample_rate: f32,
83 frame_l: [f32; OSCILLOSCOPE_FRAME_POINTS],
84 frame_r: [f32; OSCILLOSCOPE_FRAME_POINTS],
85 history_l: [f32; OSCILLOSCOPE_HISTORY_POINTS],
86 history_r: [f32; OSCILLOSCOPE_HISTORY_POINTS],
87 aligned_l: [f32; OSCILLOSCOPE_FRAME_POINTS],
88 aligned_r: [f32; OSCILLOSCOPE_FRAME_POINTS],
89 history_frames_filled: usize,
90 timestamp: u64,
91 no_signal_threshold: f32,
92 output: Option<OscilloscopeFrameProducer>,
93}
94
95impl Default for OscilloscopeTap {
96 fn default() -> Self {
97 Self {
98 sample_rate: 44_100.0,
99 frame_l: [0.0; OSCILLOSCOPE_FRAME_POINTS],
100 frame_r: [0.0; OSCILLOSCOPE_FRAME_POINTS],
101 history_l: [0.0; OSCILLOSCOPE_HISTORY_POINTS],
102 history_r: [0.0; OSCILLOSCOPE_HISTORY_POINTS],
103 aligned_l: [0.0; OSCILLOSCOPE_FRAME_POINTS],
104 aligned_r: [0.0; OSCILLOSCOPE_FRAME_POINTS],
105 history_frames_filled: 0,
106 timestamp: 0,
107 no_signal_threshold: DEFAULT_NO_SIGNAL_THRESHOLD,
108 output: None,
109 }
110 }
111}
112
113impl OscilloscopeTap {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn with_output(output: OscilloscopeFrameProducer) -> Self {
121 Self {
122 output: Some(output),
123 ..Self::default()
124 }
125 }
126
127 pub fn set_output(&mut self, output: OscilloscopeFrameProducer) {
129 self.output = Some(output);
130 }
131
132 pub fn set_sample_rate_hz(&mut self, sample_rate: f32) {
134 self.sample_rate = sample_rate;
135 }
136
137 pub fn capture_stereo(&mut self, left: &[f32], right: &[f32]) {
139 if left.is_empty() {
140 return;
141 }
142
143 let right = if right.is_empty() { left } else { right };
144
145 let left_len = left.len();
147 let right_len = right.len();
148 let mut max_abs = 0.0_f32;
149
150 for index in 0..OSCILLOSCOPE_FRAME_POINTS {
151 let source_l = index * left_len / OSCILLOSCOPE_FRAME_POINTS;
152 let source_r = index * right_len / OSCILLOSCOPE_FRAME_POINTS;
153
154 let l = left[source_l.min(left_len - 1)];
155 let r = right[source_r.min(right_len - 1)];
156
157 self.frame_l[index] = l;
158 self.frame_r[index] = r;
159 max_abs = max_abs.max(l.abs()).max(r.abs());
160 }
161
162 let no_signal = max_abs < self.no_signal_threshold;
163
164 self.history_l
168 .copy_within(OSCILLOSCOPE_FRAME_POINTS..OSCILLOSCOPE_HISTORY_POINTS, 0);
169 self.history_r
170 .copy_within(OSCILLOSCOPE_FRAME_POINTS..OSCILLOSCOPE_HISTORY_POINTS, 0);
171 self.history_l[OSCILLOSCOPE_HISTORY_TAIL_START..].copy_from_slice(&self.frame_l);
172 self.history_r[OSCILLOSCOPE_HISTORY_TAIL_START..].copy_from_slice(&self.frame_r);
173
174 self.history_frames_filled =
175 (self.history_frames_filled + 1).min(OSCILLOSCOPE_HISTORY_FRAMES);
176
177 let min_trigger_start = match self.history_frames_filled {
178 0 | 1 => None,
179 2 => Some(OSCILLOSCOPE_FRAME_POINTS + 1),
181 _ => Some(1),
182 };
183
184 let trigger_start = if no_signal {
185 OSCILLOSCOPE_HISTORY_TAIL_START
186 } else if let Some(min_start) = min_trigger_start {
187 self.find_rising_zero_crossing_in_history(min_start)
188 .unwrap_or(OSCILLOSCOPE_HISTORY_TAIL_START)
189 } else {
190 OSCILLOSCOPE_HISTORY_TAIL_START
191 };
192
193 let end = trigger_start + OSCILLOSCOPE_FRAME_POINTS;
194 self.aligned_l
195 .copy_from_slice(&self.history_l[trigger_start..end]);
196 self.aligned_r
197 .copy_from_slice(&self.history_r[trigger_start..end]);
198
199 let frame = OscilloscopeFrameSnapshot {
200 points_l: self.aligned_l,
201 points_r: self.aligned_r,
202 sample_rate: self.sample_rate,
203 timestamp: self.timestamp,
204 no_signal,
205 trigger_mode: OscilloscopeTriggerMode::RisingZeroCrossing,
206 };
207
208 self.timestamp = self.timestamp.wrapping_add(1);
209
210 if let Some(output) = self.output.as_mut() {
211 output.push(frame);
212 }
213 }
214
215 fn find_rising_zero_crossing_in_history(&self, min_start: usize) -> Option<usize> {
216 let max_start = OSCILLOSCOPE_HISTORY_POINTS - OSCILLOSCOPE_FRAME_POINTS;
220 let preferred_start = (min_start + max_start) / 2;
221 let mut best_index: Option<usize> = None;
222 let mut best_distance = usize::MAX;
223
224 for index in min_start..=max_start {
225 let prev = self.history_l[index - 1];
226 let current = self.history_l[index];
227 if prev <= 0.0 && current > 0.0 {
228 let distance = index.abs_diff(preferred_start);
229 let prefer_candidate = distance < best_distance
230 || (distance == best_distance
231 && best_index.is_none_or(|existing| index > existing));
232 if prefer_candidate {
233 best_index = Some(index);
234 best_distance = distance;
235 }
236 }
237 }
238
239 best_index
240 }
241}
242
243impl Processor for OscilloscopeTap {
244 type Params = ();
245
246 fn set_sample_rate(&mut self, sample_rate: f32) {
247 self.set_sample_rate_hz(sample_rate);
248 }
249
250 fn process(
251 &mut self,
252 buffer: &mut [&mut [f32]],
253 _transport: &Transport,
254 _params: &Self::Params,
255 ) {
256 if buffer.is_empty() {
257 return;
258 }
259
260 let left = &*buffer[0];
261 let right = if buffer.len() > 1 { &*buffer[1] } else { left };
262
263 self.capture_stereo(left, right);
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn passthrough_invariance() {
274 let mut tap = OscilloscopeTap::new();
275
276 let mut left = [0.25_f32, -0.1, 0.4, -0.3];
277 let mut right = [-0.2_f32, 0.5, -0.4, 0.1];
278 let expected_left = left;
279 let expected_right = right;
280 let mut buffer = [&mut left[..], &mut right[..]];
281
282 tap.process(&mut buffer, &Transport::default(), &());
283
284 assert_eq!(left, expected_left);
285 assert_eq!(right, expected_right);
286 }
287
288 #[test]
289 fn frame_length_is_1024() {
290 let (producer, mut consumer) = create_oscilloscope_channel(8);
291 let mut tap = OscilloscopeTap::with_output(producer);
292
293 let left = [0.5_f32; 64];
294 let right = [0.25_f32; 64];
295 tap.capture_stereo(&left, &right);
296
297 let frame = consumer.read_latest().expect("frame should exist");
298 assert_eq!(frame.points_l.len(), OSCILLOSCOPE_FRAME_POINTS);
299 assert_eq!(frame.points_r.len(), OSCILLOSCOPE_FRAME_POINTS);
300 }
301
302 #[test]
303 fn trigger_alignment_rising_zero_crossing() {
304 let (producer, mut consumer) = create_oscilloscope_channel(8);
305 let mut tap = OscilloscopeTap::with_output(producer);
306
307 let mut left = [0.0_f32; 128];
308 let mut right = [0.0_f32; 128];
309
310 for i in 0..128 {
311 let phase = (i as f32 / 128.0) * std::f32::consts::TAU;
312 left[i] = phase.sin();
313 right[i] = left[i];
314 }
315
316 tap.capture_stereo(&left, &right);
317
318 let frame = consumer.read_latest().expect("frame should exist");
319 let first = frame.points_l[0];
320 let second = frame.points_l[1];
321
322 assert!(first <= 0.05, "expected start near zero, got {first}");
323 assert!(second >= first, "expected rising edge at frame start");
324 }
325
326 #[test]
327 fn trigger_alignment_uses_contiguous_window_across_frame_boundary() {
328 let (producer, mut consumer) = create_oscilloscope_channel(8);
329 let mut tap = OscilloscopeTap::with_output(producer);
330
331 let mut left_prev = [0.5_f32; OSCILLOSCOPE_FRAME_POINTS];
332 let mut right_prev = [0.25_f32; OSCILLOSCOPE_FRAME_POINTS];
333 left_prev[1000] = -0.1;
334 left_prev[1001] = 0.1;
335 right_prev[1000] = -0.3;
336 right_prev[1001] = -0.2;
337
338 let mut left_curr = [0.0_f32; OSCILLOSCOPE_FRAME_POINTS];
339 let mut right_curr = [0.0_f32; OSCILLOSCOPE_FRAME_POINTS];
340 for index in 0..OSCILLOSCOPE_FRAME_POINTS {
341 left_curr[index] = -1.0 + (2.0 * index as f32 / OSCILLOSCOPE_FRAME_POINTS as f32);
342 right_curr[index] = 1.0 - (2.0 * index as f32 / OSCILLOSCOPE_FRAME_POINTS as f32);
343 }
344
345 tap.capture_stereo(&left_prev, &right_prev);
346 let _first = consumer.read_latest().expect("first frame should exist");
347
348 tap.capture_stereo(&left_curr, &right_curr);
349 let second = consumer.read_latest().expect("second frame should exist");
350
351 assert!((second.points_l[0] - left_prev[1001]).abs() < f32::EPSILON);
353
354 assert!((second.points_l[23] - left_curr[0]).abs() < f32::EPSILON);
356 assert!((second.points_r[23] - right_curr[0]).abs() < f32::EPSILON);
357
358 assert!((second.points_l[1023] - left_curr[1000]).abs() < f32::EPSILON);
360 assert!((second.points_r[1023] - right_curr[1000]).abs() < f32::EPSILON);
361 }
362
363 #[test]
364 fn trigger_alignment_finds_low_frequency_crossings_beyond_oldest_frame() {
365 let (producer, mut consumer) = create_oscilloscope_channel(8);
366 let mut tap = OscilloscopeTap::with_output(producer);
367
368 let mut left_prev = [-0.5_f32; OSCILLOSCOPE_FRAME_POINTS];
369 let mut right_prev = [-0.25_f32; OSCILLOSCOPE_FRAME_POINTS];
370 left_prev[200] = 0.5;
371 right_prev[200] = 0.25;
372
373 let left_curr = [-0.5_f32; OSCILLOSCOPE_FRAME_POINTS];
374 let right_curr = [-0.25_f32; OSCILLOSCOPE_FRAME_POINTS];
375
376 tap.capture_stereo(&left_prev, &right_prev);
377 let _first = consumer.read_latest().expect("first frame should exist");
378
379 tap.capture_stereo(&left_curr, &right_curr);
380 let second = consumer.read_latest().expect("second frame should exist");
381
382 assert!(
386 (second.points_l[0] - left_prev[200]).abs() < f32::EPSILON,
387 "expected left start {}, got {}",
388 left_prev[200],
389 second.points_l[0]
390 );
391 assert!(
392 (second.points_r[0] - right_prev[200]).abs() < f32::EPSILON,
393 "expected right start {}, got {}",
394 right_prev[200],
395 second.points_r[0]
396 );
397
398 assert!((second.points_l[824] - left_curr[0]).abs() < f32::EPSILON);
400 assert!((second.points_r[824] - right_curr[0]).abs() < f32::EPSILON);
401 }
402
403 #[test]
404 fn no_signal_detection() {
405 let (producer, mut consumer) = create_oscilloscope_channel(8);
406 let mut tap = OscilloscopeTap::with_output(producer);
407
408 let left = [1e-6_f32; 128];
409 let right = [1e-6_f32; 128];
410 tap.capture_stereo(&left, &right);
411
412 let frame = consumer.read_latest().expect("frame should exist");
413 assert!(frame.no_signal);
414 }
415
416 #[test]
417 fn stereo_capture_integrity() {
418 let (producer, mut consumer) = create_oscilloscope_channel(8);
419 let mut tap = OscilloscopeTap::with_output(producer);
420
421 let left = [0.75_f32; 128];
422 let right = [-0.25_f32; 128];
423 tap.capture_stereo(&left, &right);
424
425 let frame = consumer.read_latest().expect("frame should exist");
426 assert!(
427 frame
428 .points_l
429 .iter()
430 .all(|v| (*v - 0.75).abs() < f32::EPSILON)
431 );
432 assert!(
433 frame
434 .points_r
435 .iter()
436 .all(|v| (*v + 0.25).abs() < f32::EPSILON)
437 );
438 }
439}