sim-lib-sound-render 0.1.2

Offline PCM rendering, WAV encoding, and render diagnostics for sound objects.
Documentation
use crate::{
    FrequencyWeighting, GatingPolicy, LoudnessLayout, LoudnessSpec, NormalizationSpec,
    TruePeakPolicy, measure_loudness, normalize_loudness,
};

#[test]
fn ebu_reference_sine_measures_integrated_momentary_and_true_peak() {
    let samples = stereo_reference_sine(3.0);
    let report = measure_loudness(&samples, spec()).unwrap();
    let integrated = report.integrated_lufs.unwrap();

    assert!(
        (integrated - -23.0).abs() < 0.15,
        "integrated {integrated} LUFS"
    );
    assert_eq!(report.momentary.len(), 27);
    assert!(
        report
            .momentary
            .iter()
            .all(|block| (block.lufs.unwrap() - -23.0).abs() < 0.2)
    );
    assert_eq!(report.absolute_gate_lufs, Some(-70.0));
    assert_eq!(report.gated_blocks, report.momentary.len());
    assert!(report.true_peak.true_peak >= report.true_peak.sample_peak);
    assert!((report.true_peak.true_peak_dbtp.unwrap() - -23.0).abs() < 0.2);
}

#[test]
fn ebu_relative_gate_excludes_silence_without_hiding_gate_evidence() {
    let mut samples = stereo_reference_sine(2.0);
    samples.extend(vec![0.0; 48_000 * 2 * 2]);
    let report = measure_loudness(&samples, spec()).unwrap();

    assert!(report.gated_blocks < report.momentary.len());
    assert!(report.relative_gate_lufs.is_some());
    assert!((report.integrated_lufs.unwrap() - -23.0).abs() < 0.5);
    assert!(report.momentary.iter().any(|block| block.lufs.is_none()));
}

#[test]
fn true_peak_interpolation_detects_an_intersample_overshoot() {
    let pattern = [0.9, 0.9, -0.9, -0.9];
    let samples = pattern.into_iter().cycle().take(48_000).collect::<Vec<_>>();
    let report = measure_loudness(
        &samples,
        LoudnessSpec {
            layout: LoudnessLayout::mono(),
            frequency_weighting: FrequencyWeighting::Flat,
            gating: GatingPolicy::None,
            ..spec()
        },
    )
    .unwrap();
    assert_eq!(report.true_peak.sample_peak, 0.9f32 as f64);
    assert!(report.true_peak.true_peak > report.true_peak.sample_peak + 0.05);
}

#[test]
fn normalization_reports_gain_ceiling_and_unclipped_float_output() {
    let samples = stereo_reference_sine(2.0);
    let report = normalize_loudness(
        &samples,
        spec(),
        NormalizationSpec {
            target_lufs: -16.0,
            max_true_peak_dbtp: -18.0,
            max_abs_gain_db: 12.0,
        },
    )
    .unwrap();

    assert!((report.requested_gain_db - 7.0).abs() < 0.15);
    assert_eq!(report.applied_gain_db, report.requested_gain_db);
    assert!(!report.gain_limited);
    assert!((report.output.integrated_lufs.unwrap() - -16.0).abs() < 0.05);
    assert!(report.true_peak_ceiling_exceeded);
    assert_eq!(report.clipped_samples, 0);
}

fn spec() -> LoudnessSpec {
    LoudnessSpec {
        sample_rate_hz: 48_000,
        layout: LoudnessLayout::stereo(),
        frequency_weighting: FrequencyWeighting::ItuRBs1770K,
        gating: GatingPolicy::EbuR128,
        true_peak: TruePeakPolicy {
            max_work: 100_000_000,
            ..TruePeakPolicy::default()
        },
        max_frames: 48_000 * 8,
    }
}

fn stereo_reference_sine(seconds: f64) -> Vec<f32> {
    let frames = (48_000.0 * seconds) as usize;
    let peak = 2.0f64.sqrt() * 10.0f64.powf(-23.0 / 20.0) / 2.0f64.sqrt();
    let mut samples = Vec::with_capacity(frames * 2);
    for frame in 0..frames {
        let sample = (std::f64::consts::TAU * 1_000.0 * frame as f64 / 48_000.0).sin() * peak;
        samples.extend([sample as f32, sample as f32]);
    }
    samples
}