forge-audio 0.1.0

Zero-allocation, lock-free audio architecture for real-time DSP, game engines, and WebAssembly
Documentation
//! Production-grade audio metering: true peak, RMS, phase correlation.

/// Catmull-Rom cubic interpolation — produces overshoot that reveals inter-sample peaks.
fn catmull_rom(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
    let t2 = t * t;
    let t3 = t2 * t;
    0.5 * ((2.0 * p1)
        + (-p0 + p2) * t
        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
        + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}

/// True peak via 4x Catmull-Rom oversampling.
/// Catches inter-sample peaks that standard peak meters miss.
pub fn true_peak(samples: &[f32]) -> f32 {
    if samples.is_empty() { return 0.0; }
    let n = samples.len();
    let mut max = 0.0f32;
    for i in 0..n {
        max = max.max(samples[i].abs());
        if i + 1 < n {
            let p0 = if i > 0 { samples[i - 1] } else { samples[i] };
            let p1 = samples[i];
            let p2 = samples[i + 1];
            let p3 = if i + 2 < n { samples[i + 2] } else { samples[i + 1] };
            // 3 interpolated points between each sample pair
            for k in 1..4 {
                let t = k as f32 / 4.0;
                let interp = catmull_rom(p0, p1, p2, p3, t);
                max = max.max(interp.abs());
            }
        }
    }
    max
}

/// RMS level over the entire buffer.
pub fn rms_level(samples: &[f32]) -> f32 {
    if samples.is_empty() { return 0.0; }
    let sum_sq: f32 = samples.iter().map(|s| s * s).sum();
    (sum_sq / samples.len() as f32).sqrt()
}

/// RMS with NaN/Inf detection. Returns (rms, corrupted).
pub fn rms_level_checked(samples: &[f32]) -> (f32, bool) {
    if samples.is_empty() { return (0.0, false); }
    let mut sum_sq: f32 = 0.0;
    let mut corrupted = false;
    for &s in samples {
        if !s.is_finite() { corrupted = true; continue; }
        sum_sq += s * s;
    }
    ((sum_sq / samples.len() as f32).sqrt(), corrupted)
}

/// Stereo phase correlation: +1.0 = mono, 0.0 = uncorrelated, -1.0 = inverted.
pub fn phase_correlation(left: &[f32], right: &[f32]) -> f32 {
    let n = left.len().min(right.len());
    if n == 0 { return 0.0; }
    let mut sum_lr = 0.0f64;
    let mut sum_ll = 0.0f64;
    let mut sum_rr = 0.0f64;
    for i in 0..n {
        let l = left[i] as f64;
        let r = right[i] as f64;
        sum_lr += l * r;
        sum_ll += l * l;
        sum_rr += r * r;
    }
    let denom = (sum_ll * sum_rr).sqrt();
    if denom < 1e-12 { return 0.0; }
    (sum_lr / denom) as f32
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn true_peak_finds_inter_sample() {
        // Two adjacent near-full-scale samples with steep approach/departure
        // Catmull-Rom overshoot between 0.95 and 0.95 exceeds 0.95
        let samples = vec![0.0, 0.95, 0.95, 0.0];
        let peak = true_peak(&samples);
        assert!(peak > 0.95, "Expected inter-sample peak > 0.95, got {}", peak);
    }

    #[test]
    fn true_peak_of_silence_is_zero() {
        let samples = vec![0.0; 1024];
        assert_eq!(true_peak(&samples), 0.0);
    }

    #[test]
    fn true_peak_full_scale() {
        let samples = vec![0.0, 1.0, 0.0];
        assert!((true_peak(&samples) - 1.0).abs() < 0.01);
    }

    #[test]
    fn rms_of_sine() {
        let samples: Vec<f32> = (0..44100).map(|i| {
            (i as f32 * 2.0 * std::f32::consts::PI * 440.0 / 44100.0).sin()
        }).collect();
        let rms = rms_level(&samples);
        assert!((rms - 0.707).abs() < 0.01);
    }

    #[test]
    fn rms_of_silence_is_zero() {
        assert_eq!(rms_level(&[0.0; 1024]), 0.0);
    }

    #[test]
    fn phase_correlation_mono_is_one() {
        let signal: Vec<f32> = (0..1024).map(|i| (i as f32 * 0.1).sin()).collect();
        let corr = phase_correlation(&signal, &signal);
        assert!((corr - 1.0).abs() < 0.01);
    }

    #[test]
    fn phase_correlation_inverted_is_negative_one() {
        let signal: Vec<f32> = (0..1024).map(|i| (i as f32 * 0.1).sin()).collect();
        let inverted: Vec<f32> = signal.iter().map(|s| -s).collect();
        let corr = phase_correlation(&signal, &inverted);
        assert!((corr - (-1.0)).abs() < 0.01);
    }

    #[test]
    fn phase_correlation_uncorrelated_near_zero() {
        let a: Vec<f32> = (0..4096).map(|i| (i as f32 * 0.1).sin()).collect();
        let b: Vec<f32> = (0..4096).map(|i| (i as f32 * 0.1731).sin()).collect();
        let corr = phase_correlation(&a, &b);
        assert!(corr.abs() < 0.2);
    }

    #[test]
    fn rms_level_detects_nan() {
        let mut buf = vec![0.5f32; 100];
        buf[50] = f32::NAN;
        let (_, corrupted) = rms_level_checked(&buf);
        assert!(corrupted);
    }

    #[test]
    fn rms_level_clean_buffer() {
        let buf: Vec<f32> = (0..100).map(|i| (i as f32 * 0.1).sin()).collect();
        let (rms, corrupted) = rms_level_checked(&buf);
        assert!(!corrupted);
        assert!(rms > 0.0);
    }

    #[test]
    fn rms_level_detects_inf() {
        let mut buf = vec![0.5f32; 100];
        buf[10] = f32::INFINITY;
        let (_, corrupted) = rms_level_checked(&buf);
        assert!(corrupted);
    }
}