bbx_draw/visualizers/
spectrum.rs1use nannou::{
4 Draw,
5 geom::{Point2, Rect},
6};
7use rustfft::{FftPlanner, num_complex::Complex};
8
9use crate::{
10 Visualizer,
11 bridge::AudioBridgeConsumer,
12 config::{SpectrumConfig, SpectrumDisplayMode},
13};
14
15pub struct SpectrumAnalyzer {
20 consumer: AudioBridgeConsumer,
21 config: SpectrumConfig,
22 fft_planner: FftPlanner<f32>,
23 window: Vec<f32>,
24 sample_buffer: Vec<f32>,
25 fft_buffer: Vec<Complex<f32>>,
26 magnitudes: Vec<f32>,
27 smoothed_magnitudes: Vec<f32>,
28 peaks: Vec<f32>,
29 write_position: usize,
30}
31
32impl SpectrumAnalyzer {
33 pub fn new(consumer: AudioBridgeConsumer) -> Self {
35 let config = SpectrumConfig::default();
36 Self::with_config(consumer, config)
37 }
38
39 pub fn with_config(consumer: AudioBridgeConsumer, config: SpectrumConfig) -> Self {
41 let fft_size = config.fft_size;
42 let window = hann_window(fft_size);
43 let num_bins = fft_size / 2;
44
45 Self {
46 consumer,
47 fft_planner: FftPlanner::new(),
48 window,
49 sample_buffer: vec![0.0; fft_size],
50 fft_buffer: vec![Complex::new(0.0, 0.0); fft_size],
51 magnitudes: vec![0.0; num_bins],
52 smoothed_magnitudes: vec![config.min_db; num_bins],
53 peaks: vec![config.min_db; num_bins],
54 write_position: 0,
55 config,
56 }
57 }
58
59 pub fn config(&self) -> &SpectrumConfig {
61 &self.config
62 }
63
64 pub fn set_config(&mut self, config: SpectrumConfig) {
66 if config.fft_size != self.config.fft_size {
67 let fft_size = config.fft_size;
68 let num_bins = fft_size / 2;
69 self.window = hann_window(fft_size);
70 self.sample_buffer = vec![0.0; fft_size];
71 self.fft_buffer = vec![Complex::new(0.0, 0.0); fft_size];
72 self.magnitudes = vec![0.0; num_bins];
73 self.smoothed_magnitudes = vec![config.min_db; num_bins];
74 self.peaks = vec![config.min_db; num_bins];
75 self.write_position = 0;
76 }
77 self.config = config;
78 }
79
80 fn compute_fft(&mut self) {
81 let fft_size = self.config.fft_size;
82 let fft = self.fft_planner.plan_fft_forward(fft_size);
83
84 for i in 0..fft_size {
85 let idx = (self.write_position + i) % fft_size;
86 self.fft_buffer[i] = Complex::new(self.sample_buffer[idx] * self.window[i], 0.0);
87 }
88
89 fft.process(&mut self.fft_buffer);
90
91 let num_bins = fft_size / 2;
92 let scale = 2.0 / fft_size as f32;
93
94 for i in 0..num_bins {
95 let magnitude = self.fft_buffer[i].norm() * scale;
96 self.magnitudes[i] = amplitude_to_db(magnitude);
97 }
98 }
99
100 fn apply_smoothing(&mut self) {
101 let smoothing = self.config.smoothing;
102 for i in 0..self.magnitudes.len() {
103 self.smoothed_magnitudes[i] =
104 smoothing * self.smoothed_magnitudes[i] + (1.0 - smoothing) * self.magnitudes[i];
105 }
106 }
107
108 fn update_peaks(&mut self) {
109 let decay = self.config.peak_decay;
110 for i in 0..self.magnitudes.len() {
111 if self.smoothed_magnitudes[i] > self.peaks[i] {
112 self.peaks[i] = self.smoothed_magnitudes[i];
113 } else {
114 self.peaks[i] -= decay;
115 self.peaks[i] = self.peaks[i].max(self.config.min_db);
116 }
117 }
118 }
119
120 fn db_to_normalized(&self, db: f32) -> f32 {
121 let range = self.config.max_db - self.config.min_db;
122 ((db - self.config.min_db) / range).clamp(0.0, 1.0)
123 }
124}
125
126impl Visualizer for SpectrumAnalyzer {
127 fn update(&mut self) {
128 let mut has_data = false;
129
130 while let Some(frame) = self.consumer.try_pop() {
131 if let Some(channel_iter) = frame.channel_samples(0) {
132 for sample in channel_iter {
133 self.sample_buffer[self.write_position] = sample;
134 self.write_position = (self.write_position + 1) % self.config.fft_size;
135 has_data = true;
136 }
137 }
138 }
139
140 if has_data {
141 self.compute_fft();
142 self.apply_smoothing();
143 if self.config.show_peaks {
144 self.update_peaks();
145 }
146 }
147 }
148
149 fn draw(&self, draw: &Draw, bounds: Rect) {
150 let num_bins = self.smoothed_magnitudes.len();
151 if num_bins == 0 {
152 return;
153 }
154
155 match self.config.display_mode {
156 SpectrumDisplayMode::Bars => {
157 let bar_width = bounds.w() / num_bins as f32;
158
159 for (i, &db) in self.smoothed_magnitudes.iter().enumerate() {
160 let height = self.db_to_normalized(db) * bounds.h();
161 let x = bounds.left() + (i as f32 + 0.5) * bar_width;
162 let y = bounds.bottom() + height / 2.0;
163
164 draw.rect()
165 .x_y(x, y)
166 .w_h(bar_width * 0.8, height)
167 .color(self.config.bar_color);
168
169 if self.config.show_peaks {
170 let peak_height = self.db_to_normalized(self.peaks[i]) * bounds.h();
171 let peak_y = bounds.bottom() + peak_height;
172 draw.line()
173 .start(Point2::new(x - bar_width * 0.4, peak_y))
174 .end(Point2::new(x + bar_width * 0.4, peak_y))
175 .weight(2.0)
176 .color(self.config.peak_color);
177 }
178 }
179 }
180 SpectrumDisplayMode::Line => {
181 let points: Vec<Point2> = self
182 .smoothed_magnitudes
183 .iter()
184 .enumerate()
185 .map(|(i, &db)| {
186 let x = bounds.left() + (i as f32 / num_bins as f32) * bounds.w();
187 let y = bounds.bottom() + self.db_to_normalized(db) * bounds.h();
188 Point2::new(x, y)
189 })
190 .collect();
191
192 if points.len() >= 2 {
193 draw.polyline().weight(2.0).color(self.config.bar_color).points(points);
194 }
195 }
196 SpectrumDisplayMode::Filled => {
197 let mut points: Vec<Point2> = Vec::with_capacity(num_bins + 2);
198
199 points.push(Point2::new(bounds.left(), bounds.bottom()));
200
201 for (i, &db) in self.smoothed_magnitudes.iter().enumerate() {
202 let x = bounds.left() + (i as f32 / num_bins as f32) * bounds.w();
203 let y = bounds.bottom() + self.db_to_normalized(db) * bounds.h();
204 points.push(Point2::new(x, y));
205 }
206
207 points.push(Point2::new(bounds.right(), bounds.bottom()));
208
209 if points.len() >= 3 {
210 draw.polygon().color(self.config.bar_color).points(points);
211 }
212 }
213 }
214 }
215}
216
217fn hann_window(size: usize) -> Vec<f32> {
218 (0..size)
219 .map(|i| 0.5 * (1.0 - (std::f32::consts::TAU * i as f32 / size as f32).cos()))
220 .collect()
221}
222
223fn amplitude_to_db(amplitude: f32) -> f32 {
224 if amplitude <= 0.0 {
225 -120.0
226 } else {
227 20.0 * amplitude.log10()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::bridge::audio_bridge;
235
236 #[test]
237 fn test_spectrum_creation() {
238 let (_producer, consumer) = audio_bridge(4);
239 let analyzer = SpectrumAnalyzer::new(consumer);
240 assert_eq!(analyzer.sample_buffer.len(), 2048);
241 assert_eq!(analyzer.magnitudes.len(), 1024);
242 }
243
244 #[test]
245 fn test_hann_window() {
246 let window = hann_window(4);
247 assert!((window[0] - 0.0).abs() < 0.001);
248 assert!((window[1] - 0.5).abs() < 0.001);
249 assert!((window[2] - 1.0).abs() < 0.001);
250 assert!((window[3] - 0.5).abs() < 0.001);
251 }
252
253 #[test]
254 fn test_amplitude_to_db() {
255 assert!((amplitude_to_db(1.0) - 0.0).abs() < 0.001);
256 assert!((amplitude_to_db(0.1) - (-20.0)).abs() < 0.001);
257 }
258}