#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct BitrateEstimate {
pub kbps_class: u32,
pub confidence: f32,
}
impl BitrateEstimate {
#[must_use]
pub fn new(kbps_class: u32, confidence: f32) -> Self {
Self {
kbps_class,
confidence: confidence.clamp(0.0, 1.0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpectralHole {
pub center_hz: f32,
pub width_hz: f32,
pub depth_db: f32,
}
#[derive(Debug, Clone)]
pub struct CompressionScore {
pub hole_count: usize,
pub mean_hole_depth_db: f32,
pub estimated_cutoff_hz: f32,
pub score: f32,
}
#[must_use]
pub fn detect_spectral_holes(
magnitudes: &[f32],
sample_rate: f32,
threshold_db: f32,
window_bins: usize,
) -> Vec<SpectralHole> {
let n = magnitudes.len();
if n < 2 * window_bins + 1 || sample_rate <= 0.0 {
return Vec::new();
}
let n_fft = (n - 1) * 2;
let hz_per_bin = sample_rate / n_fft as f32;
let mut holes = Vec::new();
let mut skip_until = 0usize;
for i in window_bins..n - window_bins {
if i < skip_until {
continue;
}
let left_avg: f32 = magnitudes[i - window_bins..i].iter().sum::<f32>() / window_bins as f32;
let right_avg: f32 =
magnitudes[i + 1..i + 1 + window_bins].iter().sum::<f32>() / window_bins as f32;
let neighbour_avg = (left_avg + right_avg) * 0.5;
let bin_val = magnitudes[i].max(1e-10);
let ratio = neighbour_avg / bin_val;
let depth_db = 20.0 * ratio.max(1e-10).log10();
if depth_db >= threshold_db {
let center_hz = i as f32 * hz_per_bin;
holes.push(SpectralHole {
center_hz,
width_hz: hz_per_bin,
depth_db,
});
skip_until = i + window_bins; }
}
holes
}
#[must_use]
pub fn estimate_cutoff_frequency(magnitudes: &[f32], sample_rate: f32, noise_floor_db: f32) -> f32 {
let n = magnitudes.len();
if n == 0 || sample_rate <= 0.0 {
return 0.0;
}
let peak = magnitudes.iter().copied().fold(0.0_f32, f32::max);
if peak < 1e-10 {
return 0.0;
}
let min_linear = peak * 10.0_f32.powf(noise_floor_db / 20.0);
let n_fft = (n - 1) * 2;
let hz_per_bin = sample_rate / n_fft as f32;
for (i, &m) in magnitudes.iter().enumerate().rev() {
if m >= min_linear {
return i as f32 * hz_per_bin;
}
}
0.0
}
#[must_use]
pub fn estimate_bitrate_class(cutoff_hz: f32) -> BitrateEstimate {
let (kbps, confidence): (u32, f32) = if cutoff_hz < 12_000.0 {
(64, 0.8)
} else if cutoff_hz < 15_000.0 {
(96, 0.7)
} else if cutoff_hz < 17_500.0 {
(128, 0.75)
} else if cutoff_hz < 19_500.0 {
(192, 0.7)
} else {
(320, 0.6)
};
BitrateEstimate::new(kbps, confidence)
}
#[must_use]
pub fn analyse_compression(
magnitudes: &[f32],
sample_rate: f32,
noise_floor_db: f32,
) -> CompressionScore {
let holes = detect_spectral_holes(magnitudes, sample_rate, 15.0, 4);
let hole_count = holes.len();
let mean_hole_depth_db = if hole_count == 0 {
0.0
} else {
holes.iter().map(|h| h.depth_db).sum::<f32>() / hole_count as f32
};
let estimated_cutoff_hz = estimate_cutoff_frequency(magnitudes, sample_rate, noise_floor_db);
let nyquist = sample_rate / 2.0;
let cutoff_score = if nyquist > 0.0 {
1.0 - (estimated_cutoff_hz / nyquist).min(1.0)
} else {
0.0
};
let hole_score = (hole_count as f32 / 10.0).min(1.0);
let score = (cutoff_score * 0.6 + hole_score * 0.4).clamp(0.0, 1.0);
CompressionScore {
hole_count,
mean_hole_depth_db,
estimated_cutoff_hz,
score,
}
}
#[must_use]
pub fn spectral_entropy(magnitudes: &[f32]) -> f32 {
let total: f32 = magnitudes.iter().sum();
if total < 1e-10 {
return 0.0;
}
magnitudes
.iter()
.map(|&m| {
let p = m / total;
if p > 0.0 {
-p * p.ln()
} else {
0.0
}
})
.sum()
}
#[must_use]
pub fn spectral_continuity_index(magnitudes: &[f32]) -> f32 {
let n = magnitudes.len();
if n < 2 {
return 0.0;
}
let mean: f32 = magnitudes.iter().sum::<f32>() / n as f32;
if mean < 1e-10 {
return 0.0;
}
let diff_sum: f32 = magnitudes.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
diff_sum / ((n - 1) as f32 * mean)
}
#[must_use]
pub fn synthetic_compressed_spectrum(n_bins: usize, cutoff_bin: usize) -> Vec<f32> {
(0..n_bins)
.map(|i| if i <= cutoff_bin { 1.0f32 } else { 1e-6 })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_spectrum(n: usize) -> Vec<f32> {
vec![1.0f32; n]
}
#[test]
fn test_bitrate_estimate_clamp() {
let b = BitrateEstimate::new(128, 1.5);
assert!((b.confidence - 1.0).abs() < 1e-6);
let b2 = BitrateEstimate::new(128, -0.5);
assert!((b2.confidence).abs() < 1e-6);
}
#[test]
fn test_detect_spectral_holes_empty_on_flat() {
let spec = flat_spectrum(513);
let holes = detect_spectral_holes(&spec, 44100.0, 15.0, 4);
assert!(
holes.is_empty(),
"expected no holes in flat spectrum, got {}",
holes.len()
);
}
#[test]
fn test_detect_spectral_holes_finds_notch() {
let mut spec = flat_spectrum(513);
spec[100] = 1e-6;
let holes = detect_spectral_holes(&spec, 44100.0, 10.0, 3);
assert!(!holes.is_empty(), "should detect notch");
}
#[test]
fn test_detect_spectral_holes_positive_depth() {
let mut spec = flat_spectrum(513);
spec[200] = 1e-6;
let holes = detect_spectral_holes(&spec, 44100.0, 10.0, 3);
for h in &holes {
assert!(h.depth_db > 0.0, "hole depth should be positive");
}
}
#[test]
fn test_estimate_cutoff_full_spectrum() {
let spec = flat_spectrum(513);
let cutoff = estimate_cutoff_frequency(&spec, 44100.0, -60.0);
assert!(cutoff > 20_000.0, "cutoff = {cutoff}");
}
#[test]
fn test_estimate_cutoff_low_cutoff() {
let spec = synthetic_compressed_spectrum(513, 200);
let cutoff = estimate_cutoff_frequency(&spec, 44100.0, -60.0);
assert!(cutoff < 12_000.0, "cutoff = {cutoff}");
}
#[test]
fn test_estimate_bitrate_class_low_cutoff() {
let est = estimate_bitrate_class(10_000.0);
assert_eq!(est.kbps_class, 64);
}
#[test]
fn test_estimate_bitrate_class_high_cutoff() {
let est = estimate_bitrate_class(20_000.0);
assert_eq!(est.kbps_class, 320);
}
#[test]
fn test_estimate_bitrate_class_mid_cutoff() {
let est = estimate_bitrate_class(16_000.0);
assert_eq!(est.kbps_class, 128);
}
#[test]
fn test_analyse_compression_score_range() {
let spec = flat_spectrum(513);
let result = analyse_compression(&spec, 44100.0, -60.0);
assert!(result.score >= 0.0 && result.score <= 1.0);
}
#[test]
fn test_analyse_compression_compressed_higher_score() {
let compressed = synthetic_compressed_spectrum(513, 180);
let full = flat_spectrum(513);
let cs = analyse_compression(&compressed, 44100.0, -60.0);
let fs = analyse_compression(&full, 44100.0, -60.0);
assert!(
cs.score >= fs.score,
"compressed score {} should be >= full score {}",
cs.score,
fs.score
);
}
#[test]
fn test_spectral_entropy_flat() {
let spec = flat_spectrum(512);
let e = spectral_entropy(&spec);
let expected = (512.0_f32).ln();
assert!(
(e - expected).abs() < 0.01,
"entropy = {e}, expected ~{expected}"
);
}
#[test]
fn test_spectral_entropy_impulse() {
let mut spec = vec![0.0f32; 512];
spec[0] = 1.0;
let e = spectral_entropy(&spec);
assert!(e.abs() < 1e-5, "impulse entropy should be 0, got {e}");
}
#[test]
fn test_spectral_continuity_index_flat_is_zero() {
let spec = flat_spectrum(128);
let ci = spectral_continuity_index(&spec);
assert!(
ci < 1e-5,
"flat spectrum continuity index should be ~0, got {ci}"
);
}
#[test]
fn test_spectral_continuity_index_noisy_positive() {
let spec: Vec<f32> = (0..128)
.map(|i| if i % 2 == 0 { 1.0 } else { 0.0 })
.collect();
let ci = spectral_continuity_index(&spec);
assert!(
ci > 0.0,
"noisy spectrum should have positive continuity index"
);
}
#[test]
fn test_synthetic_compressed_spectrum_length() {
let spec = synthetic_compressed_spectrum(513, 300);
assert_eq!(spec.len(), 513);
}
#[test]
fn test_synthetic_compressed_spectrum_cutoff() {
let spec = synthetic_compressed_spectrum(100, 50);
assert!((spec[50] - 1.0).abs() < 1e-6);
assert!(spec[51] < 1e-4);
}
}