| 1 | //! Turning spectra into smoothly animated bars. |
| 2 | |
| 3 | use std::time::Instant; |
| 4 | |
| 5 | use super::Spectrum; |
| 6 | |
| 7 | /// Bar heights (0.0–1.0) with gravity falloff between frames. |
| 8 | #[derive(Debug)] |
| 9 | pub struct Bars { |
| 10 | heights: Vec<f32>, |
| 11 | last_update: Instant, |
| 12 | } |
| 13 | |
| 14 | impl 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. |
| 24 | const FLOOR_DB: f32 = -60.0; |
| 25 | const CEILING_DB: f32 = 0.0; |
| 26 | |
| 27 | impl 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: &Spectrum, |
| 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. |
| 56 | pub fn band_levels(spectrum: &Spectrum, 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)] |
| 85 | mod tests { |
| 86 | use super::*; |
| 87 | |
| 88 | fn spectrum_with_peak(hz: f32) -> Spectrum { |
| 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 | Spectrum { |
| 93 | magnitudes, |
| 94 | bin_hz, |
| 95 | error: None, |
| 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(&Spectrum::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(&Spectrum::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 | } |