bbx_draw/visualizers/
waveform.rs1use nannou::{
4 Draw,
5 geom::{Point2, Rect},
6};
7
8use crate::{Visualizer, bridge::AudioBridgeConsumer, config::WaveformConfig};
9
10pub struct WaveformVisualizer {
14 consumer: AudioBridgeConsumer,
15 config: WaveformConfig,
16 sample_buffer: Vec<f32>,
17 write_position: usize,
18}
19
20impl WaveformVisualizer {
21 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 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 pub fn config(&self) -> &WaveformConfig {
46 &self.config
47 }
48
49 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}