pub const NUM_BANDS: usize = 3;
pub(crate) const BASE_BUCKETS_PER_SEC: f64 = 150.0;
const COARSEST_TARGET_BUCKETS: usize = 1024;
pub(crate) const CROSSOVER_LOW_HZ: f64 = 200.0;
pub(crate) const CROSSOVER_HIGH_HZ: f64 = 2_000.0;
#[derive(Clone)]
pub struct PeakLevel {
pub buckets_per_sec: f64,
pub pos: [Vec<f32>; NUM_BANDS],
pub neg: [Vec<f32>; NUM_BANDS],
}
impl PeakLevel {
pub fn num_buckets(&self) -> usize {
self.pos[0].len()
}
}
#[derive(Clone)]
pub struct BandPeaks {
levels: Vec<PeakLevel>,
}
struct Biquad {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
z1: f64,
z2: f64,
}
impl Biquad {
fn lowpass(cutoff_hz: f64, sample_rate: f64) -> Self {
let (b0, b1, b2, a0, a1, a2) = {
let w0 = std::f64::consts::TAU * cutoff_hz / sample_rate;
let alpha = w0.sin() / std::f64::consts::SQRT_2;
let cos_w0 = w0.cos();
(
(1.0 - cos_w0) / 2.0,
1.0 - cos_w0,
(1.0 - cos_w0) / 2.0,
1.0 + alpha,
-2.0 * cos_w0,
1.0 - alpha,
)
};
Self::normalized(b0, b1, b2, a0, a1, a2)
}
fn highpass(cutoff_hz: f64, sample_rate: f64) -> Self {
let (b0, b1, b2, a0, a1, a2) = {
let w0 = std::f64::consts::TAU * cutoff_hz / sample_rate;
let alpha = w0.sin() / std::f64::consts::SQRT_2;
let cos_w0 = w0.cos();
(
(1.0 + cos_w0) / 2.0,
-(1.0 + cos_w0),
(1.0 + cos_w0) / 2.0,
1.0 + alpha,
-2.0 * cos_w0,
1.0 - alpha,
)
};
Self::normalized(b0, b1, b2, a0, a1, a2)
}
fn normalized(b0: f64, b1: f64, b2: f64, a0: f64, a1: f64, a2: f64) -> Self {
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
z1: 0.0,
z2: 0.0,
}
}
#[inline]
fn process(&mut self, x: f64) -> f64 {
let y = self.b0 * x + self.z1;
self.z1 = self.b1 * x - self.a1 * y + self.z2;
self.z2 = self.b2 * x - self.a2 * y;
y
}
}
pub(crate) fn base_num_buckets(num_frames: usize, sample_rate: u32) -> usize {
let sr = sample_rate.max(1) as f64;
((num_frames as f64 * BASE_BUCKETS_PER_SEC / sr).ceil() as usize).max(1)
}
impl BandPeaks {
pub fn compute(samples: &[f32], channels: usize, sample_rate: u32) -> Self {
let channels = channels.max(1);
let num_frames = samples.len() / channels;
let sr = sample_rate.max(1) as f64;
let num_buckets = base_num_buckets(num_frames, sample_rate);
let mut pos: [Vec<f32>; NUM_BANDS] = std::array::from_fn(|_| vec![0.0; num_buckets]);
let mut neg: [Vec<f32>; NUM_BANDS] = std::array::from_fn(|_| vec![0.0; num_buckets]);
let mut lp_low = Biquad::lowpass(CROSSOVER_LOW_HZ, sr);
let mut hp_low = Biquad::highpass(CROSSOVER_LOW_HZ, sr);
let mut lp_high = Biquad::lowpass(CROSSOVER_HIGH_HZ, sr);
let mut hp_high = Biquad::highpass(CROSSOVER_HIGH_HZ, sr);
let inv_channels = 1.0 / channels as f64;
let bucket_scale = BASE_BUCKETS_PER_SEC / sr;
for f in 0..num_frames {
let mut mono = 0.0f64;
for c in 0..channels {
mono += samples[f * channels + c] as f64;
}
mono *= inv_channels;
let low = lp_low.process(mono);
let above_low = hp_low.process(mono);
let mid = lp_high.process(above_low);
let high = hp_high.process(above_low);
let bucket = ((f as f64 * bucket_scale) as usize).min(num_buckets - 1);
for (band, sample) in [low, mid, high].into_iter().enumerate() {
let s = sample as f32;
if s > pos[band][bucket] {
pos[band][bucket] = s;
}
if s < neg[band][bucket] {
neg[band][bucket] = s;
}
}
}
Self::from_base_level(PeakLevel {
buckets_per_sec: BASE_BUCKETS_PER_SEC,
pos,
neg,
})
}
pub(crate) fn from_base_level(base: PeakLevel) -> Self {
let mut levels = vec![base];
while levels.last().unwrap().num_buckets() > COARSEST_TARGET_BUCKETS {
levels.push(halve(levels.last().unwrap()));
}
Self { levels }
}
pub fn level_index_for(&self, px_per_sec: f32) -> usize {
self.levels
.iter()
.enumerate()
.filter(|(_, l)| l.buckets_per_sec <= px_per_sec as f64)
.max_by(|(_, a), (_, b)| a.buckets_per_sec.total_cmp(&b.buckets_per_sec))
.map(|(i, _)| i)
.unwrap_or(self.levels.len() - 1)
}
pub fn level(&self, idx: usize) -> &PeakLevel {
&self.levels[idx]
}
pub fn coarsest(&self) -> &PeakLevel {
self.levels.last().unwrap()
}
}
fn halve(level: &PeakLevel) -> PeakLevel {
let n = level.num_buckets().div_ceil(2);
let mut pos: [Vec<f32>; NUM_BANDS] = std::array::from_fn(|_| Vec::with_capacity(n));
let mut neg: [Vec<f32>; NUM_BANDS] = std::array::from_fn(|_| Vec::with_capacity(n));
for band in 0..NUM_BANDS {
for pair in level.pos[band].chunks(2) {
pos[band].push(pair.iter().copied().fold(f32::MIN, f32::max));
}
for pair in level.neg[band].chunks(2) {
neg[band].push(pair.iter().copied().fold(f32::MAX, f32::min));
}
}
PeakLevel {
buckets_per_sec: level.buckets_per_sec / 2.0,
pos,
neg,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_signal(secs: f64) -> Vec<f32> {
let sr = 44_100.0;
let n = (secs * sr) as usize;
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let t = i as f64 / sr;
let s = (0.8 * (std::f64::consts::TAU * 60.0 * t).sin()
+ 0.3 * (std::f64::consts::TAU * 8_000.0 * t).sin()) as f32;
out.push(s);
out.push(s);
}
out
}
#[test]
fn base_resolution_is_150_per_sec() {
let peaks = BandPeaks::compute(&test_signal(10.0), 2, 44_100);
assert_eq!(peaks.levels[0].num_buckets(), 1500);
assert_eq!(peaks.levels[0].buckets_per_sec, 150.0);
}
#[test]
fn pyramid_halves_down_to_coarsest_target() {
let peaks = BandPeaks::compute(&test_signal(60.0), 2, 44_100);
let counts: Vec<usize> = peaks.levels.iter().map(|l| l.num_buckets()).collect();
assert_eq!(counts, vec![9000, 4500, 2250, 1125, 563]);
assert!(peaks.coarsest().num_buckets() <= COARSEST_TARGET_BUCKETS);
}
#[test]
fn halving_preserves_global_extrema() {
let peaks = BandPeaks::compute(&test_signal(30.0), 2, 44_100);
for band in 0..NUM_BANDS {
let global_max = |l: &PeakLevel| l.pos[band].iter().copied().fold(f32::MIN, f32::max);
let global_min = |l: &PeakLevel| l.neg[band].iter().copied().fold(f32::MAX, f32::min);
for pair in peaks.levels.windows(2) {
assert_eq!(global_max(&pair[0]), global_max(&pair[1]));
assert_eq!(global_min(&pair[0]), global_min(&pair[1]));
}
}
}
#[test]
fn bands_separate_low_and_high_content() {
let peaks = BandPeaks::compute(&test_signal(5.0), 2, 44_100);
let level = &peaks.levels[0];
let mid_bucket = level.num_buckets() / 2;
let low = level.pos[0][mid_bucket];
let mid = level.pos[1][mid_bucket];
let high = level.pos[2][mid_bucket];
assert!(low > 0.6, "60 Hz should land in the low band, got {low}");
assert!(high > 0.2, "8 kHz should land in the high band, got {high}");
assert!(
mid < 0.15,
"neither test tone is in the mid band, got {mid}"
);
}
#[test]
fn level_for_picks_finest_level_with_pixel_wide_buckets() {
let peaks = BandPeaks::compute(&test_signal(60.0), 2, 44_100);
let density = |px: f32| peaks.level(peaks.level_index_for(px)).buckets_per_sec;
assert_eq!(density(200.0), 150.0);
assert_eq!(density(150.0), 150.0);
assert_eq!(density(100.0), 75.0);
assert_eq!(density(40.0), 37.5);
assert_eq!(density(1.0), 9.375);
}
#[test]
fn from_base_level_matches_compute() {
let computed = BandPeaks::compute(&test_signal(60.0), 2, 44_100);
let base = PeakLevel {
buckets_per_sec: computed.levels[0].buckets_per_sec,
pos: computed.levels[0].pos.clone(),
neg: computed.levels[0].neg.clone(),
};
let rebuilt = BandPeaks::from_base_level(base);
assert_eq!(rebuilt.levels.len(), computed.levels.len());
for (a, b) in rebuilt.levels.iter().zip(&computed.levels) {
assert_eq!(a.buckets_per_sec, b.buckets_per_sec);
for band in 0..NUM_BANDS {
assert_eq!(a.pos[band], b.pos[band]);
assert_eq!(a.neg[band], b.neg[band]);
}
}
}
#[test]
fn base_num_buckets_matches_compute() {
let secs = 10.0;
let peaks = BandPeaks::compute(&test_signal(secs), 2, 44_100);
let num_frames = (secs * 44_100.0) as usize;
assert_eq!(
peaks.levels[0].num_buckets(),
base_num_buckets(num_frames, 44_100)
);
assert_eq!(base_num_buckets(0, 44_100), 1);
}
#[test]
fn empty_input_yields_single_bucket() {
let peaks = BandPeaks::compute(&[], 2, 44_100);
assert_eq!(peaks.levels[0].num_buckets(), 1);
assert_eq!(peaks.coarsest().num_buckets(), 1);
}
}