use crate::error::{Error, Result};
const SILENCE_DBFS: f64 = -600.0;
fn dbfs(x: f64) -> f64 {
if x > 0.0 {
20.0 * x.log10()
} else {
SILENCE_DBFS
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TakeQuality {
pub duration_s: f64,
pub peak_dbfs: f64,
pub rms_dbfs: f64,
pub floor_dbfs: f64,
pub snr_db: f64,
pub clipped_samples: usize,
}
pub fn measure_quality(samples: &[f32], sample_rate: u32) -> Result<TakeQuality> {
if samples.is_empty() {
return Err(Error::InvalidArgument {
arg: "samples",
reason: "sample slice is empty".to_string(),
});
}
if sample_rate == 0 {
return Err(Error::InvalidArgument {
arg: "sample_rate",
reason: "sample rate is 0".to_string(),
});
}
let mut peak = 0.0f64;
let mut sum_sq = 0.0f64; let mut clipped = 0usize;
for &s in samples {
let a = (s as f64).abs();
if a > peak {
peak = a;
}
sum_sq += a * a;
if a >= 1.0 {
clipped += 1;
}
}
let n = samples.len();
let rms = (sum_sq / n as f64).sqrt();
let block_len = ((sample_rate / 10) as usize).max(1);
let mut blocks: Vec<f64> = Vec::new();
if n > block_len {
let stop = n - block_len;
let mut i = 0usize;
while i < stop {
let block = &samples[i..i + block_len];
let block_sum_sq: f64 = block.iter().map(|&s| (s as f64) * (s as f64)).sum();
blocks.push((block_sum_sq / block_len as f64).sqrt());
i += block_len;
}
}
blocks.sort_by(f64::total_cmp);
let floor = if blocks.len() >= 20 {
blocks[blocks.len() / 20]
} else if let Some(&min) = blocks.first() {
min
} else {
0.0
};
let peak_dbfs = dbfs(peak);
let rms_dbfs = dbfs(rms);
let floor_dbfs = dbfs(floor);
Ok(TakeQuality {
duration_s: n as f64 / sample_rate as f64,
peak_dbfs,
rms_dbfs,
floor_dbfs,
snr_db: rms_dbfs - floor_dbfs,
clipped_samples: clipped,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "decode")]
use crate::decode::decode_audio_file_mono_at;
#[cfg(feature = "decode")]
use crate::test_utils::corpus_flac;
#[cfg(feature = "decode")]
use std::path::{Path, PathBuf};
const RATE: u32 = 48_000;
fn sine(amplitude: f32, freq: f64, rate: u32, len: usize) -> Vec<f32> {
(0..len)
.map(|n| {
amplitude * (std::f64::consts::TAU * freq * n as f64 / rate as f64).sin() as f32
})
.collect()
}
#[test]
fn full_scale_sine_pins_peak_and_rms() {
let samples = sine(0.999, 440.0, RATE, RATE as usize * 2);
let q = measure_quality(&samples, RATE).expect("measure");
assert!(
(q.peak_dbfs - 0.0).abs() < 0.05,
"peak_dbfs = {}",
q.peak_dbfs
);
assert!(
(q.rms_dbfs - (-3.0103)).abs() < 0.05,
"rms_dbfs = {}",
q.rms_dbfs
);
assert_eq!(q.clipped_samples, 0);
}
#[test]
fn half_amplitude_sine_is_6_02_db_quieter() {
let full = sine(1.0, 440.0, RATE, RATE as usize * 2);
let half = sine(0.5, 440.0, RATE, RATE as usize * 2);
let q_full = measure_quality(&full, RATE).expect("measure full");
let q_half = measure_quality(&half, RATE).expect("measure half");
let delta = q_full.rms_dbfs - q_half.rms_dbfs;
assert!((delta - 6.0206).abs() < 0.01, "delta = {delta}");
let delta_peak = q_full.peak_dbfs - q_half.peak_dbfs;
assert!(
(delta_peak - 6.0206).abs() < 0.01,
"delta_peak = {delta_peak}"
);
}
#[test]
fn clipping_count_is_exact() {
let mut samples = vec![0.01f32; 900];
for i in 0..100 {
samples.push(if i % 2 == 0 { 1.0 } else { -1.0 });
}
let q = measure_quality(&samples, RATE).expect("measure");
assert_eq!(q.clipped_samples, 100);
}
#[test]
fn noise_floor_tracks_the_quiet_blocks_not_the_loud_ones() {
let block_len = (RATE / 10) as usize;
let mut samples = Vec::with_capacity(block_len * 100);
for _ in 0..90 {
samples.extend(sine(0.9, 440.0, RATE, block_len));
}
for _ in 0..10 {
samples.extend(sine(0.001, 440.0, RATE, block_len));
}
samples.push(0.0);
let q = measure_quality(&samples, RATE).expect("measure");
let loud_dbfs = dbfs(0.9 / std::f64::consts::SQRT_2);
let quiet_dbfs = dbfs(0.001 / std::f64::consts::SQRT_2);
assert!(
q.floor_dbfs < (loud_dbfs + quiet_dbfs) / 2.0,
"floor_dbfs {} should be near the quiet level {quiet_dbfs}, not the loud level {loud_dbfs}",
q.floor_dbfs
);
assert!(q.snr_db > 0.0, "snr_db should be positive: {}", q.snr_db);
}
#[test]
fn empty_and_zero_rate_return_err_not_panic() {
assert!(measure_quality(&[], RATE).is_err());
assert!(measure_quality(&[0.1, 0.2], 0).is_err());
}
#[test]
fn all_silent_signal_is_finite_no_nan() {
let samples = vec![0.0f32; RATE as usize];
let q = measure_quality(&samples, RATE).expect("measure");
assert!(q.peak_dbfs.is_finite());
assert!(q.rms_dbfs.is_finite());
assert!(q.floor_dbfs.is_finite());
assert!(q.snr_db.is_finite());
assert_eq!(q.peak_dbfs, SILENCE_DBFS);
assert_eq!(q.rms_dbfs, SILENCE_DBFS);
assert_eq!(q.floor_dbfs, SILENCE_DBFS);
assert_eq!(q.snr_db, 0.0);
assert_eq!(q.clipped_samples, 0);
}
#[cfg(feature = "decode")]
struct ManifestRow {
file: String,
duration_s: f64,
floor_dbfs: f64,
snr_db: f64,
}
#[cfg(feature = "decode")]
fn manifest_for(fixture: &Path) -> Option<PathBuf> {
if let Ok(p) = std::env::var("AUDIO_CORPUS_MANIFEST") {
let path = PathBuf::from(p);
return path.exists().then_some(path);
}
let corpus_root = fixture.parent()?.parent()?.parent()?;
let path = corpus_root.join("manifests").join("sources.tsv");
path.exists().then_some(path)
}
#[cfg(feature = "decode")]
fn load_manifest_row(manifest_path: &Path, file_name: &str) -> Option<ManifestRow> {
let text = std::fs::read_to_string(manifest_path).ok()?;
for line in text.lines().skip(1) {
let cols: Vec<&str> = line.split('\t').collect();
if cols.len() < 5 {
continue;
}
if cols[0] == file_name {
return Some(ManifestRow {
file: cols[0].to_string(),
duration_s: cols[2].parse().ok()?,
floor_dbfs: cols[3].parse().ok()?,
snr_db: cols[4].parse().ok()?,
});
}
}
None
}
#[cfg(feature = "decode")]
#[test]
fn matches_the_real_corpus_manifest_where_possible() {
let Some(path) = corpus_flac() else { return };
let Some(manifest_path) = manifest_for(&path) else {
return;
};
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
return;
};
let Some(row) = load_manifest_row(&manifest_path, file_name) else {
return;
};
let samples = decode_audio_file_mono_at(&path, RATE).expect("decode corpus fixture");
let q = measure_quality(&samples, RATE).expect("measure corpus fixture");
assert!(
(q.duration_s - row.duration_s).abs() < 0.5,
"duration_s {} not within 0.5s of manifest's {} for {}",
q.duration_s,
row.duration_s,
row.file
);
assert!(
q.floor_dbfs < q.rms_dbfs,
"floor_dbfs {} should be below rms for a real recording",
q.floor_dbfs
);
assert!(q.snr_db > 0.0, "snr_db {} should be positive", q.snr_db);
let _ = row.floor_dbfs;
let _ = row.snr_db;
}
}