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 = 3.0;
fn thr_cap_scale() -> f32 {
static V: OnceLock<f32> = OnceLock::new();
*V.get_or_init(|| {
std::env::var("MP3_THR_CAP")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1.0)
})
}
fn smr_linear() -> f32 {
static V: OnceLock<f32> = OnceLock::new();
*V.get_or_init(|| 10f32.powf(-smr_offset_db() / 10.0))
}
fn smr_offset_db() -> f32 {
static V: OnceLock<f32> = OnceLock::new();
*V.get_or_init(|| {
std::env::var("MP3_SMR_DB")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(SMR_OFFSET_DB)
})
}
#[derive(Debug, Clone, Default)]
pub struct PsyResult {
pub block_type: BlockType,
pub thresholds: [f32; SFB_LONG],
pub perceptual_entropy: f32,
pub signal_energy: 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;
#[must_use]
pub fn sample_ath(freqs_hz: &[f32]) -> Vec<f32> {
freqs_hz.iter().map(|&f| super::ath_db(f)).collect()
}
#[must_use]
pub fn sample_spreading(dz: &[f32]) -> Vec<f32> {
dz.iter().map(|&d| super::spreading_db(d)).collect()
}
#[must_use]
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) -> Self {
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] = (f32::from(sfb[b]) * bin_per_line).round() as usize;
bin_hi[b] =
((f32::from(sfb[b + 1]) * bin_per_line).round() as usize).min(N_FFT / 2 + 1);
let center_line = (f32::from(sfb[b]) + f32::from(sfb[b + 1])) * 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);
}
}
Self {
bin_lo,
bin_hi,
spread,
ath_base,
}
}
}
const MPEG_RATES: [u32; 9] = [8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000];
fn cached_band_model(sample_rate: u32) -> Option<&'static BandModel> {
static MODELS: [OnceLock<BandModel>; 9] = [const { OnceLock::new() }; 9];
let i = MPEG_RATES.iter().position(|&r| r == sample_rate)?;
Some(MODELS[i].get_or_init(|| BandModel::new(sample_rate)))
}
#[must_use]
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 mut blocks = pcm[..n]
.chunks_exact(bs)
.map(|c| c.iter().map(|x| x * x).sum::<f32>() / bs as f32)
.take(BLOCKS);
let Some(first) = blocks.next() else {
return false;
};
let mut running = first.max(1e-9);
for e in blocks {
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 uncached;
let model: &BandModel = if let Some(m) = cached_band_model(sample_rate) {
m
} else {
uncached = BandModel::new(sample_rate);
&uncached
};
let mut re = [0f32; N_FFT];
let mut im = [0f32; N_FFT];
for ((r, &p), &w) in re.iter_mut().zip(pcm.iter()).zip(win.iter()) {
*r = p * w;
}
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[model.bin_lo[b]..model.bin_hi[b]] {
e += p;
}
energy[b] = e;
}
let smr = smr_linear();
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;
{
use std::sync::atomic::Ordering::Relaxed;
let m = masker * smr;
if m.max(ath) >= energy[i] {
crate::encode::prof::THR_CAPPED.fetch_add(1, Relaxed);
} else if ath > m {
crate::encode::prof::THR_ATH.fetch_add(1, Relaxed);
} else {
crate::encode::prof::THR_MASK.fetch_add(1, Relaxed);
}
}
let cap = energy[i] * thr_cap_scale();
thresholds[i] = (masker * smr).max(ath).min(cap);
}
let mut pe = 0f32;
for b in 0..SFB_LONG {
let lines = f32::from(sfb[b + 1] - sfb[b]);
pe += lines * (1.0 + energy[b] / thresholds[b]).log2().max(0.0);
}
PsyResult {
block_type: BlockType::Long, thresholds,
perceptual_entropy: pe,
signal_energy: total_energy,
}
}
#[cfg(test)]
mod tests {
#[test]
fn band_model_cache_is_one_shared_model_per_rate() {
let first = std::ptr::from_ref::<BandModel>(cached_band_model(44_100).unwrap());
for _ in 0..100 {
let _ = cached_band_model(48_000).unwrap();
assert!(std::ptr::eq(first, cached_band_model(44_100).unwrap()));
}
let from_thread = std::thread::spawn(|| {
std::ptr::from_ref::<BandModel>(cached_band_model(44_100).unwrap()) as usize
})
.join()
.unwrap();
assert_eq!(from_thread, first as usize);
assert!(cached_band_model(7_999).is_none());
let psy = analyze(&[0.25f32; 1152], 7_999);
assert!(psy.perceptual_entropy.is_finite());
}
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 = (f32::from(sfb[b]) * bin_per_line).round() as usize;
let hi =
((f32::from(sfb[b + 1]) * 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 = (f32::from(sfb[b]) + f32::from(sfb[b + 1])) * 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 = (f32::from(sfb[b]) * N_FFT as f32 / 1152.0).round() as usize;
let hi = ((f32::from(sfb[b + 1]) * 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);
}
}