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

164 lines
4.9 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/>.
//! AudioLevelMeter contract tests (levelmeter.rs), through the C ABI.
mod common;
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
use oakaudio::ffi::levelmeter::{oakaudio_levelmeter_analyze, ChannelStats, MeterStats};
fn analyze(planes: &[Vec<f32>]) -> (Vec<ChannelStats>, MeterStats) {
let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
let mut channels: Vec<ChannelStats> = (0..planes.len())
.map(|_| ChannelStats {
peak_linear: 0.0,
peak_db: 0.0,
rms_linear: 0.0,
rms_db: 0.0,
vu_db: 0.0,
})
.collect();
let mut summary = MeterStats {
max_peak_linear: 0.0,
integrated_lufs: 0.0,
silence: 0,
};
let r = unsafe {
oakaudio_levelmeter_analyze(
ptrs.as_ptr(),
planes.len() as i32,
planes.first().map_or(0, |p| p.len()) as i32,
channels.as_mut_ptr(),
channels.len() as i32,
&mut summary,
)
};
assert_eq!(r, OAKAUDIO_OK);
(channels, summary)
}
/// A silence buffer reports silence=1, all-zero linear fields, and dB
/// fields floored at -200.
#[test]
fn silence_analysis() {
let planes = common::silence_planar(2, 64);
let (channels, summary) = analyze(&planes);
assert_eq!(summary.silence, 1);
assert_eq!(summary.max_peak_linear, 0.0);
assert_eq!(summary.integrated_lufs, -200.0);
for ch in &channels {
assert_eq!(ch.peak_linear, 0.0);
assert_eq!(ch.rms_linear, 0.0);
assert_eq!(ch.peak_db, -200.0);
assert_eq!(ch.rms_db, -200.0);
assert_eq!(ch.vu_db, -200.0);
}
}
/// A constant-amplitude tone reports peak_linear == rms_linear == that
/// amplitude (power terms), peak_db matches 20*log10(amp), and silence=0.
#[test]
fn constant_tone_stats() {
let planes = common::planar_from(&[0.5f32; 64], 1);
let (channels, summary) = analyze(&planes);
assert_eq!(summary.silence, 0);
assert!((channels[0].peak_linear - 0.5).abs() < 1e-9);
assert!((channels[0].rms_linear - 0.5).abs() < 1e-9);
let expected_db = 20.0 * 0.5f64.log10();
assert!((channels[0].peak_db - expected_db).abs() < 1e-9);
assert!((channels[0].rms_db - expected_db).abs() < 1e-9);
}
/// A full-scale square wave yields max_peak_linear == 1.0 and a peak_db
/// near 0 dB; per-channel channels array is filled for each channel.
#[test]
fn full_scale_peak() {
let ch0: Vec<f32> = (0..64)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let ch1: Vec<f32> = (0..64)
.map(|i| if i % 2 == 0 { -1.0 } else { 1.0 })
.collect();
let (channels, summary) = analyze(&[ch0, ch1]);
assert_eq!(summary.max_peak_linear, 1.0);
assert_eq!(summary.silence, 0);
assert!((channels[0].peak_db - 0.0).abs() < 1e-9);
assert!((channels[1].peak_db - 0.0).abs() < 1e-9);
assert!((channels[0].rms_linear - 1.0).abs() < 1e-9);
assert!((channels[1].rms_linear - 1.0).abs() < 1e-9);
}
/// integrated_lufs stays -200 for silence and matches the BS.1770
/// mean-square formula (no K-weighting) for a tone.
#[test]
fn integrated_lufs_silence_vs_tone() {
let silence = common::silence_planar(2, 64);
let (_, summary) = analyze(&silence);
assert_eq!(summary.integrated_lufs, -200.0);
let tone = common::planar_from(&[0.5f32; 64], 2);
let (_, summary) = analyze(&tone);
// mean square over all channels = 0.25; -0.691 + 10*log10(0.25)
let expected = -0.691 + 10.0 * 0.25f64.log10();
assert!((summary.integrated_lufs - expected).abs() < 1e-9);
}
/// channel_count of 0, a NULL planar pointer, or NULL for both outputs
/// returns OAKAUDIO_E_INVALID.
#[test]
fn invalid_input() {
let planes = common::planar_from(&[0.5f32; 8], 1);
let ptr = planes[0].as_ptr();
let mut summary = MeterStats {
max_peak_linear: 0.0,
integrated_lufs: 0.0,
silence: 0,
};
// channel_count 0.
assert_eq!(
unsafe { oakaudio_levelmeter_analyze(&ptr, 0, 8, std::ptr::null_mut(), 0, &mut summary) },
OAKAUDIO_E_INVALID
);
// NULL planar.
assert_eq!(
unsafe {
oakaudio_levelmeter_analyze(
std::ptr::null(),
1,
8,
std::ptr::null_mut(),
0,
&mut summary,
)
},
OAKAUDIO_E_INVALID
);
// Both outputs NULL.
assert_eq!(
unsafe {
oakaudio_levelmeter_analyze(&ptr, 1, 8, std::ptr::null_mut(), 0, std::ptr::null_mut())
},
OAKAUDIO_E_INVALID
);
// Negative frame count.
assert_eq!(
unsafe { oakaudio_levelmeter_analyze(&ptr, 1, -1, std::ptr::null_mut(), 0, &mut summary) },
OAKAUDIO_E_INVALID
);
}