Files
oak-editor/crates/oakaudio/tests/golden_test.rs
T
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +08:00

181 lines
6.6 KiB
Rust

// 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 <http://www.gnu.org/licenses/>.
//! 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<f32> = (0..1024).map(|i| i as f32 * 0.001).collect();
let ch1: Vec<f32> = (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<f64> = (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();
}