use rustfft::{FftPlanner, num_complex::Complex};
const FRAME_LEN: usize = 720;
const HOP: usize = 240;
const CLIP_LEVEL: f32 = 0.985;
const CLIP_RUN_SAMPLES: usize = 3;
const FLOOR_QUANTILE: f64 = 0.10;
const VAD_OPEN_RATIO: f64 = 3.16; const VAD_CLOSE_RATIO: f64 = 1.0; const VAD_MIN_FLOOR_RMS: f64 = 1e-4;
const TONAL_FLATNESS_MAX: f64 = 0.02;
fn frame_rms(pcm: &[f32], start: usize) -> f64 {
let end = (start + FRAME_LEN).min(pcm.len());
let len = (end - start) as f64;
let sum: f64 = pcm[start..end]
.iter()
.map(|&value| f64::from(value) * f64::from(value))
.sum();
(sum / len).sqrt()
}
fn percentile(mut values: Vec<f64>, quantile: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
values.sort_by(|a, b| a.total_cmp(b));
let index = ((values.len() - 1) as f64 * quantile).round() as usize;
values[index]
}
fn frame_energies(pcm: &[f32]) -> Vec<f64> {
let mut energies = Vec::new();
let mut start = 0;
while start + FRAME_LEN <= pcm.len() {
energies.push(frame_rms(pcm, start));
start += HOP;
}
energies
}
#[must_use]
pub fn voice_activity_regions(pcm: &[f32]) -> Vec<(usize, usize)> {
let energies = frame_energies(pcm);
if energies.is_empty() {
return Vec::new();
}
let floor = percentile(energies.clone(), FLOOR_QUANTILE).max(VAD_MIN_FLOOR_RMS);
let open = floor * VAD_OPEN_RATIO;
let close = floor * VAD_CLOSE_RATIO;
let mut regions = Vec::new();
let mut region_start: Option<usize> = None;
for (index, &energy) in energies.iter().enumerate() {
match (region_start, energy >= open, energy >= close) {
(None, true, _) => region_start = Some(index * HOP),
(Some(_), _, false) => {
if let Some(begin) = region_start.take() {
regions.push((begin, index * HOP));
}
}
_ => {}
}
}
if let Some(begin) = region_start.take() {
regions.push((begin, pcm.len()));
}
regions
}
#[must_use]
pub fn clipping_diagnostics(pcm: &[f32]) -> (f64, usize, f64) {
if pcm.is_empty() {
return (0.0, 0, 0.0);
}
let at_rail: Vec<bool> = pcm.iter().map(|&value| value.abs() >= CLIP_LEVEL).collect();
let clipped = at_rail.iter().filter(|&&rail| rail).count();
let fraction = f64::from(clipped as u32) / pcm.len() as f64;
let mut longest_run = 0_usize;
let mut run = 0_usize;
for rail in &at_rail {
if *rail {
run += 1;
longest_run = longest_run.max(run);
} else {
run = 0;
}
}
#[allow(
clippy::cast_precision_loss,
reason = "loop indices over a 4-step interpolation; exactness is irrelevant here"
)]
let digital_peak = pcm.iter().fold(0.0_f32, |max, &value| max.max(value.abs()));
let mut true_peak = f64::from(digital_peak);
for window in pcm.windows(4) {
let [a, b, c, d] = window else {
continue;
};
if b.abs() < c.abs() || c.abs() < CLIP_LEVEL * 0.5 {
continue;
}
for step in 1..4_u32 {
let t = step as f32 / 4.0;
let interpolated = 0.5
* ((2.0 * c)
+ (-a + c) * t
+ (2.0 * a - 5.0 * b + 4.0 * c - d) * t * t
+ (-a + 3.0 * b - 3.0 * c + d) * t * t * t);
true_peak = true_peak.max(f64::from(interpolated.abs()));
}
}
let overshoot_db = if digital_peak > 0.0 && true_peak > 0.0 {
20.0 * (true_peak / f64::from(digital_peak)).log10()
} else {
0.0
};
let _ = CLIP_RUN_SAMPLES; (fraction, longest_run, overshoot_db)
}
#[must_use]
pub fn snr_estimate_db(pcm: &[f32]) -> (Option<f64>, f64) {
let energies = frame_energies(pcm);
if energies.is_empty() {
return (None, 0.0);
}
let floor_rms = percentile(energies.clone(), FLOOR_QUANTILE);
let regions = voice_activity_regions(pcm);
let mut active_power_sum = 0.0_f64;
let mut active_frames = 0_usize;
for (region_start, region_end) in regions {
let mut frame = region_start;
while frame + FRAME_LEN <= region_end {
let rms = frame_rms(pcm, frame);
active_power_sum += rms * rms;
active_frames += 1;
frame += HOP;
}
}
if active_frames == 0 || floor_rms <= 0.0 {
return (None, floor_rms);
}
let active_rms = (active_power_sum / active_frames as f64).sqrt();
(Some(20.0 * (active_rms / floor_rms).log10()), floor_rms)
}
#[must_use]
pub fn music_bed_likelihood(pcm: &[f32]) -> f64 {
const FFT_LEN: usize = 1024;
if pcm.len() < FFT_LEN * 2 {
return 0.0;
}
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(FFT_LEN);
let hann: Vec<f32> = (0..FFT_LEN)
.map(|index| {
0.5_f32 * (1.0 - (2.0 * std::f32::consts::PI * index as f32 / FFT_LEN as f32).cos())
})
.collect();
const SILENCE_ENERGY: f32 = 1e-8;
let mut tonal_frames = 0_u32;
let mut considered = 0_u32;
let mut offset = 0;
while offset + FFT_LEN <= pcm.len() {
let window_slice = &pcm[offset..offset + FFT_LEN];
let energy: f32 = window_slice.iter().map(|&value| value * value).sum();
offset += HOP;
if energy < SILENCE_ENERGY {
continue;
}
considered += 1;
let mut buffer: Vec<Complex<f32>> = window_slice
.iter()
.zip(&hann)
.map(|(&sample, &window)| Complex::new(sample * window, 0.0))
.collect();
fft.process(&mut buffer);
let band: Vec<f64> = buffer[FFT_LEN / 240..FFT_LEN / 5]
.iter()
.map(|bin| bin.norm().max(1e-9) as f64)
.collect();
let log_mean: f64 = band.iter().map(|value| value.ln()).sum::<f64>() / band.len() as f64;
let linear_mean: f64 = band.iter().sum::<f64>() / band.len() as f64;
let flatness = if linear_mean > 0.0 {
log_mean.exp() / linear_mean
} else {
1.0
};
if flatness < TONAL_FLATNESS_MAX {
tonal_frames += 1;
}
}
if considered == 0 {
return 0.0;
}
f64::from(tonal_frames) / f64::from(considered)
}
#[must_use]
pub fn stationarity_drift(pcm: &[f32]) -> f64 {
let energies = frame_energies(pcm);
if energies.len() < 8 {
return 0.0;
}
let quarter = energies.len() / 4;
let medians: [f64; 4] = [
percentile(energies[..quarter].to_vec(), 0.5),
percentile(energies[quarter..2 * quarter].to_vec(), 0.5),
percentile(energies[2 * quarter..3 * quarter].to_vec(), 0.5),
percentile(energies[3 * quarter..].to_vec(), 0.5),
];
let min = medians.iter().cloned().fold(f64::INFINITY, f64::min);
let max = medians.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if max <= 0.0 {
return 0.0;
}
(max - min) / max
}
#[derive(Debug)]
pub struct AudioDiagnostics {
pub clipping_fraction: f64,
pub longest_clip_run: usize,
pub intersample_overshoot_db: f64,
pub snr_estimate_db: Option<f64>,
pub pause_floor_dbfs: f64,
pub reverb_time_s: Option<f64>,
pub music_bed_likelihood: f64,
pub stationarity_drift: f64,
pub loudness_rms_dbfs: f64,
pub voice_activity_ratio: f64,
}
impl AudioDiagnostics {
#[must_use]
pub fn warnings(&self) -> Vec<String> {
const CLIP_WARN_FRACTION: f64 = 1e-3;
const SNR_WARN_DB_MAX: f64 = 15.0;
const MUSIC_WARN_FRACTION: f64 = 0.5;
const REVERB_WARN_SECONDS: f64 = 0.8;
let mut warnings = Vec::new();
if self.clipping_fraction > CLIP_WARN_FRACTION {
warnings.push(format!(
"reference is hard-clipped ({:.2}% of samples at rail; longest run {} samples) - clipping survives cleanup and clones as distortion",
self.clipping_fraction * 100.0,
self.longest_clip_run
));
}
if let Some(snr) = self.snr_estimate_db
&& snr < SNR_WARN_DB_MAX
{
warnings.push(format!(
"noisy reference (envelope SNR ~{snr:.0} dB < {SNR_WARN_DB_MAX:.0} dB) - \
denoising helps, but a quieter room helps more"
));
}
if self.music_bed_likelihood > MUSIC_WARN_FRACTION {
warnings.push(format!(
"sustained tonal background detected ({:.0}% of frames) - background music reproduces in the clone under continuation-style conditioning",
self.music_bed_likelihood * 100.0
));
}
if let Some(rt60) = self.reverb_time_s
&& rt60 > REVERB_WARN_SECONDS
{
warnings.push(format!(
"reverberant reference (RT60-equivalent {rt60:.2} s > \
{REVERB_WARN_SECONDS:.2} s) - consider --dereverb or a drier recording"
));
}
warnings
}
}
#[must_use]
pub fn diagnose(pcm: &[f32], reverb_time_s: Option<f64>) -> AudioDiagnostics {
let (clipping_fraction, longest_clip_run, intersample_overshoot_db) = clipping_diagnostics(pcm);
let (snr_estimate_db, floor_rms) = snr_estimate_db(pcm);
let regions = voice_activity_regions(pcm);
let voiced_samples: usize = regions.iter().map(|(start, end)| end - start).sum();
let total_rms = if pcm.is_empty() {
0.0
} else {
(pcm.iter()
.map(|&value| f64::from(value) * f64::from(value))
.sum::<f64>()
/ pcm.len() as f64)
.sqrt()
};
AudioDiagnostics {
clipping_fraction,
longest_clip_run,
intersample_overshoot_db,
snr_estimate_db,
pause_floor_dbfs: if floor_rms > 0.0 {
20.0 * floor_rms.log10()
} else {
f64::NEG_INFINITY
},
reverb_time_s,
music_bed_likelihood: music_bed_likelihood(pcm),
stationarity_drift: stationarity_drift(pcm),
loudness_rms_dbfs: if total_rms > 0.0 {
20.0 * total_rms.log10()
} else {
f64::NEG_INFINITY
},
voice_activity_ratio: if pcm.is_empty() {
0.0
} else {
f64::from(voiced_samples as u32) / pcm.len() as f64
},
}
}