// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Cross-cutting golden/parity tests: values captured from the C++
//! implementation to pin exact behavior of the Rust rewrite.
mod common;
use std::ffi::CString;
use common::write_wav_header_only;
use oakcore_rs::Rational;
use oakaudio::params::{frames_to_rational, rational_to_samples, AudioParams, SampleFormat};
use oakaudio::waveform::{extract, AudioVisualWaveform};
/// SampleFormat planar-first ordering matches the authoritative C++ enum:
/// f32_p == 4 == OAKAUDIO_PROCESSOR_OUTPUT_FORMAT. This guards the
/// oakcore-rs ordering divergence documented in params.rs.
#[test]
fn sample_format_planar_first_ordering() {
assert_eq!(SampleFormat::F32Planar as i32, 4);
assert_eq!(oakaudio::processor::OUTPUT_FORMAT as i32, 4);
// Invalid is -1 and the packed family follows planar-first.
assert_eq!(SampleFormat::Invalid as i32, -1);
assert_eq!(SampleFormat::U8Planar as i32, 0);
assert_eq!(SampleFormat::F64 as i32, 11);
}
/// Rational time<->sample conversions (frames_to_rational /
/// rational_to_samples) round-trip 48000 Hz sample counts exactly.
#[test]
fn sample_time_conversion_roundtrip() {
let rate = 48000i32;
for frames in [0i64, 1, 480, 48000, 48001, 1234567] {
let t = frames_to_rational(frames, rate);
assert_eq!(rational_to_samples(t, rate), frames);
}
assert_eq!(frames_to_rational(48000, 48000), Rational::new(1, 1));
assert_eq!(frames_to_rational(1, 48000), Rational::new(1, 48000));
}
/// AudioParams value-type conversions: channel count from the layout mask,
/// bytes-per-sample-per-channel, samples_to_bytes, and the double ->
/// rational conversion edge cases (NaN / out-of-range / tiny -> null).
#[test]
fn params_value_types() {
use oakaudio::params::rational_from_double;
let p = AudioParams {
sample_rate: 48000,
channel_layout: 3,
format: SampleFormat::F32,
};
assert_eq!(p.channel_count(), 2);
assert_eq!(p.bytes_per_sample_per_channel(), 4);
assert_eq!(p.samples_to_bytes(480), 480 * 4 * 2);
assert_eq!(rational_from_double(0.5), Rational::new(1, 2));
assert_eq!(rational_from_double(1.0), Rational::new(1, 1));
assert!(rational_from_double(f64::NAN).is_null());
assert!(rational_from_double(1e10).is_null());
assert!(rational_from_double(1e-20).is_null());
assert!((rational_from_double(0.25).to_f64() - 0.25).abs() < 1e-9);
}
/// AudioVisualWaveform mipmap layout: get_summary at a fine zoom scale
/// covers fewer source samples than at a coarse scale, so the returned
/// min/max pair brackets exactly the mipmapped window. The values are
/// captured from the Rust implementation (which mirrors the C++ mipmap
/// chain); the window coverage itself is load-bearing.
#[test]
fn waveform_mipmap_scale_parity() {
// 1024 ramp samples @ 48000 Hz, two channels.
let ch0: Vec = (0..1024).map(|i| i as f32 * 0.001).collect();
let ch1: Vec = (0..1024).map(|i| -(i as f32) * 0.001).collect();
let planes = [ch0.as_slice(), ch1.as_slice()];
let mut w = AudioVisualWaveform::new();
w.set_channel_count(2);
w.overwrite_samples(&planes, 48000, Rational::new(0, 1));
// One summary point is produced for any queried window; a 1/1024 s
// window (one 1024-rate mipmap point ~ 46.875 source samples) must
// bracket a narrower range than a 1/64 s window (~750 samples).
let fine = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 1024));
assert_eq!(fine.len(), 2);
assert_eq!(fine[0].min, 0.0);
assert!(
(fine[0].max - 0.046).abs() < 1e-5,
"fine max = {}",
fine[0].max
);
assert!(
(fine[1].min + 0.046).abs() < 1e-5,
"fine min = {}",
fine[1].min
);
assert_eq!(fine[1].max, 0.0);
let coarse = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 64));
assert_eq!(coarse.len(), 2);
assert_eq!(coarse[0].min, 0.0);
assert!(
(coarse[0].max - 0.749).abs() < 1e-5,
"coarse max = {}",
coarse[0].max
);
assert!(
(coarse[1].min + 0.749).abs() < 1e-5,
"coarse min = {}",
coarse[1].min
);
assert_eq!(coarse[1].max, 0.0);
// Coarser windows necessarily cover more source samples.
assert!(coarse[0].max > fine[0].max);
assert!(coarse[1].min < fine[1].min);
}
/// levelmeter dB conversion: peak_db == 20*log10(peak_linear) and the
/// -200 dB floor match the C++ helpers for the same sample values.
#[test]
fn levelmeter_db_golden() {
let tone = common::planar_from(&[0.5f32; 64], 1);
let refs: Vec<&[f32]> = tone.iter().map(|v| v.as_slice()).collect();
let stats = oakaudio::levelmeter::analyze_sample_buffer(&refs);
let expected_db = 20.0 * 0.5f64.log10();
assert!((stats.channels[0].peak_db - expected_db).abs() < 1e-9);
assert!((stats.channels[0].rms_db - expected_db).abs() < 1e-9);
let silence = common::silence_planar(1, 64);
let refs: Vec<&[f32]> = silence.iter().map(|v| v.as_slice()).collect();
let stats = oakaudio::levelmeter::analyze_sample_buffer(&refs);
assert_eq!(stats.channels[0].peak_db, -200.0);
assert_eq!(stats.channels[0].rms_db, -200.0);
assert_eq!(stats.channels[0].vu_db, -200.0);
}
/// waveformsync envelope offset golden: a reference ramp delayed by two
/// windows in the candidate is recovered as +2 windows with full
/// confidence.
#[test]
fn waveform_sync_offset_golden() {
let reference: Vec = (0..10).map(|i| i as f64 * 0.1 + 0.1).collect();
let mut candidate = vec![0.0f64; 10];
candidate[2..].copy_from_slice(&reference[..8]);
let out = oakaudio::waveformsync::estimate_envelope_offset(&reference, &candidate, 100, 10);
assert!(out.valid);
assert_eq!(out.offset_samples, 200);
assert!((out.confidence - 1.0).abs() < 1e-9);
}
/// waveform::extract channel cap: a stream claiming more than
/// EXTRACT_MAX_CHANNELS (64) channels is rejected rather than overflowing
/// the internal plane array.
#[test]
fn extract_channel_cap() {
let path = std::env::temp_dir().join(format!("oakaudio_cap_{}.wav", std::process::id()));
write_wav_header_only(&path, 65, 48000).unwrap();
let cpath = CString::new(path.to_str().unwrap()).unwrap();
assert!(
extract(&cpath, 0, 4).is_err(),
"oversized stream must be rejected"
);
std::fs::remove_file(&path).ok();
}