Files
oak-editor/crates/oakaudio/tests/waveform_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

406 lines
12 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), through the C ABI.
mod common;
use std::ffi::CString;
use common::{pair, write_wav};
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE};
use oakaudio::ffi::waveform::{
oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_channel_count,
oakaudio_waveform_get_summary, oakaudio_waveform_init, oakaudio_waveform_length,
oakaudio_waveform_overwrite_samples, oakaudio_waveform_overwrite_silence,
oakaudio_waveform_overwrite_sums, oakaudio_waveform_re_sum_s, oakaudio_waveform_resize,
oakaudio_waveform_set_channel_count, oakaudio_waveform_sum_samples_s,
oakaudio_waveform_trim_in, oakaudio_waveform_trim_range, MinMax,
};
/// Fill `w` with 100 samples/channel of a 0..0.99 ramp at 100 Hz (1 s).
fn fill_ramp(w: oakaudio::handle::CHandle) {
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_ptr(), ch1.as_ptr()];
assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0);
assert_eq!(
unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 100, 100, 0, 1) },
0
);
}
fn summary(
w: oakaudio::handle::CHandle,
start: (i64, i64),
length: (i64, i64),
cap: i32,
) -> Vec<MinMax> {
let mut out = vec![MinMax { min: 0.0, max: 0.0 }; cap as usize * 2];
let n = unsafe {
oakaudio_waveform_get_summary(
w,
start.0,
start.1,
length.0,
length.1,
out.as_mut_ptr(),
cap,
)
};
assert_eq!(n, cap);
out.truncate(cap as usize * 2);
out
}
/// 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 = unsafe { oakaudio_waveform_init() };
fill_ramp(w);
let (mut num, mut den) = (0i64, 0i64);
assert_eq!(
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) },
0
);
assert_eq!(num, 1);
assert_eq!(den, 1);
let out = summary(w, (0, 1), (1, 1), 1);
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);
unsafe { oakaudio_waveform_free(&mut w) };
}
/// get_summary with out_pairs NULL returns the required point count without
/// writing; a too-small capacity returns the same count and leaves the
/// buffer untouched (two-stage contract).
#[test]
fn summary_two_stage_query() {
let mut w = unsafe { oakaudio_waveform_init() };
fill_ramp(w);
// NULL out: required count only.
let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, std::ptr::null_mut(), 0) };
assert_eq!(n, 1);
// Too-small capacity: same count, buffer untouched.
let mut out = [MinMax {
min: -1.0,
max: -1.0,
}; 2];
let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, out.as_mut_ptr(), 0) };
assert_eq!(n, 1);
assert_eq!(out[0].min, -1.0);
assert_eq!(out[0].max, -1.0);
// A zero/negative length is invalid.
assert_eq!(
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 0, 1, std::ptr::null_mut(), 0) },
OAKAUDIO_E_INVALID
);
assert_eq!(
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 0, std::ptr::null_mut(), 0) },
OAKAUDIO_E_INVALID
);
unsafe { oakaudio_waveform_free(&mut w) };
}
/// overwrite_sums copies channel-interleaved pairs from another waveform
/// into a dest range; a 0/1 length copies all of src.
#[test]
fn overwrite_sums_range_copy() {
let mut src = unsafe { oakaudio_waveform_init() };
fill_ramp(src);
let mut dst = unsafe { oakaudio_waveform_init() };
assert_eq!(unsafe { oakaudio_waveform_set_channel_count(dst, 2) }, 0);
assert_eq!(
unsafe { oakaudio_waveform_overwrite_sums(dst, src, 0, 1, 0, 1, 0, 1) },
0
);
// A 0/1 length means "copy everything": dst matches src exactly.
let out = summary(dst, (0, 1), (1, 1), 1);
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);
let (mut num, mut den) = (0i64, 0i64);
unsafe { oakaudio_waveform_length(dst, &mut num, &mut den) };
assert_eq!(num, 1);
// Empty src handle is invalid.
let empty = oakaudio::handle::CHandle::null();
assert_eq!(
unsafe { oakaudio_waveform_overwrite_sums(dst, empty, 0, 1, 0, 1, 0, 1) },
OAKAUDIO_E_INVALID
);
unsafe { oakaudio_waveform_free(&mut dst) };
unsafe { oakaudio_waveform_free(&mut src) };
}
/// overwrite_silence zeroes min/max over a range without changing length.
#[test]
fn overwrite_silence() {
let mut w = unsafe { oakaudio_waveform_init() };
fill_ramp(w);
assert_eq!(
unsafe { oakaudio_waveform_overwrite_silence(w, 0, 1, 1, 2) },
0
);
// First half is silenced; second half retains the ramp data.
let out = summary(w, (0, 1), (1, 2), 1);
assert_eq!(pair(out[0].min, out[0].max), pair(0.0, 0.0));
assert_eq!(pair(out[1].min, out[1].max), pair(0.0, 0.0));
let out = summary(w, (1, 2), (1, 2), 1);
assert!(
out[0].max > 0.5,
"second half must keep ramp data, got {:?}",
out[0]
);
assert!(out[1].min < -0.5);
let (mut num, mut den) = (0i64, 0i64);
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!(
(num, den),
(1, 1),
"overwrite_silence must not change length"
);
unsafe { oakaudio_waveform_free(&mut w) };
}
/// 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 = unsafe { oakaudio_waveform_init() };
fill_ramp(w);
// Negative trim_in prepends silence: absolute end (length) unchanged.
assert_eq!(unsafe { oakaudio_waveform_trim_in(w, -1, 2) }, 0);
let (mut num, mut den) = (0i64, 0i64);
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!((num, den), (1, 1));
// Resize extends to 2 s.
assert_eq!(unsafe { oakaudio_waveform_resize(w, 2, 1) }, 0);
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!((num, den), (2, 1));
// trim_range keeps 0.5 s from the (prepended) start.
assert_eq!(unsafe { oakaudio_waveform_trim_range(w, 0, 1, 1, 2) }, 0);
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!((num, den), (1, 2));
// A negative resize target or a zero denominator is invalid.
assert_eq!(
unsafe { oakaudio_waveform_resize(w, -1, 2) },
OAKAUDIO_E_INVALID
);
assert_eq!(
unsafe { oakaudio_waveform_resize(w, 1, 0) },
OAKAUDIO_E_INVALID
);
unsafe { oakaudio_waveform_free(&mut w) };
}
/// sum_samples_s reduces planar samples into one min/max pair per channel;
/// re_sum_s 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_ptr(), ch1.as_ptr()];
let mut out = [MinMax { min: 0.0, max: 0.0 }; 2];
let r = unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 3, out.as_mut_ptr()) };
assert_eq!(r, 0);
assert_eq!(pair(out[0].min, out[0].max), pair(-2.0, 3.0));
assert_eq!(pair(out[1].min, out[1].max), pair(-5.0, 6.0));
// re_sum_s over 4 interleaved entries, 2 channels -> one pair per
// channel merging both points.
let input = [
MinMax { min: 1.0, max: 2.0 },
MinMax { min: 3.0, max: 4.0 },
MinMax { min: 5.0, max: 6.0 },
MinMax { min: 7.0, max: 8.0 },
];
let mut out = [MinMax { min: 0.0, max: 0.0 }; 2];
let r = unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 4, 2, out.as_mut_ptr()) };
assert_eq!(r, 0);
assert_eq!(pair(out[0].min, out[0].max), pair(1.0, 6.0));
assert_eq!(pair(out[1].min, out[1].max), pair(3.0, 8.0));
// Invalid arguments.
assert_eq!(
unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 0, out.as_mut_ptr()) },
OAKAUDIO_E_INVALID
);
assert_eq!(
unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 0, 2, out.as_mut_ptr()) },
OAKAUDIO_E_INVALID
);
}
/// extract probes through the oakcodec decoder C ABI; a missing file
/// returns OAKAUDIO_E_NOT_FOUND. 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 -> NOT_FOUND.
let missing = CString::new(
std::env::temp_dir()
.join(format!("oakaudio_missing_{}.wav", std::process::id()))
.to_str()
.unwrap(),
)
.unwrap();
let _ = std::fs::remove_file(std::path::Path::new(missing.to_str().unwrap()));
let r = unsafe {
oakaudio_waveform_extract(
missing.as_ptr(),
0,
4,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
)
};
assert_eq!(r, -60004);
// Real decode: 8 frames at 4 samples/point -> 2 points, 2 channels.
let c_path = CString::new(path.to_str().unwrap()).unwrap();
let mut channel_count = 0i32;
let n = unsafe {
oakaudio_waveform_extract(
c_path.as_ptr(),
0,
4,
std::ptr::null_mut(),
0,
&mut channel_count,
)
};
assert_eq!(channel_count, 2);
assert_eq!(n, 2, "8 frames at 4 samples/point must yield 2 points");
let mut out = vec![MinMax { min: 0.0, max: 0.0 }; 4];
let n = unsafe {
oakaudio_waveform_extract(
c_path.as_ptr(),
0,
4,
out.as_mut_ptr(),
2,
&mut channel_count,
)
};
assert_eq!(n, 2);
// 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();
}
/// FFI validation and empty-handle error paths.
#[test]
fn ffi_error_paths() {
let empty = oakaudio::handle::CHandle::null();
assert_eq!(
unsafe { oakaudio_waveform_get_channel_count(empty) },
OAKAUDIO_E_INVALID
);
assert_eq!(
unsafe { oakaudio_waveform_set_channel_count(empty, 2) },
OAKAUDIO_E_INVALID
);
let (mut num, mut den) = (0i64, 0i64);
assert_eq!(
unsafe { oakaudio_waveform_length(empty, &mut num, &mut den) },
OAKAUDIO_E_INVALID
);
// Negative channel count is invalid.
let mut w = unsafe { oakaudio_waveform_init() };
assert_eq!(
unsafe { oakaudio_waveform_set_channel_count(w, -1) },
OAKAUDIO_E_INVALID
);
// overwrite_samples before set_channel_count is a state error.
let data = [0.5f32; 8];
let planes = [data.as_ptr()];
assert_eq!(
unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 1) },
OAKAUDIO_E_STATE
);
// A zero denominator is rejected.
assert_eq!(
unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 0) },
OAKAUDIO_E_INVALID
);
// A non-positive frame count is invalid.
assert_eq!(
unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 0, 48000, 0, 1) },
OAKAUDIO_E_INVALID
);
// NULL planes are invalid.
assert_eq!(
unsafe { oakaudio_waveform_overwrite_samples(w, std::ptr::null(), 8, 48000, 0, 1) },
OAKAUDIO_E_INVALID
);
unsafe { oakaudio_waveform_free(&mut w) };
}
/// free(NULL)/free(empty) are no-ops.
#[test]
fn free_null_noop() {
let mut w = oakaudio::handle::CHandle::null();
unsafe { oakaudio_waveform_free(&mut w) };
unsafe { oakaudio_waveform_free(std::ptr::null_mut()) };
}