bbx_draw/visualizers/
waveform.rs

1//! Oscilloscope-style waveform visualizer.
2
3use nannou::{
4    Draw,
5    geom::{Point2, Rect},
6};
7
8use crate::{Visualizer, bridge::AudioBridgeConsumer, config::WaveformConfig};
9
10/// Displays audio waveform in oscilloscope style.
11///
12/// Features zero-crossing trigger detection for stable display of periodic waveforms.
13pub struct WaveformVisualizer {
14    consumer: AudioBridgeConsumer,
15    config: WaveformConfig,
16    sample_buffer: Vec<f32>,
17    write_position: usize,
18}
19
20impl WaveformVisualizer {
21    /// Create a new waveform visualizer with default configuration.
22    pub fn new(consumer: AudioBridgeConsumer) -> Self {
23        let config = WaveformConfig::default();
24        let buffer_size = config.time_window_samples * 2;
25        Self {
26            consumer,
27            config: config.clone(),
28            sample_buffer: vec![0.0; buffer_size],
29            write_position: 0,
30        }
31    }
32
33    /// Create a new waveform visualizer with custom configuration.
34    pub fn with_config(consumer: AudioBridgeConsumer, config: WaveformConfig) -> Self {
35        let buffer_size = config.time_window_samples * 2;
36        Self {
37            consumer,
38            sample_buffer: vec![0.0; buffer_size],
39            write_position: 0,
40            config,
41        }
42    }
43
44    /// Get the current configuration.
45    pub fn config(&self) -> &WaveformConfig {
46        &self.config
47    }
48
49    /// Update the configuration.
50    pub fn set_config(&mut self, config: WaveformConfig) {
51        let buffer_size = config.time_window_samples * 2;
52        if buffer_size != self.sample_buffer.len() {
53            self.sample_buffer.resize(buffer_size, 0.0);
54            self.write_position = 0;
55        }
56        self.config = config;
57    }
58
59    fn find_trigger_point(&self) -> usize {
60        let buffer_len = self.sample_buffer.len();
61        let search_range = buffer_len.saturating_sub(self.config.time_window_samples);
62        let trigger = self.config.trigger_level;
63
64        for i in 0..search_range {
65            let idx = (self.write_position + buffer_len - search_range + i) % buffer_len;
66            let next_idx = (idx + 1) % buffer_len;
67
68            let curr = self.sample_buffer[idx];
69            let next = self.sample_buffer[next_idx];
70
71            if curr <= trigger && next > trigger {
72                return idx;
73            }
74        }
75
76        (self.write_position + buffer_len - self.config.time_window_samples) % buffer_len
77    }
78}
79
80impl Visualizer for WaveformVisualizer {
81    fn update(&mut self) {
82        while let Some(frame) = self.consumer.try_pop() {
83            if let Some(channel_iter) = frame.channel_samples(0) {
84                for sample in channel_iter {
85                    self.sample_buffer[self.write_position] = sample;
86                    self.write_position = (self.write_position + 1) % self.sample_buffer.len();
87                }
88            }
89        }
90    }
91
92    fn draw(&self, draw: &Draw, bounds: Rect) {
93        if let Some(bg) = self.config.background_color {
94            draw.rect().xy(bounds.xy()).wh(bounds.wh()).color(bg);
95        }
96
97        let trigger_point = self.find_trigger_point();
98        let buffer_len = self.sample_buffer.len();
99        let window_samples = self.config.time_window_samples;
100
101        let points: Vec<Point2> = (0..window_samples)
102            .map(|i| {
103                let idx = (trigger_point + i) % buffer_len;
104                let sample = self.sample_buffer[idx];
105
106                let x = bounds.left() + (i as f32 / window_samples as f32) * bounds.w();
107                let y = bounds.y() + sample * (bounds.h() / 2.0);
108
109                Point2::new(x, y)
110            })
111            .collect();
112
113        if points.len() >= 2 {
114            draw.polyline()
115                .weight(self.config.line_weight)
116                .color(self.config.line_color)
117                .points(points);
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::bridge::audio_bridge;
126
127    #[test]
128    fn test_waveform_creation() {
129        let (_producer, consumer) = audio_bridge(4);
130        let visualizer = WaveformVisualizer::new(consumer);
131        assert_eq!(visualizer.sample_buffer.len(), 2048);
132    }
133
134    #[test]
135    fn test_custom_config() {
136        let (_producer, consumer) = audio_bridge(4);
137        let config = WaveformConfig {
138            time_window_samples: 512,
139            ..Default::default()
140        };
141        let visualizer = WaveformVisualizer::with_config(consumer, config);
142        assert_eq!(visualizer.sample_buffer.len(), 1024);
143    }
144}