truepeak 0.1.1

True-peak measurement and lookahead limiting for planar f32 audio
Documentation
mod common;

use std::{f64::consts::TAU, path::Path, process::Command};
use truepeak::true_peak;

fn ffmpeg_peak(path: &Path) -> f64 {
    let result = Command::new("ffmpeg")
        .args(["-hide_banner", "-nostats", "-i"])
        .arg(path)
        .args(["-af", "ebur128=peak=true", "-f", "null", "-"])
        .output()
        .expect("run ffmpeg");
    let stderr = String::from_utf8_lossy(&result.stderr);
    assert!(result.status.success(), "ffmpeg failed: {stderr}");
    let summary = stderr
        .rsplit_once("True peak:")
        .expect("FFmpeg true-peak summary")
        .1;
    summary
        .split_once("Peak:")
        .expect("summary Peak field")
        .1
        .split_whitespace()
        .next()
        .unwrap()
        .parse()
        .expect("numeric dBTP reading")
}

fn compare(rate: u32, frequency: f64) {
    let version = Command::new("ffmpeg").arg("-version").output();
    if !common::oracle_available(
        version.as_ref().is_ok_and(|r| r.status.success()),
        "ffmpeg is not on PATH",
    ) {
        return;
    }
    println!(
        "{}",
        String::from_utf8_lossy(&version.unwrap().stdout)
            .lines()
            .next()
            .unwrap()
    );
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("tone.wav");
    let mut min = f64::INFINITY;
    let mut max = f64::NEG_INFINITY;
    let mut failures = Vec::new();
    for phase in 0..32 {
        let input = common::tone(rate, frequency, TAU * phase as f64 / 32.0, -3.0);
        let spec = hound::WavSpec {
            channels: 1,
            sample_rate: rate,
            bits_per_sample: 32,
            sample_format: hound::SampleFormat::Float,
        };
        let mut writer = hound::WavWriter::create(&path, spec).unwrap();
        for &sample in &input {
            writer.write_sample(sample).unwrap();
        }
        writer.finalize().unwrap();
        let reference = ffmpeg_peak(&path);
        let peak = true_peak(&[&input], rate, 8).unwrap().dbtp;
        let delta = peak - reference;
        min = min.min(delta);
        max = max.max(delta);
        let valid = if frequency < 12000.0 {
            delta.abs() <= 0.1
        } else if frequency == 12000.0 {
            (-0.05..=0.25).contains(&delta)
        } else {
            (-0.05..=0.5).contains(&delta)
        };
        if !valid {
            failures.push(format!(
                "phase={phase}: meter={peak:.8}, FFmpeg={reference:.1}, delta={delta:.8} dB"
            ));
        }
    }
    println!(
        "meter comparison, rate={rate}, f={frequency}: meter - FFmpeg [{min:.8}, {max:.8}] dB"
    );
    assert!(
        failures.is_empty(),
        "rate={rate}, f={frequency}:\n{}",
        failures.join("\n")
    );
}

#[test]
fn low_frequency_tones() {
    for rate in [44100, 48000] {
        for frequency in [997.0, 5000.0, 12000.0] {
            compare(rate, frequency);
        }
    }
}

#[test]
fn tone_17200_hz_at_44100() {
    compare(44100, 17200.0);
}
#[test]
fn tone_17200_hz_at_48000() {
    compare(48000, 17200.0);
}
#[test]
fn tone_19600_hz_at_44100() {
    compare(44100, 19600.0);
}
#[test]
fn tone_19600_hz_at_48000() {
    compare(48000, 19600.0);
}
#[test]
fn tone_20000_hz_at_44100() {
    compare(44100, 20000.0);
}
#[test]
fn tone_20000_hz_at_48000() {
    compare(48000, 20000.0);
}

fn limited_reference(fixture: &str, rate: u32, input: &[f32]) {
    let version = Command::new("ffmpeg").arg("-version").output();
    if !common::oracle_available(
        version.as_ref().is_ok_and(|r| r.status.success()),
        "ffmpeg is not on PATH",
    ) {
        return;
    }
    println!(
        "{}",
        String::from_utf8_lossy(&version.unwrap().stdout)
            .lines()
            .next()
            .unwrap()
    );
    let output = common::run_limit(&[input, input], rate, &truepeak::Config::default());
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("limited.wav");
    let spec = hound::WavSpec {
        channels: 2,
        sample_rate: rate,
        bits_per_sample: 32,
        sample_format: hound::SampleFormat::Float,
    };
    let mut writer = hound::WavWriter::create(&path, spec).unwrap();
    for frame in 0..input.len() {
        for channel in &output.channels {
            writer.write_sample(channel[frame]).unwrap();
        }
    }
    writer.finalize().unwrap();
    let reference = ffmpeg_peak(&path);
    println!(
        "{fixture} FFmpeg, rate={rate}: input={:.8}, meter={:.8}, FFmpeg={reference:.1} dBTP; passes={}",
        output.input_true_peak_dbtp, output.output_true_peak_dbtp, output.passes
    );
    assert!(
        reference <= -0.95,
        "{fixture}, rate={rate}, FFmpeg={reference}"
    );
    assert!(
        output.output_true_peak_dbtp <= -0.95,
        "{fixture}, rate={rate}, meter={}",
        output.output_true_peak_dbtp
    );
    assert!(output.ceiling_met);
}

#[test]
fn limiter_997_hz_across_sample_rates() {
    for rate in common::RATES {
        limited_reference("997 Hz tone", rate, &common::tone(rate, 997.0, 0.3, 1.0));
    }
}

#[test]
fn limiter_17200_hz_at_44100() {
    limited_reference(
        "17.2 kHz tone",
        44100,
        &common::tone(44100, 17200.0, 0.7, 1.0),
    );
}

#[test]
fn limiter_square_wave() {
    let input: Vec<f32> = (0..4800)
        .map(|n| if n % 64 < 32 { 1.0 } else { -1.0 })
        .collect();
    limited_reference("square wave", 48000, &input);
}

#[test]
fn limiter_program_across_sample_rates() {
    for rate in common::RATES {
        limited_reference("seeded program", rate, &common::program(rate));
    }
}