Files
oak-editor/crates/oakaudio/tests/waveform_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

210 lines
7.5 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/>.
//! AudioVisualWaveform contract tests (waveform.rs), calling the public
//! Rust API.
mod common;
use std::ffi::CString;
use common::write_wav;
use oakcore_rs::Rational;
use oakaudio::waveform::{extract, AudioVisualWaveform, SamplePerChannel};
/// Fill `w` with 100 samples/channel of a 0..0.99 ramp at 100 Hz (1 s).
fn fill_ramp(w: &mut AudioVisualWaveform) {
let ch0: Vec<f32> = (0..100).map(|i| i as f32 * 0.01).collect();
let ch1: Vec<f32> = (0..100).map(|i| -(i as f32) * 0.01).collect();
let planes = [ch0.as_slice(), ch1.as_slice()];
w.set_channel_count(2);
w.overwrite_samples(&planes, 100, Rational::new(0, 1));
}
/// A one-point-per-channel summary over `start..start+length`.
fn summary(w: &AudioVisualWaveform, start: Rational, length: Rational) -> Vec<SamplePerChannel> {
w.get_summary_from_time(start, length)
}
/// set_channel_count then overwrite_samples writes planar data at the given
/// start; length() reflects the covered span and get_summary returns
/// channel-interleaved min/max pairs.
#[test]
fn overwrite_samples_and_length() {
let mut w = AudioVisualWaveform::new();
fill_ramp(&mut w);
assert_eq!(w.length(), Rational::new(1, 1));
let out = summary(&w, Rational::new(0, 1), Rational::new(1, 1));
assert_eq!(out.len(), 2);
assert_eq!(out[0].min, 0.0);
assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max);
assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min);
assert_eq!(out[1].max, 0.0);
}
/// overwrite_sums copies channel-interleaved pairs from another waveform
/// into a dest range; a null length copies all of src.
#[test]
fn overwrite_sums_range_copy() {
let mut src = AudioVisualWaveform::new();
fill_ramp(&mut src);
let mut dst = AudioVisualWaveform::new();
dst.set_channel_count(2);
dst.overwrite_sums(&src, Rational::new(0, 1), Rational::new(0, 1), Rational::NULL);
// A null length means "copy everything": dst matches src exactly.
let out = summary(&dst, Rational::new(0, 1), Rational::new(1, 1));
assert_eq!(out.len(), 2);
assert_eq!(out[0].min, 0.0);
assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max);
assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min);
assert_eq!(out[1].max, 0.0);
assert_eq!(dst.length(), Rational::new(1, 1));
}
/// overwrite_silence zeroes min/max over a range without changing length.
#[test]
fn overwrite_silence() {
let mut w = AudioVisualWaveform::new();
fill_ramp(&mut w);
w.overwrite_silence(Rational::new(0, 1), Rational::new(1, 2));
// First half is silenced; second half retains the ramp data.
let out = summary(&w, Rational::new(0, 1), Rational::new(1, 2));
assert_eq!((out[0].min, out[0].max), (0.0, 0.0));
assert_eq!((out[1].min, out[1].max), (0.0, 0.0));
let out = summary(&w, Rational::new(1, 2), Rational::new(1, 2));
assert!(
out[0].max > 0.5,
"second half must keep ramp data, got {:?}",
out[0]
);
assert!(out[1].min < -0.5);
assert_eq!(
w.length(),
Rational::new(1, 1),
"overwrite_silence must not change length"
);
}
/// trim_in/trim_range/resize adjust length and drop or pad data; a negative
/// trim_in prepends silence (C++ semantics).
#[test]
fn trim_and_resize() {
let mut w = AudioVisualWaveform::new();
fill_ramp(&mut w);
// Negative trim_in prepends silence: absolute end (length) unchanged.
w.trim_in(Rational::new(-1, 2));
assert_eq!(w.length(), Rational::new(1, 1));
// Resize extends to 2 s.
w.resize(Rational::new(2, 1));
assert_eq!(w.length(), Rational::new(2, 1));
// trim_range keeps 0.5 s from the (prepended) start.
w.trim_range(Rational::new(0, 1), Rational::new(1, 2));
assert_eq!(w.length(), Rational::new(1, 2));
}
/// sum_samples reduces planar samples into one min/max pair per channel;
/// re_sum_samples merges channel-interleaved entries into one pair per
/// channel. Both match golden vectors from the C++ implementation.
#[test]
fn sum_and_resum_golden() {
let ch0 = [1.0f32, -2.0, 3.0];
let ch1 = [4.0f32, -5.0, 6.0];
let planes = [ch0.as_slice(), ch1.as_slice()];
let out = AudioVisualWaveform::sum_samples(&planes, 0, 3);
assert_eq!((out[0].min, out[0].max), (-2.0, 3.0));
assert_eq!((out[1].min, out[1].max), (-5.0, 6.0));
// re_sum_samples over 4 interleaved entries, 2 channels -> one pair per
// channel merging both points.
let input = [
SamplePerChannel { min: 1.0, max: 2.0 },
SamplePerChannel { min: 3.0, max: 4.0 },
SamplePerChannel { min: 5.0, max: 6.0 },
SamplePerChannel { min: 7.0, max: 8.0 },
];
let out = AudioVisualWaveform::re_sum_samples(&input, 4, 2);
assert_eq!((out[0].min, out[0].max), (1.0, 6.0));
assert_eq!((out[1].min, out[1].max), (3.0, 8.0));
}
/// extract probes through oakcodec's decoder registry; a missing file
/// returns NotFound. The valid-file path decodes the fixture WAV with
/// oakcodec's in-process FFmpeg decoder and reduces it to min/max points.
#[test]
fn extract_file_and_notfound() {
let path = std::env::temp_dir().join(format!("oakaudio_extract_{}.wav", std::process::id()));
// 8 frames of stereo ramp, 48000 Hz.
let mut samples = Vec::with_capacity(16);
for i in 0..8i16 {
samples.push(i * 1000);
samples.push(-(i * 1000));
}
write_wav(&path, 2, 48000, &samples).unwrap();
// Missing file -> NotFound.
let missing = std::env::temp_dir().join(format!("oakaudio_missing_{}.wav", std::process::id()));
let _ = std::fs::remove_file(&missing);
let missing_c = CString::new(missing.to_str().unwrap()).unwrap();
assert!(extract(&missing_c, 0, 4).is_err());
// Real decode: 8 frames at 4 samples/point -> 2 points, 2 channels.
let c_path = CString::new(path.to_str().unwrap()).unwrap();
let outcome = extract(&c_path, 0, 4).unwrap();
assert_eq!(outcome.channels, 2);
assert_eq!(outcome.points.len(), 4, "2 points x 2 channels");
let out = &outcome.points;
// s16 -> f32 is /32768; the ramp is exact in both formats.
let eps = 1e-6;
// Point 0 covers frames 0..4: left 0..3000, right 0..-3000.
assert!((out[0].min - 0.0).abs() < eps);
assert!((out[0].max - 3000.0 / 32768.0).abs() < eps);
assert!((out[1].min - -3000.0 / 32768.0).abs() < eps);
assert!((out[1].max - 0.0).abs() < eps);
// Point 1 covers frames 4..8: left 4000..7000, right -4000..-7000.
assert!((out[2].min - 4000.0 / 32768.0).abs() < eps);
assert!((out[2].max - 7000.0 / 32768.0).abs() < eps);
assert!((out[3].min - -7000.0 / 32768.0).abs() < eps);
assert!((out[3].max - -4000.0 / 32768.0).abs() < eps);
std::fs::remove_file(&path).ok();
}
/// Overwriting with channel count 0 is a logged no-op (the C ABI's state
/// error was FFI-level validation; the Rust API keeps the C++ semantics of
/// returning without touching the data).
#[test]
fn no_channel_count_is_noop() {
let mut w = AudioVisualWaveform::new();
let data = [0.5f32; 8];
let planes = [data.as_slice()];
w.overwrite_samples(&planes, 48000, Rational::new(0, 1));
assert!(w.length().is_null());
assert_eq!(w.channel_count(), 0);
w.set_channel_count(2);
assert_eq!(w.channel_count(), 2);
}