#[derive(Debug, Clone, Default)]
pub struct AudioMetrics {
pub rms_level: f32,
pub peak_level: f32,
pub clipping_detected: bool,
pub clipped_sample_count: usize,
pub estimated_snr_db: f32,
}
pub fn analyze_frame(samples: &[f32], noise_floor: f32) -> AudioMetrics {
if samples.is_empty() {
return AudioMetrics::default();
}
let rms = (samples.iter().map(|s| s * s).sum::<f32>()
/ samples.len() as f32)
.sqrt();
let peak = samples
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let clipping_threshold = 0.99;
let clipped = samples
.iter()
.filter(|s| s.abs() >= clipping_threshold)
.count();
let snr = if noise_floor > 0.0001 {
20.0 * (rms / noise_floor).log10()
} else {
60.0 };
AudioMetrics {
rms_level: rms,
peak_level: peak,
clipping_detected: clipped > 0,
clipped_sample_count: clipped,
estimated_snr_db: snr.clamp(-20.0, 80.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn silence_metrics() {
let silence = vec![0.0f32; 160];
let metrics = analyze_frame(&silence, 0.0);
assert!((metrics.rms_level - 0.0).abs() < f32::EPSILON);
assert!((metrics.peak_level - 0.0).abs() < f32::EPSILON);
assert!(!metrics.clipping_detected);
assert_eq!(metrics.clipped_sample_count, 0);
assert!((metrics.estimated_snr_db - 60.0).abs() < f32::EPSILON);
}
#[test]
fn sine_wave_metrics() {
let samples: Vec<f32> = (0..160)
.map(|i| {
let t = i as f32 / 16000.0;
(2.0 * std::f32::consts::PI * 1000.0 * t).sin() * 0.5
})
.collect();
let metrics = analyze_frame(&samples, 0.01);
assert!(
(metrics.rms_level - 0.354).abs() < 0.02,
"RMS should be ~0.354, got {}",
metrics.rms_level
);
assert!(
(metrics.peak_level - 0.5).abs() < 0.01,
"Peak should be ~0.5, got {}",
metrics.peak_level
);
assert!(!metrics.clipping_detected);
assert!(metrics.estimated_snr_db > 20.0);
}
#[test]
fn clipping_detection() {
let mut samples = vec![0.5f32; 160];
samples[0] = 1.0;
samples[1] = -1.0;
samples[2] = 0.995;
let metrics = analyze_frame(&samples, 0.0);
assert!(metrics.clipping_detected);
assert_eq!(metrics.clipped_sample_count, 3);
}
#[test]
fn snr_estimation() {
let samples = vec![0.5f32; 160];
let metrics = analyze_frame(&samples, 0.01);
assert!(
(metrics.estimated_snr_db - 34.0).abs() < 1.0,
"SNR should be ~34 dB, got {}",
metrics.estimated_snr_db
);
}
#[test]
fn empty_frame_returns_defaults() {
let metrics = analyze_frame(&[], 0.0);
assert!((metrics.rms_level - 0.0).abs() < f32::EPSILON);
assert!((metrics.peak_level - 0.0).abs() < f32::EPSILON);
assert!(!metrics.clipping_detected);
assert_eq!(metrics.clipped_sample_count, 0);
assert!((metrics.estimated_snr_db - 0.0).abs() < f32::EPSILON);
}
#[test]
fn snr_clamped_to_range() {
let samples = vec![0.0001f32; 160];
let metrics = analyze_frame(&samples, 1.0);
assert!(
metrics.estimated_snr_db >= -20.0,
"SNR should be clamped at -20, got {}",
metrics.estimated_snr_db
);
}
}