use std::sync::OnceLock;
use crate::frame::{BlockType, SFB_LONG};
use crate::tables;
use super::fft;
const N_FFT: usize = 1024;
const SMR_OFFSET_DB: f32 = 12.0;
#[derive(Debug, Clone, Default)]
pub struct PsyResult {
pub block_type: BlockType,
pub thresholds: [f32; SFB_LONG],
pub perceptual_entropy: f32,
}
fn hann() -> &'static [f32; N_FFT] {
static W: OnceLock<[f32; N_FFT]> = OnceLock::new();
W.get_or_init(|| {
let mut w = [0f32; N_FFT];
for (n, wn) in w.iter_mut().enumerate() {
*wn = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * n as f32 / N_FFT as f32).cos());
}
w
})
}
fn bark(f: f32) -> f32 {
13.0 * (0.000_76 * f).atan() + 3.5 * (f / 7500.0).powi(2).atan()
}
fn spreading_db(dz: f32) -> f32 {
let x = dz + 0.474;
15.81 + 7.5 * x - 17.5 * (1.0 + x * x).sqrt()
}
fn ath_db(f: f32) -> f32 {
let k = (f / 1000.0).max(0.01);
3.64 * k.powf(-0.8) - 6.5 * (-0.6 * (k - 3.3).powi(2)).exp() + 1e-3 * k.powi(4)
}
#[cfg(feature = "prometheus-telemetry")]
pub mod prometheus {
pub const TELEMETRY_SCHEMA: u32 = 1;
pub fn sample_ath(freqs_hz: &[f32]) -> Vec<f32> {
freqs_hz.iter().map(|&f| super::ath_db(f)).collect()
}
pub fn sample_spreading(dz: &[f32]) -> Vec<f32> {
dz.iter().map(|&d| super::spreading_db(d)).collect()
}
pub fn sample_bark(freqs_hz: &[f32]) -> Vec<f32> {
freqs_hz.iter().map(|&f| super::bark(f)).collect()
}
}
struct BandModel {
bin_lo: [usize; SFB_LONG],
bin_hi: [usize; SFB_LONG],
spread: [[f32; SFB_LONG]; SFB_LONG],
ath_base: [f32; SFB_LONG],
}
impl BandModel {
fn new(sample_rate: u32) -> BandModel {
let sfb = tables::sfb_long_offsets(sample_rate);
let bin_per_line = N_FFT as f32 / 1152.0;
let mut bin_lo = [0usize; SFB_LONG];
let mut bin_hi = [0usize; SFB_LONG];
let mut center_bark = [0f32; SFB_LONG];
let mut ath_base = [0f32; SFB_LONG];
for b in 0..SFB_LONG {
bin_lo[b] = (sfb[b] as f32 * bin_per_line).round() as usize;
bin_hi[b] = ((sfb[b + 1] as f32 * bin_per_line).round() as usize).min(N_FFT / 2 + 1);
let center_line = (sfb[b] as f32 + sfb[b + 1] as f32) * 0.5;
center_bark[b] = bark(center_line * sample_rate as f32 / 1152.0);
ath_base[b] = 10f32.powf(ath_db(center_line * sample_rate as f32 / 1152.0) / 10.0);
}
let mut spread = [[0f32; SFB_LONG]; SFB_LONG];
for i in 0..SFB_LONG {
for j in 0..SFB_LONG {
spread[i][j] = 10f32.powf(spreading_db(center_bark[i] - center_bark[j]) / 10.0);
}
}
BandModel {
bin_lo,
bin_hi,
spread,
ath_base,
}
}
}
fn band_model(sample_rate: u32) -> &'static BandModel {
use std::cell::RefCell;
thread_local! {
static CACHE: RefCell<Option<(u32, &'static BandModel)>> = const { RefCell::new(None) };
}
CACHE.with(|cell| {
let mut c = cell.borrow_mut();
if let Some((sr, m)) = *c {
if sr == sample_rate {
return m;
}
}
let m: &'static BandModel = Box::leak(Box::new(BandModel::new(sample_rate)));
*c = Some((sample_rate, m));
m
})
}
pub fn detect_attack(pcm: &[f32]) -> bool {
const BLOCKS: usize = 8;
const RATIO: f32 = 10.0;
let n = pcm.len().min(crate::frame::GRANULE_LINES);
let bs = n / BLOCKS;
if bs == 0 {
return false;
}
let energy = |b: usize| -> f32 {
pcm[b * bs..(b + 1) * bs].iter().map(|x| x * x).sum::<f32>() / bs as f32
};
let mut running = energy(0).max(1e-9);
for b in 1..BLOCKS {
let e = energy(b);
if e > running * RATIO {
return true;
}
running = (running * 2.0 + e) / 3.0; }
false
}
pub fn analyze(pcm: &[f32], sample_rate: u32) -> PsyResult {
let sfb = tables::sfb_long_offsets(sample_rate);
let win = hann();
let model = band_model(sample_rate);
let mut re = [0f32; N_FFT];
let mut im = [0f32; N_FFT];
let navail = pcm.len().min(N_FFT);
for i in 0..navail {
re[i] = pcm[i] * win[i];
}
let mut power = [0f32; N_FFT / 2 + 1];
fft::power_spectrum(&mut re, &mut im, &mut power);
let mut energy = [0f32; SFB_LONG];
for b in 0..SFB_LONG {
let mut e = 1e-12f32;
for &p in power.iter().take(model.bin_hi[b]).skip(model.bin_lo[b]) {
e += p;
}
energy[b] = e;
}
let smr = 10f32.powf(-SMR_OFFSET_DB / 10.0);
let total_energy: f32 = energy.iter().sum();
let ath_scale = (total_energy / N_FFT as f32).max(1e-9) * 1e-3;
let mut thresholds = [0f32; SFB_LONG];
for i in 0..SFB_LONG {
let row = &model.spread[i];
let mut masker = 0f32;
for j in 0..SFB_LONG {
masker += energy[j] * row[j];
}
let ath = model.ath_base[i] * ath_scale;
thresholds[i] = (masker * smr).max(ath).min(energy[i]);
}
let mut pe = 0f32;
for b in 0..SFB_LONG {
let lines = (sfb[b + 1] - sfb[b]) as f32;
pe += lines * (1.0 + energy[b] / thresholds[b]).log2().max(0.0);
}
PsyResult {
block_type: BlockType::Long, thresholds,
perceptual_entropy: pe,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bark_is_monotonic_and_spans_critical_bands() {
assert!(bark(100.0) < bark(1000.0) && bark(1000.0) < bark(10000.0));
assert!(
(bark(1000.0) - 8.5).abs() < 1.5,
"1 kHz ≈ 8.5 Bark, got {}",
bark(1000.0)
);
}
#[test]
fn ath_has_a_minimum_in_the_ear_canal_band() {
assert!(ath_db(3500.0) < ath_db(200.0));
assert!(ath_db(3500.0) < ath_db(15000.0));
}
#[test]
fn spreading_peaks_at_zero_and_decays() {
assert!(spreading_db(0.0).abs() < 0.5);
assert!(spreading_db(2.0) < -3.0);
assert!(spreading_db(-2.0) < -3.0);
assert!(spreading_db(1.5) > spreading_db(-1.5));
}
#[test]
fn band_model_matches_inline() {
for &sr in &[44100u32, 48000, 32000, 24000] {
let sfb = tables::sfb_long_offsets(sr);
let m = BandModel::new(sr);
let bin_per_line = N_FFT as f32 / 1152.0;
let mut cb = [0f32; SFB_LONG];
for b in 0..SFB_LONG {
let lo = (sfb[b] as f32 * bin_per_line).round() as usize;
let hi = ((sfb[b + 1] as f32 * bin_per_line).round() as usize).min(N_FFT / 2 + 1);
assert_eq!(m.bin_lo[b], lo, "bin_lo b{b} sr{sr}");
assert_eq!(m.bin_hi[b], hi, "bin_hi b{b} sr{sr}");
let center_line = (sfb[b] as f32 + sfb[b + 1] as f32) * 0.5;
cb[b] = bark(center_line * sr as f32 / 1152.0);
let ath = 10f32.powf(ath_db(center_line * sr as f32 / 1152.0) / 10.0);
assert_eq!(m.ath_base[b], ath, "ath_base b{b} sr{sr}");
}
for i in 0..SFB_LONG {
for j in 0..SFB_LONG {
let s = 10f32.powf(spreading_db(cb[i] - cb[j]) / 10.0);
assert_eq!(m.spread[i][j], s, "spread[{i}][{j}] sr{sr}");
}
}
}
}
#[test]
fn tone_thresholds_are_bounded_by_signal_energy() {
let sr = 44100;
let pcm: Vec<f32> = (0..N_FFT)
.map(|i| 0.5 * (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / sr as f32).sin())
.collect();
let psy = analyze(&pcm, sr);
let peak_energy = {
let sfb = tables::sfb_long_offsets(sr);
let win = hann();
let mut re = [0f32; N_FFT];
let mut im = [0f32; N_FFT];
for i in 0..N_FFT {
re[i] = pcm[i] * win[i];
}
let mut p = [0f32; N_FFT / 2 + 1];
fft::power_spectrum(&mut re, &mut im, &mut p);
let mut m = 0f32;
for b in 0..SFB_LONG {
let lo = (sfb[b] as f32 * N_FFT as f32 / 1152.0).round() as usize;
let hi = ((sfb[b + 1] as f32 * N_FFT as f32 / 1152.0).round() as usize)
.min(N_FFT / 2 + 1);
let e: f32 = p[lo..hi].iter().sum();
m = m.max(e);
}
m
};
for (b, &t) in psy.thresholds.iter().enumerate() {
assert!(t.is_finite() && t > 0.0, "band {b} threshold {t}");
assert!(
t <= peak_energy * 1.01,
"band {b} threshold {t} > peak {peak_energy}"
);
}
assert!(psy.perceptual_entropy > 0.0);
}
}