//! Basic audio measurements: peak amplitude and RMS level in dBFS. /// Compute peak amplitude in dBFS. /// /// Returns -96.0 for silence. The -96 dBFS floor represents the noise floor of /// 16-bit audio (96 dB dynamic range = 16 bits * 6 dB/bit) and serves as a /// practical "silent" value. pub fn peak_db(samples: &[f32]) -> f64 { if samples.is_empty() { return -96.0; } let peak = samples .iter() .fold(0.0f32, |max, &s| max.max(s.abs())); if peak == 0.0 { -96.0 } else { 20.0 * (peak as f64).log10() } } /// Compute RMS level in dBFS. /// /// Returns -96.0 for silence or empty input (same floor convention as `peak_db`). pub fn rms_db(samples: &[f32]) -> f64 { if samples.is_empty() { return -96.0; } let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum(); let rms = (sum_sq / samples.len() as f64).sqrt(); if rms == 0.0 { -96.0 } else { 20.0 * rms.log10() } } /// Compute crest factor: peak / RMS in linear domain. /// /// High values (>8) indicate sharp transients (impacts, clicks). /// Low values (<3) indicate sustained signals (pads, drones). /// Returns 0.0 for silence or empty input. pub fn crest_factor(samples: &[f32]) -> f64 { if samples.is_empty() { return 0.0; } let peak = samples.iter().fold(0.0f32, |max, &s| max.max(s.abs())); if peak == 0.0 { return 0.0; } let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum(); let rms = (sum_sq / samples.len() as f64).sqrt(); if rms == 0.0 { 0.0 } else { peak as f64 / rms } } /// Compute attack time in seconds: time to reach 90% of peak amplitude using a 1ms RMS envelope. /// /// Fast attack (<5ms) = percussive hits, impacts. /// Slow attack (>50ms) = pads, ambience, swells. /// Returns 0.0 for silence or very short signals. pub fn attack_time(samples: &[f32], sample_rate: u32) -> f64 { if samples.is_empty() || sample_rate == 0 { return 0.0; } // 1ms RMS envelope window let window_len = (sample_rate as usize) / 1000; if window_len == 0 || samples.len() < window_len { return 0.0; } // Compute RMS envelope let mut envelope = Vec::with_capacity(samples.len() / window_len); let mut pos = 0; while pos + window_len <= samples.len() { let sum_sq: f64 = samples[pos..pos + window_len] .iter() .map(|&s| (s as f64) * (s as f64)) .sum(); envelope.push((sum_sq / window_len as f64).sqrt()); pos += window_len; } if envelope.is_empty() { return 0.0; } let peak_env = envelope.iter().cloned().fold(0.0f64, f64::max); if peak_env == 0.0 { return 0.0; } let threshold = peak_env * 0.9; for (i, &val) in envelope.iter().enumerate() { if val >= threshold { return (i as f64 * window_len as f64) / sample_rate as f64; } } // Never reached threshold (shouldn't happen with 90% of peak) 0.0 } #[cfg(test)] mod tests { use super::*; #[test] fn silence_gives_floor() { let silence = vec![0.0f32; 1024]; assert_eq!(peak_db(&silence), -96.0); assert_eq!(rms_db(&silence), -96.0); } #[test] fn full_scale_sine() { // A full-scale signal at 1.0 peak let samples: Vec = (0..44100) .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin()) .collect(); let peak = peak_db(&samples); assert!((peak - 0.0).abs() < 0.1, "peak should be ~0 dBFS, got {peak}"); let rms = rms_db(&samples); // RMS of a sine wave is -3.01 dBFS assert!((rms - (-3.01)).abs() < 0.1, "rms should be ~-3 dBFS, got {rms}"); } #[test] fn dc_offset() { let samples = vec![0.5f32; 1000]; let peak = peak_db(&samples); assert!((peak - (-6.02)).abs() < 0.1); } #[test] fn empty_gives_floor() { assert_eq!(rms_db(&[]), -96.0); } #[test] fn crest_factor_silence() { assert_eq!(crest_factor(&[0.0; 1000]), 0.0); assert_eq!(crest_factor(&[]), 0.0); } #[test] fn crest_factor_sine() { // Sine wave crest factor = sqrt(2) ≈ 1.414 let samples: Vec = (0..44100) .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin()) .collect(); let cf = crest_factor(&samples); assert!((cf - std::f64::consts::SQRT_2).abs() < 0.05, "sine crest factor should be ~1.414, got {cf}"); } #[test] fn crest_factor_impulse_is_high() { // Single spike in silence — very high crest factor let mut samples = vec![0.0f32; 44100]; samples[100] = 1.0; let cf = crest_factor(&samples); assert!(cf > 100.0, "impulse crest factor should be very high, got {cf}"); } #[test] fn attack_time_silence() { assert_eq!(attack_time(&[0.0; 1000], 44100), 0.0); assert_eq!(attack_time(&[], 44100), 0.0); } #[test] fn attack_time_immediate_onset() { // Full-scale signal from the start — attack time should be ~0 let samples = vec![1.0f32; 44100]; let at = attack_time(&samples, 44100); assert!(at < 0.002, "constant signal attack should be ~0, got {at}"); } #[test] fn attack_time_slow_ramp() { // Linear ramp from 0 to 1 over 1 second let sr = 44100u32; let samples: Vec = (0..sr) .map(|i| i as f32 / sr as f32) .collect(); let at = attack_time(&samples, sr); // 90% of peak (1.0) is 0.9, reached at t ≈ 0.9s assert!(at > 0.5, "ramp attack time should be slow, got {at}"); } }