Files
oak-editor/crates/oakaudio/tests/manager_test.rs
T
Mike-Solar 06f47235b2 feat(engine,app): source-monitor frames + real audio meter
source monitor:
- new facade exports oakengine_renderer_create_for_node (render any
  node, not just sequences) and oakengine_project_footage_at (fetch a
  footage node by index); it_render covers the e2e render plus the
  NULL/illegal matrix
- RealEngine renders the selected footage's real frames to the source
  viewer (per-node renderer slot, frame cache invalidated on selection
  change, synthetic fallback kept)
- known gap (documented): pixels stay transparent black until
  oakrender's footage decode hook lands — the render surface itself is
  real end to end

audio meter:
- oakaudio manager can now report per-channel linear peaks of the
  buffered output (PreviewAudioDevice::peek_tail + levelmeter analysis
  of the newest 8192 frames, packed/planar F32)
- new facade export oakengine_audio_output_levels (negative codes pass
  through); RealEngine's AudioMeterDataSource reads it instead of
  returning hardcoded silence
- tests: module peak readback + facade validation/readback matrix
2026-08-12 14:19:18 +08:00

479 lines
15 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/>.
//! AudioManager contract tests (manager.rs), through the C ABI. The
//! manager is a process-wide singleton, so every test holds the shared
//! `MANAGER_LOCK`.
mod common;
use std::ffi::c_char;
use common::MANAGER_LOCK;
use oakaudio::bridge::codec::EncodingParams;
use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK};
use oakaudio::ffi::manager::{
oakaudio_debug_alive_count, oakaudio_manager_clear_buffered_output,
oakaudio_manager_create_instance, oakaudio_manager_destroy_instance,
oakaudio_manager_find_config_device_by_name_s, oakaudio_manager_find_device_by_name_s,
oakaudio_manager_free, oakaudio_manager_get_input_device, oakaudio_manager_get_output_device,
oakaudio_manager_hard_reset, oakaudio_manager_instance, oakaudio_manager_push_to_output,
oakaudio_manager_reset_output_clock, oakaudio_manager_seconds,
oakaudio_manager_set_input_device, oakaudio_manager_set_output_device,
oakaudio_manager_output_levels, oakaudio_manager_set_output_notify_interval,
oakaudio_manager_start_recording,
oakaudio_manager_stop_output, oakaudio_manager_stop_recording,
};
fn instance() -> oakaudio::handle::CHandle {
unsafe { oakaudio_manager_instance() }
}
/// Lock the manager singleton for a test. The manager state persists across
/// tests (the `OnceLock` cannot be reset), so a panicked test must not
/// poison the lock for the rest of the binary.
fn lock() -> std::sync::MutexGuard<'static, ()> {
MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
fn encoding_params() -> EncodingParams {
let mut filename = [0u8; 1024];
for (i, b) in b"oakaudio_test.wav\0".iter().enumerate() {
filename[i] = *b;
}
EncodingParams {
filename,
format: 0,
video_enabled: 0,
video_codec: 0,
video_width: 0,
video_height: 0,
video_time_base_num: 0,
video_time_base_den: 0,
video_pixel_format: 0,
video_interlacing: 0,
video_pixel_aspect_num: 0,
video_pixel_aspect_den: 0,
video_bit_rate: 0,
video_min_bit_rate: 0,
video_max_bit_rate: 0,
video_buffer_size: 0,
video_threads: 0,
video_pix_fmt: [0u8; 64],
video_is_image_sequence: 0,
video_scaling_method: 0,
audio_enabled: 1,
audio_codec: 13, // PCM_S16LE (the .wav recording codec)
audio_sample_rate: 48000,
audio_channel_layout: 3,
audio_sample_format: 8,
audio_bit_rate: 128000,
subtitles_enabled: 0,
subtitles_codec: 0,
subtitles_are_sidecar: 0,
subtitles_sidecar_format: 0,
color_transform_output: [0u8; 256],
export_length_num: 0,
export_length_den: 0,
has_custom_range: 0,
custom_range_in_num: 0,
custom_range_in_den: 0,
custom_range_out_num: 0,
custom_range_out_den: 0,
}
}
/// create_instance/destroy_instance toggle the singleton; instance() returns
/// a valid borrowed handle between them and NULL after destroy.
#[test]
fn singleton_lifecycle() {
let _guard = lock();
unsafe { oakaudio_manager_destroy_instance() };
assert!(instance().ctx.is_null());
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert!(!m.ctx.is_null());
unsafe { oakaudio_manager_free(&mut m.clone()) };
unsafe { oakaudio_manager_destroy_instance() };
assert!(instance().ctx.is_null());
unsafe { oakaudio_manager_create_instance() };
assert!(!instance().ctx.is_null());
}
/// push_to_output accepts raw interleaved bytes and starts the virtual
/// playback clock; without a device it fails with a message in error_buf.
///
/// The virtual device never consumes frames (PortAudio is not bridged), so
/// the clock reads 0.0 rather than advancing.
#[test]
fn push_output_advances_clock() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
// No stream yet: seconds() reports -1.
let mut secs = 0.0f64;
assert_eq!(
unsafe { oakaudio_manager_seconds(m, &mut secs) },
OAKAUDIO_OK
);
assert_eq!(secs, -1.0);
// Without a device, push fails with a human-readable error. The
// singleton state persists across tests, so pin the no-device state
// explicitly.
assert_eq!(
unsafe { oakaudio_manager_set_output_device(m, -1) },
OAKAUDIO_OK
);
let samples = vec![0u8; 480 * 2 * 4];
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_push_to_output(
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
err.as_mut_ptr(),
err.len() as i32,
)
};
assert_eq!(r, OAKAUDIO_E_FAILED);
assert!(
err.iter().any(|&b| b != 0),
"error_buf must carry a message"
);
// After selecting a device the push succeeds and the clock starts at 0.
assert_eq!(
unsafe { oakaudio_manager_set_output_device(m, 0) },
OAKAUDIO_OK
);
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_push_to_output(
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
err.as_mut_ptr(),
err.len() as i32,
)
};
assert_eq!(r, OAKAUDIO_OK);
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, 0.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// set/get output & input device: getters report a device, setters persist
/// it; hard_reset keeps the device indices (it only stops the stream and
/// clears buffers).
#[test]
fn device_selection_roundtrip() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert_eq!(
unsafe { oakaudio_manager_set_output_device(m, 42) },
OAKAUDIO_OK
);
assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
assert_eq!(
unsafe { oakaudio_manager_set_input_device(m, 7) },
OAKAUDIO_OK
);
assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
assert_eq!(unsafe { oakaudio_manager_hard_reset(m) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
// The stream stopped, so the clock is back at -1.
let mut secs = 0.0f64;
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, -1.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// set_output_notify_interval stores the interval; clear_buffered_output
/// drops queued bytes, stop_output halts the stream, and reset_output_clock
/// restarts the counter.
#[test]
fn output_control_flags() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert_eq!(
unsafe { oakaudio_manager_set_output_notify_interval(m, 1024) },
OAKAUDIO_OK
);
assert_eq!(
unsafe { oakaudio_manager_set_output_notify_interval(m, -1) },
OAKAUDIO_E_INVALID
);
assert_eq!(
unsafe { oakaudio_manager_clear_buffered_output(m) },
OAKAUDIO_OK
);
assert_eq!(
unsafe { oakaudio_manager_reset_output_clock(m) },
OAKAUDIO_OK
);
// Push starts the stream, then stop_output halts it (clock -> -1).
unsafe { oakaudio_manager_set_output_device(m, 0) };
let samples = vec![0u8; 480 * 2 * 4];
assert_eq!(
unsafe {
oakaudio_manager_push_to_output(
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
std::ptr::null_mut(),
0,
)
},
OAKAUDIO_OK
);
assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
let mut secs = 1.0f64;
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, -1.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// start_recording validates its parameters: NULL params or a disabled
/// audio track return OAKAUDIO_E_INVALID with an error string. The full
/// encoder-open success path (oakcodec writes a real file via ffmpeg) is
/// an end-to-end concern covered by the codec crate's own tests; with the
/// real oakcodec linked, `start_recording` either opens the encoder
/// (OAKAUDIO_OK, environment-dependent) or reports the encoder's
/// last-error string — both are correct manager behavior, so this test
/// pins the manager's own validation only.
#[test]
fn recording_start_stop() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
unsafe { oakaudio_manager_set_input_device(m, 0) };
let mut err = [0 as c_char; 64];
let params = encoding_params();
// With a real encoder, the attempt must at least reach the encoder
// (a failure must surface a diagnostic in error_buf, not crash).
let r =
unsafe { oakaudio_manager_start_recording(m, &params, err.as_mut_ptr(), err.len() as i32) };
if r != 0 {
assert!(
err.iter().any(|&b| b != 0),
"failed start_recording must report a reason"
);
let _ = std::fs::remove_file("oakaudio_test.wav");
} else {
assert_eq!(unsafe { oakaudio_manager_stop_recording(m) }, OAKAUDIO_OK);
// The real encoder writes the output file during open; clean it up.
let _ = std::fs::remove_file("oakaudio_test.wav");
}
// NULL params is invalid and reports the reason in error_buf.
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_start_recording(m, std::ptr::null(), err.as_mut_ptr(), err.len() as i32)
};
assert_eq!(r, OAKAUDIO_E_INVALID);
assert!(err.iter().any(|&b| b != 0));
// A disabled audio track is likewise invalid.
let mut disabled = encoding_params();
disabled.audio_enabled = 0;
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_start_recording(m, &disabled, err.as_mut_ptr(), err.len() as i32)
};
assert_eq!(r, OAKAUDIO_E_INVALID);
assert!(err.iter().any(|&b| b != 0));
unsafe { oakaudio_manager_destroy_instance() };
}
/// Device enumeration is not bridged: every name/config lookup falls back to
/// paNoDevice (-1); a NULL name is OAKAUDIO_E_INVALID. The config-backed
/// buffer size/name helpers degrade to their defaults.
#[test]
fn device_name_lookup() {
let _guard = lock();
assert_eq!(
unsafe { oakaudio_manager_find_device_by_name_s(std::ptr::null(), 1) },
OAKAUDIO_E_INVALID
);
let name = c"anything";
assert_eq!(
unsafe { oakaudio_manager_find_device_by_name_s(name.as_ptr(), 1) },
-1
);
assert_eq!(
unsafe { oakaudio_manager_find_config_device_by_name_s(1) },
-1
);
assert_eq!(
unsafe { oakaudio_manager_find_config_device_by_name_s(0) },
-1
);
// config::output_buffer_size() reads its default (0) from the stub;
// device_name degrades to the empty string.
assert_eq!(oakaudio::config::output_buffer_size(), 0);
assert!(oakaudio::config::device_name(true).as_c_str().is_empty());
assert!(oakaudio::config::device_name(false).as_c_str().is_empty());
}
/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock) that
/// the manager path only touches indirectly.
#[test]
fn preview_device_pull_side() {
use oakaudio::params::AudioParams;
use oakaudio::previewdevice::PreviewAudioDevice;
let mut dev = PreviewAudioDevice::new();
dev.set_params(AudioParams {
sample_rate: 48000,
channel_layout: 3,
format: oakaudio::params::SampleFormat::F32,
});
assert_eq!(dev.bytes_per_frame(), 8);
let callbacks = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let cb = std::sync::Arc::clone(&callbacks);
dev.set_notify_callback(move || {
cb.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
dev.set_notify_interval(4);
dev.write(&[1u8; 10]);
// Reading 6 bytes crosses a 4-byte notify boundary.
let mut buf = [0u8; 6];
assert_eq!(dev.read(&mut buf), 6);
assert!(callbacks.load(std::sync::atomic::Ordering::Relaxed) >= 1);
assert_eq!(buf, [1u8; 6]);
// Clock accounting.
dev.add_output_frames(3);
assert_eq!(dev.output_frames_consumed(), 3);
dev.reset_output_frames();
assert_eq!(dev.output_frames_consumed(), 0);
dev.clear();
assert_eq!(dev.output_frames_consumed(), 0);
}
/// free(NULL)/free(empty) are no-ops on the manager handle.
#[test]
fn free_null_noop() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let before = unsafe { oakaudio_debug_alive_count() };
let mut empty = oakaudio::handle::CHandle::null();
unsafe { oakaudio_manager_free(&mut empty) };
unsafe { oakaudio_manager_free(std::ptr::null_mut()) };
assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
unsafe { oakaudio_manager_destroy_instance() };
}
/// output_levels: validation, the no-output case, and a real peak
/// readback over pushed packed-F32 stereo samples (left ramps to 0.25,
/// right to ~1.0 — the per-channel linear peaks).
#[test]
fn output_levels_reports_buffered_peaks() {
let _guard = lock();
unsafe { oakaudio_manager_destroy_instance() };
assert_eq!(unsafe { oakaudio_manager_create_instance() }, OAKAUDIO_OK);
let m = instance();
// Invalid out args.
let mut peaks = [0.0f32; 4];
assert_eq!(
unsafe { oakaudio_manager_output_levels(m, std::ptr::null_mut(), 4) },
OAKAUDIO_E_INVALID
);
assert_eq!(unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 0) }, OAKAUDIO_E_INVALID);
// The manager singleton retains output params across tests in this
// binary, so the "no output" case is not reachable here; instead
// verify a cleared buffer reports zeroed peaks (channel count from
// the configured layout, 0 only when nothing was ever configured).
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
let cleared = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
assert!(cleared >= 0);
for p in &peaks[..cleared as usize] {
assert_eq!(*p, 0.0, "cleared buffer must have silent peaks");
}
// Push 480 frames of packed F32 stereo (format 10), stereo layout 0x3.
let frames = 480usize;
let mut samples = Vec::with_capacity(frames * 2);
for i in 0..frames {
let t = i as f32 / frames as f32;
samples.push(0.25f32 * t);
samples.push(t);
}
let rc = unsafe {
oakaudio_manager_push_to_output(
m,
48000,
0x3,
10,
samples.as_ptr() as *const c_char,
(samples.len() * 4) as i64,
std::ptr::null_mut(),
0,
)
};
assert_eq!(rc, OAKAUDIO_OK);
let n = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
assert_eq!(n, 2);
let last = (frames - 1) as f32 / frames as f32;
assert!((peaks[0] - 0.25 * last).abs() < 1e-6, "left peak: {}", peaks[0]);
assert!((peaks[1] - last).abs() < 1e-6, "right peak: {}", peaks[1]);
// Undersized buffer truncates the write, not the reported count.
let mut one = [0.0f32; 1];
assert_eq!(unsafe { oakaudio_manager_output_levels(m, one.as_mut_ptr(), 1) }, 2);
// Leave the singleton as we found it: the push flipped
// `output_started`, which other tests' seconds() assertions depend on.
assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
}