use super::pairwise::{pairwise_mean, pairwise_sum};
use super::welch::welch;
use super::window_sum::{exact_sum, window_sums};
use crate::error::{Error, Result};
use crate::resample::resample_poly;
const HISS_FLOOR_HZ: f64 = 8000.0;
const HISS_SEGMENT: usize = 4096;
const LOUD_SECS: f64 = 0.05;
const TAIL_SECS: f64 = 0.005;
const OVERSAMPLE: usize = 4;
pub fn hiss_db(m: &[f64], rate: u32) -> Result<f64> {
let psd = welch(m, f64::from(rate), m.len().min(HISS_SEGMENT))?;
let high: Vec<f64> = psd
.freqs
.iter()
.zip(&psd.power)
.filter(|(f, _)| **f > HISS_FLOOR_HZ)
.map(|(_, p)| *p)
.collect();
let share = pairwise_sum(&high) / (pairwise_sum(&psd.power) + 1e-20);
Ok(10.0 * (share + 1e-20).log10())
}
fn loudest_rms(m: &[f64], w: usize) -> f64 {
let scale = 1.0 / w as f64;
let energy: Vec<f64> = m.iter().map(|v| v * v * scale).collect();
let peak = if m.len() >= w {
window_sums(&energy, w)
.into_iter()
.fold(f64::NEG_INFINITY, f64::max)
} else {
exact_sum(&energy)
};
(peak + 1e-20).sqrt()
}
pub fn tail_db(m: &[f64], rate: u32) -> Result<f64> {
let w = (LOUD_SECS * f64::from(rate)) as usize;
if m.is_empty() || w == 0 {
return Err(Error::InvalidArgument {
arg: "tail_db",
reason: format!(
"needs samples ({}) and a rate of at least 20 Hz ({rate})",
m.len()
),
});
}
let loud = loudest_rms(m, w);
let k = (TAIL_SECS * f64::from(rate)) as usize;
let tail = if k == 0 {
m
} else {
&m[m.len().saturating_sub(k)..]
};
let squares: Vec<f64> = tail.iter().map(|v| v * v).collect();
let rms = pairwise_mean(&squares).sqrt();
Ok(20.0 * (rms / loud + 1e-12).log10())
}
pub fn true_peak_dbtp(channels: &[Vec<f64>]) -> Result<f64> {
if channels.iter().all(Vec::is_empty) {
return Err(Error::InvalidArgument {
arg: "true_peak_dbtp",
reason: "needs at least one sample".to_string(),
});
}
let mut peak = 0.0f64;
for channel in channels {
for v in resample_poly(channel, OVERSAMPLE, 1)? {
peak = peak.max(v.abs());
}
}
Ok(20.0 * (peak + 1e-12).log10())
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn tone(freq: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| (2.0 * PI * freq * i as f64 / 48_000.0).sin())
.collect()
}
#[test]
fn a_low_tone_has_no_hiss_and_a_high_one_is_all_hiss() {
assert!(hiss_db(&tone(500.0, 48_000), 48_000).expect("low") < -60.0);
assert!(hiss_db(&tone(12_000.0, 48_000), 48_000).expect("high") > -0.1);
}
#[test]
fn a_steady_tone_ends_at_its_own_level() {
let t = tail_db(&tone(1000.0, 48_000), 48_000).expect("tail");
assert!(t.abs() < 0.1, "tail {t}");
}
#[test]
fn a_fade_out_ends_far_below() {
let mut x = tone(1000.0, 48_000);
let n = x.len() as f64;
for (i, v) in x.iter_mut().enumerate() {
*v *= 1.0 - i as f64 / n;
}
assert!(tail_db(&x, 48_000).expect("tail") < -40.0);
}
#[test]
fn true_peak_finds_the_peak_between_samples() {
let x: Vec<f64> = (0..4800)
.map(|i| (PI / 2.0 * i as f64 + PI / 4.0).sin())
.collect();
let tp = true_peak_dbtp(&[x]).expect("tp");
assert!(tp > -0.5 && tp < 0.5, "true peak {tp}");
}
#[test]
fn empty_input_is_refused() {
assert!(tail_db(&[], 48_000).is_err());
assert!(true_peak_dbtp(&[Vec::new()]).is_err());
assert!(hiss_db(&[], 48_000).is_err());
}
}