use crate::enhance::integrated_lufs;
use crate::error::Result;
use crate::pitch::{PitchOptions, estimate_pitch};
use crate::quality::measure_quality;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SignalStats {
pub duration_s: f64,
pub peak_dbfs: f64,
pub rms_dbfs: f64,
pub lufs: f64,
pub floor_dbfs: f64,
pub snr_db: f64,
pub clipped_samples: usize,
pub f0_mean_hz: Option<f64>,
pub f0_std_hz: Option<f64>,
pub voiced_fraction: f64,
pub hnr_db: Option<f64>,
}
pub fn signal_stats(samples: &[f32], sample_rate: u32) -> Result<SignalStats> {
let quality = measure_quality(samples, sample_rate)?;
let lufs = integrated_lufs(samples, sample_rate)?;
let pitch = estimate_pitch(samples, sample_rate, PitchOptions::default())?;
Ok(SignalStats {
duration_s: quality.duration_s,
peak_dbfs: quality.peak_dbfs,
rms_dbfs: quality.rms_dbfs,
lufs,
floor_dbfs: quality.floor_dbfs,
snr_db: quality.snr_db,
clipped_samples: quality.clipped_samples,
f0_mean_hz: pitch.mean_hz,
f0_std_hz: pitch.std_hz,
voiced_fraction: pitch.voiced_fraction,
hnr_db: pitch.mean_hnr_db,
})
}
#[cfg(test)]
mod tests {
use super::*;
const RATE: u32 = 16_000;
fn sine(amplitude: f32, freq: f64, 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 steady_tone_is_voiced_at_its_frequency() {
let samples = sine(0.5, 150.0, RATE as usize * 2);
let stats = signal_stats(&samples, RATE).expect("stats");
assert!((stats.duration_s - 2.0).abs() < 1e-6);
assert!(
(stats.peak_dbfs - (-6.02)).abs() < 0.1,
"{}",
stats.peak_dbfs
);
assert!(stats.lufs.is_finite());
let f0 = stats.f0_mean_hz.expect("voiced");
assert!((f0 - 150.0).abs() < 3.0, "f0 = {f0}");
assert!(stats.voiced_fraction > 0.9);
assert!(stats.hnr_db.expect("hnr") > 20.0);
assert_eq!(stats.clipped_samples, 0);
}
#[test]
fn silence_has_no_pitch_and_no_loudness() {
let samples = vec![0.0f32; RATE as usize];
let stats = signal_stats(&samples, RATE).expect("stats");
assert_eq!(stats.f0_mean_hz, None);
assert_eq!(stats.hnr_db, None);
assert_eq!(stats.voiced_fraction, 0.0);
assert_eq!(stats.lufs, f64::NEG_INFINITY);
}
#[test]
fn empty_input_is_an_error() {
assert!(signal_stats(&[], RATE).is_err());
assert!(signal_stats(&[0.1], 0).is_err());
}
}