Krolyxon
Branch master

rmptuisrcvisualizerbars.rs

130 lines · 4.1 KB · Rust

1//! Turning spectra into smoothly animated bars.
2
3use std::time::Instant;
4
5use super::AudioFrame;
6
7/// Bar heights (0.0–1.0) with gravity falloff between frames.
8#[derive(Debug)]
9pub struct Bars {
10 heights: Vec<f32>,
11 last_update: Instant,
12}
13
14impl Default for Bars {
15 fn default() -> Self {
16 Self {
17 heights: Vec::new(),
18 last_update: Instant::now(),
19 }
20 }
21}
22
23/// Decibel range mapped onto the bar height.
24const FLOOR_DB: f32 = -60.0;
25const CEILING_DB: f32 = 0.0;
26
27impl Bars {
28 /// Updates `count` bars from `spectrum`, covering `min_hz..max_hz` on a
29 /// logarithmic scale. `falloff` is in heights per second.
30 pub fn update(
31 &mut self,
32 spectrum: &AudioFrame,
33 count: usize,
34 min_hz: f32,
35 max_hz: f32,
36 falloff: f32,
37 ) -> &[f32] {
38 let now = Instant::now();
39 let dt = now.duration_since(self.last_update).as_secs_f32().min(0.25);
40 self.last_update = now;
41 self.heights.resize(count, 0.0);
42 let targets = band_levels(spectrum, count, min_hz, max_hz);
43 for (height, target) in self.heights.iter_mut().zip(targets) {
44 *height = if target >= *height {
45 // Rise quickly but not instantly, to avoid flicker.
46 *height + (target - *height) * 0.7
47 } else {
48 (*height - falloff * dt).max(target)
49 };
50 }
51 &self.heights
52 }
53}
54
55/// Levels (0.0–1.0) of `count` log-spaced bands.
56pub fn band_levels(spectrum: &AudioFrame, count: usize, min_hz: f32, max_hz: f32) -> Vec<f32> {
57 let bins = &spectrum.magnitudes;
58 if count == 0 || bins.is_empty() || spectrum.bin_hz <= 0.0 {
59 return vec![0.0; count];
60 }
61 let max_hz = max_hz.min(spectrum.bin_hz * (bins.len() - 1) as f32);
62 let ratio = (max_hz / min_hz).max(1.0);
63 (0..count)
64 .map(|band| {
65 let lo = min_hz * ratio.powf(band as f32 / count as f32);
66 let hi = min_hz * ratio.powf((band + 1) as f32 / count as f32);
67 let first = (lo / spectrum.bin_hz).floor() as usize;
68 let last = ((hi / spectrum.bin_hz).ceil() as usize)
69 .max(first + 1)
70 .min(bins.len());
71 let peak = bins[first.min(bins.len() - 1)..last]
72 .iter()
73 .copied()
74 .fold(0.0f32, f32::max);
75 // Tilt +3 dB per octave so that treble is not dwarfed by bass.
76 let center = (lo * hi).sqrt();
77 let tilt = 3.0 * (center / 1000.0).log2();
78 let db = 20.0 * peak.max(1e-9).log10() + tilt;
79 ((db - FLOOR_DB) / (CEILING_DB - FLOOR_DB)).clamp(0.0, 1.0)
80 })
81 .collect()
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn spectrum_with_peak(hz: f32) -> AudioFrame {
89 let bin_hz = 44_100.0 / 2048.0;
90 let mut magnitudes = vec![0.0; 1025];
91 magnitudes[(hz / bin_hz).round() as usize] = 1.0;
92 AudioFrame {
93 magnitudes,
94 bin_hz,
95 ..AudioFrame::default()
96 }
97 }
98
99 #[test]
100 fn peak_lands_in_the_right_band() {
101 let levels = band_levels(&spectrum_with_peak(1000.0), 20, 40.0, 16_000.0);
102 let loudest = levels
103 .iter()
104 .enumerate()
105 .max_by(|a, b| a.1.total_cmp(b.1))
106 .unwrap()
107 .0;
108 // 1 kHz sits a bit past the middle of 40 Hz–16 kHz on a log scale.
109 assert!((10..=12).contains(&loudest), "loudest band {loudest}");
110 assert!(levels[0] == 0.0);
111 }
112
113 #[test]
114 fn silence_and_empty_input_give_zero() {
115 assert_eq!(
116 band_levels(&AudioFrame::default(), 4, 40.0, 16_000.0),
117 [0.0; 4]
118 );
119 }
120
121 #[test]
122 fn bars_fall_gradually() {
123 let mut bars = Bars::default();
124 let loud = spectrum_with_peak(1000.0);
125 let peak = bars.update(&loud, 20, 40.0, 16_000.0, 1.0).to_vec();
126 let after = bars.update(&AudioFrame::default(), 20, 40.0, 16_000.0, 1.0);
127 let i = peak.iter().position(|&h| h > 0.0).unwrap();
128 assert!(after[i] > 0.0 && after[i] <= peak[i]);
129 }
130}