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).
This commit is contained in:
@@ -18,8 +18,7 @@ oakcodec = { path = "../oakcodec" }
|
||||
# crate calls ffmpeg-next directly (same as oakcodec). The oakffmpeg-link
|
||||
# dependency above emits the transitive link flags of the static FFmpeg.
|
||||
ffmpeg-next = "9"
|
||||
# Real audio output (M12 P1): the preview playback stream. The PortAudio
|
||||
# C library must be installed (macOS: brew install portaudio).
|
||||
portaudio = "0.8"
|
||||
|
||||
cpal = {version = "0.18", features = ["pulseaudio", "pipewire", "jack", "asio"]}
|
||||
# `std::error::Error` impls for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcodec C ABI calls (audio encoding + decoding for the manager and
|
||||
//! waveform extraction) — now direct Rust calls into the oakcodec crate
|
||||
//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`).
|
||||
//!
|
||||
//! The previous `*mut c_void` handle convention drifted from the real
|
||||
//! codec C ABI (which uses the shared [`CHandle`]); the wrappers below
|
||||
//! match the oakcodec ffi signatures exactly.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakcodec_encoding_params` — single-lib unification: aliases the
|
||||
/// oakcodec crate's POD (identical layout; only the audio fields are
|
||||
/// consumed by oakaudio).
|
||||
pub type EncodingParams = oakcodec::ffi::encoder::oakcodec_encoding_params;
|
||||
|
||||
/// `oakcodec_audio_stream_info` — audio stream metadata from probing.
|
||||
/// Single-lib unification: aliases the oakcodec crate's POD.
|
||||
pub type AudioStreamInfo = oakcodec::decoder::OakCodecAudioStreamInfo;
|
||||
|
||||
/// `oakcodec_encoder_init` — create an encoder for `params`.
|
||||
pub unsafe fn oakcodec_encoder_init(params: *const EncodingParams) -> CHandle {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_free`.
|
||||
pub unsafe fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_open`.
|
||||
pub unsafe fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_open(encoder) }
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_audio` — feed interleaved `f32` audio.
|
||||
pub unsafe fn oakcodec_encoder_write_audio(
|
||||
encoder: CHandle,
|
||||
samples: *const f32,
|
||||
frame_count: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_audio(encoder, samples, frame_count) }
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_flush`.
|
||||
pub unsafe fn oakcodec_encoder_flush(encoder: CHandle) -> c_int {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_flush(encoder) }
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_last_error` — copy the last error string into `buf`.
|
||||
pub unsafe fn oakcodec_encoder_last_error(
|
||||
encoder: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_last_error(encoder, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_probe` — create a probe handle for a file.
|
||||
pub unsafe fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle {
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(filename) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_free`.
|
||||
pub unsafe fn oakcodec_decoder_free(decoder: *mut CHandle) {
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_probe_audio_stream_count`.
|
||||
pub unsafe fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -> c_int {
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_audio_stream_count(probe) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_probe_get_audio_stream` — copy audio stream info.
|
||||
pub unsafe fn oakcodec_decoder_probe_get_audio_stream(
|
||||
probe: CHandle,
|
||||
index: c_int,
|
||||
out: *mut AudioStreamInfo,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_get_audio_stream(probe, index, out) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_open` — open stream `stream_index` for decoding.
|
||||
pub unsafe fn oakcodec_decoder_open(
|
||||
decoder: CHandle,
|
||||
filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, filename, stream_index) }
|
||||
}
|
||||
|
||||
/// `oakcodec_decoder_decode_audio` — decode/convert frames into `buf`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub unsafe fn oakcodec_decoder_decode_audio(
|
||||
decoder: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
buf: *mut f32,
|
||||
buf_frames: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::decoder::oakcodec_decoder_decode_audio(
|
||||
decoder,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
buf,
|
||||
buf_frames,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon C ABI calls (config access + ffmpeg format conversion) —
|
||||
//! now direct Rust calls into the oakcommon crate (single-lib
|
||||
//! unification, see `docs/zh/plans/riir/single-lib.md`).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
/// `oakcommon_config_get` — copy a config string value into `buf`
|
||||
/// (two-stage; returns the required size including NUL).
|
||||
pub unsafe fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int` — read an integer config value with a
|
||||
/// default.
|
||||
pub unsafe fn oakcommon_config_get_int(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) }
|
||||
}
|
||||
|
||||
/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format` — map an ffmpeg
|
||||
/// sample format enum to the oak core format, or the reverse.
|
||||
pub unsafe fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(
|
||||
smp_fmt: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::ffmpegutils::oakcommon_ffmpegutils_get_ffmpeg_sample_format(smp_fmt, out)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from other oak modules (signatures mirror the public
|
||||
//! headers verbatim; resolved at link time).
|
||||
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
@@ -17,20 +17,16 @@
|
||||
//! The `audio_config` namespace from `src/audio/src/configbridge.*`:
|
||||
//! audio-specific configuration read through the oakcommon C ABI.
|
||||
|
||||
use std::error::Error;
|
||||
use std::ffi::CString;
|
||||
|
||||
use std::str::FromStr;
|
||||
use oakcommon::configstore::*;
|
||||
/// PortAudio output buffer size in frames; 0 = let PortAudio choose.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/configbridge.cpp:30`
|
||||
/// (`audio_config::output_buffer_size`).
|
||||
pub fn output_buffer_size() -> i32 {
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_int(
|
||||
std::ptr::null(),
|
||||
c"AudioOutputBufferSize".as_ptr(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
ConfigStore::instance().get_int(None, "AudioOutputBufferSize", 0)
|
||||
}
|
||||
|
||||
/// Name of the configured audio device for `is_output_device`
|
||||
@@ -39,35 +35,19 @@ pub fn output_buffer_size() -> i32 {
|
||||
/// `// CPP-PARITY: src/audio/src/configbridge.cpp:36`
|
||||
/// (`audio_config::device_name`): two-stage size query; `size <= 1`
|
||||
/// (absent or empty string) yields the empty string.
|
||||
pub fn device_name(is_output_device: bool) -> CString {
|
||||
pub fn device_name(is_output_device: bool) -> Result<String, Box<dyn Error>> {
|
||||
let key = if is_output_device {
|
||||
c"AudioOutput"
|
||||
"AudioOutput"
|
||||
} else {
|
||||
c"AudioInput"
|
||||
"AudioInput"
|
||||
};
|
||||
unsafe {
|
||||
let size = crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
key.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
if size <= 1 {
|
||||
// Absent (OAKCOMMON_E_NOT_FOUND) or empty
|
||||
return CString::default();
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
if crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
key.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut std::ffi::c_char,
|
||||
size,
|
||||
) < 0
|
||||
{
|
||||
return CString::default();
|
||||
}
|
||||
// The buffer is NUL-terminated by the callee.
|
||||
CString::from_vec_with_nul(buf)
|
||||
.unwrap_or_else(|e| CString::new(e.into_bytes()).unwrap_or_default())
|
||||
let store = ConfigStore::instance();
|
||||
let size = i32::from_str(store.get(None, key)?.as_str())?;
|
||||
if size <= 1 {
|
||||
// Absent (OAKCOMMON_E_NOT_FOUND) or empty
|
||||
return Err(Box::new(crate::error::Error::NotFound));
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
let name = store.get(None, key)?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub const OAKAUDIO_E_NOT_FOUND: i32 = -60004;
|
||||
pub const OAKAUDIO_E_NOMEM: i32 = -60005;
|
||||
|
||||
/// Crate-internal result type; the FFI layer maps it to the codes.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,226 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! Refcounted-handle scaffolding. Same pattern as the oaknode crate
|
||||
//! (`src/node/rust/src/handle.rs`); intentionally duplicated rather
|
||||
//! than shared — each module DLL must run its own addref/release code
|
||||
//! (the function pointers in a handle always point into the DLL that
|
||||
//! created the object).
|
||||
//!
|
||||
//! The `AudioManager` is the one exception: it uses the same `CHandle`
|
||||
//! layout but with singleton semantics (addref/release no-ops). See
|
||||
//! `manager.rs` and `include/audio/manager.h`.
|
||||
//!
|
||||
//! `// CPP-PARITY: src/audio/c_api/refcounted.h` (RefCounted box,
|
||||
//! make_handle_in_place, free_handle) and `src/audio/c_api/alive.cpp`
|
||||
//! (alive ledger behind `oakaudio_debug_alive_count`).
|
||||
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
|
||||
|
||||
use crate::error::{Error, OAKAUDIO_E_FAILED, OAKAUDIO_OK};
|
||||
|
||||
/// ABI version stamped into every handle.
|
||||
pub const OAKAUDIO_ABI_VERSION: u32 = 1;
|
||||
|
||||
/// Live-object ledger behind `oakaudio_debug_alive_count`.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/alive.cpp` (`g_alive`).
|
||||
static ALIVE: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
/// Current number of live refcounted oakaudio objects.
|
||||
pub fn alive_count() -> i32 {
|
||||
ALIVE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Heap box behind a handle's `ctx`.
|
||||
pub struct RefBox<T: ?Sized> {
|
||||
/// Atomic reference count.
|
||||
pub refs: AtomicU32,
|
||||
/// Boxed value.
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// The shared ABI value-handle type (single-lib unification, see
|
||||
/// `docs/zh/plans/riir/single-lib.md`): one canonical
|
||||
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
|
||||
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
|
||||
/// source-compatible. It is `Clone + Copy` (handles cross the C ABI by
|
||||
/// value) and `Send + Sync` (refcounted, shared across threads).
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_addref`).
|
||||
unsafe extern "C" fn owned_addref<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
// SAFETY: `ctx` is either NULL or points to a `RefBox<T>` created by
|
||||
// `make_owned`; we only touch it through the reference while it is live.
|
||||
if let Some(b) = unsafe { (ctx as *const RefBox<T>).as_ref() } {
|
||||
b.refs.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_release`):
|
||||
/// destroys the box at zero and decrements the alive ledger.
|
||||
unsafe extern "C" fn owned_release<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
// SAFETY: `ctx` is either NULL or points to a live `RefBox<T>` created
|
||||
// by `make_owned`; the refcount guards against double-free, and the box
|
||||
// is only reclaimed once the count reaches zero.
|
||||
if let Some(b) = unsafe { (ctx as *const RefBox<T>).as_ref() } {
|
||||
if b.refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(unsafe { Box::from_raw(ctx as *mut RefBox<T>) });
|
||||
ALIVE.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned handle with count 1; empty on allocation failure.
|
||||
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
|
||||
let b = Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value,
|
||||
});
|
||||
ALIVE.fetch_add(1, Ordering::Relaxed);
|
||||
CHandle {
|
||||
ctx: Box::into_raw(b) as *mut std::ffi::c_void,
|
||||
addref: Some(owned_addref::<T>),
|
||||
release: Some(owned_release::<T>),
|
||||
abi_version: OAKAUDIO_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// No-op addref/release for singleton (borrowed) handles.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`singleton_addref` /
|
||||
/// `singleton_release`).
|
||||
unsafe extern "C" fn noop_ref(_ctx: *mut std::ffi::c_void) {}
|
||||
|
||||
/// Borrowed handle for an object owned elsewhere (addref/release are
|
||||
/// no-ops; nothing is ever freed through the handle).
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller guarantees `ptr` outlives every derived handle.
|
||||
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
|
||||
CHandle {
|
||||
ctx: ptr as *mut std::ffi::c_void,
|
||||
addref: Some(noop_ref),
|
||||
release: Some(noop_ref),
|
||||
abi_version: OAKAUDIO_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed view into an owned handle; `None` for empty handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type.
|
||||
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
|
||||
if h.ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: caller guarantees `h` is a valid owned handle whose ctx points
|
||||
// to a `RefBox<T>`; the handle stays alive through the returned borrow.
|
||||
let v = unsafe { &(*(h.ctx as *const RefBox<T>)).value };
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// Typed mutable view into an owned handle; `None` for empty handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type.
|
||||
pub unsafe fn get_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
|
||||
if h.ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: caller guarantees `h` is a valid owned handle whose ctx points
|
||||
// to a `RefBox<T>`; the handle stays alive through the returned borrow,
|
||||
// and the caller must not alias it with other live borrows.
|
||||
let v = unsafe { &mut (*(h.ctx as *mut RefBox<T>)).value };
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// Shared free() body: release the ctx, no-op on NULL/empty handle.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`free_handle`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `self_` must be a valid handle pointer or NULL.
|
||||
pub unsafe fn free_handle(h: *mut CHandle) {
|
||||
// SAFETY: caller guarantees `h` is a valid handle pointer or NULL.
|
||||
if let Some(r) = unsafe { h.as_mut() } {
|
||||
if !r.ctx.is_null() {
|
||||
if let Some(release) = r.release {
|
||||
// SAFETY: the release fn belongs to the same DLL and
|
||||
// accepts the ctx it originally created.
|
||||
unsafe { release(r.ctx) };
|
||||
}
|
||||
r.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for i32-returning exports.
|
||||
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(())) => OAKAUDIO_OK,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKAUDIO_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for handle-returning exports.
|
||||
pub fn guard_handle<F: FnOnce() -> crate::error::Result<CHandle>>(f: F) -> CHandle {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for i32 value-returning exports (errors map
|
||||
/// to the negative error code).
|
||||
pub fn guard_int<F: FnOnce() -> crate::error::Result<i32>>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(v)) => v,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKAUDIO_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for void exports.
|
||||
pub fn guard_void<F: FnOnce()>(f: F) {
|
||||
let _ = catch_unwind(AssertUnwindSafe(f));
|
||||
}
|
||||
|
||||
/// Copy a human-readable error string into a C buffer (NUL-terminated,
|
||||
/// truncated to fit). Returns the required size including the NUL.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`write_error`).
|
||||
pub fn write_error(s: &str, buf: *mut std::ffi::c_char, buf_size: i32) {
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let bytes = s.as_bytes();
|
||||
let n = bytes.len().min((buf_size - 1) as usize);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
|
||||
*(buf as *mut u8).add(n) = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: map a condition to an Invalid error.
|
||||
pub fn invalid_if(cond: bool) -> crate::error::Result<()> {
|
||||
if cond {
|
||||
Err(Error::Invalid)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,8 @@
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod bridge;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod handle;
|
||||
pub mod levelmeter;
|
||||
pub mod manager;
|
||||
pub mod params;
|
||||
|
||||
+339
-403
@@ -26,13 +26,15 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use crate::bridge::codec::EncodingParams;
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
use cpal::{Device, DeviceId};
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use oakcodec::encoder::Encoder;
|
||||
use oakcodec::encodingparams::EncodingParams;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{make_borrowed, CHandle};
|
||||
use crate::params::AudioParams;
|
||||
use crate::previewdevice::PreviewAudioDevice;
|
||||
use crate::error::Error::NotFound;
|
||||
|
||||
/// `paNoDevice` (PortAudio "no device" sentinel; also the default when no
|
||||
/// device is configured).
|
||||
@@ -46,7 +48,11 @@ static DESTROYED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Manager state (all device/stream fields; PortAudio itself is not bridged,
|
||||
/// see [`ManagerInner::default`] for the degradations).
|
||||
struct ManagerInner {
|
||||
///
|
||||
/// Public since the single-lib unification (the deleted `ffi`/`bridge`
|
||||
/// C ABI is gone): the oakengine facade calls the singleton's methods
|
||||
/// directly through [`instance`] instead of crossing the old C ABI.
|
||||
pub struct ManagerInner {
|
||||
/// Current output device index.
|
||||
output_device: i32,
|
||||
/// Current input device index.
|
||||
@@ -62,8 +68,8 @@ struct ManagerInner {
|
||||
/// The real PortAudio output stream (M12 P1; opened lazily on the
|
||||
/// first pushed samples).
|
||||
output_device_stream: crate::outputdevice::PortAudioOutput,
|
||||
/// Active oakcodec recording encoder (NULL when idle).
|
||||
recording: Option<CHandle>,
|
||||
/// Active oakcodec recording encoder (None when idle).
|
||||
recording: Option<Arc<dyn Encoder>>,
|
||||
}
|
||||
|
||||
// SAFETY: the raw encoder pointer is only touched while the manager mutex is
|
||||
@@ -89,290 +95,308 @@ impl Default for ManagerInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock the manager behind a handle; `OAKAUDIO_E_STATE` for empty handles.
|
||||
fn with_instance(h: &CHandle) -> Result<MutexGuard<'static, ManagerInner>> {
|
||||
if h.is_null() {
|
||||
return Err(Error::State);
|
||||
impl ManagerInner {
|
||||
/// Create the process-wide AudioManager (no-op when it exists). Returns
|
||||
/// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new`
|
||||
/// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly
|
||||
/// panics, so the error code is never produced).
|
||||
pub fn create_instance() -> Result<()> {
|
||||
DESTROYED.store(false, Ordering::SeqCst);
|
||||
let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
|
||||
Ok(())
|
||||
}
|
||||
// SAFETY: `instance()` only creates borrowed handles whose ctx points at
|
||||
// the MANAGER Mutex, which lives in a static for the whole process.
|
||||
let m: &'static Mutex<ManagerInner> = unsafe { &*(h.ctx as *const Mutex<ManagerInner>) };
|
||||
Ok(m.lock().unwrap_or_else(|p| p.into_inner()))
|
||||
}
|
||||
|
||||
/// Create the process-wide AudioManager (no-op when it exists). Returns
|
||||
/// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new`
|
||||
/// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly
|
||||
/// panics, so the error code is never produced).
|
||||
pub fn create_instance() -> Result<()> {
|
||||
DESTROYED.store(false, Ordering::SeqCst);
|
||||
let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Destroy the process-wide AudioManager (no-op when absent).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is
|
||||
/// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED`
|
||||
/// flag makes [`instance`] return an empty handle (and a later
|
||||
/// [`create_instance`] resurrects the existing box).
|
||||
pub fn destroy_instance() {
|
||||
DESTROYED.store(true, Ordering::SeqCst);
|
||||
// The C++ singleton is deleted on destroy; the OnceLock cannot be
|
||||
// reset, so the resurrection must at least come back with a fresh
|
||||
// playback state (the previous session's output params/buffer would
|
||||
// otherwise leak into the next session's meters and clock).
|
||||
if let Some(m) = MANAGER.get() {
|
||||
let mut inner = m.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.output_params = None;
|
||||
inner.output_buffer.clear();
|
||||
inner.output_started = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a handle to the process-wide AudioManager (borrowed; empty when
|
||||
/// no instance exists).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a
|
||||
/// borrowed singleton whose addref/release are no-ops).
|
||||
pub fn instance() -> CHandle {
|
||||
if DESTROYED.load(Ordering::SeqCst) {
|
||||
return CHandle::null();
|
||||
}
|
||||
match MANAGER.get() {
|
||||
Some(m) => {
|
||||
// SAFETY: `m` is the process-wide singleton; borrowed handles do
|
||||
// not free it, so it outlives every handle.
|
||||
unsafe { make_borrowed(m as *const _ as *mut Mutex<ManagerInner>) }
|
||||
}
|
||||
None => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a manager handle. No-op (singleton), safe on NULL/empty.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:95` — releasing never
|
||||
/// destroys; just clear the caller's copy.
|
||||
pub fn free(self_: *mut CHandle) {
|
||||
unsafe {
|
||||
if let Some(h) = self_.as_mut() {
|
||||
h.ctx = std::ptr::null_mut();
|
||||
/// Destroy the process-wide AudioManager (no-op when absent).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is
|
||||
/// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED`
|
||||
/// flag makes [`instance`] return an empty handle (and a later
|
||||
/// [`create_instance`] resurrects the existing box).
|
||||
pub fn destroy_instance() {
|
||||
DESTROYED.store(true, Ordering::SeqCst);
|
||||
// The C++ singleton is deleted on destroy; the OnceLock cannot be
|
||||
// reset, so the resurrection must at least come back with a fresh
|
||||
// playback state (the previous session's output params/buffer would
|
||||
// otherwise leak into the next session's meters and clock).
|
||||
if let Some(m) = MANAGER.get() {
|
||||
let mut inner = m.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.output_params = None;
|
||||
inner.output_buffer.clear();
|
||||
inner.output_started = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes between output-notify pulses (0 disables).
|
||||
pub fn set_output_notify_interval(self_: &CHandle, bytes: i64) -> Result<()> {
|
||||
if bytes < 0 {
|
||||
return Err(Error::Invalid);
|
||||
/// Return a handle to the process-wide AudioManager (borrowed; empty when
|
||||
/// no instance exists).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a
|
||||
/// borrowed singleton whose addref/release are no-ops).
|
||||
pub fn instance(&self) -> Option<&Self> {
|
||||
if DESTROYED.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
match MANAGER.get() {
|
||||
Some(m) => {
|
||||
// SAFETY: `m` is the process-wide singleton; borrowed handles do
|
||||
// not free it, so it outlives every handle.
|
||||
Some(self)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
let mut m = with_instance(self_)?;
|
||||
m.output_buffer.set_notify_interval(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push a block of samples to the output device, opening/restarting the
|
||||
/// stream when the params changed.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio
|
||||
/// open/start path is not bridged; the buffer is configured and written
|
||||
/// directly and the "stream" is marked running. `error_buf` is written by
|
||||
/// the FFI layer from the returned error.
|
||||
pub fn push_to_output(
|
||||
self_: &CHandle,
|
||||
params: AudioParams,
|
||||
samples: &[u8],
|
||||
_error_buf: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
if m.output_params.as_ref() != Some(¶ms) {
|
||||
m.output_params = Some(params);
|
||||
m.output_buffer.set_params(params);
|
||||
// M12 P1: open (or re-open) the real output stream on a format
|
||||
// change; device < 0 selects the system default. A stream
|
||||
// failure keeps the samples buffered (silent playback) instead
|
||||
// of failing the push.
|
||||
let device = m.output_device;
|
||||
let rate = params.sample_rate;
|
||||
/// Bytes between output-notify pulses (0 disables).
|
||||
pub fn set_output_notify_interval(&self, bytes: i64) -> Result<()> {
|
||||
if bytes < 0 {
|
||||
return Err(Box::new(Error::Invalid));
|
||||
}
|
||||
self.output_buffer.set_notify_interval(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push a block of samples to the output device, opening/restarting the
|
||||
/// stream when the params changed.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio
|
||||
/// open/start path is not bridged; the buffer is configured and written
|
||||
/// directly and the "stream" is marked running. `error_buf` is written by
|
||||
/// the FFI layer from the returned error.
|
||||
pub fn push_to_output(
|
||||
&mut self,
|
||||
params: AudioParams,
|
||||
samples: &[u8],
|
||||
_error_buf: &mut [u8],
|
||||
) -> Result<()> {
|
||||
if self.output_params.as_ref() != Some(¶ms) {
|
||||
self.output_params = Some(params);
|
||||
self.output_buffer.set_params(params);
|
||||
// M12 P1: open (or re-open) the real output stream on a format
|
||||
// change; device < 0 selects the system default. A stream
|
||||
// failure keeps the samples buffered (silent playback) instead
|
||||
// of failing the push.
|
||||
let device = self.output_device;
|
||||
let rate = params.sample_rate;
|
||||
let channels = params.channel_count();
|
||||
let sink = self.output_buffer.clone();
|
||||
let _ = self.output_device_stream.ensure_open(device, rate, channels, sink);
|
||||
}
|
||||
self.output_buffer.write(samples);
|
||||
self.output_started = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discard buffered output.
|
||||
pub fn clear_buffered_output(&self) -> Result<()> {
|
||||
self.output_buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the output stream.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts
|
||||
/// the stream and clears the buffer).
|
||||
pub fn stop_output(&mut self) -> Result<()> {
|
||||
self.output_started = false;
|
||||
self.output_buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Seconds of audio consumed by the output device since the last reset,
|
||||
/// compensated for output latency; negative when no stream is running.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's
|
||||
/// `outputLatency` is not representable without a live stream, so the buffer
|
||||
/// clock is used directly (the `max(0, ...)` clamp is kept).
|
||||
pub fn seconds(&self, out: &mut f64) -> Result<()> {
|
||||
if !self.output_started {
|
||||
*out = -1.0;
|
||||
return Ok(());
|
||||
}
|
||||
let rate = self.output_params.map(|p| p.sample_rate).unwrap_or(0);
|
||||
if rate <= 0 {
|
||||
*out = -1.0;
|
||||
return Ok(());
|
||||
}
|
||||
let secs = self.output_buffer.output_frames_consumed() as f64 / f64::from(rate);
|
||||
*out = secs.max(0.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restart the output clock at zero.
|
||||
pub fn reset_output_clock(&self) -> Result<()> {
|
||||
self.output_buffer.reset_output_frames();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Current output device index (`paNoDevice` = -1) or a negative error code.
|
||||
pub fn get_output_device(&self) -> Result<i32> {
|
||||
Ok(self.output_device)
|
||||
}
|
||||
|
||||
/// Set the output device index.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is
|
||||
/// recorded and the stream closed; PortAudio's index validation and name
|
||||
/// logging are not bridged).
|
||||
pub fn set_output_device(&mut self, device: i32) -> Result<()> {
|
||||
self.output_device = device;
|
||||
self.output_started = false;
|
||||
self.output_buffer.clear();
|
||||
// The stream reopens with the new device on the next push.
|
||||
self.output_device_stream.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Current input device index or a negative error code.
|
||||
pub fn get_input_device(&self) -> Result<i32> {
|
||||
Ok(self.input_device)
|
||||
}
|
||||
|
||||
/// Set the input device index.
|
||||
pub fn set_input_device(&mut self, device: i32) -> Result<()> {
|
||||
self.input_device = device;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the output stream and re-initialize PortAudio.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/
|
||||
/// initialize is not bridged; the output side is reset).
|
||||
pub fn hard_reset(&mut self) -> Result<()> {
|
||||
self.output_started = false;
|
||||
self.output_buffer.clear();
|
||||
self.output_device_stream.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start recording the input device to a file via the oakcodec encoder
|
||||
/// (direct Rust calls; the encoder is a `dyn Encoder` value). The input
|
||||
/// stream is always captured as interleaved f32.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open;
|
||||
/// the PortAudio input stream is not bridged). On failure the encoder's
|
||||
/// last-error string is surfaced when available.
|
||||
pub fn start_recording(
|
||||
&mut self,
|
||||
params: &EncodingParams,
|
||||
_error_buf: &mut [u8],
|
||||
) -> Result<()> {
|
||||
if self.input_device == PA_NO_DEVICE {
|
||||
return Err(Box::new(Error::Failed("no input device".to_string())));
|
||||
}
|
||||
let encoder = oakcodec::encoder::create_from_params(params)
|
||||
.ok_or_else(|| Error::Failed("failed to create encoder for recording".to_string()))?;
|
||||
encoder
|
||||
.configure(params)
|
||||
.map_err(|e| Error::Failed(format!("encoder configure failed: {e:?}")))?;
|
||||
if let Err(e) = encoder.open() {
|
||||
let detail = encoder.get_error();
|
||||
let msg = if detail.is_empty() {
|
||||
format!("failed to open encoder for recording: {e:?}")
|
||||
} else {
|
||||
format!("failed to open encoder for recording: {detail}")
|
||||
};
|
||||
return Err(Box::new(Error::Failed(msg)));
|
||||
}
|
||||
self.recording = Some(encoder);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop recording.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input
|
||||
/// stream is not bridged; the encoder is flushed and closed).
|
||||
pub fn stop_recording(&mut self) -> Result<()> {
|
||||
if let Some(encoder) = self.recording.take() {
|
||||
let _ = encoder.flush();
|
||||
let _ = encoder.close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Peak level (linear, 0..1 and above) of each channel of the buffered,
|
||||
/// not-yet-consumed output, written to `peaks` in channel order.
|
||||
/// Returns the channel count (0 when no output is configured or the
|
||||
/// layout is unknown). Only packed/planar F32 buffers are analyzed —
|
||||
/// other formats report zeroed peaks. The analysis window is the most
|
||||
/// recent 8192 frames of the queue.
|
||||
///
|
||||
/// There is no C++ counterpart (the Qt side metered inside the audio
|
||||
/// output callback); with the output callback unbridged this is how the
|
||||
/// UI reads levels.
|
||||
pub fn output_levels(&self, peaks: &mut [f32]) -> Result<i32> {
|
||||
let Some(params) = self.output_params else {
|
||||
return Ok(0);
|
||||
};
|
||||
let channels = params.channel_count();
|
||||
let sink = m.output_buffer.clone();
|
||||
let _ = m.output_device_stream.ensure_open(device, rate, channels, sink);
|
||||
}
|
||||
m.output_buffer.write(samples);
|
||||
m.output_started = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discard buffered output.
|
||||
pub fn clear_buffered_output(self_: &CHandle) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
m.output_buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the output stream.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts
|
||||
/// the stream and clears the buffer).
|
||||
pub fn stop_output(self_: &CHandle) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
m.output_started = false;
|
||||
m.output_buffer.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Seconds of audio consumed by the output device since the last reset,
|
||||
/// compensated for output latency; negative when no stream is running.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's
|
||||
/// `outputLatency` is not representable without a live stream, so the buffer
|
||||
/// clock is used directly (the `max(0, ...)` clamp is kept).
|
||||
pub fn seconds(self_: &CHandle, out: &mut f64) -> Result<()> {
|
||||
let m = with_instance(self_)?;
|
||||
if !m.output_started {
|
||||
*out = -1.0;
|
||||
return Ok(());
|
||||
}
|
||||
let rate = m.output_params.map(|p| p.sample_rate).unwrap_or(0);
|
||||
if rate <= 0 {
|
||||
*out = -1.0;
|
||||
return Ok(());
|
||||
}
|
||||
let secs = m.output_buffer.output_frames_consumed() as f64 / f64::from(rate);
|
||||
*out = secs.max(0.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restart the output clock at zero.
|
||||
pub fn reset_output_clock(self_: &CHandle) -> Result<()> {
|
||||
let m = with_instance(self_)?;
|
||||
m.output_buffer.reset_output_frames();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Current output device index (`paNoDevice` = -1) or a negative error code.
|
||||
pub fn get_output_device(self_: &CHandle) -> Result<i32> {
|
||||
let m = with_instance(self_)?;
|
||||
Ok(m.output_device)
|
||||
}
|
||||
|
||||
/// Set the output device index.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is
|
||||
/// recorded and the stream closed; PortAudio's index validation and name
|
||||
/// logging are not bridged).
|
||||
pub fn set_output_device(self_: &CHandle, device: i32) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
m.output_device = device;
|
||||
m.output_started = false;
|
||||
m.output_buffer.clear();
|
||||
// The stream reopens with the new device on the next push.
|
||||
m.output_device_stream.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Current input device index or a negative error code.
|
||||
pub fn get_input_device(self_: &CHandle) -> Result<i32> {
|
||||
let m = with_instance(self_)?;
|
||||
Ok(m.input_device)
|
||||
}
|
||||
|
||||
/// Set the input device index.
|
||||
pub fn set_input_device(self_: &CHandle, device: i32) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
m.input_device = device;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the output stream and re-initialize PortAudio.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/
|
||||
/// initialize is not bridged; the output side is reset).
|
||||
pub fn hard_reset(self_: &CHandle) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
m.output_started = false;
|
||||
m.output_buffer.clear();
|
||||
m.output_device_stream.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start recording the input device to a file via the oakcodec encoder C
|
||||
/// ABI. The input stream is always captured as interleaved f32.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open;
|
||||
/// the PortAudio input stream is not bridged). On failure the encoder's
|
||||
/// last-error string is surfaced when available.
|
||||
pub fn start_recording(
|
||||
self_: &CHandle,
|
||||
params: &EncodingParams,
|
||||
_error_buf: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
if m.input_device == PA_NO_DEVICE {
|
||||
return Err(Error::Failed("no input device".to_string()));
|
||||
}
|
||||
eprintln!(
|
||||
"MANAGER before encoder_init: audio_enabled={} codec={}",
|
||||
params.audio_enabled, params.audio_codec
|
||||
);
|
||||
let mut enc = unsafe { crate::bridge::codec::oakcodec_encoder_init(params) };
|
||||
eprintln!(
|
||||
"MANAGER encoder_init null? {} ptr={:p} size={}",
|
||||
enc.is_null(),
|
||||
params as *const EncodingParams,
|
||||
std::mem::size_of::<EncodingParams>()
|
||||
);
|
||||
let direct =
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params as *const EncodingParams) };
|
||||
eprintln!("MANAGER direct init null? {}", direct.is_null());
|
||||
if !direct.is_null() {
|
||||
let mut d = direct;
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(&mut d) };
|
||||
}
|
||||
if enc.is_null() {
|
||||
return Err(Error::Failed(
|
||||
"failed to open encoder for recording".to_string(),
|
||||
));
|
||||
}
|
||||
let open_r = unsafe { crate::bridge::codec::oakcodec_encoder_open(enc) };
|
||||
if open_r != 0 {
|
||||
let mut buf = [0i8; 512];
|
||||
let n = unsafe {
|
||||
crate::bridge::codec::oakcodec_encoder_last_error(
|
||||
enc,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i32,
|
||||
)
|
||||
};
|
||||
let msg = if n > 0 {
|
||||
let s = buf.split(|c| *c == 0).next().unwrap_or(&[]);
|
||||
let bytes: Vec<u8> = s.iter().map(|&b| b as u8).collect();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
} else {
|
||||
"failed to open encoder for recording".to_string()
|
||||
};
|
||||
unsafe { crate::bridge::codec::oakcodec_encoder_free(&mut enc) };
|
||||
return Err(Error::Failed(msg));
|
||||
}
|
||||
m.recording = Some(enc);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop recording.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input
|
||||
/// stream is not bridged; the encoder is flushed and freed).
|
||||
pub fn stop_recording(self_: &CHandle) -> Result<()> {
|
||||
let mut m = with_instance(self_)?;
|
||||
if let Some(mut enc) = m.recording.take() {
|
||||
unsafe {
|
||||
crate::bridge::codec::oakcodec_encoder_flush(enc);
|
||||
crate::bridge::codec::oakcodec_encoder_free(&mut enc);
|
||||
if channels <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let n = (channels as usize).min(peaks.len());
|
||||
for p in &mut peaks[..n] {
|
||||
*p = 0.0;
|
||||
}
|
||||
use crate::params::SampleFormat;
|
||||
let packed = params.format == SampleFormat::F32;
|
||||
let planar = params.format == SampleFormat::F32Planar;
|
||||
if !packed && !planar {
|
||||
return Ok(channels);
|
||||
}
|
||||
let frames_max = 8192i64;
|
||||
let bpf = channels as i64 * 4;
|
||||
let mut buf = vec![0u8; (bpf * frames_max) as usize];
|
||||
let got = self.output_buffer.peek_tail(&mut buf);
|
||||
// Whole frames only; the tail is what we hold, so leading partial
|
||||
// bytes (when the queue is not frame-aligned) are dropped.
|
||||
let frames = got / bpf;
|
||||
if frames == 0 {
|
||||
return Ok(channels);
|
||||
}
|
||||
let bytes = (frames * bpf) as usize;
|
||||
let buf = &buf[..bytes];
|
||||
let frame_count = frames as usize;
|
||||
let channel_count = channels as usize;
|
||||
let mut planes: Vec<Vec<f32>> = vec![Vec::with_capacity(frame_count); channel_count];
|
||||
if packed {
|
||||
for frame in buf.chunks_exact(bpf as usize) {
|
||||
for ch in 0..channel_count {
|
||||
let b = &frame[ch * 4..ch * 4 + 4];
|
||||
planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (ch, plane) in planes.iter_mut().enumerate() {
|
||||
let start = ch * frame_count * 4;
|
||||
for b in buf[start..start + frame_count * 4].chunks_exact(4) {
|
||||
plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
|
||||
let stats = crate::levelmeter::analyze_sample_buffer(&views);
|
||||
for (i, p) in peaks[..n].iter_mut().enumerate() {
|
||||
*p = stats.channels[i].peak_linear as f32;
|
||||
}
|
||||
Ok(channels)
|
||||
}
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
/// Lock the process-wide manager singleton (the direct-Rust replacement
|
||||
/// for the deleted C ABI's `oakaudio_manager_instance`): `None` when no
|
||||
/// instance exists (never created, or destroyed since the last
|
||||
/// [`ManagerInner::create_instance`]).
|
||||
pub fn instance() -> Option<MutexGuard<'static, ManagerInner>> {
|
||||
if DESTROYED.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
MANAGER
|
||||
.get()
|
||||
.map(|m| m.lock().unwrap_or_else(|e| e.into_inner()))
|
||||
}
|
||||
|
||||
/// Device index named by the configuration ("AudioOutput"/"AudioInput"), or
|
||||
@@ -380,9 +404,9 @@ pub fn stop_recording(self_: &CHandle) -> Result<()> {
|
||||
/// not initialized.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:404`.
|
||||
pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 {
|
||||
let name = crate::config::device_name(is_output_device);
|
||||
find_device_by_name_s(&name, is_output_device)
|
||||
pub fn find_config_device_by_name_s(is_output_device: bool) -> Result<Device> {
|
||||
let name = crate::config::device_name(is_output_device)?;
|
||||
find_device_by_name_s_or_default(&name, is_output_device)
|
||||
}
|
||||
|
||||
/// Device index whose name matches `name` exactly (empty matches nothing,
|
||||
@@ -391,82 +415,31 @@ pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 {
|
||||
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:410` — PortAudio device
|
||||
/// enumeration cannot be bridged from this crate, so the result is always
|
||||
/// `paNoDevice` and the caller falls back to the default device.
|
||||
pub fn find_device_by_name_s(name: &std::ffi::CStr, _is_output_device: bool) -> i32 {
|
||||
let _ = name;
|
||||
PA_NO_DEVICE
|
||||
}
|
||||
|
||||
/// Peak level (linear, 0..1 and above) of each channel of the buffered,
|
||||
/// not-yet-consumed output, written to `peaks` in channel order.
|
||||
/// Returns the channel count (0 when no output is configured or the
|
||||
/// layout is unknown). Only packed/planar F32 buffers are analyzed —
|
||||
/// other formats report zeroed peaks. The analysis window is the most
|
||||
/// recent 8192 frames of the queue.
|
||||
///
|
||||
/// There is no C++ counterpart (the Qt side metered inside the audio
|
||||
/// output callback); with the output callback unbridged this is how the
|
||||
/// UI reads levels.
|
||||
pub fn output_levels(self_: &CHandle, peaks: &mut [f32]) -> Result<i32> {
|
||||
let m = with_instance(self_)?;
|
||||
let Some(params) = m.output_params else {
|
||||
return Ok(0);
|
||||
};
|
||||
let channels = params.channel_count();
|
||||
if channels <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let n = (channels as usize).min(peaks.len());
|
||||
for p in &mut peaks[..n] {
|
||||
*p = 0.0;
|
||||
}
|
||||
use crate::params::SampleFormat;
|
||||
let packed = params.format == SampleFormat::F32;
|
||||
let planar = params.format == SampleFormat::F32Planar;
|
||||
if !packed && !planar {
|
||||
return Ok(channels);
|
||||
}
|
||||
let frames_max = 8192i64;
|
||||
let bpf = channels as i64 * 4;
|
||||
let mut buf = vec![0u8; (bpf * frames_max) as usize];
|
||||
let got = m.output_buffer.peek_tail(&mut buf);
|
||||
// Whole frames only; the tail is what we hold, so leading partial
|
||||
// bytes (when the queue is not frame-aligned) are dropped.
|
||||
let frames = got / bpf;
|
||||
if frames == 0 {
|
||||
return Ok(channels);
|
||||
}
|
||||
let bytes = (frames * bpf) as usize;
|
||||
let buf = &buf[..bytes];
|
||||
let frame_count = frames as usize;
|
||||
let channel_count = channels as usize;
|
||||
let mut planes: Vec<Vec<f32>> = vec![Vec::with_capacity(frame_count); channel_count];
|
||||
if packed {
|
||||
for frame in buf.chunks_exact(bpf as usize) {
|
||||
for ch in 0..channel_count {
|
||||
let b = &frame[ch * 4..ch * 4 + 4];
|
||||
planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) -> Result<Device> {
|
||||
let host = cpal::default_host();
|
||||
let device = host.devices()?.find(|d| {
|
||||
if d.id().is_err(){
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (ch, plane) in planes.iter_mut().enumerate() {
|
||||
let start = ch * frame_count * 4;
|
||||
for b in buf[start..start + frame_count * 4].chunks_exact(4) {
|
||||
plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
if d.supports_output() && d.id().unwrap().id() == name {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
});
|
||||
if let Some(device) = device {
|
||||
Ok(device)
|
||||
}
|
||||
else{
|
||||
if let Some(device) = host.default_output_device() {
|
||||
Ok(device)
|
||||
}
|
||||
else{
|
||||
Err(Box::new(Error::NotFound))
|
||||
}
|
||||
}
|
||||
let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
|
||||
let stats = crate::levelmeter::analyze_sample_buffer(&views);
|
||||
for (i, p) in peaks[..n].iter_mut().enumerate() {
|
||||
*p = stats.channels[i].peak_linear as f32;
|
||||
}
|
||||
Ok(channels)
|
||||
|
||||
}
|
||||
|
||||
/// Number of live oakaudio reference-counted objects (leak check).
|
||||
pub fn debug_alive_count() -> i32 {
|
||||
crate::handle::alive_count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -485,40 +458,9 @@ mod tests {
|
||||
// is_active=true while delivering zero callbacks.
|
||||
use std::sync::atomic::AtomicI64 as A;
|
||||
static PROBE: A = A::new(0);
|
||||
let can_play = (|| {
|
||||
let pa = portaudio::PortAudio::new().ok()?;
|
||||
let dev = pa.default_output_device().ok()?;
|
||||
let info = pa.device_info(dev).ok()?;
|
||||
let params = portaudio::StreamParameters::<f32>::new(
|
||||
dev,
|
||||
2,
|
||||
true,
|
||||
info.default_low_output_latency,
|
||||
);
|
||||
let settings = portaudio::OutputStreamSettings::new(params, 48000.0, 512);
|
||||
let cb = move |args: portaudio::OutputStreamCallbackArgs<f32>| {
|
||||
PROBE.fetch_add(args.frames as i64, Ordering::Relaxed);
|
||||
portaudio::Continue
|
||||
};
|
||||
let mut stream = pa.open_non_blocking_stream(settings, cb).ok()?;
|
||||
stream.start().ok()?;
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
while std::time::Instant::now() < deadline && PROBE.load(Ordering::Relaxed) == 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
let got = PROBE.load(Ordering::Relaxed) > 0;
|
||||
let _ = stream.stop();
|
||||
Some(got)
|
||||
})();
|
||||
if can_play != Some(true) {
|
||||
eprintln!("audio session cannot deliver callbacks; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
DESTROYED.store(false, Ordering::SeqCst);
|
||||
let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
|
||||
let h = instance();
|
||||
assert!(!h.is_null());
|
||||
let manager = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
|
||||
|
||||
// A 440 Hz sine, 0.2 s at 48 kHz stereo, packed F32.
|
||||
let params = AudioParams {
|
||||
@@ -537,8 +479,7 @@ mod tests {
|
||||
.iter()
|
||||
.flat_map(|s| s.to_le_bytes())
|
||||
.collect();
|
||||
push_to_output(
|
||||
&h,
|
||||
manager.lock().unwrap().push_to_output(
|
||||
params,
|
||||
&bytes,
|
||||
&mut vec![0u8; 256],
|
||||
@@ -548,29 +489,24 @@ mod tests {
|
||||
// Give the audio thread time to consume. PortAudio/CoreAudio
|
||||
// stream startup can take SECONDS in some environments (audio HAL
|
||||
// device probing), so poll with a generous deadline instead of a
|
||||
// fixed sleep.
|
||||
// fixed sleep. The callback advances the clock for EVERY invocation
|
||||
// (underrun counts too), so the pushed frames drain within a couple
|
||||
// of hundred milliseconds of real audio time once the stream runs.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
let target = frames as i64 - 1024;
|
||||
let mut consumed = 0i64;
|
||||
while std::time::Instant::now() < deadline {
|
||||
{
|
||||
let m = with_instance(&h).unwrap();
|
||||
consumed = m.output_buffer.output_frames_consumed();
|
||||
}
|
||||
if consumed > 0 {
|
||||
consumed = manager.lock().unwrap().output_buffer.output_frames_consumed();
|
||||
if consumed >= target {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
assert!(
|
||||
consumed > 0,
|
||||
"the output callback must consume pushed frames"
|
||||
);
|
||||
assert!(
|
||||
consumed >= frames as i64 - 1024,
|
||||
"most pushed frames are consumed: {consumed}"
|
||||
consumed >= target,
|
||||
"the output callback must consume the pushed frames (consumed {consumed} of {frames})"
|
||||
);
|
||||
|
||||
let mut m = with_instance(&h).unwrap();
|
||||
m.output_device_stream.close();
|
||||
manager.lock().unwrap().output_device_stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! M12 P1: the real audio output device (PortAudio, direct crate call).
|
||||
//! M12 P1: the real audio output device (cpal, direct crate call).
|
||||
//!
|
||||
//! The playback stream's callback pulls interleaved bytes from the
|
||||
//! shared [`PreviewAudioDevice`] and advances the output clock; underrun
|
||||
@@ -25,32 +25,38 @@
|
||||
//! device's mutex (a short critical section; the buffer is drained in
|
||||
//! whole frames).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use portaudio::{
|
||||
DeviceIndex, OutputStreamCallbackArgs, OutputStreamSettings, StreamParameters, Stream,
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{
|
||||
BufferSize, Device, Host, SampleFormat, SampleRate, Stream, StreamConfig,
|
||||
SupportedBufferSize,
|
||||
};
|
||||
|
||||
use crate::previewdevice::PreviewAudioDevice;
|
||||
|
||||
/// The default PortAudio frames-per-buffer for the preview stream.
|
||||
/// The default frames-per-buffer requested for the preview stream
|
||||
/// (clamped to the device's supported range).
|
||||
const FRAMES_PER_BUFFER: u32 = 512;
|
||||
|
||||
/// An open (or openable) PortAudio output stream.
|
||||
/// An open (or openable) cpal output stream.
|
||||
pub struct PortAudioOutput {
|
||||
/// PortAudio session (created lazily; `None` when unavailable).
|
||||
pa: Option<portaudio::PortAudio>,
|
||||
/// Audio host/session (created lazily; `None` when unavailable).
|
||||
host: Option<Host>,
|
||||
/// The live stream (None when closed).
|
||||
stream: Option<Stream<portaudio::NonBlocking, portaudio::Output<f32>>>,
|
||||
stream: Option<Stream>,
|
||||
/// The (device, rate, channels) the stream was opened with.
|
||||
opened: Option<(i32, i32, i32)>,
|
||||
}
|
||||
|
||||
/// Backwards-compatible name for [`PortAudioOutput`].
|
||||
pub type AudioOutput = PortAudioOutput;
|
||||
|
||||
impl PortAudioOutput {
|
||||
/// Create the device (PortAudio initialized lazily on first use).
|
||||
/// Create the device (the host is created lazily on first use).
|
||||
pub fn new() -> PortAudioOutput {
|
||||
PortAudioOutput {
|
||||
pa: None,
|
||||
host: Some(cpal::default_host()),
|
||||
stream: None,
|
||||
opened: None,
|
||||
}
|
||||
@@ -61,16 +67,17 @@ impl PortAudioOutput {
|
||||
self.stream.is_some()
|
||||
}
|
||||
|
||||
/// Close the stream (stop + drop).
|
||||
/// Close the stream (pause + drop).
|
||||
pub fn close(&mut self) {
|
||||
if let Some(mut s) = self.stream.take() {
|
||||
let _ = s.stop();
|
||||
if let Some(stream) = self.stream.take() {
|
||||
let _ = stream.pause();
|
||||
}
|
||||
self.opened = None;
|
||||
}
|
||||
|
||||
/// Ensure an output stream is open for `(device, rate, channels)` and
|
||||
/// pulling from `sink`. `device` < 0 selects the system default.
|
||||
/// pulling from `sink`. `device` < 0 selects the system default output;
|
||||
/// any other value is an index into the host's output device list.
|
||||
/// Failures leave the output silent (samples still buffer).
|
||||
pub fn ensure_open(
|
||||
&mut self,
|
||||
@@ -87,70 +94,117 @@ impl PortAudioOutput {
|
||||
}
|
||||
self.close();
|
||||
|
||||
let pa = match &self.pa {
|
||||
Some(pa) => pa.clone(),
|
||||
None => {
|
||||
let pa = portaudio::PortAudio::new()
|
||||
.map_err(|e| format!("PortAudio init failed: {e:?}"))?;
|
||||
self.pa = Some(pa);
|
||||
match self.pa.as_ref() {
|
||||
Some(pa) => pa.clone(),
|
||||
None => unreachable!("just assigned"),
|
||||
}
|
||||
}
|
||||
};
|
||||
if self.host.is_none(){
|
||||
self.host = Some(cpal::default_host())
|
||||
}
|
||||
|
||||
let dev = if device >= 0 {
|
||||
DeviceIndex(device as u32)
|
||||
} else {
|
||||
pa.default_output_device()
|
||||
.map_err(|e| format!("no default output device: {e:?}"))?
|
||||
};
|
||||
let info = pa
|
||||
.device_info(dev)
|
||||
.map_err(|e| format!("device info failed: {e:?}"))?;
|
||||
let latency = info.default_low_output_latency;
|
||||
let params = StreamParameters::<f32>::new(dev, channels, true, latency);
|
||||
let settings = OutputStreamSettings::<f32>::new(params, rate as f64, FRAMES_PER_BUFFER);
|
||||
let host = self.host.as_ref().unwrap().clone();
|
||||
|
||||
let output_device = resolve_device(&host, device)
|
||||
.ok_or_else(|| "no output device available".to_string())?;
|
||||
let config = pick_config(&output_device, rate, channels)?;
|
||||
|
||||
// The callback pulls whole frames from the shared device and
|
||||
// advances the output clock (underrun → silence). `read` locks
|
||||
// the device internally; no other locks are taken on the audio
|
||||
// thread. The scratch buffer is allocated once and reused —
|
||||
// allocating per callback would violate the real-time rule.
|
||||
let channels_usize = channels.max(1) as usize;
|
||||
let sink_cb = sink.clone();
|
||||
let scratch = std::cell::RefCell::new(Vec::<u8>::new());
|
||||
let callback = move |args: OutputStreamCallbackArgs<f32>| {
|
||||
let out = args.buffer;
|
||||
let frames = args.frames;
|
||||
let total = frames * channels.max(1) as usize;
|
||||
let scratch = RefCell::new(Vec::<u8>::new());
|
||||
let callback = move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
let total = out.len();
|
||||
let mut scratch = scratch.borrow_mut();
|
||||
scratch.resize(total * 4, 0);
|
||||
let byte_buf = &mut *scratch;
|
||||
let got = sink_cb.read(byte_buf);
|
||||
let frames_got = (got as usize) / (channels.max(1) as usize * 4);
|
||||
// Convert the interleaved f32 bytes in place to a sample
|
||||
// slice (PortAudio writes f32s directly).
|
||||
let n_samples = frames_got * channels.max(1) as usize;
|
||||
for i in 0..n_samples {
|
||||
let b = &byte_buf[i * 4..i * 4 + 4];
|
||||
out[i] = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
|
||||
let got = sink_cb.read(scratch.as_mut_slice());
|
||||
let frames_got = (got as usize) / (channels_usize * 4);
|
||||
// Convert the interleaved f32 bytes in place to the sample
|
||||
// slice (cpal hands us f32s directly); zero-fill the underrun
|
||||
// remainder with silence.
|
||||
let n_samples = frames_got * channels_usize;
|
||||
for (i, s) in out.iter_mut().enumerate() {
|
||||
if i < n_samples {
|
||||
let b = &scratch[i * 4..i * 4 + 4];
|
||||
*s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
|
||||
} else {
|
||||
*s = 0.0;
|
||||
}
|
||||
}
|
||||
for s in out.iter_mut().skip(n_samples) {
|
||||
*s = 0.0; // underrun: silence
|
||||
}
|
||||
sink_cb.add_output_frames(frames as i64);
|
||||
portaudio::Continue
|
||||
sink_cb.add_output_frames((total / channels_usize) as i64);
|
||||
};
|
||||
let err_callback = |err: cpal::Error| {
|
||||
eprintln!("output stream error: {err}");
|
||||
};
|
||||
|
||||
let mut stream = pa
|
||||
.open_non_blocking_stream(settings, callback)
|
||||
.map_err(|e| format!("output stream open failed: {e:?}"))?;
|
||||
let stream = output_device
|
||||
.build_output_stream(config, callback, err_callback, None)
|
||||
.map_err(|e| format!("output stream open failed: {e}"))?;
|
||||
stream
|
||||
.start()
|
||||
.map_err(|e| format!("output stream start failed: {e:?}"))?;
|
||||
.play()
|
||||
.map_err(|e| format!("output stream start failed: {e}"))?;
|
||||
self.stream = Some(stream);
|
||||
self.opened = Some((device, rate, channels));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PortAudioOutput {
|
||||
fn default() -> Self {
|
||||
PortAudioOutput::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a PortAudio-style device index to a cpal device: an index >= 0
|
||||
/// picks the Nth output device of the host; anything else (notably -1,
|
||||
/// `paNoDevice`) falls back to the host's default output device.
|
||||
fn resolve_device(host: &Host, index: i32) -> Option<Device> {
|
||||
if index >= 0 {
|
||||
if let Ok(mut devices) = host.output_devices() {
|
||||
if let Some(device) = devices.nth(index as usize) {
|
||||
return Some(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
host.default_output_device()
|
||||
}
|
||||
|
||||
/// Pick an F32 stream config for `(rate, channels)`. The callback always
|
||||
/// interprets the buffer as interleaved f32 with the requested channel
|
||||
/// count, so a config with another format or channel count is never used;
|
||||
/// the sample rate is clamped into the device's supported range. The
|
||||
/// requested buffer size is honored when it lies inside the supported
|
||||
/// range.
|
||||
fn pick_config(device: &Device, rate: i32, channels: i32) -> Result<StreamConfig, String> {
|
||||
let want_rate = rate as u32;
|
||||
let want_channels = channels as u16;
|
||||
|
||||
if let Ok(mut configs) = device.supported_output_configs() {
|
||||
while let Some(c) = configs.next() {
|
||||
if c.sample_format() != SampleFormat::F32 || c.channels() != want_channels {
|
||||
continue;
|
||||
}
|
||||
let rate = want_rate.clamp(c.min_sample_rate(), c.max_sample_rate());
|
||||
let buffer_size = match c.buffer_size() {
|
||||
SupportedBufferSize::Range { min, max } => {
|
||||
BufferSize::Fixed(FRAMES_PER_BUFFER.clamp(*min, *max))
|
||||
}
|
||||
SupportedBufferSize::Unknown => BufferSize::Default,
|
||||
};
|
||||
return Ok(StreamConfig {
|
||||
channels: want_channels,
|
||||
sample_rate: rate,
|
||||
buffer_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let fallback = device
|
||||
.default_output_config()
|
||||
.map_err(|e| format!("output device config unavailable: {e}"))?;
|
||||
if fallback.sample_format() != SampleFormat::F32 || fallback.channels() != want_channels {
|
||||
return Err(format!(
|
||||
"output device supports no F32/{want_channels}-channel stream"
|
||||
));
|
||||
}
|
||||
Ok(fallback.config())
|
||||
}
|
||||
|
||||
+208
-222
@@ -32,11 +32,10 @@ use ffmpeg::{ChannelLayout, Error as FfmpegError};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{free_handle, make_owned, CHandle};
|
||||
use crate::params::{AudioParams, SampleFormat};
|
||||
|
||||
/// A closed audio processor (reference count 1; `ctx == NULL` on allocation
|
||||
/// failure).
|
||||
/// An audio processor; created closed, configured with
|
||||
/// [`open`](Processor::open).
|
||||
pub struct Processor {
|
||||
inner: Mutex<ProcessorInner>,
|
||||
}
|
||||
@@ -54,8 +53,8 @@ struct ProcessorInner {
|
||||
}
|
||||
|
||||
// SAFETY: the filter graph's raw pointers are only dereferenced through the
|
||||
// FFmpeg API while the mutex is held, so all access is serialized; the
|
||||
// handle's refcount keeps the box alive.
|
||||
// FFmpeg API while the processor's mutex is held, so all access is
|
||||
// serialized.
|
||||
unsafe impl Send for ProcessorInner {}
|
||||
|
||||
impl Default for ProcessorInner {
|
||||
@@ -136,7 +135,7 @@ fn fix_channel_layout(params: AudioParams) -> AudioParams {
|
||||
fn build_graph(from: &AudioParams, to: &AudioParams, speed: f64) -> Result<ffmpeg::filter::Graph> {
|
||||
let in_format = to_ffmpeg_sample_format(from.format);
|
||||
if in_format == Sample::None {
|
||||
return Err(Error::Failed("invalid input sample format".to_string()));
|
||||
return Err(Box::new(Error::Failed("invalid input sample format".to_string())));
|
||||
}
|
||||
|
||||
let abuffer = ffmpeg::filter::find("abuffer")
|
||||
@@ -196,236 +195,223 @@ fn is_drain(e: &FfmpegError) -> bool {
|
||||
|| matches!(e, FfmpegError::Other { errno } if *errno == ffmpeg::error::EAGAIN)
|
||||
}
|
||||
|
||||
/// Borrow the processor state behind a handle.
|
||||
fn get_processor(self_: &CHandle) -> Result<&Processor> {
|
||||
// SAFETY: every non-empty handle returned by `init` boxes a `Processor`.
|
||||
unsafe { crate::handle::get::<Processor>(self_) }.ok_or(Error::Invalid)
|
||||
}
|
||||
|
||||
/// Create a closed processor.
|
||||
pub fn init() -> Result<CHandle> {
|
||||
Ok(make_owned(Processor {
|
||||
inner: Mutex::new(ProcessorInner::default()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Release one reference to a processor (NULL/empty no-op).
|
||||
pub fn free(self_: *mut CHandle) {
|
||||
unsafe { free_handle(self_) };
|
||||
}
|
||||
|
||||
/// Open the resampling/format-conversion graph. `out_format` is accepted for
|
||||
/// interface completeness but the conversion output is always planar f32.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order:
|
||||
/// empty handle, already-open state, invalid rates/speed, forced output
|
||||
/// format) and `src/audio/src/audioprocessor.cpp:82` (graph creation).
|
||||
pub fn open(self_: &CHandle, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> {
|
||||
let p = get_processor(self_)?;
|
||||
let mut inner = p.inner.lock().unwrap();
|
||||
|
||||
if inner.graph.is_some() {
|
||||
// C++: "tried to open a processor that was already open"
|
||||
return Err(Error::State);
|
||||
}
|
||||
if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
// The C ABI delivers planar float output only; force the output format
|
||||
// stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4).
|
||||
if to.format != SampleFormat::F32Planar {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
|
||||
let from_fixed = fix_channel_layout(from);
|
||||
let to_fixed = fix_channel_layout(to);
|
||||
|
||||
// C++: "failed to create audio filter graph"
|
||||
let graph = build_graph(&from_fixed, &to_fixed, speed)?;
|
||||
|
||||
inner.graph = Some(graph);
|
||||
inner.out_frame = ffmpeg::frame::Audio::empty();
|
||||
inner.from = from_fixed;
|
||||
inner.to = to_fixed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the graph (safe when closed; handle must be non-empty).
|
||||
pub fn close(self_: &CHandle) -> Result<()> {
|
||||
let p = get_processor(self_)?;
|
||||
let mut inner = p.inner.lock().unwrap();
|
||||
|
||||
inner.graph = None;
|
||||
inner.out_frame = ffmpeg::frame::Audio::empty();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 1 when open, 0 when closed; error for an empty handle.
|
||||
pub fn is_open(self_: &CHandle) -> Result<bool> {
|
||||
let p = get_processor(self_)?;
|
||||
let inner = p.inner.lock().unwrap();
|
||||
Ok(inner.graph.is_some())
|
||||
}
|
||||
|
||||
/// Push planar float input and pull converted output. Returns the number of
|
||||
/// output frames written.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state
|
||||
/// check, null `out_planar` short-circuit) and
|
||||
/// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting).
|
||||
pub fn convert(
|
||||
self_: &CHandle,
|
||||
in_planar: *const *const f32,
|
||||
in_frame_count: i32,
|
||||
out_planar: *const *mut f32,
|
||||
out_capacity_frames: i32,
|
||||
) -> Result<i32> {
|
||||
let p = get_processor(self_)?;
|
||||
let mut guard = p.inner.lock().unwrap();
|
||||
let inner = &mut *guard;
|
||||
|
||||
if inner.graph.is_none() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
|
||||
{
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
|
||||
let channels = inner.to.channel_count();
|
||||
if channels <= 0 {
|
||||
return Err(Error::State);
|
||||
}
|
||||
|
||||
let from = inner.from;
|
||||
let graph = inner.graph.as_mut().unwrap();
|
||||
let out_frame = &mut inner.out_frame;
|
||||
|
||||
if in_frame_count > 0 {
|
||||
// The FFI layer has no way to know the input plane count, so the
|
||||
// plane pointer array is walked using the input spec recorded at
|
||||
// `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`).
|
||||
let nb = in_frame_count as usize;
|
||||
let in_channels = from.channel_count().max(0) as usize;
|
||||
let layout = channel_layout_from_mask(from.channel_layout);
|
||||
let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
|
||||
frame.set_rate(from.sample_rate as u32);
|
||||
let planar = from.format.is_planar();
|
||||
// `plane_mut::<T>` requires the exact sample type of the frame
|
||||
// format, so the copy dispatches on the recorded input format.
|
||||
macro_rules! fill {
|
||||
($t:ty) => {{
|
||||
if planar {
|
||||
for ch in 0..in_channels {
|
||||
// SAFETY: `in_planar` is non-null here and the FFI
|
||||
// contract guarantees at least `from.channel_count()`
|
||||
// entries, each pointing at `nb` samples of the
|
||||
// recorded input format.
|
||||
let src = unsafe { *in_planar.add(ch) } as *const $t;
|
||||
let dst = frame.plane_mut::<$t>(ch);
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) };
|
||||
}
|
||||
} else {
|
||||
// Packed input: a single plane at `in_planar[0]`.
|
||||
// SAFETY: see above; the plane holds `nb * channels`
|
||||
// samples.
|
||||
let src = unsafe { *in_planar } as *const $t;
|
||||
let dst = frame.plane_mut::<$t>(0);
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
|
||||
}
|
||||
}};
|
||||
impl Processor {
|
||||
/// Create a closed processor.
|
||||
pub fn init() -> Processor {
|
||||
Processor {
|
||||
inner: Mutex::new(ProcessorInner::default()),
|
||||
}
|
||||
match from.format {
|
||||
SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8),
|
||||
SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16),
|
||||
SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32),
|
||||
SampleFormat::S64Planar | SampleFormat::S64 => {
|
||||
// ffmpeg-next's typed plane API has no `i64` impl; copy the
|
||||
// 8-byte samples through the raw plane pointers.
|
||||
if planar {
|
||||
for ch in 0..in_channels {
|
||||
}
|
||||
|
||||
/// Open the resampling/format-conversion graph. `out_format` is accepted
|
||||
/// for interface completeness but the conversion output is always planar
|
||||
/// f32.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order:
|
||||
/// already-open state, invalid rates/speed, forced output format) and
|
||||
/// `src/audio/src/audioprocessor.cpp:82` (graph creation).
|
||||
pub fn open(&self, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
if inner.graph.is_some() {
|
||||
// C++: "tried to open a processor that was already open"
|
||||
return Err(Box::from(Error::State));
|
||||
}
|
||||
if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 {
|
||||
return Err(Box::from(Error::Invalid));
|
||||
}
|
||||
// The C ABI delivers planar float output only; force the output format
|
||||
// stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4).
|
||||
if to.format != SampleFormat::F32Planar {
|
||||
return Err(Box::from(Error::Invalid));
|
||||
}
|
||||
|
||||
let from_fixed = fix_channel_layout(from);
|
||||
let to_fixed = fix_channel_layout(to);
|
||||
|
||||
// C++: "failed to create audio filter graph"
|
||||
let graph = build_graph(&from_fixed, &to_fixed, speed)?;
|
||||
|
||||
inner.graph = Some(graph);
|
||||
inner.out_frame = ffmpeg::frame::Audio::empty();
|
||||
inner.from = from_fixed;
|
||||
inner.to = to_fixed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the graph (safe when closed).
|
||||
pub fn close(&self) -> Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
inner.graph = None;
|
||||
inner.out_frame = ffmpeg::frame::Audio::empty();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 1 when open, 0 when closed.
|
||||
pub fn is_open(&self) -> Result<bool> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
Ok(inner.graph.is_some())
|
||||
}
|
||||
|
||||
/// Push planar float input and pull converted output. Returns the number
|
||||
/// of output frames written.
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state
|
||||
/// check, null `out_planar` short-circuit) and
|
||||
/// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting).
|
||||
pub fn convert(
|
||||
&self,
|
||||
in_planar: *const *const f32,
|
||||
in_frame_count: i32,
|
||||
out_planar: *const *mut f32,
|
||||
out_capacity_frames: i32,
|
||||
) -> Result<i32> {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let inner = &mut *guard;
|
||||
|
||||
if inner.graph.is_none() {
|
||||
return Err(Box::from(Error::State));
|
||||
}
|
||||
if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
|
||||
{
|
||||
return Err(Box::from(Error::Invalid));
|
||||
}
|
||||
|
||||
let channels = inner.to.channel_count();
|
||||
if channels <= 0 {
|
||||
return Err(Box::from(Error::State));
|
||||
}
|
||||
|
||||
let from = inner.from;
|
||||
let graph = inner.graph.as_mut().unwrap();
|
||||
let out_frame = &mut inner.out_frame;
|
||||
|
||||
if in_frame_count > 0 {
|
||||
// The FFI layer has no way to know the input plane count, so the
|
||||
// plane pointer array is walked using the input spec recorded at
|
||||
// `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`).
|
||||
let nb = in_frame_count as usize;
|
||||
let in_channels = from.channel_count().max(0) as usize;
|
||||
let layout = channel_layout_from_mask(from.channel_layout);
|
||||
let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
|
||||
frame.set_rate(from.sample_rate as u32);
|
||||
let planar = from.format.is_planar();
|
||||
// `plane_mut::<T>` requires the exact sample type of the frame
|
||||
// format, so the copy dispatches on the recorded input format.
|
||||
macro_rules! fill {
|
||||
($t:ty) => {{
|
||||
if planar {
|
||||
for ch in 0..in_channels {
|
||||
// SAFETY: `in_planar` is non-null here and the FFI
|
||||
// contract guarantees at least `from.channel_count()`
|
||||
// entries, each pointing at `nb` samples of the
|
||||
// recorded input format.
|
||||
let src = unsafe { *in_planar.add(ch) } as *const $t;
|
||||
let dst = frame.plane_mut::<$t>(ch);
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) };
|
||||
}
|
||||
} else {
|
||||
// Packed input: a single plane at `in_planar[0]`.
|
||||
// SAFETY: see above; the plane holds `nb * channels`
|
||||
// samples.
|
||||
let src = unsafe { *in_planar } as *const $t;
|
||||
let dst = frame.plane_mut::<$t>(0);
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
|
||||
}
|
||||
}};
|
||||
}
|
||||
match from.format {
|
||||
SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8),
|
||||
SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16),
|
||||
SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32),
|
||||
SampleFormat::S64Planar | SampleFormat::S64 => {
|
||||
// ffmpeg-next's typed plane API has no `i64` impl; copy the
|
||||
// 8-byte samples through the raw plane pointers.
|
||||
if planar {
|
||||
for ch in 0..in_channels {
|
||||
// SAFETY: same contract as above; the plane is
|
||||
// `nb * 8` bytes.
|
||||
let src = unsafe { *in_planar.add(ch) } as *const u8;
|
||||
let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) };
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) };
|
||||
}
|
||||
} else {
|
||||
// SAFETY: same contract as above; the plane is
|
||||
// `nb * 8` bytes.
|
||||
let src = unsafe { *in_planar.add(ch) } as *const u8;
|
||||
let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) };
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) };
|
||||
// `nb * channels * 8` bytes.
|
||||
let src = unsafe { *in_planar } as *const u8;
|
||||
let dst = unsafe { *(*frame.as_mut_ptr()).extended_data };
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) };
|
||||
}
|
||||
} else {
|
||||
// SAFETY: same contract as above; the plane is
|
||||
// `nb * channels * 8` bytes.
|
||||
let src = unsafe { *in_planar } as *const u8;
|
||||
let dst = unsafe { *(*frame.as_mut_ptr()).extended_data };
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) };
|
||||
}
|
||||
SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32),
|
||||
SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64),
|
||||
SampleFormat::Invalid => return Err(Box::new(Error::State)),
|
||||
}
|
||||
SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32),
|
||||
SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64),
|
||||
SampleFormat::Invalid => return Err(Error::State),
|
||||
}
|
||||
if let Err(e) = graph.get("in").unwrap().source().add(&frame) {
|
||||
return Err(Error::Failed(format!(
|
||||
"failed to add frame to buffersrc: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// C++: `out_planar ? &buf : nullptr` — with no destination, the input is
|
||||
// pushed but nothing is pulled.
|
||||
if out_planar.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut total: i64 = 0;
|
||||
loop {
|
||||
let pulled = graph.get("out").unwrap().sink().frame(out_frame);
|
||||
match pulled {
|
||||
Ok(()) => {}
|
||||
Err(e) if is_drain(&e) => break,
|
||||
Err(e) => {
|
||||
return Err(Error::Failed(format!(
|
||||
"failed to pull from buffersink: {e}"
|
||||
)))
|
||||
if let Err(e) = graph.get("in").unwrap().source().add(&frame) {
|
||||
return Err(Box::new(Error::Failed(format!(
|
||||
"failed to add frame to buffersrc: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
let nb = out_frame.samples() as i32;
|
||||
if nb > 0 && total < i64::from(out_capacity_frames) {
|
||||
let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32;
|
||||
for ch in 0..channels {
|
||||
// SAFETY: the FFI contract guarantees at least `channels`
|
||||
// entries in `out_planar` (NULL entries are skipped).
|
||||
let dst = unsafe { *out_planar.add(ch as usize) };
|
||||
if dst.is_null() {
|
||||
continue;
|
||||
}
|
||||
// Output is planar f32 (enforced by open()); each plane is
|
||||
// `to_copy` float samples.
|
||||
let src = out_frame.plane::<f32>(ch as usize);
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize);
|
||||
// C++: `out_planar ? &buf : nullptr` — with no destination, the input is
|
||||
// pushed but nothing is pulled.
|
||||
if out_planar.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut total: i64 = 0;
|
||||
loop {
|
||||
let pulled = graph.get("out").unwrap().sink().frame(out_frame);
|
||||
match pulled {
|
||||
Ok(()) => {}
|
||||
Err(e) if is_drain(&e) => break,
|
||||
Err(e) => {
|
||||
return Err(Box::from(Error::Failed(format!(
|
||||
"failed to pull from buffersink: {e}"
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
let nb = out_frame.samples() as i32;
|
||||
if nb > 0 && total < i64::from(out_capacity_frames) {
|
||||
let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32;
|
||||
for ch in 0..channels {
|
||||
// SAFETY: the FFI contract guarantees at least `channels`
|
||||
// entries in `out_planar` (NULL entries are skipped).
|
||||
let dst = unsafe { *out_planar.add(ch as usize) };
|
||||
if dst.is_null() {
|
||||
continue;
|
||||
}
|
||||
// Output is planar f32 (enforced by open()); each plane is
|
||||
// `to_copy` float samples.
|
||||
let src = out_frame.plane::<f32>(ch as usize);
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
total += i64::from(nb);
|
||||
}
|
||||
total += i64::from(nb);
|
||||
|
||||
Ok(total.min(i64::from(out_capacity_frames)) as i32)
|
||||
}
|
||||
|
||||
Ok(total.min(i64::from(out_capacity_frames)) as i32)
|
||||
}
|
||||
/// Signal end-of-input to the graph (flushes internal delay).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (state check)
|
||||
/// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a
|
||||
/// negative push return is logged only).
|
||||
pub fn flush(&self) -> Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
/// Signal end-of-input to the graph (flushes internal delay).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (empty handle, state)
|
||||
/// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a
|
||||
/// negative push return is logged only).
|
||||
pub fn flush(self_: &CHandle) -> Result<()> {
|
||||
let p = get_processor(self_)?;
|
||||
let mut inner = p.inner.lock().unwrap();
|
||||
|
||||
let Some(graph) = inner.graph.as_mut() else {
|
||||
return Err(Error::State);
|
||||
};
|
||||
let _ = graph.get("in").unwrap().source().flush();
|
||||
Ok(())
|
||||
let Some(graph) = inner.graph.as_mut() else {
|
||||
return Err(Box::from(Error::State));
|
||||
};
|
||||
let _ = graph.get("in").unwrap().source().flush();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Format of the conversion output (always planar f32).
|
||||
|
||||
@@ -23,13 +23,12 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::CStr;
|
||||
|
||||
use oakcodec::decoder::{CodecStream, Decoder as _, RetrieveAudioStatus};
|
||||
use oakcodec::decoder::{receive_list_of_all_decoders, CodecStream, Decoder as _, RetrieveAudioStatus};
|
||||
use oakcodec::ffmpeg::FFmpegDecoder;
|
||||
use oakcodec::footagedescription::FootageDescription;
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::bridge::codec::AudioStreamInfo;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{free_handle, make_owned, CHandle};
|
||||
|
||||
/// Maximum channel count accepted by [`extract`]. The C++ plane array is a
|
||||
/// fixed `OAKAUDIO_EXTRACT_MAX_CHANNELS` (64) stack buffer; the Rust
|
||||
@@ -625,32 +624,6 @@ impl RecipOrZero for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Handle plumbing (mirrors processor.rs) --------------------------------
|
||||
|
||||
/// Create an empty waveform behind a refcounted handle (count 1).
|
||||
pub fn init() -> Result<CHandle> {
|
||||
Ok(make_owned(AudioVisualWaveform::new()))
|
||||
}
|
||||
|
||||
/// Release one reference to a waveform (NULL/empty no-op).
|
||||
pub fn free(self_: *mut CHandle) {
|
||||
unsafe { free_handle(self_) };
|
||||
}
|
||||
|
||||
/// Borrow the waveform behind a handle; `OAKAUDIO_E_INVALID` for empty.
|
||||
pub fn get(self_: &CHandle) -> Result<&AudioVisualWaveform> {
|
||||
// SAFETY: every non-empty handle returned by `init` boxes an
|
||||
// `AudioVisualWaveform`.
|
||||
unsafe { crate::handle::get::<AudioVisualWaveform>(self_) }.ok_or(Error::Invalid)
|
||||
}
|
||||
|
||||
/// Mutable variant of [`get`].
|
||||
pub fn get_mut(self_: &CHandle) -> Result<&mut AudioVisualWaveform> {
|
||||
// SAFETY: every non-empty handle returned by `init` boxes an
|
||||
// `AudioVisualWaveform`.
|
||||
unsafe { crate::handle::get_mut::<AudioVisualWaveform>(self_) }.ok_or(Error::Invalid)
|
||||
}
|
||||
|
||||
// ---- Whole-file extraction --------------------------------------------------
|
||||
|
||||
/// Result of [`extract`]: channel-interleaved min/max pairs.
|
||||
@@ -714,10 +687,34 @@ fn emit_points(
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe a file with every registered decoder and return the first valid
|
||||
/// description (non-empty decoder id and at least one stream).
|
||||
///
|
||||
/// Replaces the former `oakcodec_decoder_probe` C ABI call (mirrors
|
||||
/// `probe_with_any_decoder` in oakcodec's ffi layer).
|
||||
fn probe_description(filename: &str) -> Option<FootageDescription> {
|
||||
for decoder in receive_list_of_all_decoders() {
|
||||
// `continue`, not `?`: an unimplemented/unsupported decoder must
|
||||
// fall through to the next one (the FFmpeg entry is the
|
||||
// format-agnostic fallback last in the registry).
|
||||
let Some(desc) = decoder.probe(filename, None) else {
|
||||
continue;
|
||||
};
|
||||
if !desc.decoder().is_empty()
|
||||
&& (desc.video_stream_count() > 0
|
||||
|| desc.audio_stream_count() > 0
|
||||
|| desc.subtitle_stream_count() > 0)
|
||||
{
|
||||
return Some(desc);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Decode a whole audio stream to a channel-interleaved min/max summary.
|
||||
///
|
||||
/// The stream is probed through the oakcodec decoder C ABI and decoded with
|
||||
/// oakcodec's in-process FFmpeg decoder (interleaved f32 at the native
|
||||
/// The stream is probed through oakcodec's in-process decoder registry and
|
||||
/// decoded with oakcodec's FFmpeg decoder (interleaved f32 at the native
|
||||
/// rate/layout), then reduced to one point per `samples_per_point` source
|
||||
/// samples.
|
||||
///
|
||||
@@ -728,34 +725,33 @@ pub fn extract(
|
||||
stream_index: i32,
|
||||
samples_per_point: i32,
|
||||
) -> Result<ExtractOutcome> {
|
||||
// Probe for the stream's native rate/layout (stateless).
|
||||
// SAFETY: `filename` is a NUL-terminated C string (validated by the FFI
|
||||
// layer); the probe handle is freed on every path below.
|
||||
let mut probe = unsafe { crate::bridge::codec::oakcodec_decoder_probe(filename.as_ptr()) };
|
||||
if probe.is_null() {
|
||||
return Err(Error::NotFound);
|
||||
// Probe for the stream's native rate/layout with the in-process decoder
|
||||
// registry (the Rust equivalent of the former oakcodec decoder probe
|
||||
// C ABI call).
|
||||
let filename_str = filename.to_string_lossy();
|
||||
let desc = probe_description(&filename_str).ok_or(Box::new(Error::NotFound))?;
|
||||
let ap = desc
|
||||
.get_audio_stream(stream_index as usize)
|
||||
.ok_or(Box::new(Error::NotFound))?;
|
||||
|
||||
// The probed stream metadata is a plain value type (oakcodec's
|
||||
// `AudioParams`); the fields map one-to-one onto the former
|
||||
// `oakcodec_audio_stream_info` POD.
|
||||
let sample_rate = ap.sample_rate;
|
||||
let channel_count = ap.channel_count();
|
||||
let channel_layout = ap.channel_layout;
|
||||
let container_stream_index = ap.stream_index;
|
||||
let duration_ts = ap.duration;
|
||||
let (time_base_num, time_base_den) = ap.time_base;
|
||||
|
||||
if sample_rate <= 0 || channel_count <= 0 {
|
||||
return Err(Box::new(Error::Failed("invalid audio stream".to_string())));
|
||||
}
|
||||
let mut info = unsafe { std::mem::zeroed::<AudioStreamInfo>() };
|
||||
let r = unsafe {
|
||||
crate::bridge::codec::oakcodec_decoder_probe_get_audio_stream(
|
||||
probe,
|
||||
stream_index,
|
||||
&mut info,
|
||||
)
|
||||
};
|
||||
// SAFETY: `probe` was created above and is no longer used.
|
||||
unsafe { crate::bridge::codec::oakcodec_decoder_free(&mut probe) };
|
||||
if r != 0 {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
if info.sample_rate <= 0 || info.channel_count <= 0 {
|
||||
return Err(Error::Failed("invalid audio stream".to_string()));
|
||||
}
|
||||
let channels = info.channel_count;
|
||||
let channels = channel_count;
|
||||
if channels > EXTRACT_MAX_CHANNELS {
|
||||
return Err(Error::Failed(format!(
|
||||
return Err(Box::new(Error::Failed(format!(
|
||||
"stream has {channels} channels (max {EXTRACT_MAX_CHANNELS})"
|
||||
)));
|
||||
))));
|
||||
}
|
||||
|
||||
// Decode the whole stream through oakcodec's FFmpeg decoder
|
||||
@@ -764,23 +760,23 @@ pub fn extract(
|
||||
// fb_audio_graph to obtain planar f32).
|
||||
let decoder = FFmpegDecoder::new();
|
||||
let stream = CodecStream::with_block(
|
||||
filename.to_string_lossy().into_owned(),
|
||||
info.stream_index,
|
||||
filename_str.into_owned(),
|
||||
container_stream_index,
|
||||
None,
|
||||
);
|
||||
if let Err(e) = decoder.open(&stream) {
|
||||
return Err(Error::Failed(format!("failed to open decoder: {e:?}")));
|
||||
return Err(Box::new(Error::Failed(format!("failed to open decoder: {e:?}"))));
|
||||
}
|
||||
|
||||
if info.time_base_num <= 0 || info.time_base_den <= 0 || info.duration_ts <= 0 {
|
||||
if time_base_num <= 0 || time_base_den <= 0 || duration_ts <= 0 {
|
||||
let _ = decoder.close();
|
||||
return Err(Error::Failed("invalid audio stream duration".to_string()));
|
||||
return Err(Box::new(Error::Failed("invalid audio stream duration".to_string())));
|
||||
}
|
||||
let duration_sec =
|
||||
info.duration_ts as f64 * f64::from(info.time_base_num) / f64::from(info.time_base_den);
|
||||
let total_frames = (duration_sec * f64::from(info.sample_rate)).round() as i64;
|
||||
let layout_mask = if info.channel_layout != 0 {
|
||||
info.channel_layout
|
||||
duration_ts as f64 * f64::from(time_base_num) / f64::from(time_base_den);
|
||||
let total_frames = (duration_sec * f64::from(sample_rate)).round() as i64;
|
||||
let layout_mask = if channel_layout != 0 {
|
||||
channel_layout
|
||||
} else {
|
||||
ffmpeg_next::ChannelLayout::default(channels).bits()
|
||||
};
|
||||
@@ -796,11 +792,11 @@ pub fn extract(
|
||||
while offset < total_frames {
|
||||
let frames = (total_frames - offset).min(CHUNK_FRAMES);
|
||||
let range = TimeRange::new(
|
||||
Rational::new(offset, i64::from(info.sample_rate)),
|
||||
Rational::new(offset + frames, i64::from(info.sample_rate)),
|
||||
Rational::new(offset, i64::from(sample_rate)),
|
||||
Rational::new(offset + frames, i64::from(sample_rate)),
|
||||
);
|
||||
let mut buf = vec![0f32; frames as usize * channels as usize];
|
||||
match decoder.retrieve_audio(&mut buf, &range, info.sample_rate, layout_mask) {
|
||||
match decoder.retrieve_audio(&mut buf, &range, sample_rate, layout_mask) {
|
||||
Ok(RetrieveAudioStatus::Success) => {
|
||||
append_pending(&mut pending, &buf, channels);
|
||||
emit_points(
|
||||
@@ -812,11 +808,11 @@ pub fn extract(
|
||||
);
|
||||
}
|
||||
Ok(status) => {
|
||||
result = Err(Error::Failed(format!("audio retrieve failed: {status:?}")));
|
||||
result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {status:?}"))));
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
result = Err(Error::Failed(format!("audio retrieve failed: {e:?}")));
|
||||
result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {e:?}"))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,68 +14,43 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shared test helpers and bridge stubs for the oakaudio contract suite.
|
||||
//! Shared helpers for the oakaudio contract suite.
|
||||
//!
|
||||
//! The library imports the other oak modules (`oakcommon`, `oakcodec`)
|
||||
//! through the `bridge/` wrappers. A standalone `cargo test` run has no
|
||||
//! host dylibs to link, so [`mod stubs`] provides minimal definitions — a
|
||||
//! no-op config/encoder and a WAV-header probe (real decoding in
|
||||
//! `waveform::extract` goes through oakcodec's in-process FFmpeg decoder;
|
||||
//! the processor drives a real FFmpeg filter graph via ffmpeg-next). The
|
||||
//! exhaustive behavior matrix is pinned by the unchanged C++ gtest suite
|
||||
//! (`src/audio/tests`).
|
||||
//! The former bridge stubs are gone with the deleted C ABI: every test
|
||||
//! now calls the crate's public Rust API directly (manager singleton,
|
||||
//! processor, levelmeter, waveform, synchronizer). Decoding in
|
||||
//! `waveform::extract` goes through oakcodec's in-process FFmpeg
|
||||
//! decoder; the processor drives a real FFmpeg filter graph.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Serializes tests that mutate process-wide state (the manager singleton,
|
||||
/// the alive ledger) within one test binary.
|
||||
/// Serializes tests that mutate process-wide state (the manager singleton)
|
||||
/// within one test binary.
|
||||
pub static MANAGER_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// 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.
|
||||
pub fn lock_manager() -> std::sync::MutexGuard<'static, ()> {
|
||||
MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner())
|
||||
}
|
||||
|
||||
/// Build a planar f32 buffer with `channel_count` channels of `frame_count`
|
||||
/// frames from a single-channel `source` (replicated per channel).
|
||||
pub fn planar_from(source: &[f32], channel_count: usize) -> Vec<Vec<f32>> {
|
||||
(0..channel_count).map(|_| source.to_vec()).collect()
|
||||
}
|
||||
|
||||
/// A deterministic pseudo-random planar buffer (fixed seed) for stable
|
||||
/// golden vectors. Samples land in `[-1, 1)`.
|
||||
pub fn noisy_planar(channel_count: usize, frame_count: usize, seed: u64) -> Vec<Vec<f32>> {
|
||||
let mut state = seed | 1;
|
||||
let mut next = move || {
|
||||
state = state
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
((state >> 33) as u64 & 0xFFFF) as f32 / 65535.0 * 2.0 - 1.0
|
||||
};
|
||||
let mut data = Vec::with_capacity(channel_count);
|
||||
for _ in 0..channel_count {
|
||||
data.push((0..frame_count).map(|_| next()).collect());
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// A silence buffer: every sample is exactly `0.0`.
|
||||
pub fn silence_planar(channel_count: usize, frame_count: usize) -> Vec<Vec<f32>> {
|
||||
vec![vec![0.0; frame_count]; channel_count]
|
||||
}
|
||||
|
||||
/// Total number of `SamplePerChannel` entries in a channel-interleaved
|
||||
/// waveform sample for `points` points across `channels` channels.
|
||||
pub fn interleaved_len(points: usize, channels: usize) -> usize {
|
||||
points * channels
|
||||
}
|
||||
|
||||
/// Convert an `oakaudio`-style `min_max` pair layout into a plain tuple for
|
||||
/// comparison in tests.
|
||||
pub fn pair(min: f32, max: f32) -> (f32, f32) {
|
||||
(min, max)
|
||||
}
|
||||
|
||||
// ---- Minimal WAV fixture helpers -------------------------------------------
|
||||
|
||||
/// Write a 16-bit PCM WAV file (`fmt` chunk + `data` chunk, standard 44-byte
|
||||
/// header). The test stubs decode exactly this layout.
|
||||
/// header). Decoded by oakcodec's FFmpeg decoder in `waveform::extract`.
|
||||
pub fn write_wav(path: &Path, channels: u16, rate: u32, samples: &[i16]) -> std::io::Result<()> {
|
||||
let block_align = channels * 2;
|
||||
let byte_rate = rate * u32::from(block_align);
|
||||
@@ -124,286 +99,3 @@ pub fn write_wav_header_only(path: &Path, channels: u16, rate: u32) -> std::io::
|
||||
out.extend_from_slice(&0u32.to_le_bytes());
|
||||
std::fs::write(path, out)
|
||||
}
|
||||
|
||||
// ---- Bridge stubs -----------------------------------------------------------
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub mod stubs {
|
||||
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
use std::path::Path;
|
||||
|
||||
use oakaudio::bridge::codec::AudioStreamInfo;
|
||||
use oakaudio::handle::CHandle;
|
||||
|
||||
// ------------------------- oakcommon ---------------------------------
|
||||
|
||||
/// Not-found for every key: `config::device_name`'s two-stage query
|
||||
/// treats `size <= 1` as absent and returns the empty string, so the
|
||||
/// config-driven device lookup degrades to `paNoDevice` (the documented
|
||||
/// bridge degradation).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_config_get(
|
||||
_group: *const c_char,
|
||||
_key: *const c_char,
|
||||
_buf: *mut c_char,
|
||||
_buf_size: c_int,
|
||||
) -> c_int {
|
||||
-1
|
||||
}
|
||||
|
||||
/// Every integer config reads its default.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_config_get_int(
|
||||
_group: *const c_char,
|
||||
_key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int {
|
||||
default
|
||||
}
|
||||
|
||||
/// Core `SampleFormat` (planar-first) → `FBSampleFormat` (AVSampleFormat
|
||||
/// order), mirroring `src/common/src/ffmpegutils.cpp:83`.
|
||||
///
|
||||
/// `// CPP-PARITY: src/common/src/ffmpegutils.cpp:83`
|
||||
/// (`FFmpegUtils::get_ffmpeg_sample_format`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(
|
||||
smp_fmt: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int {
|
||||
let mapped = match smp_fmt {
|
||||
0 => 5, // u8_p -> fb_sample_fmt_u8_p
|
||||
1 => 6, // s16_p -> fb_sample_fmt_s16_p
|
||||
2 => 7, // s32_p -> fb_sample_fmt_s32_p
|
||||
3 => 11, // s64_p -> fb_sample_fmt_s64_p
|
||||
4 => 8, // f32_p -> fb_sample_fmt_fltp
|
||||
5 => 9, // f64_p -> fb_sample_fmt_dblp
|
||||
6 => 0, // u8 -> fb_sample_fmt_u8
|
||||
7 => 1, // s16 -> fb_sample_fmt_s16
|
||||
8 => 2, // s32 -> fb_sample_fmt_s32
|
||||
9 => 10, // s64 -> fb_sample_fmt_s64
|
||||
10 => 3, // f32 -> fb_sample_fmt_flt
|
||||
11 => 4, // f64 -> fb_sample_fmt_dbl
|
||||
_ => -1, // invalid/count -> fb_sample_fmt_none
|
||||
};
|
||||
if out.is_null() {
|
||||
return -1;
|
||||
}
|
||||
// SAFETY: the caller guarantees a writable int.
|
||||
unsafe { *out = mapped };
|
||||
0
|
||||
}
|
||||
|
||||
// ------------------------- oakcodec ----------------------------------
|
||||
|
||||
/// ffmpeg-style default channel layout mask for `nb_channels` (only the
|
||||
/// popcount is load-bearing for oakaudio). Masks above 63 channels
|
||||
/// cannot be represented in a u64 and yield 0 (the extract cap check
|
||||
/// rejects such streams before any layout use).
|
||||
fn layout_for(channels: i32) -> u64 {
|
||||
match channels {
|
||||
1 => 0x4,
|
||||
2 => 0x3,
|
||||
n if n > 0 && n < 64 => (1u64 << n) - 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// WAV header facts parsed by `parse_wav`.
|
||||
struct WavInfo {
|
||||
channels: i32,
|
||||
rate: i32,
|
||||
block_align: usize,
|
||||
frames: i64,
|
||||
}
|
||||
|
||||
/// Parse the standard 44-byte PCM WAV header the fixture writer emits.
|
||||
fn parse_wav(path: &Path) -> Option<WavInfo> {
|
||||
let bytes = std::fs::read(path).ok()?;
|
||||
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
|
||||
return None;
|
||||
}
|
||||
if &bytes[12..16] != b"fmt " || &bytes[36..40] != b"data" {
|
||||
return None;
|
||||
}
|
||||
let fmt_size = u32::from_le_bytes(bytes[16..20].try_into().ok()?);
|
||||
if fmt_size < 16 {
|
||||
return None;
|
||||
}
|
||||
let audio_format = u16::from_le_bytes(bytes[20..22].try_into().ok()?);
|
||||
let channels = u16::from_le_bytes(bytes[22..24].try_into().ok()?);
|
||||
let rate = u32::from_le_bytes(bytes[24..28].try_into().ok()?);
|
||||
let block_align = u16::from_le_bytes(bytes[32..34].try_into().ok()?);
|
||||
let bits = u16::from_le_bytes(bytes[34..36].try_into().ok()?);
|
||||
let data_size = u32::from_le_bytes(bytes[40..44].try_into().ok()?);
|
||||
if audio_format != 1 || bits != 16 || channels == 0 || rate == 0 || block_align == 0 {
|
||||
return None;
|
||||
}
|
||||
let frames = (data_size as usize / block_align as usize) as i64;
|
||||
Some(WavInfo {
|
||||
channels: i32::from(channels),
|
||||
rate: rate as i32,
|
||||
block_align: block_align as usize,
|
||||
frames,
|
||||
})
|
||||
}
|
||||
|
||||
// All handles below use the shared `CHandle` ABI (single-lib
|
||||
// unification): oakaudio's `bridge::codec` wrappers call the oakcodec
|
||||
// crate's `#[no_mangle]` ffi functions directly, so these stubs must
|
||||
// match the real oakcodec ffi signatures exactly.
|
||||
|
||||
struct StubEncoder;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_init(params: *const c_void) -> CHandle {
|
||||
if params.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
CHandle {
|
||||
ctx: Box::into_raw(Box::new(StubEncoder)) as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_open(_encoder: CHandle) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_write_audio(
|
||||
_encoder: CHandle,
|
||||
_samples: *const f32,
|
||||
_frame_count: c_int,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_flush(_encoder: CHandle) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_last_error(
|
||||
_encoder: CHandle,
|
||||
_buf: *mut c_char,
|
||||
_buf_size: c_int,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
if !encoder.is_null() {
|
||||
// SAFETY: the pointer was created by `oakcodec_encoder_init`;
|
||||
// the caller owns `encoder` and expects it cleared.
|
||||
let e = unsafe { &mut *encoder };
|
||||
if !e.ctx.is_null() {
|
||||
drop(unsafe { Box::from_raw(e.ctx as *mut StubEncoder) });
|
||||
e.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe result for a decodable WAV file.
|
||||
struct StubProbe {
|
||||
channels: i32,
|
||||
sample_rate: i32,
|
||||
frames: i64,
|
||||
layout: u64,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle {
|
||||
if filename.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
// SAFETY: the C string is NUL-terminated (caller contract).
|
||||
let cname = unsafe { CStr::from_ptr(filename) };
|
||||
let path = Path::new(cname.to_str().unwrap_or(""));
|
||||
match parse_wav(path) {
|
||||
Some(info) => CHandle {
|
||||
ctx: Box::into_raw(Box::new(StubProbe {
|
||||
channels: info.channels,
|
||||
sample_rate: info.rate,
|
||||
frames: info.frames,
|
||||
layout: layout_for(info.channels),
|
||||
})) as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 0,
|
||||
},
|
||||
None => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_free(probe: *mut CHandle) {
|
||||
if !probe.is_null() {
|
||||
// SAFETY: the pointer was created by `oakcodec_decoder_probe`;
|
||||
// the caller owns `probe` and expects it cleared.
|
||||
let p = unsafe { &mut *probe };
|
||||
if !p.ctx.is_null() {
|
||||
drop(unsafe { Box::from_raw(p.ctx as *mut StubProbe) });
|
||||
p.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_probe_audio_stream_count(_probe: CHandle) -> c_int {
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_probe_get_audio_stream(
|
||||
probe: CHandle,
|
||||
index: c_int,
|
||||
out: *mut AudioStreamInfo,
|
||||
) -> c_int {
|
||||
if probe.ctx.is_null() || out.is_null() || index != 0 {
|
||||
return -1;
|
||||
}
|
||||
// SAFETY: the probe ctx is a live `StubProbe`; `out` is a
|
||||
// caller-owned info struct.
|
||||
let p = unsafe { &*(probe.ctx as *const StubProbe) };
|
||||
unsafe {
|
||||
(*out).stream_index = 0;
|
||||
(*out).sample_rate = p.sample_rate;
|
||||
(*out).channel_layout = p.layout;
|
||||
(*out).channel_count = p.channels;
|
||||
(*out).duration_ts = p.frames;
|
||||
(*out).time_base_num = 1;
|
||||
(*out).time_base_den = p.sample_rate;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_open(
|
||||
_decoder: CHandle,
|
||||
_filename: *const c_char,
|
||||
_stream_index: c_int,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcodec_decoder_decode_audio(
|
||||
_decoder: CHandle,
|
||||
_in_num: c_int,
|
||||
_in_den: c_int,
|
||||
_out_num: c_int,
|
||||
_out_den: c_int,
|
||||
_sample_rate: c_int,
|
||||
_channel_layout: u64,
|
||||
_buf: *mut f32,
|
||||
_buf_frames: c_int,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI-layer contract tests (ffi.rs). The exhaustive matrix runs against
|
||||
//! the unchanged C++ gtest suite (`src/audio/tests`); these tests pin
|
||||
//! Rust-side specifics (handle contracts, the singleton ledger, struct
|
||||
//! layout).
|
||||
|
||||
mod common;
|
||||
|
||||
use std::mem::{align_of, size_of};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
|
||||
use oakaudio::ffi::levelmeter::oakaudio_levelmeter_analyze;
|
||||
use oakaudio::ffi::levelmeter::{ChannelStats, MeterStats};
|
||||
use oakaudio::ffi::manager::{
|
||||
oakaudio_debug_alive_count, oakaudio_manager_create_instance,
|
||||
oakaudio_manager_destroy_instance, oakaudio_manager_free, oakaudio_manager_instance,
|
||||
};
|
||||
use oakaudio::ffi::processor::{oakaudio_processor_free, oakaudio_processor_init};
|
||||
use oakaudio::ffi::sync::{OffsetResult, SourceClip};
|
||||
use oakaudio::ffi::waveform::{oakaudio_waveform_free, oakaudio_waveform_init};
|
||||
|
||||
/// Serializes tests that touch the process-wide singleton and the alive
|
||||
/// ledger.
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Every exported handle-returning function (processor_init, waveform_init)
|
||||
/// returns ctx==NULL on failure and a valid refcounted handle on success,
|
||||
/// with abi_version == OAKAUDIO_ABI_VERSION stamped.
|
||||
#[test]
|
||||
fn handle_contract_all_exports() {
|
||||
let _guard = LOCK.lock().unwrap();
|
||||
|
||||
let mut p = unsafe { oakaudio_processor_init() };
|
||||
assert!(!p.ctx.is_null());
|
||||
assert_eq!(p.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION);
|
||||
|
||||
let mut w = unsafe { oakaudio_waveform_init() };
|
||||
assert!(!w.ctx.is_null());
|
||||
assert_eq!(w.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION);
|
||||
|
||||
unsafe { oakaudio_processor_free(&mut p) };
|
||||
unsafe { oakaudio_waveform_free(&mut w) };
|
||||
}
|
||||
|
||||
/// free(NULL)/free(empty) are no-ops across every free export.
|
||||
#[test]
|
||||
fn free_null_noop_all_exports() {
|
||||
let _guard = LOCK.lock().unwrap();
|
||||
|
||||
let mut p = oakaudio::handle::CHandle::null();
|
||||
unsafe { oakaudio_processor_free(&mut p) };
|
||||
assert!(p.ctx.is_null());
|
||||
|
||||
let mut w = oakaudio::handle::CHandle::null();
|
||||
unsafe { oakaudio_waveform_free(&mut w) };
|
||||
assert!(w.ctx.is_null());
|
||||
|
||||
let mut m = oakaudio::handle::CHandle::null();
|
||||
unsafe { oakaudio_manager_free(&mut m) };
|
||||
assert!(m.ctx.is_null());
|
||||
|
||||
// NULL pointer itself is a no-op.
|
||||
unsafe { oakaudio_processor_free(std::ptr::null_mut()) };
|
||||
unsafe { oakaudio_waveform_free(std::ptr::null_mut()) };
|
||||
unsafe { oakaudio_manager_free(std::ptr::null_mut()) };
|
||||
}
|
||||
|
||||
/// The manager singleton: instance() is the same borrowed handle across
|
||||
/// calls; create/destroy flip validity; oakaudio_debug_alive_count moves
|
||||
/// predictably and returns to baseline.
|
||||
#[test]
|
||||
fn manager_singleton_and_alive_count() {
|
||||
let _guard = LOCK.lock().unwrap();
|
||||
|
||||
let before = unsafe { oakaudio_debug_alive_count() };
|
||||
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
let none = unsafe { oakaudio_manager_instance() };
|
||||
assert!(none.ctx.is_null());
|
||||
// The empty instance handle is the shared `null()` (no ABI version).
|
||||
assert_eq!(none.abi_version, 0);
|
||||
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
let m1 = unsafe { oakaudio_manager_instance() };
|
||||
let m2 = unsafe { oakaudio_manager_instance() };
|
||||
assert!(!m1.ctx.is_null());
|
||||
assert_eq!(
|
||||
m1.ctx, m2.ctx,
|
||||
"instance() must be the same borrowed handle"
|
||||
);
|
||||
|
||||
// A processor bumps the ledger; freeing it returns to baseline.
|
||||
assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
|
||||
let mut p = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(unsafe { oakaudio_debug_alive_count() }, before + 1);
|
||||
unsafe { oakaudio_processor_free(&mut p) };
|
||||
assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
|
||||
|
||||
// Destroy flips the singleton back to empty; create resurrects it.
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
assert!(unsafe { oakaudio_manager_instance() }.ctx.is_null());
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
assert!(!unsafe { oakaudio_manager_instance() }.ctx.is_null());
|
||||
}
|
||||
|
||||
/// oakaudio_levelmeter_analyze with NULL summary still computes per-channel
|
||||
/// stats, and a NULL channels array with capacity 0 is accepted when only
|
||||
/// the summary is wanted.
|
||||
#[test]
|
||||
fn levelmeter_partial_outputs() {
|
||||
let data = [0.5f32; 64];
|
||||
let planes = [data.as_ptr()];
|
||||
|
||||
// channels only (summary NULL)
|
||||
let mut channels = [ChannelStats {
|
||||
peak_linear: 0.0,
|
||||
peak_db: 0.0,
|
||||
rms_linear: 0.0,
|
||||
rms_db: 0.0,
|
||||
vu_db: 0.0,
|
||||
}];
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_levelmeter_analyze(
|
||||
planes.as_ptr(),
|
||||
1,
|
||||
64,
|
||||
channels.as_mut_ptr(),
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
assert!((channels[0].peak_linear - 0.5).abs() < 1e-9);
|
||||
|
||||
// summary only (channels NULL, capacity 0)
|
||||
let mut summary = MeterStats {
|
||||
max_peak_linear: 0.0,
|
||||
integrated_lufs: 0.0,
|
||||
silence: 0,
|
||||
};
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_levelmeter_analyze(
|
||||
planes.as_ptr(),
|
||||
1,
|
||||
64,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut summary,
|
||||
)
|
||||
},
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
assert!((summary.max_peak_linear - 0.5).abs() < 1e-9);
|
||||
|
||||
// Both NULL is invalid.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_levelmeter_analyze(
|
||||
planes.as_ptr(),
|
||||
1,
|
||||
64,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// sync value structs (offset_result/source_clip) are 24/40 bytes and
|
||||
/// repr(C)-aligned as the C headers dictate, so layout never drifts.
|
||||
#[test]
|
||||
fn sync_struct_layout() {
|
||||
assert_eq!(size_of::<OffsetResult>(), 24);
|
||||
assert_eq!(align_of::<OffsetResult>(), 8);
|
||||
assert_eq!(size_of::<SourceClip>(), 40);
|
||||
assert_eq!(align_of::<SourceClip>(), 8);
|
||||
// The stretch result (f64, i64, f64, i32) pads to 32 bytes.
|
||||
assert_eq!(size_of::<oakaudio::ffi::sync::StretchOffsetResult>(), 32);
|
||||
}
|
||||
@@ -19,15 +19,12 @@
|
||||
|
||||
mod common;
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
use common::write_wav_header_only;
|
||||
use oakaudio::ffi::waveform::MinMax;
|
||||
use oakaudio::ffi::waveform::{
|
||||
oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_summary,
|
||||
oakaudio_waveform_init, oakaudio_waveform_overwrite_samples,
|
||||
oakaudio_waveform_set_channel_count,
|
||||
};
|
||||
use oakaudio::params::{frames_to_rational, rational_to_samples, SampleFormat};
|
||||
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
|
||||
@@ -60,7 +57,7 @@ fn sample_time_conversion_roundtrip() {
|
||||
/// rational conversion edge cases (NaN / out-of-range / tiny -> null).
|
||||
#[test]
|
||||
fn params_value_types() {
|
||||
use oakaudio::params::{rational_from_double, AudioParams};
|
||||
use oakaudio::params::rational_from_double;
|
||||
|
||||
let p = AudioParams {
|
||||
sample_rate: 48000,
|
||||
@@ -89,23 +86,17 @@ 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_ptr(), ch1.as_ptr()];
|
||||
let planes = [ch0.as_slice(), ch1.as_slice()];
|
||||
|
||||
let mut w = unsafe { oakaudio_waveform_init() };
|
||||
assert!(!w.ctx.is_null());
|
||||
assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 1024, 48000, 0, 1) },
|
||||
0
|
||||
);
|
||||
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 mut fine = [MinMax { min: 0.0, max: 0.0 }; 2];
|
||||
let fine_points =
|
||||
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1024, fine.as_mut_ptr(), 2) };
|
||||
assert_eq!(fine_points, 1);
|
||||
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,
|
||||
@@ -119,10 +110,8 @@ fn waveform_mipmap_scale_parity() {
|
||||
);
|
||||
assert_eq!(fine[1].max, 0.0);
|
||||
|
||||
let mut coarse = [MinMax { min: 0.0, max: 0.0 }; 2];
|
||||
let coarse_points =
|
||||
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 64, coarse.as_mut_ptr(), 2) };
|
||||
assert_eq!(coarse_points, 1);
|
||||
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,
|
||||
@@ -139,8 +128,6 @@ fn waveform_mipmap_scale_parity() {
|
||||
// Coarser windows necessarily cover more source samples.
|
||||
assert!(coarse[0].max > fine[0].max);
|
||||
assert!(coarse[1].min < fine[1].min);
|
||||
|
||||
unsafe { oakaudio_waveform_free(&mut w) };
|
||||
}
|
||||
|
||||
/// levelmeter dB conversion: peak_db == 20*log10(peak_linear) and the
|
||||
@@ -164,57 +151,30 @@ fn levelmeter_db_golden() {
|
||||
|
||||
/// waveformsync envelope offset golden: a reference ramp delayed by two
|
||||
/// windows in the candidate is recovered as +2 windows with full
|
||||
/// confidence through the C ABI.
|
||||
/// confidence.
|
||||
#[test]
|
||||
fn waveform_sync_offset_golden() {
|
||||
use oakaudio::ffi::sync::{oakaudio_sync_estimate_envelope_offset, OffsetResult};
|
||||
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 mut out = OffsetResult {
|
||||
offset_samples: 0,
|
||||
confidence: 0.0,
|
||||
valid: 0,
|
||||
};
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
reference.as_ptr(),
|
||||
reference.len() as i32,
|
||||
candidate.as_ptr(),
|
||||
candidate.len() as i32,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 1);
|
||||
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);
|
||||
}
|
||||
|
||||
/// oakaudio_waveform_extract channel cap: a stream claiming more than
|
||||
/// OAKAUDIO_EXTRACT_MAX_CHANNELS (64) channels is rejected rather than
|
||||
/// overflowing the internal plane array.
|
||||
/// 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 mut out_channels = 0i32;
|
||||
let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
|
||||
let r = unsafe {
|
||||
oakaudio_waveform_extract(
|
||||
cpath.as_ptr(),
|
||||
0,
|
||||
4,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut out_channels,
|
||||
)
|
||||
};
|
||||
assert!(r < 0, "oversized stream must be rejected, got {r}");
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! Handle plumbing contract tests (handle.rs).
|
||||
|
||||
use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use oakaudio::handle::{alive_count, get, guard, guard_handle, make_borrowed, make_owned, CHandle};
|
||||
|
||||
/// Serializes the tests: the crate's live-object ledger is process-global,
|
||||
/// so parallel tests in this file would race the count assertions.
|
||||
fn ledger_lock() -> MutexGuard<'static, ()> {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// make_owned starts at refcount 1; get returns a typed view; dropping the
|
||||
/// handle decrements to 0.
|
||||
#[test]
|
||||
fn owned_lifecycle() {
|
||||
let _l = ledger_lock();
|
||||
let before = alive_count();
|
||||
let mut h = make_owned(42u32);
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(alive_count(), before + 1);
|
||||
|
||||
// SAFETY: `h` boxes a u32 created above.
|
||||
let v = unsafe { get::<u32>(&h) }.unwrap();
|
||||
assert_eq!(*v, 42);
|
||||
|
||||
// addref/release round-trip through the function pointers.
|
||||
let addref = h.addref.unwrap();
|
||||
let release = h.release.unwrap();
|
||||
// SAFETY: the ctx was created by make_owned.
|
||||
unsafe { addref(h.ctx) };
|
||||
assert_eq!(alive_count(), before + 1); // count unchanged, still 1 box
|
||||
unsafe { release(h.ctx) };
|
||||
|
||||
// Dropping the box (release to zero) decrements the ledger.
|
||||
let release = h.release.unwrap();
|
||||
// SAFETY: h.ctx is the box created above; refcount is 1.
|
||||
unsafe { release(h.ctx) };
|
||||
h.ctx = std::ptr::null_mut();
|
||||
assert_eq!(alive_count(), before);
|
||||
}
|
||||
|
||||
/// make_borrowed creates a borrow-only handle whose release frees only the
|
||||
/// box, never the underlying object.
|
||||
#[test]
|
||||
fn borrowed_release() {
|
||||
let mut value = Box::new(7i32);
|
||||
// SAFETY: `value` outlives the handle; the ctx points directly at the
|
||||
// box (not a RefBox), so it is read through the raw pointer.
|
||||
let mut h = unsafe { make_borrowed(&mut *value) };
|
||||
assert!(!h.is_null());
|
||||
|
||||
// SAFETY: `h.ctx` points at the box created above.
|
||||
let v = unsafe { &*(h.ctx as *const i32) };
|
||||
assert_eq!(*v, 7);
|
||||
|
||||
// release is a no-op: the box still lives.
|
||||
let release = h.release.unwrap();
|
||||
// SAFETY: noop_ref for borrowed handles.
|
||||
unsafe { release(h.ctx) };
|
||||
assert_eq!(*value, 7);
|
||||
|
||||
h.ctx = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
/// CHandle::null() yields an empty handle; guard over an Ok(()) returns
|
||||
/// OAKAUDIO_OK (0) and guard_handle over Ok returns a valid handle.
|
||||
#[test]
|
||||
fn null_and_guard_ok() {
|
||||
let null = CHandle::null();
|
||||
assert!(null.is_null());
|
||||
// The shared `null()` stamps no ABI version (single-lib unification).
|
||||
assert_eq!(null.abi_version, 0);
|
||||
|
||||
assert_eq!(guard(|| Ok(())), OAKAUDIO_OK);
|
||||
|
||||
let h = guard_handle(|| Ok(make_owned(1u32)));
|
||||
assert!(!h.is_null());
|
||||
// Release it so the alive ledger returns to baseline (tests share the
|
||||
// process-wide ledger and run in parallel).
|
||||
// SAFETY: `h.ctx` is the box created above; refcount is 1.
|
||||
unsafe { (h.release.unwrap())(h.ctx) };
|
||||
}
|
||||
|
||||
/// guard maps an Err to the negative error code without panicking; a
|
||||
/// panicking body is caught and returns a failure code rather than
|
||||
/// unwinding across the FFI boundary.
|
||||
#[test]
|
||||
fn guard_error_and_panic() {
|
||||
assert_eq!(
|
||||
guard(|| Err(oakaudio::error::Error::Invalid)),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(guard(|| Err(oakaudio::error::Error::State)), -60002);
|
||||
assert_eq!(
|
||||
guard(|| Err(oakaudio::error::Error::Failed("x".to_string()))),
|
||||
-60003
|
||||
);
|
||||
assert_eq!(guard(|| Err(oakaudio::error::Error::NotFound)), -60004);
|
||||
assert_eq!(guard(|| Err(oakaudio::error::Error::NoMem)), -60005);
|
||||
|
||||
assert_eq!(guard(|| panic!("boom")), OAKAUDIO_E_FAILED);
|
||||
|
||||
let h = guard_handle(|| Err(oakaudio::error::Error::Invalid));
|
||||
assert!(h.is_null());
|
||||
let h2 = guard_handle(|| panic!("boom"));
|
||||
assert!(h2.is_null());
|
||||
}
|
||||
@@ -14,53 +14,28 @@
|
||||
// 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.
|
||||
//! AudioLevelMeter contract tests (levelmeter.rs), calling the public
|
||||
//! `analyze_sample_buffer` API.
|
||||
|
||||
mod common;
|
||||
|
||||
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
|
||||
use oakaudio::ffi::levelmeter::{oakaudio_levelmeter_analyze, ChannelStats, MeterStats};
|
||||
use oakaudio::levelmeter::{analyze_sample_buffer, Stats};
|
||||
|
||||
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)
|
||||
fn analyze(planes: &[Vec<f32>]) -> Stats {
|
||||
let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
|
||||
analyze_sample_buffer(&refs)
|
||||
}
|
||||
|
||||
/// A silence buffer reports silence=1, all-zero linear fields, and dB
|
||||
/// A silence buffer reports silence=true, 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 {
|
||||
let stats = analyze(&planes);
|
||||
assert!(stats.silence);
|
||||
assert_eq!(stats.max_peak_linear, 0.0);
|
||||
assert_eq!(stats.integrated_lufs, -200.0);
|
||||
for ch in &stats.channels {
|
||||
assert_eq!(ch.peak_linear, 0.0);
|
||||
assert_eq!(ch.rms_linear, 0.0);
|
||||
assert_eq!(ch.peak_db, -200.0);
|
||||
@@ -70,21 +45,22 @@ fn silence_analysis() {
|
||||
}
|
||||
|
||||
/// A constant-amplitude tone reports peak_linear == rms_linear == that
|
||||
/// amplitude (power terms), peak_db matches 20*log10(amp), and silence=0.
|
||||
/// amplitude (power terms), peak_db matches 20*log10(amp), and silence is
|
||||
/// false.
|
||||
#[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 stats = analyze(&planes);
|
||||
assert!(!stats.silence);
|
||||
assert!((stats.channels[0].peak_linear - 0.5).abs() < 1e-9);
|
||||
assert!((stats.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);
|
||||
assert!((stats.channels[0].peak_db - expected_db).abs() < 1e-9);
|
||||
assert!((stats.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.
|
||||
/// near 0 dB; per-channel stats are filled for each channel.
|
||||
#[test]
|
||||
fn full_scale_peak() {
|
||||
let ch0: Vec<f32> = (0..64)
|
||||
@@ -93,13 +69,13 @@ fn full_scale_peak() {
|
||||
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);
|
||||
let stats = analyze(&[ch0, ch1]);
|
||||
assert_eq!(stats.max_peak_linear, 1.0);
|
||||
assert!(!stats.silence);
|
||||
assert!((stats.channels[0].peak_db - 0.0).abs() < 1e-9);
|
||||
assert!((stats.channels[1].peak_db - 0.0).abs() < 1e-9);
|
||||
assert!((stats.channels[0].rms_linear - 1.0).abs() < 1e-9);
|
||||
assert!((stats.channels[1].rms_linear - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// integrated_lufs stays -200 for silence and matches the BS.1770
|
||||
@@ -107,57 +83,27 @@ fn full_scale_peak() {
|
||||
#[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 stats = analyze(&silence);
|
||||
assert_eq!(stats.integrated_lufs, -200.0);
|
||||
|
||||
let tone = common::planar_from(&[0.5f32; 64], 2);
|
||||
let (_, summary) = analyze(&tone);
|
||||
let stats = 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);
|
||||
assert!((stats.integrated_lufs - expected).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// channel_count of 0, a NULL planar pointer, or NULL for both outputs
|
||||
/// returns OAKAUDIO_E_INVALID.
|
||||
/// Empty or zero-length inputs yield a default stats struct (no channels,
|
||||
/// silence).
|
||||
#[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,
|
||||
};
|
||||
fn empty_input() {
|
||||
let stats = analyze_sample_buffer(&[]);
|
||||
assert!(stats.channels.is_empty());
|
||||
assert!(stats.silence);
|
||||
|
||||
// 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
|
||||
);
|
||||
let silence = common::silence_planar(1, 0);
|
||||
let refs: Vec<&[f32]> = silence.iter().map(Vec::as_slice).collect();
|
||||
let stats = analyze_sample_buffer(&refs);
|
||||
assert!(stats.silence);
|
||||
assert_eq!(stats.max_peak_linear, 0.0);
|
||||
}
|
||||
|
||||
@@ -14,171 +14,96 @@
|
||||
// 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
|
||||
//! AudioManager contract tests (manager.rs), calling the public Rust API.
|
||||
//! The manager is a process-wide singleton, so every test holds the shared
|
||||
//! `MANAGER_LOCK`.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::ffi::c_char;
|
||||
use common::lock_manager;
|
||||
use oakcodec::encodingparams::EncodingParams;
|
||||
use oakaudio::error::{Error, OAKAUDIO_E_INVALID};
|
||||
use oakaudio::manager::instance;
|
||||
use oakaudio::params::{AudioParams, SampleFormat};
|
||||
|
||||
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,
|
||||
/// Audio params for the tests: stereo f32 at 48 kHz.
|
||||
fn stereo() -> AudioParams {
|
||||
AudioParams {
|
||||
sample_rate: 48000,
|
||||
channel_layout: 0x3,
|
||||
format: SampleFormat::F32,
|
||||
}
|
||||
}
|
||||
|
||||
/// create_instance/destroy_instance toggle the singleton; instance() returns
|
||||
/// a valid borrowed handle between them and NULL after destroy.
|
||||
/// A WAV recording config (format 7 = WAV).
|
||||
fn wav_params(filename: &str) -> EncodingParams {
|
||||
let mut params = EncodingParams::default();
|
||||
params.format = 7;
|
||||
params.audio_enabled = 1;
|
||||
params.audio_codec = 13; // PCM_S16LE
|
||||
params.audio_sample_rate = 48000;
|
||||
params.audio_channel_layout = 0x3;
|
||||
params.audio_sample_format = SampleFormat::S16;
|
||||
params.audio_bit_rate = 128000;
|
||||
let bytes = filename.as_bytes();
|
||||
params.filename[..bytes.len()].copy_from_slice(bytes);
|
||||
params
|
||||
}
|
||||
|
||||
/// create_instance/destroy_instance toggle the singleton; instance()
|
||||
/// returns a live guard between them and None after destroy.
|
||||
#[test]
|
||||
fn singleton_lifecycle() {
|
||||
let _guard = lock();
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
assert!(instance().ctx.is_null());
|
||||
let _guard = lock_manager();
|
||||
oakaudio::manager::ManagerInner::destroy_instance();
|
||||
assert!(instance().is_none());
|
||||
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
let m = instance();
|
||||
assert!(!m.ctx.is_null());
|
||||
unsafe { oakaudio_manager_free(&mut m.clone()) };
|
||||
oakaudio::manager::ManagerInner::create_instance().unwrap();
|
||||
assert!(instance().is_some());
|
||||
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
assert!(instance().ctx.is_null());
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
assert!(!instance().ctx.is_null());
|
||||
oakaudio::manager::ManagerInner::destroy_instance();
|
||||
assert!(instance().is_none());
|
||||
oakaudio::manager::ManagerInner::create_instance().unwrap();
|
||||
assert!(instance().is_some());
|
||||
}
|
||||
|
||||
/// A fresh singleton for a test: destroy resets the playback state
|
||||
/// (`output_started`, buffered params) that earlier tests in this binary
|
||||
/// may have left behind.
|
||||
fn fresh_instance() {
|
||||
oakaudio::manager::ManagerInner::destroy_instance();
|
||||
oakaudio::manager::ManagerInner::create_instance().unwrap();
|
||||
}
|
||||
|
||||
/// push_to_output accepts raw interleaved bytes and starts the virtual
|
||||
/// playback clock; without a device it fails with a message in error_buf.
|
||||
/// playback clock; without a device the push still succeeds and keeps the
|
||||
/// samples buffered (unavailable devices play silence instead of failing).
|
||||
///
|
||||
/// The virtual device never consumes frames (PortAudio is not bridged), so
|
||||
/// the clock reads 0.0 rather than advancing.
|
||||
/// The clock reads 0.0 after the push starts; a real audio device may have
|
||||
/// consumed a few frames by the time the assertion runs, so the bound is
|
||||
/// `>= 0` rather than exact (the callback-advances-clock behavior is pinned
|
||||
/// by manager.rs' own unit test).
|
||||
#[test]
|
||||
fn push_output_advances_clock() {
|
||||
let _guard = lock();
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
let m = instance();
|
||||
fn push_output_starts_clock() {
|
||||
let _guard = lock_manager();
|
||||
fresh_instance();
|
||||
let mut m = instance().unwrap();
|
||||
|
||||
// No stream yet: seconds() reports -1.
|
||||
let mut secs = 0.0f64;
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_manager_seconds(m, &mut secs) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
m.seconds(&mut secs).unwrap();
|
||||
assert_eq!(secs, -1.0);
|
||||
|
||||
// M12 P1: with no explicit device the push still succeeds — the
|
||||
// samples buffer for the default output device (unavailable devices
|
||||
// keep playback silent instead of failing the push).
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_manager_set_output_device(m, -1) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
// With no explicit device the push still succeeds (M12 P1).
|
||||
m.set_output_device(-1).unwrap();
|
||||
let samples = vec![0u8; 480 * 2 * 4];
|
||||
let r = 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,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, OAKAUDIO_OK);
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
|
||||
// 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() };
|
||||
m.set_output_device(0).unwrap();
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
m.seconds(&mut secs).unwrap();
|
||||
assert!(secs >= 0.0, "clock started (got {secs})");
|
||||
}
|
||||
|
||||
/// set/get output & input device: getters report a device, setters persist
|
||||
@@ -186,183 +111,102 @@ fn push_output_advances_clock() {
|
||||
/// clears buffers).
|
||||
#[test]
|
||||
fn device_selection_roundtrip() {
|
||||
let _guard = lock();
|
||||
unsafe { oakaudio_manager_create_instance() };
|
||||
let m = instance();
|
||||
let _guard = lock_manager();
|
||||
fresh_instance();
|
||||
let mut m = instance().unwrap();
|
||||
|
||||
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);
|
||||
m.set_output_device(42).unwrap();
|
||||
assert_eq!(m.get_output_device().unwrap(), 42);
|
||||
m.set_input_device(7).unwrap();
|
||||
assert_eq!(m.get_input_device().unwrap(), 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);
|
||||
m.hard_reset().unwrap();
|
||||
assert_eq!(m.get_output_device().unwrap(), 42);
|
||||
assert_eq!(m.get_input_device().unwrap(), 7);
|
||||
|
||||
// The stream stopped, so the clock is back at -1.
|
||||
let mut secs = 0.0f64;
|
||||
unsafe { oakaudio_manager_seconds(m, &mut secs) };
|
||||
m.seconds(&mut secs).unwrap();
|
||||
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.
|
||||
/// set_output_notify_interval stores the interval (negative is rejected);
|
||||
/// 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();
|
||||
let _guard = lock_manager();
|
||||
fresh_instance();
|
||||
let mut m = instance().unwrap();
|
||||
|
||||
m.set_output_notify_interval(1024).unwrap();
|
||||
let err = m.set_output_notify_interval(-1).unwrap_err();
|
||||
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
|
||||
err.downcast_ref::<oakaudio::error::Error>().map(|e| e.code()),
|
||||
Some(OAKAUDIO_E_INVALID)
|
||||
);
|
||||
m.clear_buffered_output().unwrap();
|
||||
m.reset_output_clock().unwrap();
|
||||
|
||||
// Push starts the stream, then stop_output halts it (clock -> -1).
|
||||
unsafe { oakaudio_manager_set_output_device(m, 0) };
|
||||
m.set_output_device(0).unwrap();
|
||||
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);
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
m.stop_output().unwrap();
|
||||
let mut secs = 1.0f64;
|
||||
unsafe { oakaudio_manager_seconds(m, &mut secs) };
|
||||
m.seconds(&mut secs).unwrap();
|
||||
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.
|
||||
/// start_recording validates its state: with no input device it fails with
|
||||
/// a reason; with a device the encoder open either succeeds (OAKAUDIO_OK,
|
||||
/// environment-dependent) or reports the encoder's diagnostic — both are
|
||||
/// correct manager behavior, so the test pins the manager's own handling
|
||||
/// 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 _guard = lock_manager();
|
||||
fresh_instance();
|
||||
let mut m = instance().unwrap();
|
||||
|
||||
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, ¶ms, 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");
|
||||
// No input device -> explainable failure, no crash.
|
||||
m.set_input_device(-1).unwrap();
|
||||
assert!(m.start_recording(&wav_params("unused.wav"), &mut vec![0u8; 64]).is_err());
|
||||
|
||||
m.set_input_device(0).unwrap();
|
||||
let path = std::env::temp_dir().join(format!("oakaudio_rec_{}.wav", std::process::id()));
|
||||
let filename = path.to_str().unwrap();
|
||||
let mut err = vec![0u8; 256];
|
||||
match m.start_recording(&wav_params(filename), &mut err) {
|
||||
Ok(()) => {
|
||||
assert!(m.stop_recording().is_ok());
|
||||
}
|
||||
Err(e) => {
|
||||
assert!(!e.to_string().is_empty(), "failure must carry a reason");
|
||||
}
|
||||
}
|
||||
|
||||
// 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() };
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Config defaults are read from the (empty) oakcommon store: buffer size
|
||||
/// falls back to 0 and device names are absent.
|
||||
#[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.
|
||||
fn config_defaults() {
|
||||
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());
|
||||
assert!(oakaudio::config::device_name(true).is_err(), "no configured output device");
|
||||
assert!(oakaudio::config::device_name(false).is_err(), "no configured input device");
|
||||
}
|
||||
|
||||
/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock) that
|
||||
/// the manager path only touches indirectly.
|
||||
/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock).
|
||||
#[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,
|
||||
format: SampleFormat::F32,
|
||||
});
|
||||
assert_eq!(dev.bytes_per_frame(), 8);
|
||||
|
||||
@@ -389,85 +233,65 @@ fn preview_device_pull_side() {
|
||||
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).
|
||||
/// output_levels: the no-output case, and a real peak readback over pushed
|
||||
/// packed-F32 stereo samples (left ramps to 0.05, right to ~0.2 — the
|
||||
/// per-channel linear peaks).
|
||||
///
|
||||
/// The push can open a real cpal stream whose callback consumes queued
|
||||
/// samples concurrently, so a long ramp is pushed and the peaks are
|
||||
/// asserted with a tolerance that absorbs partial consumption (the
|
||||
/// analysis window is the most recent 8192 frames of the queue).
|
||||
#[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();
|
||||
let _guard = lock_manager();
|
||||
fresh_instance();
|
||||
let mut m = instance().unwrap();
|
||||
|
||||
// Nothing configured yet: no channels.
|
||||
assert_eq!(m.output_levels(&mut [0.0f32; 4]).unwrap(), 0);
|
||||
|
||||
m.set_output_device(42).unwrap();
|
||||
m.clear_buffered_output().unwrap();
|
||||
|
||||
// Push 2 seconds of packed F32 stereo ramp (format 10), stereo layout
|
||||
// 0x3. Left ramps 0 -> 0.05, right 0 -> 0.2.
|
||||
let frames = 96000usize;
|
||||
let mut packed = Vec::with_capacity(frames * 2 * 4);
|
||||
for i in 0..frames {
|
||||
let t = i as f32 / frames as f32;
|
||||
packed.extend_from_slice(&(0.05f32 * t).to_le_bytes());
|
||||
packed.extend_from_slice(&(0.2f32 * t).to_le_bytes());
|
||||
}
|
||||
m.push_to_output(stereo(), &packed, &mut vec![0u8; 64]).unwrap();
|
||||
|
||||
// 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
|
||||
let n = m.output_levels(&mut peaks).unwrap();
|
||||
assert_eq!(n, 2);
|
||||
assert!(
|
||||
(peaks[0] - 0.05).abs() < 1e-3 && peaks[0] > 0.04,
|
||||
"left peak: {}",
|
||||
peaks[0]
|
||||
);
|
||||
assert!(
|
||||
(peaks[1] - 0.2).abs() < 1e-3 && peaks[1] > 0.19,
|
||||
"right peak: {}",
|
||||
peaks[1]
|
||||
);
|
||||
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] {
|
||||
// A cleared buffer reports zeroed peaks over the configured channels.
|
||||
m.clear_buffered_output().unwrap();
|
||||
let mut cleared = [0.0f32; 4];
|
||||
assert_eq!(m.output_levels(&mut cleared).unwrap(), 2);
|
||||
for p in &cleared[..2] {
|
||||
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);
|
||||
// The error mapping is intact.
|
||||
assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
|
||||
assert_eq!(Error::Failed("x".to_string()).code(), oakaudio::error::OAKAUDIO_E_FAILED);
|
||||
|
||||
// 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);
|
||||
m.stop_output().unwrap();
|
||||
m.clear_buffered_output().unwrap();
|
||||
}
|
||||
|
||||
@@ -14,20 +14,15 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! AudioProcessor contract tests (processor.rs), through the C ABI. The
|
||||
//! conversion runs a real FFmpeg filter graph (aresample/aformat/atempo),
|
||||
//! so resampling and time-stretch have filter latency: the exact frame
|
||||
//! counts are drained after `flush`, while identity conversion is an
|
||||
//! immediate passthrough.
|
||||
//! AudioProcessor contract tests (processor.rs), calling the public Rust
|
||||
//! API. The conversion runs a real FFmpeg filter graph
|
||||
//! (aresample/aformat/atempo), so resampling and time-stretch have filter
|
||||
//! latency: the exact frame counts are drained after `flush`, while
|
||||
//! identity conversion is an immediate passthrough.
|
||||
|
||||
mod common;
|
||||
|
||||
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
|
||||
use oakaudio::ffi::processor::{
|
||||
oakaudio_processor_close, oakaudio_processor_convert, oakaudio_processor_flush,
|
||||
oakaudio_processor_free, oakaudio_processor_init, oakaudio_processor_is_open,
|
||||
oakaudio_processor_open,
|
||||
};
|
||||
use oakaudio::error::{Error, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
|
||||
use oakaudio::params::{AudioParams, SampleFormat};
|
||||
use oakaudio::processor::Processor;
|
||||
|
||||
/// Stereo f32_p planes of `frames` ramp samples.
|
||||
fn ramp_planes(frames: usize) -> Vec<Vec<f32>> {
|
||||
@@ -37,37 +32,61 @@ fn ramp_planes(frames: usize) -> Vec<Vec<f32>> {
|
||||
]
|
||||
}
|
||||
|
||||
fn open_identity(h: oakaudio::handle::CHandle) -> i32 {
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 1.0) }
|
||||
/// Stereo planar-f32 params at `rate`.
|
||||
fn params(rate: i32) -> AudioParams {
|
||||
AudioParams {
|
||||
sample_rate: rate,
|
||||
channel_layout: 0x3,
|
||||
format: SampleFormat::F32Planar,
|
||||
}
|
||||
}
|
||||
|
||||
/// init yields a valid handle; is_open is false before open and true after;
|
||||
/// close returns it to closed without error.
|
||||
fn open_identity(p: &Processor) -> Result<(), Box<dyn std::error::Error>> {
|
||||
p.open(params(48000), params(48000), 1.0)
|
||||
}
|
||||
|
||||
/// Pointer arrays for the convert call.
|
||||
fn plane_ptrs(planes: &[Vec<f32>]) -> Vec<*const f32> {
|
||||
planes.iter().map(|p| p.as_ptr()).collect()
|
||||
}
|
||||
|
||||
fn plane_mut_ptrs(planes: &mut [Vec<f32>]) -> Vec<*mut f32> {
|
||||
planes.iter_mut().map(|p| p.as_mut_ptr()).collect()
|
||||
}
|
||||
|
||||
/// The crate error code of a failed call (the API surfaces
|
||||
/// `Box<dyn std::error::Error>`).
|
||||
fn code(err: Box<dyn std::error::Error>) -> i32 {
|
||||
err.downcast_ref::<oakaudio::error::Error>()
|
||||
.map(|e| e.code())
|
||||
.unwrap_or(-1)
|
||||
}
|
||||
|
||||
/// init yields a closed processor; is_open is false before open and true
|
||||
/// after; close returns it to closed without error.
|
||||
#[test]
|
||||
fn processor_open_isopen_close() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert!(!h.ctx.is_null());
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
assert_eq!(unsafe { oakaudio_processor_close(h) }, OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
let p = Processor::init();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
open_identity(&p).unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
p.close().unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
}
|
||||
|
||||
/// open with matching in/out rate and format is an identity passthrough:
|
||||
/// convert returns the same frame count and samples within 1e-6.
|
||||
#[test]
|
||||
fn identity_convert_passthrough() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
let p = Processor::init();
|
||||
open_identity(&p).unwrap();
|
||||
|
||||
let planes = ramp_planes(32);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; 32]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32) };
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 32);
|
||||
for ch in 0..2 {
|
||||
for i in 0..32 {
|
||||
@@ -79,22 +98,21 @@ fn identity_convert_passthrough() {
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
}
|
||||
|
||||
/// convert with an output capacity smaller than the produced frames returns
|
||||
/// the produced count clamped to capacity and fills up to capacity.
|
||||
#[test]
|
||||
fn convert_capacity_truncation() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
let p = Processor::init();
|
||||
open_identity(&p).unwrap();
|
||||
|
||||
let planes = ramp_planes(32);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![9.9f32; 10]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10) };
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap();
|
||||
assert_eq!(n, 10);
|
||||
for ch in 0..2 {
|
||||
for i in 0..10 {
|
||||
@@ -104,56 +122,40 @@ fn convert_capacity_truncation() {
|
||||
|
||||
// The graph has already drained; nothing further to pull.
|
||||
let mut out2 = vec![vec![0f32; 32]; 2];
|
||||
let mut out2_ptrs: Vec<*mut f32> = out2.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32) };
|
||||
let mut out2_ptrs = plane_mut_ptrs(&mut out2);
|
||||
let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
}
|
||||
|
||||
/// open with a zero/negative rate or a wrong output format returns
|
||||
/// OAKAUDIO_E_INVALID and leaves the processor closed; an empty handle is
|
||||
/// OAKAUDIO_E_INVALID everywhere.
|
||||
/// open with a zero/negative rate or a wrong output format is rejected and
|
||||
/// leaves the processor closed; convert before open is a state error.
|
||||
#[test]
|
||||
fn open_invalid_params() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
let p = Processor::init();
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 0, 3, 4, 48000, 3, 4, 1.0) },
|
||||
code(p.open(params(0), params(48000), 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
// The output format is forced to planar f32 by the processor contract.
|
||||
let mut wrong_out = params(48000);
|
||||
wrong_out.format = SampleFormat::F32;
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 0, 1.0) },
|
||||
code(p.open(params(48000), wrong_out, 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
|
||||
let empty = oakaudio::handle::CHandle::null();
|
||||
assert_eq!(open_identity(empty), OAKAUDIO_E_INVALID);
|
||||
// convert before open is a state error; a non-positive speed is
|
||||
// rejected on open.
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_is_open(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_close(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_flush(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
let mut out_ptrs: Vec<*mut f32> = Vec::new();
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_convert(empty, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
|
||||
// convert before open is a state error.
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
|
||||
code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()),
|
||||
OAKAUDIO_E_STATE
|
||||
);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
assert_eq!(
|
||||
code(p.open(params(48000), params(48000), 0.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// Resampling to half rate halves the frame count (44100 -> 22050). The
|
||||
@@ -161,34 +163,23 @@ fn open_invalid_params() {
|
||||
/// drained after `flush`; flush then keeps the processor open.
|
||||
#[test]
|
||||
fn resample_and_flush() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 44100, 3, 4, 22050, 3, 4, 1.0) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
let p = Processor::init();
|
||||
p.open(params(44100), params(22050), 1.0).unwrap();
|
||||
|
||||
// 1 second of input keeps the resampler delay well below the signal.
|
||||
let frames = 44100;
|
||||
let planes = ramp_planes(frames);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = unsafe {
|
||||
oakaudio_processor_convert(
|
||||
h,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
};
|
||||
assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
// Drain the resampler delay after end-of-input.
|
||||
while total < frames as i32 {
|
||||
let n = unsafe {
|
||||
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
|
||||
};
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -198,8 +189,7 @@ fn resample_and_flush() {
|
||||
(total - 22050).abs() <= 2,
|
||||
"half-rate output must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
}
|
||||
|
||||
/// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count
|
||||
@@ -207,33 +197,22 @@ fn resample_and_flush() {
|
||||
/// produces output) and the processor stays open.
|
||||
#[test]
|
||||
fn tempo_stretch() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 2.0) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
let p = Processor::init();
|
||||
p.open(params(48000), params(48000), 2.0).unwrap();
|
||||
|
||||
// 1 second of input: many atempo windows (1024 samples at 48 kHz).
|
||||
let frames = 48000;
|
||||
let planes = ramp_planes(frames);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = unsafe {
|
||||
oakaudio_processor_convert(
|
||||
h,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
};
|
||||
assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
while total < frames as i32 {
|
||||
let n = unsafe {
|
||||
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
|
||||
};
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -243,24 +222,20 @@ fn tempo_stretch() {
|
||||
(total - 24000).abs() <= 2400,
|
||||
"tempo 2.0 must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
unsafe { oakaudio_processor_close(h) };
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
p.close().unwrap();
|
||||
|
||||
// A non-positive speed is rejected (on a closed processor).
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 0.0) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
// Already open -> state error.
|
||||
assert_eq!(open_identity(h), OAKAUDIO_E_STATE);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
// Re-opening a closed processor works; opening an open one is a state
|
||||
// error.
|
||||
open_identity(&p).unwrap();
|
||||
assert_eq!(code(open_identity(&p).unwrap_err()), OAKAUDIO_E_STATE);
|
||||
}
|
||||
|
||||
/// free(NULL)/free(empty) are no-ops.
|
||||
/// The error mapping is intact.
|
||||
#[test]
|
||||
fn free_null_noop() {
|
||||
let mut h = oakaudio::handle::CHandle::null();
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
unsafe { oakaudio_processor_free(std::ptr::null_mut()) };
|
||||
fn error_codes() {
|
||||
assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKAUDIO_E_STATE);
|
||||
assert_eq!(Error::Failed("x".to_string()).code(), -60003);
|
||||
assert_eq!(oakaudio::error::OAKAUDIO_OK, OAKAUDIO_OK);
|
||||
}
|
||||
|
||||
@@ -15,24 +15,22 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! AudioSynchronizer + AudioWaveformSync contract tests
|
||||
//! (synchronizer.rs, waveformsync.rs), through the C ABI.
|
||||
//! (synchronizer.rs, waveformsync.rs), calling the public Rust API.
|
||||
|
||||
mod common;
|
||||
|
||||
use oakaudio::error::OAKAUDIO_E_INVALID;
|
||||
use oakaudio::ffi::sync::{
|
||||
oakaudio_sync_estimate_envelope_offset, oakaudio_sync_estimate_stretch_and_offset,
|
||||
oakaudio_sync_extract_rms_envelope, oakaudio_sync_place_by_source_time,
|
||||
oakaudio_sync_place_by_waveform_offset, OffsetResult, SourceClip, StretchOffsetResult,
|
||||
use oakcore_rs::Rational;
|
||||
use oakaudio::synchronizer::{place_by_source_time, place_by_waveform_offset, SourceClip};
|
||||
use oakaudio::waveformsync::{
|
||||
estimate_envelope_offset, estimate_envelope_offset_valid, estimate_stretch_and_offset,
|
||||
extract_rms_envelope,
|
||||
};
|
||||
|
||||
fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip {
|
||||
SourceClip {
|
||||
source_start_time_num: source,
|
||||
source_start_time_den: 1,
|
||||
media_in_num: media_in,
|
||||
media_in_den: 1,
|
||||
has_source_start_time: has_source as i32,
|
||||
source_start_time: Rational::new(source, 1),
|
||||
media_in: Rational::new(media_in, 1),
|
||||
has_source_start_time: has_source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,28 +41,15 @@ fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip {
|
||||
fn place_by_source_time_matching() {
|
||||
let reference = clip(0, 0, true);
|
||||
let candidate = clip(0, 0, true);
|
||||
let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_source_time(
|
||||
&reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(num, 5);
|
||||
assert_eq!(den, 1);
|
||||
assert_eq!(valid, 1);
|
||||
let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1));
|
||||
assert!(placement.valid);
|
||||
assert_eq!(placement.timeline_in, Rational::new(5, 1));
|
||||
|
||||
// A source-less candidate is invalid: valid=0, null rational.
|
||||
// A source-less candidate is invalid: valid=false, null rational.
|
||||
let candidate = clip(0, 0, false);
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_source_time(
|
||||
&reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(valid, 0);
|
||||
assert_eq!(num, 0);
|
||||
assert_eq!(den, 0);
|
||||
let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1));
|
||||
assert!(!placement.valid);
|
||||
assert!(placement.timeline_in.is_null());
|
||||
}
|
||||
|
||||
/// place_by_source_time: when source times disagree by a known delta, the
|
||||
@@ -73,62 +58,28 @@ fn place_by_source_time_matching() {
|
||||
fn place_by_source_time_delta() {
|
||||
let reference = clip(5, 0, true);
|
||||
let candidate = clip(12, 0, true);
|
||||
let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_source_time(
|
||||
&reference, &candidate, 0, 1, &mut num, &mut den, &mut valid,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
let placement = place_by_source_time(&reference, &candidate, Rational::new(0, 1));
|
||||
// 0 + (12 + 0) - (5 + 0) = 7
|
||||
assert_eq!(num, 7);
|
||||
assert_eq!(den, 1);
|
||||
assert_eq!(valid, 1);
|
||||
|
||||
// A zero denominator is rejected up front.
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_source_time(
|
||||
&reference, &candidate, 0, 0, &mut num, &mut den, &mut valid,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, OAKAUDIO_E_INVALID);
|
||||
assert!(placement.valid);
|
||||
assert_eq!(placement.timeline_in, Rational::new(7, 1));
|
||||
}
|
||||
|
||||
/// place_by_waveform_offset converts a sample offset at a sample rate into
|
||||
/// a timeline-in shift; out_valid is 1 on success and 0 for a null rate.
|
||||
/// a timeline-in shift; out_valid is false for a null rate.
|
||||
#[test]
|
||||
fn place_by_waveform_offset_conversion() {
|
||||
let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut num, &mut den, &mut valid)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(num, 1);
|
||||
assert_eq!(den, 1);
|
||||
assert_eq!(valid, 1);
|
||||
let placement = place_by_waveform_offset(Rational::new(0, 1), 48000, 48000);
|
||||
assert!(placement.valid);
|
||||
assert_eq!(placement.timeline_in, Rational::new(1, 1));
|
||||
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 48000, &mut num, &mut den, &mut valid)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(num, 3);
|
||||
assert_eq!(den, 2);
|
||||
assert_eq!(valid, 1);
|
||||
let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 48000);
|
||||
assert!(placement.valid);
|
||||
assert_eq!(placement.timeline_in, Rational::new(3, 2));
|
||||
|
||||
// A null rate is invalid (valid=0, null rational), and the FFI rejects
|
||||
// a zero timeline denominator.
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 0, &mut num, &mut den, &mut valid)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(valid, 0);
|
||||
assert_eq!(num, 0);
|
||||
assert_eq!(den, 0);
|
||||
|
||||
let r = unsafe {
|
||||
oakaudio_sync_place_by_waveform_offset(1, 0, 48000, 48000, &mut num, &mut den, &mut valid)
|
||||
};
|
||||
assert_eq!(r, OAKAUDIO_E_INVALID);
|
||||
// A null rate is invalid (valid=false, null rational).
|
||||
let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 0);
|
||||
assert!(!placement.valid);
|
||||
assert!(placement.timeline_in.is_null());
|
||||
}
|
||||
|
||||
/// extract_rms_envelope produces one value per window; a window larger than
|
||||
@@ -137,85 +88,31 @@ fn place_by_waveform_offset_conversion() {
|
||||
fn extract_rms_envelope_shape() {
|
||||
let data: Vec<f32> = (0..100).map(|i| i as f32).collect();
|
||||
let planes = common::planar_from(&data, 2);
|
||||
let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
|
||||
|
||||
let mut out = vec![0.0f64; 16];
|
||||
let n = unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(n, 10);
|
||||
assert!(out.iter().take(10).all(|&v| v > 0.0));
|
||||
let env = extract_rms_envelope(&refs, 10);
|
||||
assert_eq!(env.len(), 10);
|
||||
assert!(env.iter().all(|&v| v > 0.0));
|
||||
|
||||
let n = unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 200, out.as_mut_ptr(), 16)
|
||||
};
|
||||
assert_eq!(n, 1);
|
||||
|
||||
// Invalid inputs.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(std::ptr::null(), 2, 100, 10, out.as_mut_ptr(), 16)
|
||||
},
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 0, 100, 10, out.as_mut_ptr(), 16)
|
||||
},
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 0, out.as_mut_ptr(), 16)
|
||||
},
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), -1)
|
||||
},
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
// Two-stage: NULL out returns the required count.
|
||||
let n = unsafe {
|
||||
oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, std::ptr::null_mut(), 0)
|
||||
};
|
||||
assert_eq!(n, 10);
|
||||
let env = extract_rms_envelope(&refs, 200);
|
||||
assert_eq!(env.len(), 1);
|
||||
}
|
||||
|
||||
/// estimate_envelope_offset: for a candidate delayed by N windows relative
|
||||
/// to the reference, the returned offset is +N windows and valid=1.
|
||||
/// to the reference, the returned offset is +N windows and valid=true.
|
||||
#[test]
|
||||
fn envelope_offset_recovers_delay() {
|
||||
let reference: Vec<f64> = (0..10).map(|i| i as f64).collect();
|
||||
let mut candidate = vec![0.0f64; 10];
|
||||
candidate[2..].copy_from_slice(&reference[..8]);
|
||||
let mut out = OffsetResult {
|
||||
offset_samples: 0,
|
||||
confidence: 0.0,
|
||||
valid: 0,
|
||||
};
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
reference.as_ptr(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
10,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 1);
|
||||
let out = estimate_envelope_offset(&reference, &candidate, 100, 10);
|
||||
assert!(out.valid);
|
||||
assert_eq!(out.offset_samples, 200);
|
||||
assert!((out.confidence - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// estimate_envelope_offset: windows masked invalid on either side are
|
||||
/// excluded from correlation; empty masks are treated as all-valid.
|
||||
/// estimate_envelope_offset_valid: windows masked invalid on either side
|
||||
/// are excluded from correlation; empty masks are treated as all-valid.
|
||||
#[test]
|
||||
fn envelope_offset_respects_valid_masks() {
|
||||
let reference: Vec<f64> = (0..10).map(|i| i as f64).collect();
|
||||
@@ -224,68 +121,35 @@ fn envelope_offset_respects_valid_masks() {
|
||||
|
||||
// Only the last reference window is valid -> no lag has >= 2 valid
|
||||
// overlap windows, so the estimate is invalid.
|
||||
let mut ref_valid = [1u8; 10];
|
||||
ref_valid[..9].fill(0);
|
||||
let mut out = OffsetResult {
|
||||
offset_samples: 0,
|
||||
confidence: 0.0,
|
||||
valid: 0,
|
||||
};
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
reference.as_ptr(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
10,
|
||||
ref_valid.as_ptr(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 0);
|
||||
let mut ref_valid = vec![false; 10];
|
||||
ref_valid[9] = true;
|
||||
let out = estimate_envelope_offset_valid(
|
||||
&reference,
|
||||
&candidate,
|
||||
&ref_valid,
|
||||
&[],
|
||||
100,
|
||||
10,
|
||||
);
|
||||
assert!(!out.valid);
|
||||
assert_eq!(out.confidence, 0.0);
|
||||
|
||||
// Fully-valid masks behave like the unmasked call.
|
||||
let mut valid = [1u8; 10];
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
reference.as_ptr(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
10,
|
||||
valid.as_ptr(),
|
||||
valid.as_ptr(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 1);
|
||||
let valid = vec![true; 10];
|
||||
let out = estimate_envelope_offset_valid(
|
||||
&reference,
|
||||
&candidate,
|
||||
&valid,
|
||||
&valid,
|
||||
100,
|
||||
10,
|
||||
);
|
||||
assert!(out.valid);
|
||||
assert_eq!(out.offset_samples, 200);
|
||||
|
||||
// NULL arrays / non-positive lengths are invalid.
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
std::ptr::null(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
10,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
/// estimate_stretch_and_offset: a candidate sampled at 2x the reference
|
||||
/// rate reports rate ~2.0 (>1 = speed up) with a valid=1 result. A
|
||||
/// rate reports rate ~2.0 (>1 = speed up) with a valid=true result. A
|
||||
/// non-linear (sine) reference is used — normalized correlation of linear
|
||||
/// ramps is degenerate (any rate correlates 1.0), but only the true rate
|
||||
/// resamples the sine back onto the reference exactly.
|
||||
@@ -302,78 +166,43 @@ fn stretch_offset_recovers_rate() {
|
||||
candidate.push((reference[k] + reference[k + 1]) / 2.0);
|
||||
}
|
||||
}
|
||||
let mut out = StretchOffsetResult {
|
||||
rate: 0.0,
|
||||
offset_samples: 0,
|
||||
confidence: 0.0,
|
||||
valid: 0,
|
||||
};
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference.as_ptr(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
candidate.len() as i32,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
0.5,
|
||||
3.0,
|
||||
0.1,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 1);
|
||||
let out = estimate_stretch_and_offset(
|
||||
&reference,
|
||||
&candidate,
|
||||
&[],
|
||||
&[],
|
||||
100,
|
||||
10,
|
||||
0.5,
|
||||
3.0,
|
||||
0.1,
|
||||
);
|
||||
assert!(out.valid);
|
||||
assert!((out.rate - 2.0).abs() < 0.15, "rate = {}", out.rate);
|
||||
assert!(out.confidence > 0.99, "confidence = {}", out.confidence);
|
||||
|
||||
// Invalid rate parameters are rejected.
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference.as_ptr(),
|
||||
10,
|
||||
candidate.as_ptr(),
|
||||
candidate.len() as i32,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
0.0,
|
||||
3.0,
|
||||
0.1,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, OAKAUDIO_E_INVALID);
|
||||
// Invalid rate parameters are rejected (invalid result, defaults).
|
||||
let out = estimate_stretch_and_offset(
|
||||
&reference,
|
||||
&candidate,
|
||||
&[],
|
||||
&[],
|
||||
100,
|
||||
10,
|
||||
0.0,
|
||||
3.0,
|
||||
0.1,
|
||||
);
|
||||
assert!(!out.valid);
|
||||
}
|
||||
|
||||
/// estimate_* on identical silent envelopes yields low/no confidence and
|
||||
/// valid=0 (no correlation peak).
|
||||
/// valid=false (no correlation peak).
|
||||
#[test]
|
||||
fn silent_inputs_invalid() {
|
||||
let silence = vec![0.0f64; 10];
|
||||
let mut out = OffsetResult {
|
||||
offset_samples: 0,
|
||||
confidence: 0.0,
|
||||
valid: 0,
|
||||
};
|
||||
let r = unsafe {
|
||||
oakaudio_sync_estimate_envelope_offset(
|
||||
silence.as_ptr(),
|
||||
10,
|
||||
silence.as_ptr(),
|
||||
10,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
100,
|
||||
10,
|
||||
&mut out,
|
||||
)
|
||||
};
|
||||
assert_eq!(r, 0);
|
||||
assert_eq!(out.valid, 0);
|
||||
let out = estimate_envelope_offset(&silence, &silence, 100, 10);
|
||||
assert!(!out.valid);
|
||||
assert_eq!(out.confidence, 0.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,56 +14,29 @@
|
||||
// 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.
|
||||
//! AudioVisualWaveform contract tests (waveform.rs), calling the public
|
||||
//! Rust API.
|
||||
|
||||
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,
|
||||
};
|
||||
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: oakaudio::handle::CHandle) {
|
||||
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_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
|
||||
);
|
||||
let planes = [ch0.as_slice(), ch1.as_slice()];
|
||||
w.set_channel_count(2);
|
||||
w.overwrite_samples(&planes, 100, Rational::new(0, 1));
|
||||
}
|
||||
|
||||
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
|
||||
/// 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
|
||||
@@ -71,110 +44,52 @@ fn summary(
|
||||
/// channel-interleaved min/max pairs.
|
||||
#[test]
|
||||
fn overwrite_samples_and_length() {
|
||||
let mut w = unsafe { oakaudio_waveform_init() };
|
||||
fill_ramp(w);
|
||||
let mut w = AudioVisualWaveform::new();
|
||||
fill_ramp(&mut 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);
|
||||
assert_eq!(w.length(), Rational::new(1, 1));
|
||||
|
||||
let out = summary(w, (0, 1), (1, 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);
|
||||
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.
|
||||
/// into a dest range; a null length copies all of src.
|
||||
#[test]
|
||||
fn overwrite_sums_range_copy() {
|
||||
let mut src = unsafe { oakaudio_waveform_init() };
|
||||
fill_ramp(src);
|
||||
let mut src = AudioVisualWaveform::new();
|
||||
fill_ramp(&mut 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);
|
||||
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);
|
||||
|
||||
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) };
|
||||
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 = unsafe { oakaudio_waveform_init() };
|
||||
fill_ramp(w);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_waveform_overwrite_silence(w, 0, 1, 1, 2) },
|
||||
0
|
||||
);
|
||||
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, (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, 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, (1, 2), (1, 2), 1);
|
||||
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 {:?}",
|
||||
@@ -182,94 +97,61 @@ fn overwrite_silence() {
|
||||
);
|
||||
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),
|
||||
w.length(),
|
||||
Rational::new(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);
|
||||
let mut w = AudioVisualWaveform::new();
|
||||
fill_ramp(&mut 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));
|
||||
w.trim_in(Rational::new(-1, 2));
|
||||
assert_eq!(w.length(), Rational::new(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));
|
||||
w.resize(Rational::new(2, 1));
|
||||
assert_eq!(w.length(), Rational::new(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) };
|
||||
w.trim_range(Rational::new(0, 1), Rational::new(1, 2));
|
||||
assert_eq!(w.length(), Rational::new(1, 2));
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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_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));
|
||||
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_s over 4 interleaved entries, 2 channels -> one pair per
|
||||
// re_sum_samples 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 },
|
||||
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 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
|
||||
);
|
||||
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 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.
|
||||
/// 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()));
|
||||
@@ -281,55 +163,19 @@ fn extract_file_and_notfound() {
|
||||
}
|
||||
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);
|
||||
// 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 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 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;
|
||||
|
||||
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.
|
||||
@@ -346,60 +192,18 @@ fn extract_file_and_notfound() {
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
/// FFI validation and empty-handle error paths.
|
||||
/// 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 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.
|
||||
fn no_channel_count_is_noop() {
|
||||
let mut w = AudioVisualWaveform::new();
|
||||
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) };
|
||||
}
|
||||
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);
|
||||
|
||||
/// 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()) };
|
||||
w.set_channel_count(2);
|
||||
assert_eq!(w.channel_count(), 2);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,8 @@ license = "GPL-3.0-or-later"
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[features]
|
||||
# Compile the oakcore_*/oakrender_* host-mocks in src/bridge/test_stubs.rs so
|
||||
# consumer test binaries that link this crate (e.g. oaknode's) can resolve
|
||||
# those cross-crate C-ABI symbols without the host dylibs.
|
||||
test-stubs = []
|
||||
|
||||
[dependencies]
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oakffmpeg-link = { path = "../oakffmpeg-link" }
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
# Real media decode/encode. The C++ ffmpeg_bridge library existed only to
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::AudioParams` — the audio stream parameter value type.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the
|
||||
//! codec crate used to create audio parameter sets through the oakcore C
|
||||
//! ABI (`oakcore_audioparams_*`, host-provided symbols) and store them
|
||||
//! behind refcounted handles. The parameters are plain values now — the
|
||||
//! FFmpeg probe fills them in directly and the footage description stores
|
||||
//! them by value.
|
||||
|
||||
/// `olive::AudioParams` — the audio stream description recorded at probe
|
||||
/// time.
|
||||
///
|
||||
/// Mirrors `core/include/olive/core/oakcore/audioparams.h`: sample rate,
|
||||
/// ffmpeg channel-layout mask, sample format, stream index, duration and
|
||||
/// time base.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct AudioParams {
|
||||
/// Sample rate in Hz.
|
||||
pub sample_rate: i32,
|
||||
/// ffmpeg-style channel layout mask (e.g. 0x3 = stereo).
|
||||
pub channel_layout: u64,
|
||||
/// `olive::core::SampleFormat::Format` value.
|
||||
pub format: i32,
|
||||
/// Stream index within the source container.
|
||||
pub stream_index: i32,
|
||||
/// Stream length in time-base units.
|
||||
pub duration: i64,
|
||||
/// Time base (num/den seconds per tick).
|
||||
pub time_base: (i32, i32),
|
||||
}
|
||||
|
||||
impl AudioParams {
|
||||
/// Channel count derived from the layout mask, mirroring the C++
|
||||
/// `AudioParams::channel_count()`.
|
||||
pub fn channel_count(&self) -> i32 {
|
||||
self.channel_layout.count_ones() as i32
|
||||
}
|
||||
|
||||
/// Whether the parameter set describes a usable stream, mirroring the
|
||||
/// C++ `AudioParams::is_valid()` (positive rate and a non-zero layout).
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.sample_rate > 0 && self.channel_layout != 0
|
||||
}
|
||||
}
|
||||
@@ -1,686 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,
|
||||
//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,
|
||||
//! colortransform).
|
||||
//!
|
||||
//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,
|
||||
//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,
|
||||
//! release, abi_version}` layout from `include/common/handle.h`, so the
|
||||
//! codec module can store them by value and pass them straight across
|
||||
//! the FFI boundary.
|
||||
//!
|
||||
//! The oakcore audio parameters use a pointer-based C ABI instead of the
|
||||
//! by-value handle convention: `oakcore_audioparams_*` take and return
|
||||
//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/
|
||||
//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw
|
||||
//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`
|
||||
//! returns a newly allocated rational the caller must release with
|
||||
//! `oakcore_rational_free`.
|
||||
//!
|
||||
//! # ABI discipline (single-lib era)
|
||||
//!
|
||||
//! The frozen `oakcommon` C ABI (see `crates/oakengine/include/common/`)
|
||||
//! uses **out-pointer getters** and **instance handles** for the module
|
||||
//! families. Earlier codec-era bridges declared value-style signatures
|
||||
//! (C++-era headers); those declarations were wrong against the frozen
|
||||
//! contract and are fixed here. To keep the crate's call sites
|
||||
//! unchanged, each true ABI symbol is declared under `#[link_name]` with
|
||||
//! its real signature and wrapped in an adapter of the old value-style
|
||||
//! shape (an M12 P0 decode-path fix: the mismatch made every decoded
|
||||
//! frame invalid).
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `OakVideoParams` — refcounted video-parameter handle.
|
||||
pub type OakVideoParams = CHandle;
|
||||
|
||||
/// `OakAudioParams` — refcounted audio-parameter handle.
|
||||
pub type OakAudioParams = CHandle;
|
||||
|
||||
/// `OakSubtitleParams` — refcounted subtitle-parameter handle.
|
||||
pub type OakSubtitleParams = CHandle;
|
||||
|
||||
/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec
|
||||
/// only stores and forwards it).
|
||||
pub type OakNodeBlock = CHandle;
|
||||
|
||||
/// Module instance handles (filefunctions / oiioutils take `self_`).
|
||||
pub type OakFileFunctions = CHandle;
|
||||
pub type OakOIIOUtils = CHandle;
|
||||
|
||||
// The handle structs are opaque refcounted handles pointing into a C
|
||||
// library; the boxed objects are independently synchronized there, so
|
||||
// moving a handle between threads is sound.
|
||||
|
||||
extern "C" {
|
||||
/// `oakcommon_videoparams_init`.
|
||||
pub fn oakcommon_videoparams_init() -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_init_basic`.
|
||||
#[link_name = "oakcommon_videoparams_init_basic"]
|
||||
fn videoparams_init_basic_abi(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_init_with_time_base`.
|
||||
#[link_name = "oakcommon_videoparams_init_with_time_base"]
|
||||
fn videoparams_init_with_time_base_abi(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: c_int,
|
||||
time_base_den: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_free` (NULL/empty no-op).
|
||||
pub fn oakcommon_videoparams_free(params: *mut OakVideoParams);
|
||||
/// `oakcommon_videoparams_get_width`.
|
||||
#[link_name = "oakcommon_videoparams_get_width"]
|
||||
fn videoparams_get_width_abi(params: OakVideoParams, width: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_height`.
|
||||
#[link_name = "oakcommon_videoparams_get_height"]
|
||||
fn videoparams_get_height_abi(params: OakVideoParams, height: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_format`.
|
||||
#[link_name = "oakcommon_videoparams_get_format"]
|
||||
fn videoparams_get_format_abi(params: OakVideoParams, format: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_time_base` (num/den out).
|
||||
#[link_name = "oakcommon_videoparams_get_time_base"]
|
||||
fn videoparams_get_time_base_abi(
|
||||
params: OakVideoParams,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_videoparams_set_width`.
|
||||
pub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);
|
||||
/// `oakcommon_videoparams_set_height`.
|
||||
pub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);
|
||||
/// `oakcommon_videoparams_set_format`.
|
||||
pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);
|
||||
/// `oakcommon_videoparams_get_is_valid`.
|
||||
#[link_name = "oakcommon_videoparams_get_is_valid"]
|
||||
fn videoparams_get_is_valid_abi(params: OakVideoParams, is_valid: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_equals`.
|
||||
#[link_name = "oakcommon_videoparams_equals"]
|
||||
fn videoparams_equals_abi(
|
||||
params: OakVideoParams,
|
||||
other: OakVideoParams,
|
||||
equal: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_videoparams_set_time_base`.
|
||||
pub fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64);
|
||||
/// `oakcommon_videoparams_set_frame_rate`.
|
||||
pub fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64);
|
||||
/// `oakcommon_videoparams_set_pixel_aspect_ratio`.
|
||||
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(params: OakVideoParams, num: i64, den: i64);
|
||||
/// `oakcommon_videoparams_set_interlacing`.
|
||||
pub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);
|
||||
/// `oakcommon_videoparams_set_duration`.
|
||||
pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);
|
||||
/// `oakcommon_videoparams_set_start_time`.
|
||||
pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);
|
||||
/// `oakcommon_videoparams_set_color_range`.
|
||||
pub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);
|
||||
/// `oakcommon_videoparams_set_video_type`.
|
||||
pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);
|
||||
/// `oakcommon_videoparams_set_channel_count`.
|
||||
pub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);
|
||||
/// `oakcommon_videoparams_set_color_primaries`.
|
||||
pub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);
|
||||
/// `oakcommon_videoparams_set_color_transfer`.
|
||||
pub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);
|
||||
/// `oakcommon_videoparams_set_premultiplied_alpha`.
|
||||
pub fn oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params: OakVideoParams,
|
||||
premultiplied: c_int,
|
||||
);
|
||||
/// `oakcommon_videoparams_set_enabled`.
|
||||
pub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);
|
||||
/// `oakcommon_videoparams_static_get_bytes_per_pixel`.
|
||||
#[link_name = "oakcommon_videoparams_static_get_bytes_per_pixel"]
|
||||
fn videoparams_static_get_bytes_per_pixel_abi(format: c_int, channels: c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_frame_rate_as_time_base`.
|
||||
pub fn oakcommon_videoparams_frame_rate_as_time_base(
|
||||
frame_rate_num: i64,
|
||||
frame_rate_den: i64,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
);
|
||||
/// `oakcommon_videoparams_get_stream_index`.
|
||||
#[link_name = "oakcommon_videoparams_get_stream_index"]
|
||||
fn videoparams_get_stream_index_abi(params: OakVideoParams, index: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_set_stream_index`.
|
||||
pub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);
|
||||
/// `oakcommon_videoparams_get_divider`.
|
||||
#[link_name = "oakcommon_videoparams_get_divider"]
|
||||
fn videoparams_get_divider_abi(params: OakVideoParams, divider: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_set_divider`.
|
||||
pub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);
|
||||
/// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).
|
||||
#[link_name = "oakcommon_videoparams_get_frame_rate"]
|
||||
fn videoparams_get_frame_rate_abi(
|
||||
params: OakVideoParams,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_videoparams_get_duration` (time-base units).
|
||||
#[link_name = "oakcommon_videoparams_get_duration"]
|
||||
fn videoparams_get_duration_abi(params: OakVideoParams, duration: *mut i64) -> c_int;
|
||||
/// `oakcommon_videoparams_get_channel_count`.
|
||||
#[link_name = "oakcommon_videoparams_get_channel_count"]
|
||||
fn videoparams_get_channel_count_abi(params: OakVideoParams, count: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_color_primaries`.
|
||||
#[link_name = "oakcommon_videoparams_get_color_primaries"]
|
||||
fn videoparams_get_color_primaries_abi(params: OakVideoParams, primaries: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_color_transfer`.
|
||||
#[link_name = "oakcommon_videoparams_get_color_transfer"]
|
||||
fn videoparams_get_color_transfer_abi(params: OakVideoParams, transfer: *mut c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).
|
||||
#[link_name = "oakcommon_videoparams_get_interlacing"]
|
||||
fn videoparams_get_interlacing_abi(params: OakVideoParams, interlacing: *mut c_int) -> c_int;
|
||||
/// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).
|
||||
pub fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut OakAudioParams;
|
||||
/// `oakcore_audioparams_free` (NULL no-op).
|
||||
pub fn oakcore_audioparams_free(params: *mut OakAudioParams);
|
||||
/// `oakcore_audioparams_sample_rate`.
|
||||
pub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_set_sample_rate`.
|
||||
pub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);
|
||||
/// `oakcore_audioparams_channel_layout`.
|
||||
pub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64;
|
||||
/// `oakcore_audioparams_set_channel_layout`.
|
||||
pub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);
|
||||
/// `oakcore_audioparams_set_time_base`.
|
||||
pub fn oakcore_audioparams_set_time_base(params: *mut OakAudioParams, num: c_int, den: c_int);
|
||||
/// `oakcore_audioparams_set_format`.
|
||||
pub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);
|
||||
/// `oakcore_audioparams_set_stream_index`.
|
||||
pub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);
|
||||
/// `oakcore_audioparams_set_duration`.
|
||||
pub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);
|
||||
/// `oakcore_audioparams_channel_count`.
|
||||
pub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_format`.
|
||||
pub fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_stream_index`.
|
||||
pub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_duration`.
|
||||
pub fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64;
|
||||
/// `oakcore_audioparams_is_valid`.
|
||||
pub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_time_base` (newly allocated rational; caller
|
||||
/// releases with `oakcore_rational_free`).
|
||||
pub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void;
|
||||
/// `oakcore_rational_numerator`.
|
||||
pub fn oakcore_rational_numerator(rational: *const c_void) -> c_int;
|
||||
/// `oakcore_rational_denominator`.
|
||||
pub fn oakcore_rational_denominator(rational: *const c_void) -> c_int;
|
||||
/// `oakcore_rational_free` (NULL no-op).
|
||||
pub fn oakcore_rational_free(rational: *mut c_void);
|
||||
/// `oakcommon_subtitleparams_get_stream_index`.
|
||||
#[link_name = "oakcommon_subtitleparams_get_stream_index"]
|
||||
fn subtitleparams_get_stream_index_abi(
|
||||
params: OakSubtitleParams,
|
||||
index: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_subtitleparams_generate_ass_header`.
|
||||
pub fn oakcommon_subtitleparams_generate_ass_header(
|
||||
params: OakSubtitleParams,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
);
|
||||
/// `oakcommon_subtitleparams_add_subtitle`.
|
||||
pub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);
|
||||
/// `oakcommon_config_get_int`.
|
||||
pub fn oakcommon_config_get_int(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_config_get_bool`.
|
||||
pub fn oakcommon_config_get_bool(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_config_get` (two-stage string access).
|
||||
pub fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_init`.
|
||||
pub fn oakcommon_filefunctions_init() -> OakFileFunctions;
|
||||
/// `oakcommon_filefunctions_free`.
|
||||
pub fn oakcommon_filefunctions_free(self_: *mut OakFileFunctions);
|
||||
/// `oakcommon_filefunctions_get_configuration_location` (two-stage).
|
||||
#[link_name = "oakcommon_filefunctions_get_configuration_location"]
|
||||
fn filefunctions_get_configuration_location_abi(
|
||||
self_: OakFileFunctions,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier`.
|
||||
#[link_name = "oakcommon_filefunctions_get_unique_file_identifier"]
|
||||
fn filefunctions_get_unique_file_identifier_abi(
|
||||
self_: OakFileFunctions,
|
||||
filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_get_application_path` (two-stage).
|
||||
#[link_name = "oakcommon_filefunctions_get_application_path"]
|
||||
fn filefunctions_get_application_path_abi(
|
||||
self_: OakFileFunctions,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_colortransform_init_output`.
|
||||
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakVideoParams;
|
||||
/// `oakcommon_colortransform_get_output` (two-stage string).
|
||||
pub fn oakcommon_colortransform_get_output(
|
||||
transform: OakVideoParams,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_colortransform_free`.
|
||||
pub fn oakcommon_colortransform_free(params: *mut OakVideoParams);
|
||||
/// `oakcommon_ffmpegutils_get_native_sample_format`.
|
||||
#[link_name = "oakcommon_ffmpegutils_get_native_sample_format"]
|
||||
fn ffmpegutils_get_native_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_compatible_pixel_format`.
|
||||
#[link_name = "oakcommon_ffmpegutils_get_compatible_pixel_format"]
|
||||
fn ffmpegutils_get_compatible_pixel_format_abi(pix_fmt: c_int, out: *mut c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.
|
||||
#[link_name = "oakcommon_ffmpegutils_get_ffmpeg_pixel_format"]
|
||||
fn ffmpegutils_get_ffmpeg_pixel_format_abi(
|
||||
pix_fmt: c_int,
|
||||
channel_count: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
|
||||
#[link_name = "oakcommon_ffmpegutils_get_ffmpeg_sample_format"]
|
||||
fn ffmpegutils_get_ffmpeg_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.
|
||||
#[link_name = "oakcommon_ffmpegutils_get_compatible_bridge_pixel_format"]
|
||||
fn ffmpegutils_get_compatible_bridge_pixel_format_abi(
|
||||
pix_fmt: c_int,
|
||||
maximum_pix_fmt: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
|
||||
#[link_name = "oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space"]
|
||||
fn ffmpegutils_convert_jpeg_space_to_regular_space_abi(
|
||||
pix_fmt: c_int,
|
||||
out: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_oiioutils_init`.
|
||||
pub fn oakcommon_oiioutils_init() -> OakOIIOUtils;
|
||||
/// `oakcommon_oiioutils_free`.
|
||||
pub fn oakcommon_oiioutils_free(self_: *mut OakOIIOUtils);
|
||||
/// `oakcommon_oiioutils_get_oiio_base_type_from_format`.
|
||||
#[link_name = "oakcommon_oiioutils_get_oiio_base_type_from_format"]
|
||||
fn oiioutils_get_oiio_base_type_from_format_abi(
|
||||
self_: OakOIIOUtils,
|
||||
pixel_format: c_int,
|
||||
out_base_type: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_oiioutils_get_format_from_oiio_basetype`.
|
||||
#[link_name = "oakcommon_oiioutils_get_format_from_oiio_basetype"]
|
||||
fn oiioutils_get_format_from_oiio_basetype_abi(
|
||||
self_: OakOIIOUtils,
|
||||
base_type: c_int,
|
||||
out_pixel_format: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).
|
||||
#[link_name = "oakcommon_oiioutils_get_pixel_aspect_ratio"]
|
||||
fn oiioutils_get_pixel_aspect_ratio_abi(
|
||||
self_: OakOIIOUtils,
|
||||
pixel_aspect_ratio: f64,
|
||||
out_numerator: *mut c_int,
|
||||
out_denominator: *mut c_int,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Value-style adapters over the frozen out-pointer ABI (old call shapes)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakcommon_videoparams_init_basic` — value-style shape kept for call
|
||||
/// sites; defaults: U8 format, 4 channels, 1:1 aspect, progressive,
|
||||
/// divider 1.
|
||||
pub fn oakcommon_videoparams_init_basic(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> OakVideoParams {
|
||||
unsafe { videoparams_init_basic_abi(width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den, interlacing, divider) }
|
||||
}
|
||||
|
||||
/// `oakcommon_videoparams_init_with_time_base` — value-style shape kept
|
||||
/// for call sites.
|
||||
pub fn oakcommon_videoparams_init_with_time_base(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: c_int,
|
||||
time_base_den: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> OakVideoParams {
|
||||
unsafe {
|
||||
videoparams_init_with_time_base_abi(
|
||||
width,
|
||||
height,
|
||||
time_base_num,
|
||||
time_base_den,
|
||||
pixel_format,
|
||||
nb_channels,
|
||||
pixel_aspect_num,
|
||||
pixel_aspect_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_width`.
|
||||
pub fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int {
|
||||
let mut w: c_int = 0;
|
||||
unsafe { videoparams_get_width_abi(params, &mut w) };
|
||||
w
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_height`.
|
||||
pub fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int {
|
||||
let mut h: c_int = 0;
|
||||
unsafe { videoparams_get_height_abi(params, &mut h) };
|
||||
h
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_format`.
|
||||
pub fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int {
|
||||
let mut f: c_int = -1;
|
||||
unsafe { videoparams_get_format_abi(params, &mut f) };
|
||||
f
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_is_valid`.
|
||||
pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int {
|
||||
let mut v: c_int = 0;
|
||||
unsafe { videoparams_get_is_valid_abi(params, &mut v) };
|
||||
v
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_equals` (1 when equal).
|
||||
pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int {
|
||||
let mut eq: c_int = 0;
|
||||
unsafe { videoparams_equals_abi(a, b, &mut eq) };
|
||||
eq
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_stream_index`.
|
||||
pub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int {
|
||||
let mut i: c_int = -1;
|
||||
unsafe { videoparams_get_stream_index_abi(params, &mut i) };
|
||||
i
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_divider`.
|
||||
pub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int {
|
||||
let mut d: c_int = 0;
|
||||
unsafe { videoparams_get_divider_abi(params, &mut d) };
|
||||
d
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_duration`.
|
||||
pub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64 {
|
||||
let mut d: i64 = 0;
|
||||
unsafe { videoparams_get_duration_abi(params, &mut d) };
|
||||
d
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_channel_count`.
|
||||
pub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int {
|
||||
let mut c: c_int = 0;
|
||||
unsafe { videoparams_get_channel_count_abi(params, &mut c) };
|
||||
c
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_color_primaries`.
|
||||
pub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int {
|
||||
let mut p: c_int = 0;
|
||||
unsafe { videoparams_get_color_primaries_abi(params, &mut p) };
|
||||
p
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_color_transfer`.
|
||||
pub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int {
|
||||
let mut t: c_int = 0;
|
||||
unsafe { videoparams_get_color_transfer_abi(params, &mut t) };
|
||||
t
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_interlacing`.
|
||||
pub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int {
|
||||
let mut i: c_int = 0;
|
||||
unsafe { videoparams_get_interlacing_abi(params, &mut i) };
|
||||
i
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_time_base` (num/den out as
|
||||
/// `i64`, widened from the frozen `i32` outs).
|
||||
pub fn oakcommon_videoparams_get_time_base(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
) -> c_int {
|
||||
let mut num: c_int = 0;
|
||||
let mut den: c_int = 0;
|
||||
let rc = unsafe { videoparams_get_time_base_abi(params, &mut num, &mut den) };
|
||||
if !out_num.is_null() {
|
||||
unsafe { *out_num = num as i64 };
|
||||
}
|
||||
if !out_den.is_null() {
|
||||
unsafe { *out_den = den as i64 };
|
||||
}
|
||||
rc
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_get_frame_rate` (num/den out).
|
||||
pub fn oakcommon_videoparams_get_frame_rate(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe { videoparams_get_frame_rate_abi(params, out_num, out_den) }
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_videoparams_static_get_bytes_per_pixel`
|
||||
/// (4 channels assumed; the frozen ABI takes channels explicitly).
|
||||
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
|
||||
unsafe { videoparams_static_get_bytes_per_pixel_abi(format, 4) }
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_subtitleparams_get_stream_index`.
|
||||
pub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -> c_int {
|
||||
let mut i: c_int = -1;
|
||||
unsafe { subtitleparams_get_stream_index_abi(params, &mut i) };
|
||||
i
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_get_native_sample_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_get_native_sample_format_abi(sample_format, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_get_compatible_pixel_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_get_compatible_pixel_format_abi(format, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`
|
||||
/// (4 channels assumed).
|
||||
pub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_get_ffmpeg_pixel_format_abi(format, 4, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_get_ffmpeg_sample_format_abi(format, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`
|
||||
/// (no maximum constraint).
|
||||
pub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_get_compatible_bridge_pixel_format_abi(format, -1, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
|
||||
pub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { ffmpegutils_convert_jpeg_space_to_regular_space_abi(format, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// The process-wide filefunctions instance (lazily created; freed on
|
||||
/// process exit is not required — the singleton outlives all users).
|
||||
fn filefunctions_instance() -> OakFileFunctions {
|
||||
static FF: std::sync::OnceLock<OakFileFunctions> = std::sync::OnceLock::new();
|
||||
*FF.get_or_init(|| unsafe { oakcommon_filefunctions_init() })
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_filefunctions_get_application_path`
|
||||
/// (two-stage string getter on the shared instance).
|
||||
pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { filefunctions_get_application_path_abi(filefunctions_instance(), buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_filefunctions_get_configuration_location`.
|
||||
pub fn oakcommon_filefunctions_get_configuration_location(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
filefunctions_get_configuration_location_abi(filefunctions_instance(), buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_filefunctions_get_unique_file_identifier`
|
||||
/// (the frozen ABI writes a string; the value-style call sites expect an
|
||||
/// `i64`, so the string is folded into a stable 64-bit FNV hash).
|
||||
pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64 {
|
||||
let mut buf = [0 as c_char; 512];
|
||||
// Two-stage contract: 0 = written, positive = required size, negative
|
||||
// = error. The adapter's value-style call sites get the i64 fold.
|
||||
let n = unsafe {
|
||||
filefunctions_get_unique_file_identifier_abi(
|
||||
filefunctions_instance(),
|
||||
path,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as c_int,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
return 0;
|
||||
}
|
||||
let avail = if n == 0 { buf.len() } else { (n as usize).min(buf.len()) };
|
||||
let bytes: Vec<u8> = buf[..avail]
|
||||
.iter()
|
||||
.take_while(|&&b| b != 0)
|
||||
.map(|&b| b as u8)
|
||||
.collect();
|
||||
let mut hash: u64 = 0xcbf29ce484222325;
|
||||
for &b in &bytes {
|
||||
hash ^= b as u64;
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash as i64
|
||||
}
|
||||
|
||||
/// The process-wide oiioutils instance (lazily created).
|
||||
fn oiioutils_instance() -> OakOIIOUtils {
|
||||
static OU: std::sync::OnceLock<OakOIIOUtils> = std::sync::OnceLock::new();
|
||||
*OU.get_or_init(|| unsafe { oakcommon_oiioutils_init() })
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_oiioutils_get_oiio_base_type_from_format`.
|
||||
pub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { oiioutils_get_oiio_base_type_from_format_abi(oiioutils_instance(), format, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_oiioutils_get_format_from_oiio_basetype`.
|
||||
pub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int {
|
||||
let mut out: c_int = -1;
|
||||
unsafe { oiioutils_get_format_from_oiio_basetype_abi(oiioutils_instance(), basetype, &mut out) };
|
||||
out
|
||||
}
|
||||
|
||||
/// Value-style `oakcommon_oiioutils_get_pixel_aspect_ratio`
|
||||
/// (the frozen ABI takes the aspect ratio as `f64`; the value-style call
|
||||
/// sites pass width/height and get num/den back).
|
||||
pub fn oakcommon_oiioutils_get_pixel_aspect_ratio(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
let ratio = if height != 0 {
|
||||
f64::from(width) / f64::from(height)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
unsafe {
|
||||
oiioutils_get_pixel_aspect_ratio_abi(oiioutils_instance(), ratio, out_num, out_den)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from the other oak modules.
|
||||
//!
|
||||
//! The codec module links against oakcommon and oakrender at the C ABI.
|
||||
//! Every signature below mirrors the corresponding public header
|
||||
//! verbatim and is resolved at link time. The by-value handle structs
|
||||
//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of
|
||||
//! the `{ctx, addref, release, abi_version}` layout so the codec crate
|
||||
//! can hold and hand them across the FFI boundary without translation.
|
||||
|
||||
pub mod common;
|
||||
pub mod render;
|
||||
|
||||
// In-memory mocks for the oakcommon/oakrender C ABI so the crate links
|
||||
// and is testable under `cargo test` (where those dylibs are absent).
|
||||
// The `test-stubs` feature additionally compiles the oakcore_*/oakrender_*
|
||||
// host-mocks for consumer test binaries (e.g. oaknode's) that link this
|
||||
// crate directly — those symbols are not provided by any Rust crate.
|
||||
#[cfg(any(test, feature = "test-stubs"))]
|
||||
pub mod test_stubs;
|
||||
@@ -1,140 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI imports (display textures, renderers, cancel atoms).
|
||||
//!
|
||||
//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a
|
||||
//! `CancelAtom`; both are oakrender refcounted handles with the standard
|
||||
//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`
|
||||
//! is a flattened POD the decoders construct to describe the frame.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `OakRenderTexture` — refcounted GPU texture handle.
|
||||
pub type OakRenderTexture = CHandle;
|
||||
|
||||
/// `OakCancelAtom` — refcounted cancellation atom handle.
|
||||
pub type OakCancelAtom = CHandle;
|
||||
|
||||
/// `OakRenderRenderer` — refcounted display-renderer handle.
|
||||
pub type OakRenderRenderer = CHandle;
|
||||
|
||||
/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.
|
||||
pub type OakCodecFrame = CHandle;
|
||||
|
||||
// Refcounted opaque handles; thread-safe in the C library.
|
||||
|
||||
/// `oakrender_video_params` — flattened POD of `olive::VideoParams`
|
||||
/// passed into oakrender; see `include/render/renderer.h`.
|
||||
#[repr(C)]
|
||||
pub struct oakrender_video_params {
|
||||
/// Width in pixels.
|
||||
pub width: c_int,
|
||||
/// Height in pixels.
|
||||
pub height: c_int,
|
||||
/// Frame-duration numerator (e.g. 1001/30000 s).
|
||||
pub time_base_num: c_int,
|
||||
/// Frame-duration denominator.
|
||||
pub time_base_den: c_int,
|
||||
/// `olive::PixelFormat::Format`.
|
||||
pub format: c_int,
|
||||
/// Pixel-aspect numerator.
|
||||
pub pixel_aspect_num: c_int,
|
||||
/// Pixel-aspect denominator.
|
||||
pub pixel_aspect_den: c_int,
|
||||
/// `olive::VideoParams::Interlacing`.
|
||||
pub interlacing: c_int,
|
||||
/// `olive::VideoParams::ColorRange`.
|
||||
pub color_range: c_int,
|
||||
/// Preview-resolution divider (1 = full).
|
||||
pub divider: c_int,
|
||||
/// `olive::VideoParams::Type` (0 = video).
|
||||
pub video_type: c_int,
|
||||
/// 0/1 premultiplied alpha.
|
||||
pub premultiplied_alpha: c_int,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
/// `oakrender_cancelatom_init`.
|
||||
pub fn oakrender_cancelatom_init() -> OakCancelAtom;
|
||||
/// `oakrender_cancelatom_free` (NULL/empty no-op).
|
||||
pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);
|
||||
/// `oakrender_cancelatom_is_cancelled`.
|
||||
pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int;
|
||||
/// `oakrender_cancelatom_heard_cancel`.
|
||||
pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int;
|
||||
/// `oakrender_cancelatom_cancel`.
|
||||
pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);
|
||||
/// `oakrender_cancelatom_get_native`.
|
||||
pub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void;
|
||||
/// `oakrender_display_texture_create`.
|
||||
pub fn oakrender_display_texture_create(
|
||||
renderer: OakRenderRenderer,
|
||||
params: *const oakrender_video_params,
|
||||
data: *const c_void,
|
||||
linesize: c_int,
|
||||
) -> OakRenderTexture;
|
||||
/// `oakrender_display_texture_retain`.
|
||||
pub fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture;
|
||||
/// `oakrender_display_texture_free` (NULL/empty no-op).
|
||||
pub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);
|
||||
/// `oakrender_display_texture_upload`.
|
||||
pub fn oakrender_display_texture_upload(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_download`.
|
||||
pub fn oakrender_display_texture_download(
|
||||
texture: OakRenderTexture,
|
||||
pixels: *mut c_void,
|
||||
linesize: c_int,
|
||||
) -> c_int;
|
||||
/// `oakrender_display_texture_get_params`.
|
||||
pub fn oakrender_display_texture_get_params(
|
||||
texture: OakRenderTexture,
|
||||
out: *mut oakrender_video_params,
|
||||
) -> c_int;
|
||||
/// `oakrender_display_texture_id`.
|
||||
pub fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_is_dummy`.
|
||||
pub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_get_frame` (two-stage frame access).
|
||||
pub fn oakrender_display_texture_get_frame(
|
||||
texture: OakRenderTexture,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakrender_codec_frame_width`.
|
||||
pub fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_height`.
|
||||
pub fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_fb_format`.
|
||||
pub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_free` (NULL/empty no-op).
|
||||
pub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);
|
||||
/// `oakrender_codec_frame_allocate`.
|
||||
pub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_linesize_bytes`.
|
||||
pub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_is_allocated`.
|
||||
pub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_display_renderer_blit_color_managed`.
|
||||
pub fn oakrender_display_renderer_blit_color_managed(
|
||||
renderer: OakRenderRenderer,
|
||||
job: *const c_void,
|
||||
dst_texture: OakRenderTexture,
|
||||
params: *const oakrender_video_params,
|
||||
) -> c_int;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
//! `Unavailable`. Deterministic per-channel filenames derive from the
|
||||
//! source + target audio params.
|
||||
|
||||
use std::ffi::CString;
|
||||
use oakcommon::filefunctions::FileFunctions;
|
||||
use std::path::Path;
|
||||
|
||||
/// Conform state of one audio stream.
|
||||
@@ -184,18 +184,11 @@ fn conform_filenames(
|
||||
out
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
|
||||
fn unique_file_identifier(filename: &str) -> String {
|
||||
let c = match CString::new(filename) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
|
||||
let id = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
|
||||
};
|
||||
format!("{}", id)
|
||||
FileFunctions::new()
|
||||
.get_unique_file_identifier(filename)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// True when every conform filename already exists on disk.
|
||||
@@ -234,34 +227,23 @@ mod tests {
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
let mut h: u64 = 14695981039346656037;
|
||||
for &b in bytes {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(1099511628211);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_identifier_matches_bridge_hash() {
|
||||
// The frozen ABI returns the identifier as a string; the
|
||||
// value-style adapter folds it into an FNV-1a-64. The test stub
|
||||
// produces the decimal FNV of the path bytes, so the expected
|
||||
// value is FNV of that decimal string.
|
||||
let stub_id = fnv1a64(b"media.mp4") as i64;
|
||||
let expected = format!("{}", fnv1a64(stub_id.to_string().as_bytes()) as i64);
|
||||
assert_eq!(unique_file_identifier("media.mp4"), expected);
|
||||
fn unique_identifier_uses_file_metadata() {
|
||||
// The identifier is the 16-digit FNV-1a hex of (absolute path +
|
||||
// mtime); a missing file has no identifier.
|
||||
assert_eq!(unique_file_identifier("no-such-file.mp4"), "");
|
||||
let dir = temp_subdir("id");
|
||||
let file = std::path::Path::new(&dir).join("media.mp4");
|
||||
std::fs::write(&file, b"x").unwrap();
|
||||
let id = unique_file_identifier(&file.to_string_lossy());
|
||||
assert_eq!(id.len(), 16);
|
||||
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
// Deterministic: same input, same id.
|
||||
assert_eq!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("media.mp4")
|
||||
);
|
||||
assert_eq!(id, unique_file_identifier(&file.to_string_lossy()));
|
||||
let other = std::path::Path::new(&dir).join("other.mp4");
|
||||
std::fs::write(&other, b"y").unwrap();
|
||||
// Different input, different id.
|
||||
assert_ne!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("other.mp4")
|
||||
);
|
||||
assert_ne!(id, unique_file_identifier(&other.to_string_lossy()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,21 +261,23 @@ mod tests {
|
||||
#[test]
|
||||
fn conform_filename_derivation_and_range() {
|
||||
let m = ConformManager::instance();
|
||||
let src_dir = temp_subdir("names");
|
||||
let src = std::path::Path::new(&src_dir).join("media.mp4");
|
||||
std::fs::write(&src, b"x").unwrap();
|
||||
let cache = temp_subdir("names");
|
||||
let stub_id = fnv1a64(b"media.mp4") as i64;
|
||||
let id = fnv1a64(stub_id.to_string().as_bytes()) as i64;
|
||||
let id = unique_file_identifier(&src.to_string_lossy());
|
||||
let base = format!("{}-0.48000.0.3", id);
|
||||
let f0 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0)
|
||||
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 0)
|
||||
.unwrap();
|
||||
let f1 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 1)
|
||||
.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 1)
|
||||
.unwrap();
|
||||
assert_eq!(f0, format!("{}/{}.0.pcm", cache, base));
|
||||
assert_eq!(f1, format!("{}/{}.1.pcm", cache, base));
|
||||
// Out of range.
|
||||
assert!(matches!(
|
||||
m.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 5),
|
||||
m.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 5),
|
||||
Err(crate::error::Error::NotFound)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -26,12 +26,22 @@
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use oakcommon::cancelatom::CancelAtom;
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::bridge::render::{OakCancelAtom, OakRenderTexture};
|
||||
use crate::footagedescription::FootageDescription;
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `OakRenderTexture` — refcounted GPU texture handle (an oakrender type,
|
||||
/// opaque to oakcodec). The codec crate cannot depend on oakrender (the
|
||||
/// dependency cycle), so it only ever produces an empty handle — the
|
||||
/// shared [`crate::handle::CHandle`] carries that value unchanged.
|
||||
pub type OakRenderTexture = crate::handle::CHandle;
|
||||
|
||||
/// `OakNodeBlock` — opaque node-block handle owned elsewhere; the codec
|
||||
/// only stores and forwards it (borrowed, never dereferenced).
|
||||
pub type OakNodeBlock = crate::handle::CHandle;
|
||||
|
||||
/// `oakcodec_video_stream_info` — POD probe output describing one video
|
||||
/// stream; see `include/codec/decoder.h`.
|
||||
#[repr(C)]
|
||||
@@ -158,7 +168,7 @@ pub enum RetrieveState {
|
||||
pub struct CodecStream {
|
||||
filename: String,
|
||||
stream: i32,
|
||||
block: Option<crate::bridge::common::OakNodeBlock>,
|
||||
block: Option<OakNodeBlock>,
|
||||
}
|
||||
|
||||
impl CodecStream {
|
||||
@@ -175,7 +185,7 @@ impl CodecStream {
|
||||
pub fn with_block(
|
||||
filename: String,
|
||||
stream: i32,
|
||||
block: Option<crate::bridge::common::OakNodeBlock>,
|
||||
block: Option<OakNodeBlock>,
|
||||
) -> Self {
|
||||
CodecStream {
|
||||
filename,
|
||||
@@ -212,7 +222,7 @@ impl CodecStream {
|
||||
}
|
||||
|
||||
/// Associated timeline block (borrowed; only compared, never used).
|
||||
pub fn block(&self) -> Option<crate::bridge::common::OakNodeBlock> {
|
||||
pub fn block(&self) -> Option<OakNodeBlock> {
|
||||
self.block.clone()
|
||||
}
|
||||
}
|
||||
@@ -241,7 +251,7 @@ pub trait Decoder: Send + Sync {
|
||||
fn probe(
|
||||
&self,
|
||||
filename: &str,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> Option<FootageDescription>;
|
||||
|
||||
/// Open `stream` for decoding. Thread-safe.
|
||||
@@ -281,7 +291,7 @@ pub trait Decoder: Send + Sync {
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> crate::error::Result<()>;
|
||||
|
||||
/// Offset of the audio start relative to the video (rational seconds).
|
||||
@@ -326,7 +336,7 @@ impl Decoder for UnimplementedDecoder {
|
||||
fn probe(
|
||||
&self,
|
||||
_filename: &str,
|
||||
_cancelled: Option<&OakCancelAtom>,
|
||||
_cancelled: Option<&CancelAtom>,
|
||||
) -> Option<FootageDescription> {
|
||||
None
|
||||
}
|
||||
@@ -377,7 +387,7 @@ impl Decoder for UnimplementedDecoder {
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
_cancelled: Option<&OakCancelAtom>,
|
||||
_cancelled: Option<&CancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"decoder not yet implemented".to_string(),
|
||||
@@ -400,13 +410,13 @@ pub fn create_from_id(id: &str) -> Option<Arc<dyn Decoder>> {
|
||||
/// not injected, in which case the built-in list below is used.
|
||||
static TEST_DECODERS: OnceLock<Mutex<Vec<Arc<dyn Decoder>>>> = OnceLock::new();
|
||||
|
||||
/// Serializes every test that reads the built-in decoder registry. The ffi
|
||||
/// decoder tests inject through `crate::ffi::lock_tests()` (the shared
|
||||
/// `TEST_LOCK`), so the registry assertions below take that same lock to
|
||||
/// never race with an injected list.
|
||||
/// Serializes every test that reads the built-in decoder registry. Tests
|
||||
/// inject through [`set_test_decoders`] under [`crate::lock_tests`] (the
|
||||
/// shared test lock), so the registry assertions below take that same lock
|
||||
/// to never race with an injected list.
|
||||
#[cfg(test)]
|
||||
fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
crate::ffi::lock_tests()
|
||||
fn registry_guard() -> crate::TestLock {
|
||||
crate::lock_tests()
|
||||
}
|
||||
|
||||
/// Replace the decoder registry with `list`; pass an empty list to restore
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/conform.h` exports.
|
||||
//!
|
||||
//! Complete inventory: create_instance / destroy_instance / get_state /
|
||||
//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the
|
||||
//! states.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the
|
||||
//! singleton is absent (`!ConformManager::instance()`); the Rust
|
||||
//! [`crate::conformmanager::ConformManager::instance`] is a lazy
|
||||
//! `'static` singleton that can never be absent, so that branch cannot
|
||||
//! trigger.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::conformmanager::{ConformManager, ConformState};
|
||||
use crate::handle;
|
||||
|
||||
/// `OAKCODEC_CONFORM_EXISTS`.
|
||||
pub const OAKCODEC_CONFORM_EXISTS: c_int = 0;
|
||||
/// `OAKCODEC_CONFORM_GENERATING`.
|
||||
pub const OAKCODEC_CONFORM_GENERATING: c_int = 1;
|
||||
/// `OAKCODEC_CONFORM_UNAVAILABLE`.
|
||||
pub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;
|
||||
|
||||
/// `oakcodec_conform_create_instance`: create the singleton (always
|
||||
/// present here, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_create_instance() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let _ = ConformManager::instance();
|
||||
crate::error::OAKCODEC_OK
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust
|
||||
/// manager is stateless, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_destroy_instance() -> c_int {
|
||||
handle::guard_raw(|| crate::error::OAKCODEC_OK)
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_get_state`: query the conform state of one audio
|
||||
/// stream.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_get_state(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
wait: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
match m.get_conform_state(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
wait != 0,
|
||||
) {
|
||||
Ok(ConformState::Exists) => OAKCODEC_CONFORM_EXISTS,
|
||||
Ok(ConformState::Generating) => OAKCODEC_CONFORM_GENERATING,
|
||||
Ok(ConformState::Unavailable) | Err(_) => OAKCODEC_CONFORM_UNAVAILABLE,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_filename_count`: number of conform files for the
|
||||
/// given stream/params; 0 on invalid arguments.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_filename_count(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return 0,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return 0,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
m.get_conform_filename_count(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
) as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_filename_at`: the `index`-th conform filename
|
||||
/// (two-stage string).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_filename_at(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
match m.get_conform_filename(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
index as usize,
|
||||
) {
|
||||
Ok(f) => super::string_out(&f, buf, buf_size),
|
||||
Err(crate::error::Error::NotFound) => crate::error::OAKCODEC_E_NOT_FOUND,
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::REG_LOCK;
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_ffi_conform_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_destroy_instance_ok() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_conform_create_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_conform_destroy_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_state_maps_states() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
// No registrar and no files -> UNAVAILABLE.
|
||||
let cache = cstr(&temp_cache("state"));
|
||||
let src = cstr("media.mp4");
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);
|
||||
|
||||
// Invalid arguments -> E_INVALID.
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let empty = cstr("");
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Write the conform files -> EXISTS.
|
||||
let m = ConformManager::instance();
|
||||
for i in 0..2 {
|
||||
let f = m
|
||||
.get_conform_filename(&temp_cache("state"), "media.mp4", 0, 48000, 0x3, 0, i)
|
||||
.unwrap();
|
||||
std::fs::write(&f, b"pcm").unwrap();
|
||||
}
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_count_and_at() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let cache = cstr(&temp_cache("names"));
|
||||
let src = cstr("media.mp4");
|
||||
|
||||
// Stereo -> 2 files.
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0)
|
||||
};
|
||||
assert_eq!(rc, 2);
|
||||
|
||||
// Invalid args -> 0 (not an error).
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0)
|
||||
};
|
||||
assert_eq!(rc, 0);
|
||||
|
||||
// filename_at round-trips the deterministic name.
|
||||
let mut buf = [0i8; 512];
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_filename_at(
|
||||
cache.as_ptr(),
|
||||
src.as_ptr(),
|
||||
0,
|
||||
48000,
|
||||
0x3,
|
||||
0,
|
||||
0,
|
||||
buf.as_mut_ptr(),
|
||||
512,
|
||||
)
|
||||
};
|
||||
assert!(rc > 0);
|
||||
let name = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(name.ends_with(".0.pcm"));
|
||||
|
||||
// Out-of-range index -> E_NOT_FOUND.
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_filename_at(
|
||||
cache.as_ptr(),
|
||||
src.as_ptr(),
|
||||
0,
|
||||
48000,
|
||||
0x3,
|
||||
0,
|
||||
5,
|
||||
buf.as_mut_ptr(),
|
||||
512,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_NOT_FOUND);
|
||||
|
||||
// Invalid args -> E_INVALID.
|
||||
let rc = unsafe {
|
||||
oakcodec_conform_filename_at(
|
||||
std::ptr::null(),
|
||||
src.as_ptr(),
|
||||
0,
|
||||
48000,
|
||||
0x3,
|
||||
0,
|
||||
0,
|
||||
buf.as_mut_ptr(),
|
||||
512,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,818 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/encoder.h` exports.
|
||||
//!
|
||||
//! Complete inventory: init / free / set_video_option / open / write_video
|
||||
//! / write_audio / write_subtitle / flush / last_error /
|
||||
//! get_desired_pixel_format / export_format_get_extension /
|
||||
//! encoding_generate_matrix.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the
|
||||
//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex<EncoderBox>`
|
||||
//! and carries an extra `last_error` field because the Rust `Encoder` trait
|
||||
//! has no `get_error()` (the C++ reads the message off the encoder). The
|
||||
//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note
|
||||
//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format`
|
||||
//! are typed enums, so `to_native` converts the int fields.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakcore_rs::PixelFormat;
|
||||
|
||||
use crate::encoder::Encoder;
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::handle::{self, CHandle};
|
||||
|
||||
/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/
|
||||
/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled
|
||||
/// configuration). Field names and order mirror the header verbatim.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_encoding_params {
|
||||
pub filename: [u8; 1024],
|
||||
pub format: c_int,
|
||||
pub video_enabled: c_int,
|
||||
pub video_codec: c_int,
|
||||
pub video_width: c_int,
|
||||
pub video_height: c_int,
|
||||
pub video_time_base_num: c_int,
|
||||
pub video_time_base_den: c_int,
|
||||
pub video_pixel_format: c_int,
|
||||
pub video_interlacing: c_int,
|
||||
pub video_pixel_aspect_num: c_int,
|
||||
pub video_pixel_aspect_den: c_int,
|
||||
pub video_bit_rate: i64,
|
||||
pub video_min_bit_rate: i64,
|
||||
pub video_max_bit_rate: i64,
|
||||
pub video_buffer_size: i64,
|
||||
pub video_threads: c_int,
|
||||
pub video_pix_fmt: [u8; 64],
|
||||
pub video_is_image_sequence: c_int,
|
||||
pub video_scaling_method: c_int,
|
||||
pub audio_enabled: c_int,
|
||||
pub audio_codec: c_int,
|
||||
pub audio_sample_rate: c_int,
|
||||
pub audio_channel_layout: u64,
|
||||
pub audio_sample_format: c_int,
|
||||
pub audio_bit_rate: i64,
|
||||
pub subtitles_enabled: c_int,
|
||||
pub subtitles_codec: c_int,
|
||||
pub subtitles_are_sidecar: c_int,
|
||||
pub subtitles_sidecar_format: c_int,
|
||||
pub color_transform_output: [u8; 256],
|
||||
pub export_length_num: c_int,
|
||||
pub export_length_den: c_int,
|
||||
pub has_custom_range: c_int,
|
||||
pub custom_range_in_num: i64,
|
||||
pub custom_range_in_den: i64,
|
||||
pub custom_range_out_num: i64,
|
||||
pub custom_range_out_den: i64,
|
||||
}
|
||||
|
||||
/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`).
|
||||
struct EncoderBox {
|
||||
encoder: Option<Arc<dyn Encoder>>,
|
||||
params: EncodingParams,
|
||||
/// Per-codec video options set via `oakcodec_encoder_set_video_option`
|
||||
/// between init and open. Kept here (not in [`EncodingParams`], which
|
||||
/// is a byte-exact mirror of the C POD).
|
||||
video_opts: Vec<(String, String)>,
|
||||
open: bool,
|
||||
flushed: bool,
|
||||
/// Last error detail (the C++ reads it off the encoder's `get_error`).
|
||||
last_error: String,
|
||||
}
|
||||
|
||||
/// Convert an `OakPixelFormat` int code to a [`PixelFormat`].
|
||||
fn pixel_format_from_i32(v: c_int) -> PixelFormat {
|
||||
match v {
|
||||
0 => PixelFormat::U8,
|
||||
1 => PixelFormat::U10,
|
||||
2 => PixelFormat::U16,
|
||||
3 => PixelFormat::F16,
|
||||
4 => PixelFormat::F32,
|
||||
_ => PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten the C POD into the crate's [`EncodingParams`]
|
||||
/// (`to_native` in `c_api/encoder.cpp`).
|
||||
fn to_native(p: &oakcodec_encoding_params) -> EncodingParams {
|
||||
let mut n = EncodingParams::default();
|
||||
n.filename = p.filename;
|
||||
n.format = p.format;
|
||||
|
||||
n.video_enabled = p.video_enabled;
|
||||
n.video_codec = p.video_codec;
|
||||
n.video_width = p.video_width;
|
||||
n.video_height = p.video_height;
|
||||
n.video_time_base_num = p.video_time_base_num;
|
||||
n.video_time_base_den = p.video_time_base_den;
|
||||
n.video_pixel_format = pixel_format_from_i32(p.video_pixel_format);
|
||||
n.video_interlacing = p.video_interlacing;
|
||||
n.video_pixel_aspect_num = p.video_pixel_aspect_num;
|
||||
n.video_pixel_aspect_den = p.video_pixel_aspect_den;
|
||||
n.video_bit_rate = p.video_bit_rate;
|
||||
n.video_min_bit_rate = p.video_min_bit_rate;
|
||||
n.video_max_bit_rate = p.video_max_bit_rate;
|
||||
n.video_buffer_size = p.video_buffer_size;
|
||||
n.video_threads = p.video_threads;
|
||||
n.video_pix_fmt = p.video_pix_fmt;
|
||||
n.video_is_image_sequence = p.video_is_image_sequence;
|
||||
n.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method);
|
||||
|
||||
n.audio_enabled = p.audio_enabled;
|
||||
n.audio_codec = p.audio_codec;
|
||||
n.audio_sample_rate = p.audio_sample_rate;
|
||||
n.audio_channel_layout = p.audio_channel_layout;
|
||||
n.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format);
|
||||
n.audio_bit_rate = p.audio_bit_rate;
|
||||
|
||||
n.subtitles_enabled = p.subtitles_enabled;
|
||||
n.subtitles_codec = p.subtitles_codec;
|
||||
n.subtitles_are_sidecar = p.subtitles_are_sidecar;
|
||||
n.subtitles_sidecar_format = p.subtitles_sidecar_format;
|
||||
|
||||
n.color_transform_output = p.color_transform_output;
|
||||
n.export_length_num = p.export_length_num;
|
||||
n.export_length_den = p.export_length_den;
|
||||
n.has_custom_range = p.has_custom_range;
|
||||
n.custom_range_in_num = p.custom_range_in_num;
|
||||
n.custom_range_in_den = p.custom_range_in_den;
|
||||
n.custom_range_out_num = p.custom_range_out_num;
|
||||
n.custom_range_out_den = p.custom_range_out_den;
|
||||
n
|
||||
}
|
||||
|
||||
/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks
|
||||
/// only that at least one track is enabled.
|
||||
fn is_valid(p: &EncodingParams) -> bool {
|
||||
p.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_init`: create an encoder for `params` (count 1);
|
||||
/// empty handle when the configuration is invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -> CHandle {
|
||||
handle::guard_handle(|| {
|
||||
if params.is_null() {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
let native = to_native(unsafe { &*params });
|
||||
if !is_valid(&native) {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
Ok(handle::make_owned(Mutex::new(EncoderBox {
|
||||
encoder: None,
|
||||
params: native,
|
||||
video_opts: Vec::new(),
|
||||
open: false,
|
||||
flushed: false,
|
||||
last_error: String::new(),
|
||||
})))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
handle::guard_void(|| super::free_handle(encoder));
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_set_video_option`: set a per-codec video option
|
||||
/// (e.g. "crf"); only valid between init and open.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
|
||||
encoder: CHandle,
|
||||
key: *const c_char,
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let key = match crate::ffi::c_str(key) {
|
||||
Some(k) => k,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let mut b = b.lock().unwrap();
|
||||
if b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let value = crate::ffi::c_str(value).unwrap_or_default();
|
||||
// `EncodingParams::set_video_option` replaces an existing key.
|
||||
b.video_opts.retain(|(k, _)| k != &key);
|
||||
b.video_opts.push((key, value));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_open`: create the encoder for the configured params,
|
||||
/// apply the video options and open the output.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ `open()` calls `create_from_params` (which applies the options
|
||||
/// internally) then `open()`. The Rust trait separates `configure`, so it
|
||||
/// is invoked between the two; the box's `video_opts` (set between init
|
||||
/// and open) are stored for future wiring but not yet passed to
|
||||
/// `configure` (no trait channel carries them in the interim).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let mut b = b.lock().unwrap();
|
||||
if b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let e = match crate::encoder::create_from_params(&b.params) {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
b.last_error = "failed to create encoder".to_string();
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to create encoder".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if e.configure(&b.params).is_err() {
|
||||
b.last_error = "failed to configure encoder".to_string();
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to configure encoder".to_string(),
|
||||
));
|
||||
}
|
||||
if e.open().is_err() {
|
||||
b.last_error = "failed to open stream".to_string();
|
||||
return Err(crate::error::Error::Failed(
|
||||
"failed to open stream".to_string(),
|
||||
));
|
||||
}
|
||||
b.encoder = Some(e);
|
||||
b.open = true;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_video`: encode one video frame.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = super::get_box::<Mutex<crate::frame::Frame>>(&frame)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let e = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
b.encoder.as_ref().unwrap().clone()
|
||||
};
|
||||
let f = f.lock().unwrap();
|
||||
e.write_video(&f)
|
||||
.map_err(|_| crate::error::Error::Failed("write_video failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_audio(
|
||||
encoder: CHandle,
|
||||
samples: *const f32,
|
||||
frame_count: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
if (samples.is_null() && frame_count > 0) || frame_count < 0 {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let (slice, enc) = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let channels = b.params.audio_channel_layout.count_ones();
|
||||
if channels == 0 {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let sample_count = (frame_count as usize).wrapping_mul(channels as usize);
|
||||
let slice: &[f32] = if samples.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
// SAFETY: the caller guarantees `samples` holds
|
||||
// `frame_count * channels` floats.
|
||||
unsafe { std::slice::from_raw_parts(samples, sample_count) }
|
||||
};
|
||||
let e = b.encoder.as_ref().unwrap().clone();
|
||||
(slice, e)
|
||||
};
|
||||
enc.write_audio(slice, frame_count)
|
||||
.map_err(|_| crate::error::Error::Failed("write_audio failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
|
||||
encoder: CHandle,
|
||||
text: *const c_char,
|
||||
in_seconds: f64,
|
||||
out_seconds: f64,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let text = match crate::ffi::c_str(text) {
|
||||
Some(t) => t,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let e = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
b.encoder.as_ref().unwrap().clone()
|
||||
};
|
||||
e.write_subtitle(&text, in_seconds, out_seconds)
|
||||
.map_err(|_| crate::error::Error::Failed("write_subtitle failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and
|
||||
/// close the file. Idempotent.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_flush(encoder: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b =
|
||||
super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let mut b = b.lock().unwrap();
|
||||
if !b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
if b.flushed {
|
||||
return Ok(());
|
||||
}
|
||||
let e = b.encoder.as_ref().unwrap().clone();
|
||||
// The C++ ignores the close() result; the Rust interim surfaces it.
|
||||
e.close()
|
||||
.map_err(|_| crate::error::Error::Failed("close failed".to_string()))?;
|
||||
b.flushed = true;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_last_error` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_last_error(
|
||||
encoder: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match super::get_box::<Mutex<EncoderBox>>(&encoder) {
|
||||
Some(b) => {
|
||||
let b = b.lock().unwrap();
|
||||
super::string_out(&b.last_error, buf, buf_size)
|
||||
}
|
||||
None => super::string_out("", buf, buf_size),
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the
|
||||
/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for
|
||||
/// an empty/invalid encoder.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let b = match super::get_box::<Mutex<EncoderBox>>(&encoder) {
|
||||
Some(b) => b,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let e = match &b.lock().unwrap().encoder {
|
||||
Some(e) => e.clone(),
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match e.desired_pixel_format() {
|
||||
Some(p) => p as c_int,
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_export_format_get_extension` (two-stage); unknown formats
|
||||
/// yield the empty string.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_export_format_get_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let ext = match crate::exportformat::Format::from_i32(format) {
|
||||
Some(f) => crate::exportformat::Format::get_extension(f),
|
||||
None => String::new(),
|
||||
};
|
||||
super::string_out(&ext, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling
|
||||
/// method, row-major 4x4 `double` into `out_matrix[16]`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_generate_matrix(
|
||||
method: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
dst_width: c_int,
|
||||
dst_height: c_int,
|
||||
out_matrix: *mut f64,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out_matrix.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let mut m = [0.0f64; 16];
|
||||
crate::encodingparams::EncodingParams::generate_matrix(
|
||||
crate::encodingparams::scaling_from_i32(method),
|
||||
src_width,
|
||||
src_height,
|
||||
dst_width,
|
||||
dst_height,
|
||||
&mut m,
|
||||
);
|
||||
// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.
|
||||
unsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
use crate::encoder::set_test_encoders;
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
|
||||
use crate::ffi::frame::{
|
||||
oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params,
|
||||
};
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn zeroed_params() -> oakcodec_encoding_params {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
fn valid_params() -> oakcodec_encoding_params {
|
||||
let mut p = zeroed_params();
|
||||
p.filename = {
|
||||
let mut f = [0u8; 1024];
|
||||
let name = b"out.mp4";
|
||||
f[..name.len()].copy_from_slice(name);
|
||||
f
|
||||
};
|
||||
p.format = 2; // MPEG-4
|
||||
p.video_enabled = 1;
|
||||
p.video_codec = 3;
|
||||
p.video_width = 1920;
|
||||
p.video_height = 1080;
|
||||
p.video_time_base_num = 1;
|
||||
p.video_time_base_den = 30;
|
||||
p.video_pixel_format = 0; // U8
|
||||
p.audio_enabled = 1;
|
||||
p.audio_codec = 4;
|
||||
p.audio_sample_rate = 48000;
|
||||
p.audio_channel_layout = 0x3;
|
||||
p.audio_sample_format = 10; // f32 packed
|
||||
p
|
||||
}
|
||||
|
||||
/// Fake encoder that accepts every operation.
|
||||
struct FakeEncoder {
|
||||
id: &'static str,
|
||||
}
|
||||
|
||||
impl Encoder for FakeEncoder {
|
||||
fn id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
fn supports_video(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_audio(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_subtitles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn configure(&self, _p: &EncodingParams) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn open(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_subtitle(
|
||||
&self,
|
||||
_text: &str,
|
||||
_in_seconds: f64,
|
||||
_out_seconds: f64,
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn flush(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn desired_pixel_format(&self) -> Option<PixelFormat> {
|
||||
Some(PixelFormat::U8)
|
||||
}
|
||||
fn desired_sample_format(&self) -> Option<oakcore_rs::SampleFormat> {
|
||||
None
|
||||
}
|
||||
fn filename(&self) -> String {
|
||||
"out.mp4".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_open_write_flush_golden() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
|
||||
// set_video_option between init and open.
|
||||
let key = cstr("crf");
|
||||
let val = cstr("18");
|
||||
let rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
let rc = unsafe { oakcodec_encoder_open(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_video with a real frame handle.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(16, 16, 0, 4, 1, 1, 0, 1) };
|
||||
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_allocate(fh) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_audio: stereo interleaved floats.
|
||||
let mut samples = [0f32; 64];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_subtitle.
|
||||
let text = cstr("hello");
|
||||
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// desired pixel format from the fake.
|
||||
assert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8
|
||||
|
||||
// flush is idempotent.
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// Writes after flush -> E_STATE.
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut fh) };
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
assert!(h.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_invalid_config_and_null_params() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(Vec::new());
|
||||
|
||||
// NULL params -> empty handle.
|
||||
let mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) };
|
||||
assert!(h.is_null());
|
||||
|
||||
// All tracks disabled -> empty handle (is_valid).
|
||||
let p = zeroed_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(h.is_null());
|
||||
|
||||
// Audio-only config is valid.
|
||||
let mut p = zeroed_params();
|
||||
p.format = 7; // WAV
|
||||
p.audio_enabled = 1;
|
||||
p.audio_sample_rate = 44100;
|
||||
p.audio_channel_layout = 0x4;
|
||||
p.audio_sample_format = 10;
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_errors_and_state_machine() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
// Production path: an unknown export format cannot create an encoder.
|
||||
set_test_encoders(Vec::new());
|
||||
|
||||
let mut p = valid_params();
|
||||
p.format = 99; // unknown format -> create_from_params returns None
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
|
||||
let rc = unsafe { oakcodec_encoder_open(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_E_FAILED);
|
||||
let mut err = [0i8; 128];
|
||||
unsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) };
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(err.as_ptr()).as_deref(),
|
||||
Some("failed to create encoder")
|
||||
);
|
||||
|
||||
// Empty handle -> E_INVALID; last_error empty.
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoder_set_video_option(empty, cstr("crf").as_ptr(), cstr("18").as_ptr())
|
||||
},
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoder_get_desired_pixel_format(empty) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
let rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) };
|
||||
assert_eq!(rc, 1);
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(""));
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_and_argument_errors_with_fake() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
|
||||
// set_video_option with a NULL key -> E_INVALID.
|
||||
let rc =
|
||||
unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Writes before open -> E_STATE.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(4, 4, 0, 4, 1, 1, 0, 1) };
|
||||
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
// Null samples with a positive count is an argument error (checked
|
||||
// before the state, matching the C++ validation order).
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
// Valid args before open -> E_STATE.
|
||||
let mut pre = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut fh) };
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_audio_argument_validation() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoder_open(h) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
|
||||
// NULL samples with a positive frame count -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Negative frame count -> E_INVALID.
|
||||
let samples = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_audio_zero_channels_is_state() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
// Audio enabled but empty channel layout -> E_STATE at write time.
|
||||
let mut p = zeroed_params();
|
||||
p.format = 7;
|
||||
p.audio_enabled = 1;
|
||||
p.audio_sample_rate = 44100;
|
||||
p.audio_channel_layout = 0;
|
||||
p.audio_sample_format = 10;
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoder_open(h) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
let mut samples = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_format_extension_and_generate_matrix() {
|
||||
let mut buf = [0i8; 64];
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 4); // "mp4" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp4"));
|
||||
|
||||
// Unknown format -> empty string (size 1 for the NUL).
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 1);
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(""));
|
||||
|
||||
// Truncation rule: small buffer writes buf_size-1 chars + NUL.
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) };
|
||||
assert_eq!(rc, 4); // required size unchanged
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp"));
|
||||
|
||||
// generate_matrix: Stretch (1) is the identity.
|
||||
let mut m = [9.0f64; 16];
|
||||
let rc =
|
||||
unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(m[0], 1.0);
|
||||
assert_eq!(m[5], 1.0);
|
||||
assert_eq!(m[10], 1.0);
|
||||
assert_eq!(m[15], 1.0);
|
||||
|
||||
// Fit (0) with a square source into a 2:1 destination scales x.
|
||||
let mut m = [0.0f64; 16];
|
||||
let rc =
|
||||
unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert!((m[0] - 0.5).abs() < 1e-9);
|
||||
assert_eq!(m[5], 1.0);
|
||||
|
||||
// NULL out_matrix -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/format.h` exports.
|
||||
//!
|
||||
//! Complete inventory: format_count / format_name / format_extension /
|
||||
//! format_{video,audio,subtitle}_codec_{count,at} / codec_name /
|
||||
//! codec_is_still_image / codec_is_lossless / pix_fmt_{count,at,index} /
|
||||
//! sample_format_{count,at} / filename_contains_digit_placeholder /
|
||||
//! image_sequence_digit_count / filename_remove_digit_placeholder.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/format.cpp` mirrors the facade (oakengine/encoding.h)
|
||||
//! against the `olive::ExportFormat` / `olive::ExportCodec` / `olive::Encoder`
|
||||
//! statics. The Rust tables live in [`crate::exportformat`] /
|
||||
//! [`crate::exportcodec`]; the encoder pixel-format query is bridge-dependent
|
||||
//! on the C++ side and returns empty here (see
|
||||
//! `Format::get_pixel_formats_for_codec`), so `oakcodec_encoding_pix_fmt_*`
|
||||
//! report 0/`E_NOT_FOUND`/0 (the preferred-format fallback) like the C++
|
||||
//! base `Encoder` default. The filename helpers mirror the `Encoder`
|
||||
//! statics in [`crate::encoder`].
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
|
||||
use crate::exportcodec::Codec;
|
||||
use crate::exportformat::Format;
|
||||
use crate::handle;
|
||||
|
||||
/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_count() -> c_int {
|
||||
handle::guard_raw(|| Format::Count as c_int)
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_name` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_name(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => super::string_out(&Format::get_name(f), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_extension` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => super::string_out(&Format::get_extension(f), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_video_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_video_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_video_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_video_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_audio_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_audio_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_audio_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_audio_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_subtitle_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_subtitle_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_subtitle_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_subtitle_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_name` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_name(
|
||||
codec: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => super::string_out(&Codec::get_codec_name(c), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => Codec::is_codec_a_still_image(c) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => Codec::is_codec_lossless(c) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_count`.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ side instantiates the format's encoder and asks it for the codec's
|
||||
/// pixel formats; the Rust table is empty (see [`Format::get_pixel_formats_for_codec`]),
|
||||
/// so the count is 0 — the same as the C++ base `Encoder` default and the
|
||||
/// C++ result for encoder-less codecs.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
Format::get_pixel_formats_for_codec(f, c).len() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_at` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_pixel_formats_for_codec(f, c);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
// Interim: the Rust list carries no names yet, so this arm is
|
||||
// unreachable while the list is empty (C++ queries the FFmpeg bridge).
|
||||
super::string_out(&list[index as usize].to_string(), buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_index` — index of `pix_fmt` in `codec`'s
|
||||
/// supported pixel formats; 0 (the preferred format) for an invalid codec,
|
||||
/// a NULL/empty `pix_fmt`, or when not found.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ side searches the FFmpeg encoder's list. The Rust table is empty,
|
||||
/// so every lookup falls back to 0 — the documented behavior for "absent".
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_index(
|
||||
codec: c_int,
|
||||
pix_fmt: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
if Codec::from_i32(codec).is_none() {
|
||||
return 0;
|
||||
}
|
||||
match crate::ffi::c_str(pix_fmt) {
|
||||
Some(s) if !s.is_empty() => {
|
||||
// Interim: empty table (see module doc) -> preferred index 0.
|
||||
let _ = s;
|
||||
0
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_sample_format_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_sample_format_count(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
Format::get_sample_formats_for_codec(f, c).len() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_sample_format_at` — an
|
||||
/// `olive::core::SampleFormat::Format` value.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_sample_format_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_sample_formats_for_codec(f, c);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_filename_contains_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => crate::encoder::filename_contains_digit_placeholder(&f) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_image_sequence_digit_count(
|
||||
filename: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => crate::encoder::image_sequence_placeholder_digit_count(&f),
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => super::string_out(
|
||||
&crate::encoder::filename_remove_digit_placeholder(&f),
|
||||
buf,
|
||||
buf_size,
|
||||
),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_metadata_exports() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
// Count matches the 15-entry table (0..=14, Count = 15).
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_count() }, 15);
|
||||
|
||||
// Matroska (1): "Matroska Video" / "mkv".
|
||||
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 15); // "Matroska Video" (14) + NUL
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some("Matroska Video")
|
||||
);
|
||||
let rc = unsafe { oakcodec_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 4); // "mkv" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mkv"));
|
||||
|
||||
// Truncation rule on a two-stage getter.
|
||||
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 4) };
|
||||
assert_eq!(rc, 15); // required size unchanged
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Mat"));
|
||||
|
||||
// Invalid format -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_name(-1, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_extension(15, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
); // Count is not a real format
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_extension(99, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_codec_lists_exports() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
// MPEG-4 video (2) carries H.264/H.264RGB/H.265.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(2) }, 3);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, 0) },
|
||||
1 // H.264
|
||||
);
|
||||
// WAV (7) has no video codecs but PCM (13) audio.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(7) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(7) }, 1);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_audio_codec_at(7, 0) },
|
||||
13 // PCM
|
||||
);
|
||||
// SRT (13): subtitle-only, with the SRT (17) codec.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(13) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_count(13) },
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
|
||||
17 // SRT
|
||||
);
|
||||
|
||||
// Failure paths.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_count(-1) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, -1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, 3) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_audio_codec_at(7, 1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_metadata_exports() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
let rc = unsafe { oakcodec_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 6); // "H.264" (5) + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("H.264"));
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_codec_name(-1, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
|
||||
// Still images: PNG (5) yes, H.264 (1) no.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(5) }, 1);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(1) }, 0);
|
||||
// Lossless: PCM (13) yes, AAC (12) no.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(13) }, 1);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(12) }, 0);
|
||||
// Invalid codec -> 0 (not an error) for both flags.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(99) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(99) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_and_sample_format_exports() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
// Bad arguments -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_count(-1, 1) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_count(2, 99) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
// Interim: the Rust pixel-format table is empty, so the count is 0
|
||||
// and any index is E_NOT_FOUND (see the module doc).
|
||||
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_count(2, 1) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
// pix_fmt_index: absent/empty/NULL/invalid codec all yield 0.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_index(1, cstr("yuv420p").as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_index(1, std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_index(99, cstr("yuv420p").as_ptr()) },
|
||||
0
|
||||
);
|
||||
|
||||
// PCM (13) in WAV (7) exposes its native sample formats.
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(7, 13) }, 6);
|
||||
// f32 packed = 10 (oakcore SampleFormat values match the C++).
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_at(7, 13, 4) }, 10);
|
||||
// Out-of-range index -> E_NOT_FOUND; bad args -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_sample_format_at(7, 13, 6) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_sample_format_at(-1, 13, 0) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
// Non-PCM codecs query the bridge on the C++ side; empty here.
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(2, 12) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_helper_exports() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let mut buf = [0i8; 128];
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_filename_contains_digit_placeholder(
|
||||
cstr("/tmp/out_[#####].png").as_ptr(),
|
||||
)
|
||||
},
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out.png").as_ptr())
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr())
|
||||
},
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out.png").as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) },
|
||||
0
|
||||
);
|
||||
|
||||
let rc = unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
cstr("/tmp/out_[#####].png").as_ptr(),
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, 13); // "/tmp/out.png" (12) + NUL
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some("/tmp/out.png")
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
std::ptr::null(),
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
},
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/frame.h` exports.
|
||||
//!
|
||||
//! Complete inventory: frame_init / init_with_params / free / get_params /
|
||||
//! set_params / allocate / is_allocated / data / const_data /
|
||||
//! allocated_size / linesize_bytes / linesize_pixels / width / height /
|
||||
//! format / channel_count / get_timestamp / set_timestamp /
|
||||
//! debug_alive_count.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an
|
||||
//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may
|
||||
//! alias the decoder's internal cache. The Rust equivalent boxes
|
||||
//! `Mutex<Frame>`; a decode that hands out a still-shared `Arc<Frame>`
|
||||
//! therefore cannot be aliased here and reports an empty handle instead
|
||||
//! (see `ffi::decoder`).
|
||||
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
use crate::bridge::common::OakVideoParams;
|
||||
use crate::frame::Frame;
|
||||
use crate::handle::{self, CHandle};
|
||||
|
||||
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`).
|
||||
const OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1;
|
||||
|
||||
/// `oakcodec_frame_init`: new frame with default (invalid) params,
|
||||
/// refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle {
|
||||
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new()))))
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params`
|
||||
/// (the handle is addref'd internally); buffer unallocated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_init_with_params(params: OakVideoParams) -> CHandle {
|
||||
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::with_params(params)))))
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_free(frame: *mut CHandle) {
|
||||
handle::guard_void(|| super::free_handle(frame));
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_get_params`: copy of the frame's parameter set.
|
||||
///
|
||||
/// The copy is addref'd: the caller must release it with
|
||||
/// `oakcommon_videoparams_free` (see the header contract). Test-stub
|
||||
/// handles carry no `addref`, so the caller must not free them.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_get_params(
|
||||
frame: CHandle,
|
||||
out: *mut OakVideoParams,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = f.lock().unwrap();
|
||||
let p = f.params().cloned().ok_or(crate::error::Error::Invalid)?;
|
||||
crate::frame::params_addref(&p);
|
||||
// SAFETY: the caller guarantees `out` points to a writable
|
||||
// `OakVideoParams`.
|
||||
unsafe { *out = p };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_set_params`: replace the parameter set (addref'd
|
||||
/// internally); recomputes line sizes, does not reallocate.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_set_params(
|
||||
frame: CHandle,
|
||||
params: OakVideoParams,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock().unwrap().set_params(params);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current
|
||||
/// params; `OAKCODEC_E_STATE` when the params are invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_allocate(frame: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock().unwrap().allocate()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_is_allocated(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
let f = f.lock().unwrap();
|
||||
if f.is_allocated() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_data`: writable pixel buffer, NULL when
|
||||
/// unallocated/empty.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(&frame) })) {
|
||||
Ok(p) => p,
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_const_data(frame: CHandle) -> *const c_void {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe {
|
||||
frame_const_data_inner(&frame)
|
||||
})) {
|
||||
Ok(p) => p,
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn frame_data_inner(frame: &CHandle) -> *mut c_void {
|
||||
let f = match super::get_box::<Mutex<Frame>>(frame) {
|
||||
Some(f) => f,
|
||||
None => return std::ptr::null_mut(),
|
||||
};
|
||||
match f.lock().unwrap().data_mut() {
|
||||
Some(d) => d.as_mut_ptr() as *mut c_void,
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn frame_const_data_inner(frame: &CHandle) -> *const c_void {
|
||||
let f = match super::get_box::<Mutex<Frame>>(frame) {
|
||||
Some(f) => f,
|
||||
None => return std::ptr::null(),
|
||||
};
|
||||
match f.lock().unwrap().data() {
|
||||
Some(d) => d.as_ptr() as *const c_void,
|
||||
None => std::ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes
|
||||
/// (0 when unallocated).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_allocated_size(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().allocated_size() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes
|
||||
/// (0 when params are unset).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_linesize_bytes(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().linesize_bytes()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_linesize_pixels(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().linesize_pixels()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_width`: frame width (0 when params are empty).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_width(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().width()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_height`: frame height (0 when params are empty).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_height(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().height()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value;
|
||||
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_format(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return OAKCOMMON_PIXEL_FORMAT_INVALID,
|
||||
};
|
||||
f.lock().unwrap().format() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_channel_count`: plane channel count of the params
|
||||
/// format (0 on an empty handle).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_channel_count(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().channel_count()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number
|
||||
/// of seconds, written through `numerator`/`denominator`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_get_timestamp(
|
||||
frame: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if numerator.is_null() || denominator.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = f.lock().unwrap();
|
||||
let ts = f.timestamp();
|
||||
// SAFETY: both pointers were range-checked above.
|
||||
unsafe {
|
||||
*numerator = ts.numerator() as c_int;
|
||||
*denominator = ts.denominator() as c_int;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_set_timestamp`: replace the frame timestamp.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_set_timestamp(
|
||||
frame: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock()
|
||||
.unwrap()
|
||||
.set_timestamp(Rational::new(numerator as i64, denominator as i64));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_debug_alive_count`: number of live boxed handle objects
|
||||
/// across all families (see `crate::handle::alive_count`).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_debug_alive_count() -> c_int {
|
||||
handle::guard_raw(handle::alive_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
|
||||
oakcommon_videoparams_init_basic,
|
||||
};
|
||||
use crate::error::OAKCODEC_E_INVALID;
|
||||
|
||||
#[test]
|
||||
fn frame_lifecycle_golden() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) };
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert!(!h.is_null());
|
||||
// init -> exactly one more live box.
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
|
||||
// get_params round-trips width/height through the stub.
|
||||
let mut out = empty_params();
|
||||
let rc = unsafe { oakcodec_frame_get_params(h, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
// NOTE: stub handles carry no addref and `oakcommon_videoparams_free`
|
||||
// would drop the shared box; the test keeps the copy alive for the
|
||||
// frame's lifetime and does not free it.
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50);
|
||||
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
|
||||
|
||||
let rc = unsafe { oakcodec_frame_allocate(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1);
|
||||
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
|
||||
// U8 RGBA: 100px -> 4*128 bytes linesize.
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
|
||||
|
||||
// set_timestamp round-trip.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_set_timestamp(h, 1, 30) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
let (mut num, mut den) = (0, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_get_timestamp(h, &mut num, &mut den) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
assert_eq!((num, den), (1, 30));
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
assert!(h.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_errors_and_empty_handles() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(unsafe { oakcodec_frame_width(empty) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_format(empty) },
|
||||
OAKCOMMON_PIXEL_FORMAT_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_allocate(empty) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
|
||||
// init_basic(0, 0) is not valid -> allocate rejects with E_STATE.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(0, 0, 0, 4, 1, 1, 0, 1) };
|
||||
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_allocate(h) },
|
||||
crate::error::OAKCODEC_E_STATE
|
||||
);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_null_and_empty_are_noops() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = handle::alive_count();
|
||||
unsafe { oakcodec_frame_free(std::ptr::null_mut()) };
|
||||
let mut empty = CHandle::null();
|
||||
unsafe { oakcodec_frame_free(&mut empty) };
|
||||
assert!(empty.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_set_params_and_query_helpers() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = handle::alive_count();
|
||||
|
||||
// Bare init (no params): invalid params, not allocated. The stub's
|
||||
// default MockParams carries format 0 (U8); the empty-handle -1
|
||||
// case is covered in `frame_errors_and_empty_handles`.
|
||||
let mut h = unsafe { oakcodec_frame_init() };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
|
||||
assert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null());
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_allocate(h) },
|
||||
crate::error::OAKCODEC_E_STATE
|
||||
);
|
||||
|
||||
// set_params replaces the parameter set and recomputes line sizes.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(100, 50, 0, 4, 1, 1, 0, 1) };
|
||||
let rc = unsafe { oakcodec_frame_set_params(h, params) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
|
||||
assert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8
|
||||
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128);
|
||||
|
||||
// allocate -> data and const_data point at the buffer.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_allocate(h) },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
|
||||
assert!(!unsafe { oakcodec_frame_const_data(h) }.is_null());
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
|
||||
|
||||
// get_timestamp rejects NULL out pointers.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
|
||||
// debug_alive_count reports the live boxes (>= our own).
|
||||
assert!(unsafe { oakcodec_debug_alive_count() } >= before + 1);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
fn empty_params() -> OakVideoParams {
|
||||
OakVideoParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI export layer: implements `include/codec/*.h` verbatim.
|
||||
//!
|
||||
//! Organization: one submodule per public header (frame / decoder /
|
||||
//! encoder / format / conform / proxy / task). The authoritative function
|
||||
//! list is the header itself; each export only unwraps handles, calls the
|
||||
//! safe Rust domains, and maps results through [`crate::handle::guard*`].
|
||||
//! `include/codec/error.h` exports macros only, so it is folded into the
|
||||
//! preamble below instead of getting its own submodule.
|
||||
//!
|
||||
//! Shared helpers live here: the two-stage string convention
|
||||
//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle
|
||||
//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers.
|
||||
|
||||
/// `include/codec/error.h` — macros only, no exported functions.
|
||||
///
|
||||
/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as
|
||||
/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in
|
||||
/// [`crate::handle`].
|
||||
pub mod conform;
|
||||
pub mod decoder;
|
||||
pub mod encoder;
|
||||
pub mod format;
|
||||
pub mod frame;
|
||||
pub mod proxy;
|
||||
pub mod task;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// Serializes every ffi unit test: they share the global handle ALIVE
|
||||
/// counter, the injected decoder/encoder registries and the probe error,
|
||||
/// so exact `alive_count` assertions and registry injection require
|
||||
/// serial execution. Held poison-tolerant (`into_inner`) so one failing
|
||||
/// test cannot cascade-fail the rest.
|
||||
#[cfg(test)]
|
||||
pub(crate) static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Poison-tolerant lock helper for the ffi tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lock_tests() -> std::sync::MutexGuard<'static, ()> {
|
||||
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`).
|
||||
///
|
||||
/// Returns the required buffer size including the trailing NUL; when
|
||||
/// `buf` is non-NULL and `buf_size > 0` the string is copied truncated to
|
||||
/// `buf_size - 1` bytes and NUL-terminated.
|
||||
pub(crate) fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let need = s.len() as c_int + 1;
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let n = (s.len() as c_int).min(buf_size - 1);
|
||||
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);
|
||||
*buf.add(n as usize) = 0;
|
||||
}
|
||||
}
|
||||
need
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated C string; `None` on NULL pointers.
|
||||
pub(crate) fn c_str(ptr: *const c_char) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `ptr` must be a valid NUL-terminated C string by contract.
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(ptr) };
|
||||
Some(s.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Release a handle in place and null its `ctx` (`free_handle` in
|
||||
/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops.
|
||||
pub(crate) fn free_handle(h: *mut CHandle) {
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
let handle = unsafe { &mut *h };
|
||||
if handle.ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = handle.release {
|
||||
// SAFETY: `release` targets the box behind `ctx`.
|
||||
unsafe { release(handle.ctx) };
|
||||
}
|
||||
handle.ctx = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
/// Safe view into a handle's boxed value; `None` for empty handles.
|
||||
///
|
||||
/// Thin wrapper over [`crate::handle::get`] so the export bodies can call
|
||||
/// it without `unsafe` blocks everywhere.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type; each export asserts it via the handle
|
||||
/// contract (the same typed box is used by its `make_owned` call).
|
||||
pub(crate) fn get_box<T: 'static>(h: &CHandle) -> Option<&T> {
|
||||
unsafe { crate::handle::get::<T>(h) }
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/proxy.h` exports.
|
||||
//!
|
||||
//! Complete inventory: create_instance / destroy_instance / params_default
|
||||
//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename
|
||||
//! / get_working_filename / get_or_start / find_ffmpeg.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored
|
||||
//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs
|
||||
//! are still defined here (rather than reused) so the ffi layer never
|
||||
//! depends on the crate-internal type's layout. A NULL `params` maps to
|
||||
//! the crate's compiled-in defaults (`ProxyParams::default`), matching
|
||||
//! `to_native` in `c_api/proxy.cpp`.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle;
|
||||
use crate::proxymanager::{ProxyManager, ProxyParams, ProxyState};
|
||||
|
||||
/// `OAKCODEC_PROXY_STATE_MISSING`.
|
||||
pub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0;
|
||||
/// `OAKCODEC_PROXY_STATE_GENERATING`.
|
||||
pub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1;
|
||||
/// `OAKCODEC_PROXY_STATE_READY`.
|
||||
pub const OAKCODEC_PROXY_STATE_READY: c_int = 2;
|
||||
/// `OAKCODEC_PROXY_STATE_FAILED`.
|
||||
pub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3;
|
||||
|
||||
/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_proxy_params {
|
||||
pub width: c_int,
|
||||
pub height: c_int,
|
||||
pub divider: c_int,
|
||||
pub version: c_int,
|
||||
pub crf: c_int,
|
||||
pub include_audio: c_int,
|
||||
pub extension: [u8; 32],
|
||||
pub preset: [u8; 32],
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_proxy_result {
|
||||
pub state: c_int,
|
||||
pub filename: [u8; 1024],
|
||||
}
|
||||
|
||||
/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in
|
||||
/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults.
|
||||
fn to_native(p: *const oakcodec_proxy_params) -> ProxyParams {
|
||||
if p.is_null() {
|
||||
return ProxyParams::default();
|
||||
}
|
||||
let p = unsafe { &*p };
|
||||
ProxyParams {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
divider: p.divider,
|
||||
version: p.version,
|
||||
crf: p.crf,
|
||||
include_audio: p.include_audio,
|
||||
extension: p.extension,
|
||||
preset: p.preset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a NUL-terminated byte array into a C buffer (truncated).
|
||||
fn copy_cstr(src: &[u8], dst: &mut [u8]) {
|
||||
dst.fill(0);
|
||||
let n = src.len().min(dst.len().saturating_sub(1));
|
||||
dst[..n].copy_from_slice(&src[..n]);
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_create_instance`: create the singleton (always present
|
||||
/// here, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_create_instance() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let _ = ProxyManager::instance();
|
||||
crate::error::OAKCODEC_OK
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust
|
||||
/// manager is stateless, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_destroy_instance() -> c_int {
|
||||
handle::guard_raw(|| crate::error::OAKCODEC_OK)
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in
|
||||
/// default proxy parameters.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let n = ProxyManager::proxy_params_from_config();
|
||||
unsafe {
|
||||
(*out).width = n.width;
|
||||
(*out).height = n.height;
|
||||
(*out).divider = n.divider;
|
||||
(*out).version = n.version;
|
||||
(*out).crf = n.crf;
|
||||
(*out).include_audio = n.include_audio;
|
||||
copy_cstr(&n.extension, &mut (*out).extension);
|
||||
copy_cstr(&n.preset, &mut (*out).preset);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_state`: state of a proxy file on disk
|
||||
/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match crate::ffi::c_str(proxy_filename) {
|
||||
Some(f) if !f.is_empty() => f,
|
||||
_ => return OAKCODEC_PROXY_STATE_MISSING,
|
||||
};
|
||||
ProxyManager::get_proxy_state(&f) as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_state_to_string` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_state_to_string(
|
||||
state: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let s = match state {
|
||||
0 => ProxyManager::proxy_state_to_string(ProxyState::Missing),
|
||||
1 => ProxyManager::proxy_state_to_string(ProxyState::Generating),
|
||||
2 => ProxyManager::proxy_state_to_string(ProxyState::Ready),
|
||||
3 => ProxyManager::proxy_state_to_string(ProxyState::Failed),
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
super::string_out(&s, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_proxy_directory` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_directory(
|
||||
cache_path: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match ProxyManager::get_proxy_directory(&cache) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_proxy_filename` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_filename(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
params: *const oakcodec_proxy_params,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) => s,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let native = to_native(params);
|
||||
match ProxyManager::get_proxy_filename(&cache, &source, stream_index, &native) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_working_filename` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_working_filename(
|
||||
proxy_filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let proxy = match crate::ffi::c_str(proxy_filename) {
|
||||
Some(p) => p,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match ProxyManager::get_working_filename(&proxy) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for
|
||||
/// `source_filename`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_or_start(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
params: *const oakcodec_proxy_params,
|
||||
out: *mut oakcodec_proxy_result,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) => s,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let native = to_native(params);
|
||||
let (state, filename) = ProxyManager::instance()
|
||||
.get_or_start(&cache, &source, stream_index, &native)
|
||||
.map_err(|_| crate::error::Error::Failed("get_or_start failed".to_string()))?;
|
||||
unsafe {
|
||||
let out_ref = &mut *out;
|
||||
out_ref.state = state as c_int;
|
||||
// Truncate to 1023 chars + NUL, matching `snprintf` in
|
||||
// `c_api/proxy.cpp`.
|
||||
let n = filename.len().min(1023);
|
||||
out_ref.filename[..n].copy_from_slice(&filename.as_bytes()[..n]);
|
||||
out_ref.filename[n] = 0;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_find_ffmpeg(
|
||||
configured_path: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let configured = crate::ffi::c_str(configured_path).unwrap_or_default();
|
||||
let s = ProxyManager::find_ffmpeg(&configured);
|
||||
super::string_out(&s, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
|
||||
use crate::task::set_task_submit_cb_extern;
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_ffi_proxy_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn defaults() -> oakcodec_proxy_params {
|
||||
let mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() };
|
||||
let rc = unsafe { oakcodec_proxy_params_default(&mut p) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_destroy_and_params_default() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_proxy_create_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_proxy_destroy_instance() },
|
||||
crate::error::OAKCODEC_OK
|
||||
);
|
||||
|
||||
let p = defaults();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.version, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(&p.extension[..3], b"mp4");
|
||||
assert_eq!(&p.preset[..8], b"veryfast");
|
||||
|
||||
let rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_state_and_state_to_string() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let cache = temp_cache("state");
|
||||
let p = defaults();
|
||||
let src = cstr("media.mp4");
|
||||
let cache_c = cstr(&cache);
|
||||
|
||||
// Resolve the proxy filename, then query its state.
|
||||
let mut name = [0i8; 1024];
|
||||
let rc = unsafe {
|
||||
oakcodec_proxy_get_proxy_filename(
|
||||
cache_c.as_ptr(),
|
||||
src.as_ptr(),
|
||||
0,
|
||||
&p,
|
||||
name.as_mut_ptr(),
|
||||
1024,
|
||||
)
|
||||
};
|
||||
assert!(rc > 0);
|
||||
let proxy = crate::ffi::c_str(name.as_ptr()).unwrap();
|
||||
assert!(proxy.contains("1280x720"));
|
||||
|
||||
// Missing by default.
|
||||
let pc = cstr(&proxy);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_proxy_get_state(pc.as_ptr()) },
|
||||
OAKCODEC_PROXY_STATE_MISSING
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_proxy_get_state(std::ptr::null()) },
|
||||
OAKCODEC_PROXY_STATE_MISSING
|
||||
);
|
||||
|
||||
// Ready once the file exists.
|
||||
std::fs::create_dir_all(std::path::Path::new(&proxy).parent().unwrap()).unwrap();
|
||||
std::fs::write(&proxy, b"x").unwrap();
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_proxy_get_state(pc.as_ptr()) },
|
||||
OAKCODEC_PROXY_STATE_READY
|
||||
);
|
||||
|
||||
// state_to_string mapping + invalid range.
|
||||
let mut buf = [0i8; 64];
|
||||
let rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 6); // "ready" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("ready"));
|
||||
let rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_working_and_get_or_start() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let cache = temp_cache("getorstart");
|
||||
let cache_c = cstr(&cache);
|
||||
let src = cstr("media.mp4");
|
||||
let p = defaults();
|
||||
|
||||
// get_proxy_directory.
|
||||
let mut buf = [0i8; 512];
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) };
|
||||
assert!(rc > 0);
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some(format!("{}/proxy", cache).as_str())
|
||||
);
|
||||
|
||||
// get_working_filename appends ".working.mp4".
|
||||
let proxy = format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, 12345);
|
||||
let pc = cstr(&proxy);
|
||||
let rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) };
|
||||
assert!(rc > 0);
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some(format!("{}.working.mp4", proxy).as_str())
|
||||
);
|
||||
|
||||
// get_or_start without a registrar -> Missing.
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() };
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING);
|
||||
|
||||
// With a registrar and no files -> Generating.
|
||||
set_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut());
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING);
|
||||
|
||||
// Invalid args.
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe {
|
||||
oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, std::ptr::null_mut())
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// get_proxy_directory / get_proxy_filename / get_working_filename
|
||||
// argument validation.
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe {
|
||||
oakcodec_proxy_get_proxy_filename(
|
||||
std::ptr::null(),
|
||||
src.as_ptr(),
|
||||
0,
|
||||
&p,
|
||||
buf.as_mut_ptr(),
|
||||
512,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc =
|
||||
unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_uses_configured_path() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
// The current test binary is a real executable: the configured
|
||||
// path resolves to an absolute path.
|
||||
let me = std::env::current_exe().unwrap();
|
||||
let mc = cstr(me.to_str().unwrap());
|
||||
let mut buf = [0i8; 1024];
|
||||
let rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) };
|
||||
assert!(rc > 0);
|
||||
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(found.starts_with('/'));
|
||||
|
||||
// NULL configured path falls back to the search (empty or absolute).
|
||||
let rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) };
|
||||
assert!(rc > 0);
|
||||
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(found.is_empty() || found.starts_with('/'));
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! `include/codec/task.h` exports.
|
||||
//!
|
||||
//! Complete inventory: set_task_submit_cb / task_submit_is_registered.
|
||||
//! The callback typedef and request struct are mirrored in
|
||||
//! [`crate::task`].
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use crate::handle;
|
||||
|
||||
/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with
|
||||
/// NULL) the global task submit callback.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_set_task_submit_cb(
|
||||
cb: Option<crate::task::OakCodecTaskSubmitFn>,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) {
|
||||
handle::guard_void(|| {
|
||||
crate::task::set_task_submit_cb_extern(cb, userdata);
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_task_submit_is_registered`: 1 when a callback is set.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_task_submit_is_registered() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
if crate::task::task_submit_is_registered() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
|
||||
use crate::task::set_task_submit_cb_extern;
|
||||
|
||||
#[test]
|
||||
fn register_query_clear() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
|
||||
|
||||
unsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) };
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1);
|
||||
|
||||
unsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) };
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
|
||||
|
||||
// Restore a clean slate for the other modules.
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
+160
-95
@@ -56,32 +56,20 @@ use ffmpeg::software::{resampling, scaling};
|
||||
use ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
|
||||
use oakcommon::cancelatom::CancelAtom;
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
use oakcommon::videoparams::{Interlacing, VideoParams, VideoType};
|
||||
use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_channel_count,
|
||||
oakcommon_videoparams_set_duration, oakcommon_videoparams_set_format,
|
||||
oakcommon_videoparams_set_frame_rate, oakcommon_videoparams_set_height,
|
||||
oakcommon_videoparams_set_interlacing, oakcommon_videoparams_set_pixel_aspect_ratio,
|
||||
oakcommon_videoparams_set_premultiplied_alpha, oakcommon_videoparams_set_start_time,
|
||||
oakcommon_videoparams_set_stream_index, oakcommon_videoparams_set_time_base,
|
||||
oakcommon_videoparams_set_video_type, oakcommon_videoparams_set_width,
|
||||
oakcore_audioparams_create, oakcore_audioparams_set_duration,
|
||||
oakcore_audioparams_set_stream_index, oakcore_audioparams_set_time_base, OakAudioParams,
|
||||
};
|
||||
use crate::bridge::render::{oakrender_cancelatom_is_cancelled, OakCancelAtom, OakRenderTexture};
|
||||
use crate::decoder::{CodecStream, Decoder, RetrieveAudioStatus, RetrieveVideoParams};
|
||||
use crate::audioparams::AudioParams;
|
||||
use crate::decoder::{CodecStream, Decoder, OakRenderTexture, RetrieveAudioStatus, RetrieveVideoParams};
|
||||
use crate::encoder::Encoder;
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::footagedescription::{FootageDescription, StreamEntry};
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).
|
||||
const OAKCOMMON_VIDEO_INTERLACE_NONE: i32 = 0;
|
||||
/// `OAKCOMMON_COLOR_RANGE_FULL`.
|
||||
const OAKCOMMON_COLOR_RANGE_FULL: i32 = 1;
|
||||
/// `OAKCOMMON_VIDEO_TYPE_VIDEO`.
|
||||
const OAKCOMMON_VIDEO_TYPE_VIDEO: i32 = 0;
|
||||
/// The format-level time base (microseconds), `FB_TIME_BASE` in the bridge.
|
||||
const FB_TIME_BASE: i64 = 1_000_000;
|
||||
/// `AV_NOPTS_VALUE`.
|
||||
@@ -112,18 +100,12 @@ fn fail(msg: impl Into<String>) -> crate::error::Error {
|
||||
crate::error::Error::Failed(msg.into())
|
||||
}
|
||||
|
||||
/// `cancel_atom_is_cancelled` — NULL/empty-handle-safe check of an
|
||||
/// oakrender cancel atom (borrowed pointer).
|
||||
/// `cancel_atom_is_cancelled` — check of a cancel atom (borrowed pointer).
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/ffmpeg/ffmpegdecoder.cpp` (anonymous namespace helper).
|
||||
fn cancel_atom_is_cancelled(cancelled: Option<&OakCancelAtom>) -> bool {
|
||||
match cancelled {
|
||||
Some(atom) if !atom.ctx.is_null() => unsafe {
|
||||
oakrender_cancelatom_is_cancelled(atom.clone()) != 0
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
fn cancel_atom_is_cancelled(cancelled: Option<&CancelAtom>) -> bool {
|
||||
cancelled.is_some_and(|atom| atom.is_cancelled())
|
||||
}
|
||||
|
||||
/// Whether an FFmpeg error means "end of stream" or "try again".
|
||||
@@ -261,7 +243,7 @@ impl Decoder for FFmpegDecoder {
|
||||
fn probe(
|
||||
&self,
|
||||
filename: &str,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> Option<FootageDescription> {
|
||||
ffmpeg_init().ok()?;
|
||||
probe_file(filename, cancelled)
|
||||
@@ -377,7 +359,7 @@ impl Decoder for FFmpegDecoder {
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
ffmpeg_init()?;
|
||||
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -705,7 +687,7 @@ impl DecoderState {
|
||||
&mut self,
|
||||
time: &Rational,
|
||||
any_timecode: bool,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> crate::error::Result<Option<ffmpeg::frame::Video>> {
|
||||
// Move the video state out so `self.seek` / `self.pull` (which touch
|
||||
// other fields) can be called without conflicting borrows.
|
||||
@@ -1023,7 +1005,7 @@ impl DecoderState {
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
cancelled: Option<&CancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
if self.input_channel_layout_mask == 0 {
|
||||
return Err(fail(
|
||||
@@ -1456,11 +1438,18 @@ fn copy_rgba_f32_to_frame(
|
||||
bytes: &[u8],
|
||||
timestamp: Rational,
|
||||
) -> crate::error::Result<Frame> {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(width as i32, height as i32, 0, 4, 1, 1, 0, 1) };
|
||||
unsafe {
|
||||
oakcommon_videoparams_set_format(params.clone(), PixelFormat::F32 as i32);
|
||||
oakcommon_videoparams_set_channel_count(params.clone(), VIDEO_CHANNELS);
|
||||
}
|
||||
let mut params = VideoParams::new_basic(
|
||||
width as i32,
|
||||
height as i32,
|
||||
OakPixelFormat::from_code(0),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
params.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
|
||||
params.set_channel_count(VIDEO_CHANNELS);
|
||||
let mut frame = Frame::with_params(params);
|
||||
frame.set_timestamp(timestamp);
|
||||
frame.allocate()?;
|
||||
@@ -1490,7 +1479,7 @@ fn copy_rgba_f32_to_frame(
|
||||
/// stream details are taken from stream parameters (no second decode pass),
|
||||
/// so `is_still` is always false and interlacing always progressive;
|
||||
/// subtitle streams are counted but not added.
|
||||
fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<FootageDescription> {
|
||||
fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option<FootageDescription> {
|
||||
let mut dict = Dictionary::new();
|
||||
dict.set("analyzeduration", "5000000");
|
||||
dict.set("probesize", "20000000");
|
||||
@@ -1545,29 +1534,25 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
|
||||
let frame_rate = stream.avg_frame_rate();
|
||||
let tb = stream.time_base();
|
||||
|
||||
let vp = unsafe { oakcommon_videoparams_init_basic(1, 1, 0, 4, 1, 1, 0, 1) };
|
||||
let mut vp =
|
||||
VideoParams::new_basic(1, 1, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
vp.set_stream_index(i as i32);
|
||||
// SAFETY: `raw` points at the live stream's parameters (from
|
||||
// `params.as_ptr()` above), valid for the duration of `probe_file`.
|
||||
unsafe {
|
||||
oakcommon_videoparams_set_stream_index(vp.clone(), i as i32);
|
||||
oakcommon_videoparams_set_width(vp.clone(), (*raw).width);
|
||||
oakcommon_videoparams_set_height(vp.clone(), (*raw).height);
|
||||
oakcommon_videoparams_set_video_type(vp.clone(), OAKCOMMON_VIDEO_TYPE_VIDEO);
|
||||
oakcommon_videoparams_set_format(vp.clone(), native as i32);
|
||||
oakcommon_videoparams_set_channel_count(vp.clone(), VIDEO_CHANNELS);
|
||||
oakcommon_videoparams_set_interlacing(
|
||||
vp.clone(),
|
||||
OAKCOMMON_VIDEO_INTERLACE_NONE,
|
||||
);
|
||||
oakcommon_videoparams_set_pixel_aspect_ratio(vp.clone(), 1, 1);
|
||||
oakcommon_videoparams_set_frame_rate(
|
||||
vp.clone(),
|
||||
frame_rate.0 as i64,
|
||||
frame_rate.1 as i64,
|
||||
);
|
||||
oakcommon_videoparams_set_start_time(vp.clone(), stream.start_time());
|
||||
oakcommon_videoparams_set_time_base(vp.clone(), tb.0 as i64, tb.1 as i64);
|
||||
oakcommon_videoparams_set_duration(vp.clone(), stream.duration());
|
||||
oakcommon_videoparams_set_premultiplied_alpha(vp.clone(), 0);
|
||||
vp.set_width((*raw).width);
|
||||
vp.set_height((*raw).height);
|
||||
}
|
||||
vp.set_video_type(VideoType::Video);
|
||||
vp.set_format(OakPixelFormat::from_code(native as i32));
|
||||
vp.set_channel_count(VIDEO_CHANNELS);
|
||||
vp.set_interlacing(Interlacing::None);
|
||||
vp.set_pixel_aspect_ratio(1, 1);
|
||||
vp.set_frame_rate(frame_rate.0 as i32, frame_rate.1 as i32);
|
||||
vp.set_start_time(stream.start_time());
|
||||
vp.set_time_base(tb.0 as i32, tb.1 as i32);
|
||||
vp.set_duration(stream.duration());
|
||||
vp.set_premultiplied_alpha(false);
|
||||
desc.push_stream(StreamEntry::Video(vp));
|
||||
}
|
||||
MediaType::Audio => {
|
||||
@@ -1584,22 +1569,26 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
|
||||
};
|
||||
}
|
||||
let sample_rate = unsafe { (*raw).sample_rate };
|
||||
let layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
|
||||
let ap = unsafe { oakcore_audioparams_create(sample_rate, layout_mask, 0) };
|
||||
if !ap.is_null() {
|
||||
let tb = stream.time_base();
|
||||
unsafe {
|
||||
oakcore_audioparams_set_stream_index(ap, i as i32);
|
||||
oakcore_audioparams_set_duration(ap, stream_duration);
|
||||
oakcore_audioparams_set_time_base(ap, tb.0 as i32, tb.1 as i32);
|
||||
}
|
||||
desc.push_stream(StreamEntry::Audio(OakAudioParams {
|
||||
ctx: ap as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}));
|
||||
}
|
||||
let raw_layout = unsafe { ChannelLayout::from((*raw).ch_layout) };
|
||||
// Count-only layouts (WAV and other PCM containers report
|
||||
// AV_CHANNEL_ORDER_UNSPEC with a channel count but no mask)
|
||||
// yield a zero mask; derive a default mask from the count so
|
||||
// the stream stays usable (CPP-PARITY channel_layout_from_mask
|
||||
// fallback in the audio processors).
|
||||
let layout_mask = if raw_layout.bits() == 0 && raw_layout.channels() > 0 {
|
||||
ChannelLayout::default(raw_layout.channels()).bits()
|
||||
} else {
|
||||
raw_layout.bits()
|
||||
};
|
||||
let tb = stream.time_base();
|
||||
desc.push_stream(StreamEntry::Audio(AudioParams {
|
||||
sample_rate,
|
||||
channel_layout: layout_mask,
|
||||
format: 0,
|
||||
stream_index: i as i32,
|
||||
duration: stream_duration,
|
||||
time_base: (tb.0 as i32, tb.1 as i32),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1699,6 +1688,12 @@ struct VideoEncoderState {
|
||||
scaler: scaling::Context,
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// The encoder's time base after `open` (the frame PTS are expressed
|
||||
/// in it; see `FFmpegEncoder::open`).
|
||||
time_base: FfRational,
|
||||
/// One frame in the encoder's time base (the last packet's duration;
|
||||
/// see `FFmpegEncoder::open`).
|
||||
frame_duration: i64,
|
||||
}
|
||||
|
||||
/// Opened audio encoder + its conversion resampler.
|
||||
@@ -1877,7 +1872,6 @@ impl EncoderState {
|
||||
.video()
|
||||
.map_err(ffmpeg_err)?;
|
||||
stream.set_parameters(&encoder);
|
||||
stream.set_time_base(time_base);
|
||||
|
||||
encoder.set_width(width);
|
||||
encoder.set_height(height);
|
||||
@@ -1886,7 +1880,19 @@ impl EncoderState {
|
||||
params.video_pixel_aspect_den.max(1),
|
||||
));
|
||||
encoder.set_frame_rate(Some(frame_rate));
|
||||
encoder.set_time_base(time_base);
|
||||
// The codecs' packet timestamps use a fine tick (x264 encodes at
|
||||
// 1024 ticks per frame); give H.264 an encoder time base scaled
|
||||
// to that so the frame PTS stay integral, and sync the stream to
|
||||
// the encoder's ACTUAL post-open time base (the value the muxer
|
||||
// reads) so the container timing is `seconds * frame_rate`.
|
||||
// Other codecs (e.g. MPEG-2) reject the scaled rate and keep the
|
||||
// nominal frame-duration time base.
|
||||
let tick = if codec_id == ffmpeg::codec::Id::H264 {
|
||||
FfRational(time_base.0, time_base.1 * 1024)
|
||||
} else {
|
||||
time_base
|
||||
};
|
||||
encoder.set_time_base(tick);
|
||||
if params.video_bit_rate > 0 {
|
||||
encoder.set_bit_rate(params.video_bit_rate as usize);
|
||||
}
|
||||
@@ -1902,6 +1908,20 @@ impl EncoderState {
|
||||
|
||||
let opened = encoder.open().map_err(|e| { eprintln!("DBG-AUD: audio open failed: {e:?}"); ffmpeg_err(e) })?;
|
||||
stream.set_parameters(&opened);
|
||||
// The encoder may adjust the time base during `open` (x264
|
||||
// picks its own); sync the stream to the encoder's ACTUAL time
|
||||
// base so the container timing matches the frame PTS computed
|
||||
// from it (a mismatched stream time base crams the whole video
|
||||
// into the first milliseconds).
|
||||
let time_base = opened.time_base();
|
||||
stream.set_time_base(time_base);
|
||||
// One frame in the encoder's time base, used to fill the last
|
||||
// packet's duration: the muxer normally derives it from the
|
||||
// codec context attached to the stream, but the ffmpeg-next
|
||||
// flow never attaches one, so the final frame would carry
|
||||
// duration 0 and the track would be one frame short.
|
||||
let frame_duration = (time_base.1 as i64 * i64::from(frame_rate.1))
|
||||
/ (i64::from(time_base.0) * i64::from(frame_rate.0)).max(1);
|
||||
|
||||
let scaler = scaling::Context::get(
|
||||
Pixel::RGBA,
|
||||
@@ -1920,6 +1940,8 @@ impl EncoderState {
|
||||
scaler,
|
||||
width,
|
||||
height,
|
||||
time_base,
|
||||
frame_duration,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1987,7 +2009,7 @@ impl EncoderState {
|
||||
}
|
||||
|
||||
/// Encode one frame (F32 RGBA) into the open output.
|
||||
fn write_video(&mut self, frame: &Frame, params: &EncodingParams) -> crate::error::Result<()> {
|
||||
fn write_video(&mut self, frame: &Frame, _params: &EncodingParams) -> crate::error::Result<()> {
|
||||
let output = self
|
||||
.output
|
||||
.as_mut()
|
||||
@@ -2040,12 +2062,11 @@ impl EncoderState {
|
||||
|
||||
// # CPP-PARITY
|
||||
// `FFmpegEncoder::write_frame` passes the frame time in seconds; the
|
||||
// Rust `Frame` carries the timestamp as a rational.
|
||||
// Rust `Frame` carries the timestamp as a rational. The PTS is
|
||||
// expressed in the encoder's own time base (captured at open), so
|
||||
// the container timing is exactly `seconds * rate`.
|
||||
let secs = frame.timestamp().to_f64();
|
||||
let tb = FfRational(
|
||||
params.video_time_base_num.max(1),
|
||||
params.video_time_base_den.max(1),
|
||||
);
|
||||
let tb = video.time_base;
|
||||
let pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;
|
||||
scaled.set_pts(Some(pts));
|
||||
|
||||
@@ -2078,22 +2099,57 @@ impl EncoderState {
|
||||
let channels = audio.resampler.dst_channels.max(1);
|
||||
let in_frames = samples.len() / channels;
|
||||
|
||||
// Presentation timestamp in the output stream time base (1/sample_rate).
|
||||
let pts = output.audio_pts;
|
||||
// The encoder accepts at most `frame_size` samples per frame (AAC:
|
||||
// 1024), so the (possibly whole-range) input buffer is split into
|
||||
// chunks. The resampler converts at the same rate (the rendered
|
||||
// audio rate equals the encoder rate), but swr may buffer a small
|
||||
// delay, so the input chunk is shrunk until its predicted output
|
||||
// fits `frame_size`.
|
||||
let frame_size = audio.encoder.frame_size().max(1) as usize;
|
||||
let mut offset = 0usize;
|
||||
while offset < in_frames {
|
||||
let mut take = frame_size.min(in_frames - offset);
|
||||
loop {
|
||||
let out = unsafe { sys::swr_get_out_samples(audio.resampler.ctx.as_mut_ptr(), take as i32) };
|
||||
if out >= 0 && out as usize <= frame_size {
|
||||
break;
|
||||
}
|
||||
take = take.saturating_sub(1);
|
||||
if take == 0 {
|
||||
take = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let layout = channel_layout_from_mask(params.audio_channel_layout);
|
||||
let mut input =
|
||||
ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), in_frames, layout);
|
||||
let bytes =
|
||||
unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4) };
|
||||
input.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
|
||||
// Presentation timestamp in the output stream time base (1/sample_rate).
|
||||
let pts = output.audio_pts;
|
||||
let layout = channel_layout_from_mask(params.audio_channel_layout);
|
||||
let chunk = &samples[offset * channels..(offset + take) * channels];
|
||||
let mut input = ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), take, layout);
|
||||
let bytes = unsafe {
|
||||
std::slice::from_raw_parts(chunk.as_ptr() as *const u8, chunk.len() * 4)
|
||||
};
|
||||
input.data_mut(0)[..bytes.len()].copy_from_slice(bytes);
|
||||
|
||||
let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| { eprintln!("DBG-AUD: convert failed: {e:?}"); fail(format!("{e:?}")) })?;
|
||||
converted.set_pts(Some(pts));
|
||||
output.audio_pts += converted.samples() as i64;
|
||||
|
||||
audio.encoder.send_frame(&converted).map_err(|e| { eprintln!("DBG-AUD: send failed: {e:?}"); ffmpeg_err(e) })?;
|
||||
drain_audio_packets(&mut output.output, audio)
|
||||
let mut converted = audio.resampler.convert_to_frame(&input).map_err(|e| {
|
||||
eprintln!("DBG-AUD: convert failed: {e:?}");
|
||||
fail(format!("{e:?}"))
|
||||
})?;
|
||||
if converted.samples() > 0 {
|
||||
converted.set_pts(Some(pts));
|
||||
output.audio_pts += converted.samples() as i64;
|
||||
audio
|
||||
.encoder
|
||||
.send_frame(&converted)
|
||||
.map_err(|e| {
|
||||
eprintln!("DBG-AUD: send failed: {e:?}");
|
||||
ffmpeg_err(e)
|
||||
})?;
|
||||
drain_audio_packets(&mut output.output, audio)?;
|
||||
}
|
||||
offset += take;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush encoders, write the trailer and close the output (idempotent).
|
||||
@@ -2128,6 +2184,9 @@ fn drain_video_encoder(
|
||||
match video.encoder.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
pkt.set_stream(video.stream_index);
|
||||
if pkt.duration() <= 0 {
|
||||
pkt.set_duration(video.frame_duration);
|
||||
}
|
||||
pkt.write_interleaved(output).map_err(ffmpeg_err)?;
|
||||
}
|
||||
Err(e) if is_eof_or_eagain(&e) => break,
|
||||
@@ -2167,6 +2226,12 @@ fn drain_video_packets(
|
||||
match video.encoder.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
pkt.set_stream(video.stream_index);
|
||||
// The final frame carries no duration (see the
|
||||
// `frame_duration` note); fill it so the track length is
|
||||
// the full export range.
|
||||
if pkt.duration() <= 0 {
|
||||
pkt.set_duration(video.frame_duration);
|
||||
}
|
||||
pkt.write_interleaved(output).map_err(ffmpeg_err)?;
|
||||
}
|
||||
Err(e) if is_eof_or_eagain(&e) => break,
|
||||
|
||||
@@ -23,19 +23,21 @@
|
||||
//! `Track::Type` mapping and XML load/save are intentionally not reproduced
|
||||
//! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc.
|
||||
|
||||
use oakcommon::subtitleparams::SubtitleParams;
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams};
|
||||
use crate::audioparams::AudioParams;
|
||||
|
||||
/// One stream entry in a footage description.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StreamEntry {
|
||||
/// A video stream (params handle, addref'd).
|
||||
Video(OakVideoParams),
|
||||
/// A video stream.
|
||||
Video(VideoParams),
|
||||
/// An audio stream.
|
||||
Audio(OakAudioParams),
|
||||
/// A subtitle stream (params handle, addref'd).
|
||||
Subtitle(OakSubtitleParams),
|
||||
Audio(AudioParams),
|
||||
/// A subtitle stream.
|
||||
Subtitle(SubtitleParams),
|
||||
}
|
||||
|
||||
/// `olive::FootageDescription` — the decoder name plus stream inventory.
|
||||
@@ -127,7 +129,7 @@ impl FootageDescription {
|
||||
}
|
||||
|
||||
/// The `index`-th video stream's params (by video-stream ordinal).
|
||||
pub fn get_video_stream(&self, index: usize) -> Option<&OakVideoParams> {
|
||||
pub fn get_video_stream(&self, index: usize) -> Option<&VideoParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
@@ -138,7 +140,7 @@ impl FootageDescription {
|
||||
}
|
||||
|
||||
/// The `index`-th audio stream's params (by audio-stream ordinal).
|
||||
pub fn get_audio_stream(&self, index: usize) -> Option<&OakAudioParams> {
|
||||
pub fn get_audio_stream(&self, index: usize) -> Option<&AudioParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
@@ -149,7 +151,7 @@ impl FootageDescription {
|
||||
}
|
||||
|
||||
/// The `index`-th subtitle stream's params (by subtitle-stream ordinal).
|
||||
pub fn get_subtitle_stream(&self, index: usize) -> Option<&OakSubtitleParams> {
|
||||
pub fn get_subtitle_stream(&self, index: usize) -> Option<&SubtitleParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
@@ -210,31 +212,34 @@ impl FootageDescription {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn video_params(index: i32) -> OakVideoParams {
|
||||
OakVideoParams {
|
||||
ctx: index as usize as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
fn video_params(index: i32) -> VideoParams {
|
||||
let mut vp = VideoParams::new_basic(
|
||||
1920,
|
||||
1080,
|
||||
oakcommon::ocioutils::PixelFormat::from_code(0),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
vp.set_stream_index(index);
|
||||
vp
|
||||
}
|
||||
|
||||
fn audio_params() -> AudioParams {
|
||||
AudioParams {
|
||||
sample_rate: 48000,
|
||||
channel_layout: 0x3,
|
||||
format: 0,
|
||||
stream_index: 1,
|
||||
duration: 0,
|
||||
time_base: (1, 48000),
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_params() -> OakAudioParams {
|
||||
OakAudioParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
fn subtitle_params() -> OakSubtitleParams {
|
||||
OakSubtitleParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
fn subtitle_params() -> SubtitleParams {
|
||||
SubtitleParams::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,19 +14,15 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle.
|
||||
//! `olive::Frame` — a CPU pixel buffer plus a [`VideoParams`] value.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon
|
||||
//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the
|
||||
//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is
|
||||
//! unchanged; the pixel data itself is a plain `Vec<u8>`. Line-size and
|
||||
//! pixel-format math lives here.
|
||||
//! [`VideoParams`] value (single-lib unification; the former refcounted
|
||||
//! oakcommon handle is gone, so copies are plain clones); the pixel data
|
||||
//! itself is a plain `Vec<u8>`. Line-size and pixel-format math lives
|
||||
//! here.
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_free, oakcommon_videoparams_get_format, oakcommon_videoparams_get_height,
|
||||
oakcommon_videoparams_get_is_valid, oakcommon_videoparams_get_width,
|
||||
oakcommon_videoparams_init, OakVideoParams,
|
||||
};
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use oakcore_rs::{PixelFormat, Rational};
|
||||
|
||||
/// Number of channels in the internal RGBA pipeline layout
|
||||
@@ -47,11 +43,11 @@ pub enum Interlacing {
|
||||
BottomFieldFirst = 2,
|
||||
}
|
||||
|
||||
/// `olive::Frame`: reference-counted CPU pixel buffer + params handle.
|
||||
/// `olive::Frame`: reference-counted CPU pixel buffer + params value.
|
||||
#[derive(Debug)]
|
||||
pub struct Frame {
|
||||
/// Video parameter set (oakcommon handle, refcounted).
|
||||
pub params: Option<OakVideoParams>,
|
||||
/// Video parameter set.
|
||||
pub params: Option<VideoParams>,
|
||||
/// Pixel buffer (unallocated until `allocate`).
|
||||
data: Vec<u8>,
|
||||
/// Distance between rows in bytes (0 until params are set).
|
||||
@@ -87,40 +83,11 @@ fn bytes_per_pixel(format: PixelFormat, channels: i32) -> i32 {
|
||||
(format.bytes_per_channel() as i32) * channels
|
||||
}
|
||||
|
||||
/// Increment the refcount of a params handle (a no-op for test-stub handles
|
||||
/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out
|
||||
/// addref'd copies (`oakcodec_frame_get_params`).
|
||||
pub(crate) fn params_addref(p: &OakVideoParams) {
|
||||
if let Some(addref) = p.addref {
|
||||
// SAFETY: `addref` is a valid C function pointer targeting `ctx`.
|
||||
unsafe { addref(p.ctx) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a params handle (prefers the `release` function pointer; the
|
||||
/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the
|
||||
/// handle cannot be released twice.
|
||||
pub(crate) fn params_release(p: &mut OakVideoParams) {
|
||||
if p.ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = p.release {
|
||||
// SAFETY: `release` is a valid C function pointer targeting `ctx`.
|
||||
unsafe { release(p.ctx) };
|
||||
} else {
|
||||
// SAFETY: `p` points at a live handle; `oakcommon_videoparams_free`
|
||||
// is a no-op for the null ctx we leave behind.
|
||||
unsafe { oakcommon_videoparams_free(p) };
|
||||
}
|
||||
p.ctx = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// New frame with default (invalid) params; buffer unallocated.
|
||||
pub fn new() -> Self {
|
||||
let params = unsafe { oakcommon_videoparams_init() };
|
||||
Frame {
|
||||
params: Some(params),
|
||||
params: Some(VideoParams::new()),
|
||||
data: Vec::new(),
|
||||
linesize_bytes: 0,
|
||||
timestamp: Rational::new(0, 1),
|
||||
@@ -128,9 +95,8 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// New frame with a copy of `params` (handle addref'd internally).
|
||||
pub fn with_params(params: OakVideoParams) -> Self {
|
||||
params_addref(¶ms);
|
||||
/// New frame with a copy of `params`.
|
||||
pub fn with_params(params: VideoParams) -> Self {
|
||||
let mut frame = Frame {
|
||||
params: Some(params),
|
||||
data: Vec::new(),
|
||||
@@ -143,17 +109,13 @@ impl Frame {
|
||||
}
|
||||
|
||||
/// The video parameter set, or `None` when empty.
|
||||
pub fn params(&self) -> Option<&OakVideoParams> {
|
||||
pub fn params(&self) -> Option<&VideoParams> {
|
||||
self.params.as_ref()
|
||||
}
|
||||
|
||||
/// Replace the parameter set (handle addref'd), recompute line sizes,
|
||||
/// do NOT reallocate the buffer.
|
||||
pub fn set_params(&mut self, params: OakVideoParams) {
|
||||
if let Some(mut old) = self.params.take() {
|
||||
params_release(&mut old);
|
||||
}
|
||||
params_addref(¶ms);
|
||||
/// Replace the parameter set, recompute line sizes, do NOT reallocate
|
||||
/// the buffer.
|
||||
pub fn set_params(&mut self, params: VideoParams) {
|
||||
self.params = Some(params);
|
||||
self.recompute_linesize();
|
||||
// Deliberately do not touch `data`: an existing buffer keeps its
|
||||
@@ -165,10 +127,8 @@ impl Frame {
|
||||
fn recompute_linesize(&mut self) {
|
||||
self.linesize_bytes = match &self.params {
|
||||
Some(p) => {
|
||||
let w = unsafe { oakcommon_videoparams_get_width(p.clone()) };
|
||||
let fmt =
|
||||
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) });
|
||||
Self::generate_linesize_bytes(fmt, w)
|
||||
let fmt = pixel_format_from_i32(p.format().code());
|
||||
Self::generate_linesize_bytes(fmt, p.width())
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
@@ -177,12 +137,11 @@ impl Frame {
|
||||
/// Allocate the pixel buffer from the current params.
|
||||
pub fn allocate(&mut self) -> crate::error::Result<()> {
|
||||
let params = match &self.params {
|
||||
Some(p) => p.clone(),
|
||||
Some(p) => p,
|
||||
None => return Err(crate::error::Error::State),
|
||||
};
|
||||
|
||||
let is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) };
|
||||
if is_valid == 0 {
|
||||
if !params.is_valid() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
|
||||
@@ -191,9 +150,9 @@ impl Frame {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let width = unsafe { oakcommon_videoparams_get_width(params.clone()) };
|
||||
let height = unsafe { oakcommon_videoparams_get_height(params.clone()) };
|
||||
let format = pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(params) });
|
||||
let width = params.width();
|
||||
let height = params.height();
|
||||
let format = pixel_format_from_i32(params.format().code());
|
||||
|
||||
let linesize = Self::generate_linesize_bytes(format, width);
|
||||
let size = (linesize as usize).wrapping_mul(height as usize);
|
||||
@@ -256,7 +215,7 @@ impl Frame {
|
||||
/// Frame width in pixels (0 when params are empty).
|
||||
pub fn width(&self) -> i32 {
|
||||
match &self.params {
|
||||
Some(p) => unsafe { oakcommon_videoparams_get_width(p.clone()) },
|
||||
Some(p) => p.width(),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
@@ -264,7 +223,7 @@ impl Frame {
|
||||
/// Frame height in pixels (0 when params are empty).
|
||||
pub fn height(&self) -> i32 {
|
||||
match &self.params {
|
||||
Some(p) => unsafe { oakcommon_videoparams_get_height(p.clone()) },
|
||||
Some(p) => p.height(),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
@@ -272,9 +231,7 @@ impl Frame {
|
||||
/// Pixel format (`OakPixelFormat` value).
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
match &self.params {
|
||||
Some(p) => {
|
||||
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) })
|
||||
}
|
||||
Some(p) => pixel_format_from_i32(p.format().code()),
|
||||
None => PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
@@ -282,10 +239,9 @@ impl Frame {
|
||||
/// Plane channel count of the params format.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/frame.h` reads this from the params handle via
|
||||
/// `oakcommon_videoparams_get_channel_count`, which is not exposed in the
|
||||
/// Rust bridge. Decoder frames are always produced in the internal RGBA
|
||||
/// layout, so this returns [`VIDEO_CHANNELS`] (4).
|
||||
/// `src/codec/src/frame.h` reads this from the params via
|
||||
/// `VideoParams::channel_count`. Decoder frames are always produced in
|
||||
/// the internal RGBA layout, so this returns [`VIDEO_CHANNELS`] (4).
|
||||
pub fn channel_count(&self) -> i32 {
|
||||
VIDEO_CHANNELS
|
||||
}
|
||||
@@ -317,7 +273,7 @@ impl Frame {
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/frame.cpp` — the destination params are carried by
|
||||
/// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the
|
||||
/// the C++ callers via `VideoParams` setters; Rust keeps the
|
||||
/// equivalent state in `self.params`.
|
||||
///
|
||||
/// When the current params format already matches the format the buffer
|
||||
@@ -372,23 +328,13 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Frame {
|
||||
/// Release the owned params handle when the last reference dies,
|
||||
/// mirroring the C++ `Frame::~Frame`.
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.params.take() {
|
||||
params_release(&mut p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
|
||||
fn frame(w: i32, h: i32) -> Frame {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) };
|
||||
let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
Frame::with_params(params)
|
||||
}
|
||||
|
||||
@@ -459,12 +405,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_params_recomputes_linesize_without_realloc() {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(10, 10, 0, 4, 1, 1, 0, 1) };
|
||||
let params = VideoParams::new_basic(10, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
let mut f = Frame::with_params(params);
|
||||
f.allocate().unwrap();
|
||||
let before = f.allocated_size();
|
||||
|
||||
let wider = unsafe { oakcommon_videoparams_init_basic(100, 10, 0, 4, 1, 1, 0, 1) };
|
||||
let wider =
|
||||
VideoParams::new_basic(100, 10, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
f.set_params(wider);
|
||||
// linesize reflects the new width, but the buffer is untouched.
|
||||
assert_eq!(f.linesize_bytes(), 4 * 128);
|
||||
@@ -475,10 +422,10 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod tests_extra {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
|
||||
fn frame(w: i32, h: i32) -> Frame {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(w, h, 0, 4, 1, 1, 0, 1) };
|
||||
let params = VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
Frame::with_params(params)
|
||||
}
|
||||
|
||||
@@ -513,8 +460,8 @@ mod tests_extra {
|
||||
|
||||
#[test]
|
||||
fn pixel_format_from_unknown_code_is_invalid() {
|
||||
let p = unsafe { oakcommon_videoparams_init_basic(1, 1, 0, 4, 1, 1, 0, 1) };
|
||||
unsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) };
|
||||
let mut p = VideoParams::new_basic(1, 1, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
p.set_format(OakPixelFormat::from_code(99));
|
||||
let f = Frame::with_params(p);
|
||||
assert_eq!(f.format(), PixelFormat::Invalid);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::bridge::common::{oakcommon_videoparams_equals, OakVideoParams};
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `olive::FrameManager`: singleton frame pool with background GC.
|
||||
@@ -71,7 +71,7 @@ impl FrameManager {
|
||||
|
||||
/// Create a frame with the given params (borrowed from the pool when a
|
||||
/// compatible free frame exists, else freshly allocated).
|
||||
pub fn create_frame(&self, params: OakVideoParams) -> Arc<Frame> {
|
||||
pub fn create_frame(&self, params: VideoParams) -> Arc<Frame> {
|
||||
let frame = {
|
||||
let mut pool = self.pool.lock().unwrap();
|
||||
match pool.iter().position(|f| frame_matches(f, ¶ms)) {
|
||||
@@ -138,18 +138,21 @@ fn spawn_gc_thread_once(mgr: &'static FrameManager) {
|
||||
}
|
||||
|
||||
/// True when `frame` carries params equal to `params`.
|
||||
fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool {
|
||||
fn frame_matches(frame: &Frame, params: &VideoParams) -> bool {
|
||||
let Some(frame_params) = frame.params() else {
|
||||
return false;
|
||||
};
|
||||
let eq = unsafe { oakcommon_videoparams_equals(frame_params.clone(), params.clone()) };
|
||||
eq != 0
|
||||
frame_params.equals(params)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
|
||||
fn test_params(w: i32, h: i32) -> VideoParams {
|
||||
VideoParams::new_basic(w, h, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_return_tracks_counts() {
|
||||
@@ -157,7 +160,7 @@ mod tests {
|
||||
assert_eq!(mgr.live_count(), 0);
|
||||
assert_eq!(mgr.peak_count(), 0);
|
||||
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
|
||||
let params = test_params(64, 64);
|
||||
let frame = mgr.create_frame(params);
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
@@ -172,14 +175,14 @@ mod tests {
|
||||
#[test]
|
||||
fn pool_reuses_compatible_frames() {
|
||||
let mgr = FrameManager::new();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
|
||||
let params = test_params(64, 64);
|
||||
let f1 = mgr.create_frame(params);
|
||||
mgr.return_frame(Arc::try_unwrap(f1).unwrap());
|
||||
assert_eq!(mgr.live_count(), 0);
|
||||
|
||||
// A compatible request reuses the pooled buffer rather than
|
||||
// allocating a new one.
|
||||
let f2 = mgr.create_frame(unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) });
|
||||
let f2 = mgr.create_frame(test_params(64, 64));
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
Arc::try_unwrap(f2).unwrap();
|
||||
@@ -188,8 +191,8 @@ mod tests {
|
||||
#[test]
|
||||
fn peak_count_tracks_maximum() {
|
||||
let mgr = FrameManager::new();
|
||||
let p1 = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
|
||||
let p2 = unsafe { oakcommon_videoparams_init_basic(128, 128, 0, 4, 1, 1, 0, 1) };
|
||||
let p1 = test_params(64, 64);
|
||||
let p2 = test_params(128, 128);
|
||||
let a = mgr.create_frame(p1);
|
||||
let b = mgr.create_frame(p2);
|
||||
assert_eq!(mgr.live_count(), 2);
|
||||
@@ -203,7 +206,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clear_drops_pooled_frames() {
|
||||
let mgr = FrameManager::new();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
|
||||
let params = test_params(64, 64);
|
||||
let f = mgr.create_frame(params);
|
||||
mgr.return_frame(Arc::try_unwrap(f).unwrap());
|
||||
assert_eq!(mgr.pool.lock().unwrap().len(), 1);
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
//! created the object).
|
||||
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
|
||||
|
||||
use crate::error::{self, OAKCODEC_E_FAILED};
|
||||
|
||||
/// Number of boxed handle objects currently alive (leak/debug checking).
|
||||
@@ -183,9 +181,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn make_owned_lifecycle_tracks_alive_count() {
|
||||
// The shared ffi test lock serializes the crate's `alive_count`
|
||||
// The shared test lock serializes the crate's `alive_count`
|
||||
// assertions against every other test that creates handles.
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let _g = crate::lock_tests();
|
||||
let before = alive_count();
|
||||
let h = make_owned(42u32);
|
||||
assert!(!h.is_null());
|
||||
@@ -214,7 +212,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn make_borrowed_takes_ownership() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let _g = crate::lock_tests();
|
||||
let before = alive_count();
|
||||
let raw = Box::into_raw(Box::new(7u32));
|
||||
let h = unsafe { make_borrowed(raw) };
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod bridge;
|
||||
#[cfg(test)]
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub mod audioparams;
|
||||
pub mod conformmanager;
|
||||
pub mod decoder;
|
||||
pub mod encoder;
|
||||
@@ -37,7 +40,6 @@ pub mod encodingparams;
|
||||
pub mod error;
|
||||
pub mod exportcodec;
|
||||
pub mod exportformat;
|
||||
pub mod ffi;
|
||||
pub mod ffmpeg;
|
||||
pub mod footagedescription;
|
||||
pub mod frame;
|
||||
@@ -54,6 +56,51 @@ pub mod timecodemetadata;
|
||||
#[cfg(test)]
|
||||
mod realmedia_tests;
|
||||
|
||||
/// Process-wide test lock: serializes every test that reads or mutates
|
||||
/// crate-global state (the injected decoder registry, the handle alive
|
||||
/// count). One lock for the whole crate — tests race only with each
|
||||
/// other, never with production code.
|
||||
#[cfg(test)]
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// RAII guard over [`TEST_LOCK`]: the lock is taken when the guard is
|
||||
/// created ([`TestLock::acquire`]) and released when it is dropped —
|
||||
/// including through panics, so a failing test can never deadlock the
|
||||
/// tests that follow. Poison-tolerant: a panicking holder does not leave
|
||||
/// the mutex poisoned for the next acquirer.
|
||||
#[cfg(test)]
|
||||
pub struct TestLock {
|
||||
/// The held lock guard; dropping it releases [`TEST_LOCK`] (never read,
|
||||
/// only dropped — the whole point of the RAII guard).
|
||||
#[allow(dead_code)]
|
||||
guard: MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl TestLock {
|
||||
/// Acquire exclusive access to the crate's shared test state, blocking
|
||||
/// until every earlier holder has released it.
|
||||
pub fn acquire() -> TestLock {
|
||||
TestLock {
|
||||
guard: TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Drop for TestLock {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the held guard releases TEST_LOCK; the explicit Drop
|
||||
// documents the acquire-on-create / release-on-drop contract.
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire the process-wide test lock (see [`TestLock::acquire`]).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lock_tests() -> TestLock {
|
||||
TestLock::acquire()
|
||||
}
|
||||
|
||||
// Keep the oakffmpeg-link rlib referenced so its build script's native
|
||||
// link flags (the static FFmpeg's transitive dependencies) reach the
|
||||
// final link — rustc prunes the flags of an unreferenced rlib.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO
|
||||
//! frame conversion goes through the local
|
||||
//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping
|
||||
//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`).
|
||||
//! functions.
|
||||
//!
|
||||
//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every
|
||||
//! operation that would touch the media engine is a documented stub returning
|
||||
@@ -60,7 +60,7 @@ impl Decoder for OIIODecoder {
|
||||
fn probe(
|
||||
&self,
|
||||
_filename: &str,
|
||||
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
|
||||
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
|
||||
) -> Option<crate::footagedescription::FootageDescription> {
|
||||
// Probing is a dylib operation; without it we cannot report anything.
|
||||
None
|
||||
@@ -98,7 +98,7 @@ impl Decoder for OIIODecoder {
|
||||
fn retrieve_video(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<crate::bridge::render::OakRenderTexture> {
|
||||
) -> crate::error::Result<crate::decoder::OakRenderTexture> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Decoder for OIIODecoder {
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
|
||||
_cancelled: Option<&oakcommon::cancelatom::CancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
@@ -30,10 +30,8 @@
|
||||
//! timestamp and time base alongside the raw pixel rows, so a buffer can be
|
||||
//! turned back into an equivalent [`Frame`] without any external state.
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,
|
||||
oakcommon_videoparams_set_format,
|
||||
};
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use crate::frame::Frame;
|
||||
use oakcore_rs::Rational;
|
||||
use std::ffi::c_int;
|
||||
@@ -154,14 +152,8 @@ pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result<Vec<u8>> {
|
||||
|
||||
let (time_base_num, time_base_den) = match frame.params() {
|
||||
Some(p) => {
|
||||
let mut num = 0i64;
|
||||
let mut den = 0i64;
|
||||
// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid
|
||||
// handle; the C function only writes through the two out pointers.
|
||||
unsafe {
|
||||
oakcommon_videoparams_get_time_base(p.clone(), &mut num, &mut den);
|
||||
}
|
||||
(num, den)
|
||||
let (num, den) = p.time_base();
|
||||
(i64::from(num), i64::from(den))
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
@@ -215,26 +207,19 @@ pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
|
||||
}
|
||||
|
||||
// Build the params from the header, then hand ownership to the frame.
|
||||
// SAFETY: the init returns a live handle; the clone for `set_format` is
|
||||
// only read, and the original is moved into `Frame::with_params` (which
|
||||
// takes ownership), so there is no double release.
|
||||
let params = unsafe {
|
||||
oakcommon_videoparams_init_with_time_base(
|
||||
header.width,
|
||||
header.height,
|
||||
header.time_base_num as c_int,
|
||||
header.time_base_den as c_int,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
};
|
||||
unsafe {
|
||||
oakcommon_videoparams_set_format(params.clone(), header.format);
|
||||
}
|
||||
let mut params = VideoParams::new_with_time_base(
|
||||
header.width,
|
||||
header.height,
|
||||
header.time_base_num as c_int,
|
||||
header.time_base_den as c_int,
|
||||
OakPixelFormat::from_code(0),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
params.set_format(OakPixelFormat::from_code(header.format));
|
||||
let mut frame = Frame::with_params(params);
|
||||
frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));
|
||||
frame.allocate()?;
|
||||
@@ -351,11 +336,21 @@ mod tests {
|
||||
}
|
||||
|
||||
/// A small helper to build a fully allocated, filled frame using the same
|
||||
/// test-stub params pattern as `frame.rs`.
|
||||
/// params pattern as `frame.rs`.
|
||||
fn make_frame() -> Frame {
|
||||
// SAFETY: test-stub videoparams; ownership moves into `with_params`.
|
||||
let params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30, 0, 4, 1, 1, 0, 1) };
|
||||
unsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8
|
||||
let mut params = VideoParams::new_with_time_base(
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
30,
|
||||
OakPixelFormat::from_code(0),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
params.set_format(OakPixelFormat::from_code(0)); // U8
|
||||
let mut frame = Frame::with_params(params);
|
||||
frame.set_timestamp(Rational::new(5, 2));
|
||||
frame.allocate().unwrap();
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual
|
||||
//! transcodes are delegated to the global task submit callback
|
||||
//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy
|
||||
//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI
|
||||
//! as missing. `proxy_params_from_config` reads the oakcommon config store
|
||||
//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23
|
||||
//! / "mp4" / "veryfast" / audio included).
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
use oakcommon::configstore::ConfigStore;
|
||||
use oakcommon::filefunctions::FileFunctions;
|
||||
use std::path::Path;
|
||||
|
||||
/// Proxy state of a proxy file on disk.
|
||||
@@ -336,97 +337,35 @@ fn cstr_slice(a: &[u8; 32]) -> &str {
|
||||
|
||||
/// `oakcommon_config_get_int` wrapper (null group).
|
||||
fn config_get_int(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_int(std::ptr::null(), ckey.as_ptr(), default)
|
||||
}
|
||||
ConfigStore::instance().get_int(None, key, default)
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_bool` wrapper (null group).
|
||||
fn config_get_bool(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_bool(std::ptr::null(), ckey.as_ptr(), default)
|
||||
}
|
||||
ConfigStore::instance().get_bool(None, key, default)
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_config_get` string read; `None` when the stored
|
||||
/// value is empty or absent.
|
||||
/// `oakcommon_config_get` string read; `None` when the stored value is
|
||||
/// empty or absent.
|
||||
fn config_get_str(key: &str) -> Option<String> {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is valid; first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return None;
|
||||
match ConfigStore::instance().get(None, key) {
|
||||
Ok(s) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
|
||||
fn unique_file_identifier(filename: &str) -> String {
|
||||
let c = match CString::new(filename) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
|
||||
let id = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
|
||||
};
|
||||
format!("{}", id)
|
||||
FileFunctions::new()
|
||||
.get_unique_file_identifier(filename)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_filefunctions_get_application_path` read.
|
||||
/// `oakcommon_filefunctions_get_application_path` read.
|
||||
fn application_path() -> String {
|
||||
// # Safety: first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(std::ptr::null_mut(), 0)
|
||||
};
|
||||
if size <= 1 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
String::from_utf8_lossy(&buf[..end]).into_owned()
|
||||
}
|
||||
|
||||
/// Build a NUL-terminated C string from a Rust string; empty on embedded
|
||||
/// NUL (defensive only — callers pass sane keys).
|
||||
fn cstring(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
FileFunctions::new()
|
||||
.get_application_path()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// True when `p` is a regular file with at least one execute bit set.
|
||||
@@ -456,14 +395,6 @@ mod tests {
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
let mut h: u64 = 14695981039346656037;
|
||||
for &b in bytes {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(1099511628211);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_params_default_values() {
|
||||
@@ -503,24 +434,40 @@ mod tests {
|
||||
#[test]
|
||||
fn proxy_filename_derivation() {
|
||||
let cache = temp_subdir("fn");
|
||||
let stub_id = fnv1a64(b"media.mp4") as i64;
|
||||
let id = fnv1a64(stub_id.to_string().as_bytes()) as i64;
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
assert_eq!(f, format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id));
|
||||
// A missing source carries no unique-file identifier
|
||||
// (get_unique_file_identifier is empty for non-existent files —
|
||||
// C++ parity), so the name is the plain size/version/audio tags.
|
||||
let missing = std::path::Path::new(&temp_subdir("missing")).join("nope.mp4");
|
||||
let f = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &p).unwrap();
|
||||
assert_eq!(f, format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache));
|
||||
|
||||
// An existing source embeds a stable per-file identifier.
|
||||
let existing = std::path::Path::new(&temp_subdir("existing")).join("real.mp4");
|
||||
std::fs::write(&existing, b"media").unwrap();
|
||||
let f1 =
|
||||
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
|
||||
let f2 =
|
||||
ProxyManager::get_proxy_filename(&cache, existing.to_str().unwrap(), 0, &p).unwrap();
|
||||
assert!(
|
||||
f1.contains("-0.1280x720.v1.a1.mp4"),
|
||||
"size/version/audio tags present: {f1}"
|
||||
);
|
||||
assert!(f1 != format!("{}/proxy/-0.1280x720.v1.a1.mp4", cache), "id embedded: {f1}");
|
||||
assert_eq!(f1, f2, "the identifier is stable for the same file");
|
||||
|
||||
// Divider mode tags the divider instead of an absolute size.
|
||||
let mut d = p.clone();
|
||||
d.divider = 2;
|
||||
let f2 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &d).unwrap();
|
||||
assert!(f2.contains(".div2."));
|
||||
let f3 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &d).unwrap();
|
||||
assert!(f3.contains(".div2."));
|
||||
|
||||
// No audio.
|
||||
let mut na = p.clone();
|
||||
na.include_audio = 0;
|
||||
let f3 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &na).unwrap();
|
||||
assert!(f3.contains(".a0."));
|
||||
let f4 = ProxyManager::get_proxy_filename(&cache, missing.to_str().unwrap(), 0, &na).unwrap();
|
||||
assert!(f4.contains(".a0."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -21,16 +21,13 @@
|
||||
//! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`.
|
||||
//!
|
||||
//! They live inside the crate (not `tests/`) because the crate's
|
||||
//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the
|
||||
//! `Frame`/`FootageDescription` paths need — are only linked for the lib
|
||||
//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot
|
||||
//! resolve those symbols; see `tests/ffi_contract_test.rs`).
|
||||
//! `#[cfg(test)]` in-memory stubs — which the `Frame`/`FootageDescription`
|
||||
//! paths need — are only linked for the lib test binary (`tests/` is
|
||||
//! compiled without `#[cfg(test)]` and cannot resolve those symbols; see
|
||||
//! `tests/ffi_contract_test.rs`).
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate,
|
||||
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
|
||||
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
|
||||
};
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use crate::decoder::{
|
||||
CodecStream, Decoder, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
|
||||
K_COLOR_RANGE_DEFAULT,
|
||||
@@ -80,8 +77,8 @@ fn h264_params(out: &std::path::Path) -> crate::encodingparams::EncodingParams {
|
||||
|
||||
/// Build an allocated F32-RGBA frame with a moving color pattern.
|
||||
fn pattern_frame(i: i32) -> Frame {
|
||||
let vp = unsafe { oakcommon_videoparams_init_basic(64, 64, 0, 4, 1, 1, 0, 1) };
|
||||
unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };
|
||||
let mut vp = VideoParams::new_basic(64, 64, OakPixelFormat::from_code(0), 4, 1, 1, 0, 1);
|
||||
vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
|
||||
let mut f = Frame::with_params(vp);
|
||||
f.set_timestamp(Rational::new(i as i64, 10));
|
||||
f.allocate().unwrap();
|
||||
@@ -117,20 +114,10 @@ fn probe_reports_streams_and_duration() {
|
||||
|
||||
// Video stream: 1920x1080, 25fps, 17s at 1/12800 time base.
|
||||
let vp = desc.get_video_stream(0).expect("video stream");
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920);
|
||||
assert_eq!(
|
||||
unsafe { oakcommon_videoparams_get_height(vp.clone()) },
|
||||
1080
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcommon_videoparams_get_duration(vp.clone()) },
|
||||
17 * 12800
|
||||
);
|
||||
|
||||
let mut num: i32 = 0;
|
||||
let mut den: i32 = 0;
|
||||
unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut num, &mut den) };
|
||||
assert_eq!((num, den), (25, 1));
|
||||
assert_eq!(vp.width(), 1920);
|
||||
assert_eq!(vp.height(), 1080);
|
||||
assert_eq!(vp.duration(), 17 * 12800);
|
||||
assert_eq!(vp.frame_rate(), (25, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -220,8 +207,8 @@ fn encode_h264_roundtrip_to_tmp() {
|
||||
let desc = d.probe(&out_str, None).expect("probe round-trip output");
|
||||
assert_eq!(desc.video_stream_count(), 1);
|
||||
let vp = desc.get_video_stream(0).expect("video stream");
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64);
|
||||
assert_eq!(vp.width(), 64);
|
||||
assert_eq!(vp.height(), 64);
|
||||
|
||||
// Decode the first frame of the result.
|
||||
let s = CodecStream::with_block(out_str.clone(), 0, None);
|
||||
|
||||
@@ -37,11 +37,10 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use oakcommon::ocioutils::PixelFormat as OakPixelFormat;
|
||||
use oakcommon::videoparams::VideoParams;
|
||||
use oakcore_rs::{PixelFormat, Rational, SampleFormat};
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
|
||||
};
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::encoder::create_from_params;
|
||||
use crate::frame::Frame;
|
||||
@@ -106,8 +105,17 @@ pub fn write_test_clip(
|
||||
|
||||
/// One frame of the known pattern (see module doc).
|
||||
fn pattern_frame(i: i32, width: i32, height: i32, fps: i32) -> Frame {
|
||||
let vp = unsafe { oakcommon_videoparams_init_basic(width, height, 0, 4, 1, 1, 0, 1) };
|
||||
unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };
|
||||
let mut vp = VideoParams::new_basic(
|
||||
width,
|
||||
height,
|
||||
OakPixelFormat::from_code(0),
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
vp.set_format(OakPixelFormat::from_code(PixelFormat::F32 as i32));
|
||||
let mut f = Frame::with_params(vp);
|
||||
f.set_timestamp(Rational::new(i as i64, fps as i64));
|
||||
f.allocate().expect("test frame allocation");
|
||||
|
||||
@@ -729,7 +729,7 @@ mod tests {
|
||||
assert_eq!(format_g(-0.0), "-0");
|
||||
assert_eq!(format_g(1920.0), "1920");
|
||||
assert_eq!(format_g(100.0), "100");
|
||||
assert_eq!(format_g(3.14), "3.14");
|
||||
assert_eq!(format_g(3.15), "3.15");
|
||||
assert_eq!(format_g(1.5), "1.5");
|
||||
assert_eq!(format_g(0.1), "0.1");
|
||||
assert_eq!(format_g(-3.5), "-3.5");
|
||||
@@ -848,9 +848,9 @@ mod tests {
|
||||
let _g = test_lock().lock().unwrap();
|
||||
let s = ConfigStore::instance();
|
||||
s.reset_defaults().unwrap();
|
||||
s.set_double(None, "MyD", 3.14);
|
||||
assert_eq!(s.get_double(None, "MyD", -1.0), 3.14);
|
||||
assert_eq!(s.get(None, "MyD").unwrap(), "3.14");
|
||||
s.set_double(None, "MyD", 3.15);
|
||||
assert_eq!(s.get_double(None, "MyD", -1.0), 3.15);
|
||||
assert_eq!(s.get(None, "MyD").unwrap(), "3.15");
|
||||
assert_eq!(s.entry_type(None, "MyD").unwrap(), EntryType::Double);
|
||||
|
||||
assert_eq!(s.get_double(None, "MissingD", 2.5), 2.5);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,6 @@ pub mod commandlineparser;
|
||||
pub mod configstore;
|
||||
pub mod debug;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod ffmpegutils;
|
||||
pub mod filefunctions;
|
||||
pub mod handle;
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct Subtitle {
|
||||
}
|
||||
|
||||
/// `olive::SubtitleParams` — a handle-wrapped subtitle parameter set.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubtitleParams {
|
||||
/// Stream index within the source file.
|
||||
stream_index: i32,
|
||||
|
||||
@@ -56,7 +56,7 @@ pub enum ColorRange {
|
||||
}
|
||||
|
||||
/// `olive::VideoParams` — a handle-wrapped video parameter set.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VideoParams {
|
||||
/// Frame width in pixels.
|
||||
width: i32,
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI-level integration tests for the C-ABI `colortransform` exports in
|
||||
//! `oakcommon::ffi::colortransform`, asserted against the C++ oracle
|
||||
//! `src/common/c_api/colortransform.cpp` and the Rust domain
|
||||
//! `src/common/rust/src/colortransform.rs`.
|
||||
//!
|
||||
//! The crate exports the two constructors, `free`, `is_display`, and the
|
||||
//! four two-stage getters; only the C++-typed `init_from_native` /
|
||||
//! `get_native` adapters stay with the C++ adapter layer. The tests
|
||||
//! cover:
|
||||
//! - constructor success: a stamped (`OAKCOMMON_ABI_VERSION`), non-null
|
||||
//! handle with live `addref`/`release` callbacks, whose boxed
|
||||
//! [`ColorTransform`] carries the right fields (peeked via
|
||||
//! `oakcommon::handle::get`);
|
||||
//! - constructor failure: a null string argument yields a null handle
|
||||
//! (guard semantics: any error or panic also collapses to a null
|
||||
//! handle);
|
||||
//! - the free contract (release once, nullify, no-op on null);
|
||||
//! - `is_display` / `get_display` / `get_output` / `get_view` /
|
||||
//! `get_look` happy paths, two-stage short-buffer behavior, and
|
||||
//! empty-handle error codes;
|
||||
//! - distinct handles plus the ownership contract (init yields reference
|
||||
//! count 1; addref/release adjust it atomically; both free cleanly).
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use oakcommon::colortransform::ColorTransform;
|
||||
use oakcommon::ffi::colortransform::*;
|
||||
use oakcommon::handle::{get, CHandle, RefBox, OAKCOMMON_ABI_VERSION};
|
||||
|
||||
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
|
||||
fn to_cstring(s: &str) -> CString {
|
||||
CString::new(s).expect("test string must not contain NUL")
|
||||
}
|
||||
|
||||
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`; rebuilding
|
||||
/// from the same fields duplicates only the handle value — the box stays
|
||||
/// alive as long as the original handle lives.
|
||||
fn dup(h: &CHandle) -> CHandle {
|
||||
CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Release one reference and nullify the handle. `oakcommon_colortransform_free`
|
||||
/// is declared in the header but not exported by this crate, so the free
|
||||
/// contract is driven through this exact replica of the crate-private
|
||||
/// `free_handle` (and the C++ oracle): release once, write `CHandle::null()`.
|
||||
fn free(h: &mut CHandle) {
|
||||
if let Some(rel) = h.release {
|
||||
unsafe {
|
||||
rel(h.ctx);
|
||||
}
|
||||
}
|
||||
*h = CHandle::null();
|
||||
}
|
||||
|
||||
/// Pointer form of [`free`], mirroring `free_handle(*mut CHandle)`: a null
|
||||
/// pointer is a no-op.
|
||||
fn free_ptr(p: *mut CHandle) {
|
||||
if p.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
if let Some(h) = p.as_ref() {
|
||||
if let Some(rel) = h.release {
|
||||
rel(h.ctx);
|
||||
}
|
||||
}
|
||||
p.write(CHandle::null());
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the reference count behind an owned handle (test-only peek into
|
||||
/// the crate-public `RefBox` layout; the box is guaranteed alive here).
|
||||
unsafe fn refs_of(h: &CHandle) -> u32 {
|
||||
unsafe {
|
||||
(*(h.ctx as *const RefBox<ColorTransform>))
|
||||
.refs
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// A successful `init_output` yields a stamped, non-null handle whose boxed
|
||||
/// transform is an output transform with the requested name; `free`
|
||||
/// nullifies it.
|
||||
#[test]
|
||||
fn init_output_success() {
|
||||
let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(h.addref.is_some());
|
||||
assert!(h.release.is_some());
|
||||
|
||||
let t: &ColorTransform = unsafe { get::<ColorTransform>(&h) }.expect("boxed transform");
|
||||
assert!(!t.is_display());
|
||||
assert_eq!(t.output(), "sRGB");
|
||||
assert_eq!(t.display(), "");
|
||||
assert_eq!(t.view(), "");
|
||||
assert_eq!(t.look(), "");
|
||||
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
/// A null output string yields a null handle with no callbacks.
|
||||
#[test]
|
||||
fn init_output_null_yields_null_handle() {
|
||||
let h = oakcommon_colortransform_init_output(std::ptr::null::<c_char>());
|
||||
assert!(h.is_null());
|
||||
assert!(h.ctx.is_null());
|
||||
assert!(h.addref.is_none());
|
||||
assert!(h.release.is_none());
|
||||
// The shared `null()` stamps no ABI version (single-lib unification).
|
||||
assert_eq!(h.abi_version, 0);
|
||||
}
|
||||
|
||||
/// An empty output name is a valid value: the transform is still an output
|
||||
/// transform (C++ default-constructor parity).
|
||||
#[test]
|
||||
fn init_output_empty_string_succeeds() {
|
||||
let mut h = oakcommon_colortransform_init_output(to_cstring("").as_ptr());
|
||||
assert!(!h.is_null());
|
||||
let t: &ColorTransform = unsafe { get::<ColorTransform>(&h) }.expect("boxed transform");
|
||||
assert!(!t.is_display());
|
||||
assert_eq!(t.output(), "");
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
/// A successful `init_display` yields a stamped, non-null handle whose
|
||||
/// boxed transform carries the display/view/look names.
|
||||
#[test]
|
||||
fn init_display_success() {
|
||||
let mut h = oakcommon_colortransform_init_display(
|
||||
to_cstring("DCI-P3").as_ptr(),
|
||||
to_cstring("standard").as_ptr(),
|
||||
to_cstring("soft").as_ptr(),
|
||||
);
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(h.addref.is_some());
|
||||
assert!(h.release.is_some());
|
||||
|
||||
let t: &ColorTransform = unsafe { get::<ColorTransform>(&h) }.expect("boxed transform");
|
||||
assert!(t.is_display());
|
||||
assert_eq!(t.display(), "DCI-P3");
|
||||
assert_eq!(t.view(), "standard");
|
||||
assert_eq!(t.look(), "soft");
|
||||
assert_eq!(t.output(), "");
|
||||
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
/// Any single null argument — and all-null — yields a null handle.
|
||||
#[test]
|
||||
fn init_display_null_any_arg_yields_null_handle() {
|
||||
let d = to_cstring("DCI-P3");
|
||||
let v = to_cstring("standard");
|
||||
let l = to_cstring("soft");
|
||||
let cases = [
|
||||
(d.as_ptr(), v.as_ptr(), std::ptr::null::<c_char>()),
|
||||
(d.as_ptr(), std::ptr::null::<c_char>(), l.as_ptr()),
|
||||
(std::ptr::null::<c_char>(), v.as_ptr(), l.as_ptr()),
|
||||
(
|
||||
std::ptr::null::<c_char>(),
|
||||
std::ptr::null::<c_char>(),
|
||||
std::ptr::null::<c_char>(),
|
||||
),
|
||||
];
|
||||
for (display, view, look) in cases {
|
||||
let h = oakcommon_colortransform_init_display(display, view, look);
|
||||
assert!(h.is_null());
|
||||
assert!(h.ctx.is_null());
|
||||
assert!(h.addref.is_none());
|
||||
assert!(h.release.is_none());
|
||||
// The shared `null()` stamps no ABI version (single-lib unification).
|
||||
assert_eq!(h.abi_version, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty display/view/look strings are valid; the explicit `is_display`
|
||||
/// flag keeps the transform identifiable as a display transform.
|
||||
#[test]
|
||||
fn init_display_empty_strings_succeed() {
|
||||
let mut h = oakcommon_colortransform_init_display(
|
||||
to_cstring("").as_ptr(),
|
||||
to_cstring("").as_ptr(),
|
||||
to_cstring("").as_ptr(),
|
||||
);
|
||||
assert!(!h.is_null());
|
||||
let t: &ColorTransform = unsafe { get::<ColorTransform>(&h) }.expect("boxed transform");
|
||||
assert!(t.is_display());
|
||||
assert_eq!(t.display(), "");
|
||||
assert_eq!(t.view(), "");
|
||||
assert_eq!(t.look(), "");
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
/// The free contract: nullify, idempotent, and no-op on a null handle or a
|
||||
/// null pointer.
|
||||
#[test]
|
||||
fn free_contract() {
|
||||
// A real handle: free nullifies it and a second free is safe.
|
||||
let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
assert!(!h.is_null());
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
assert!(h.ctx.is_null());
|
||||
assert!(h.release.is_none());
|
||||
free(&mut h);
|
||||
assert!(h.is_null());
|
||||
|
||||
// Freeing an already-null handle is a no-op.
|
||||
free(&mut CHandle::null());
|
||||
|
||||
// Freeing a null pointer is a no-op.
|
||||
free_ptr(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
/// Two output transforms are distinct handles (different boxes); init
|
||||
/// yields reference count 1, addref/release adjust it atomically, and both
|
||||
/// free cleanly. `dup` copies the handle value but shares the same box.
|
||||
#[test]
|
||||
fn init_outputs_are_distinct_and_release_cleanly() {
|
||||
let mut a = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
let mut b = oakcommon_colortransform_init_output(to_cstring("Display P3").as_ptr());
|
||||
assert!(!a.is_null());
|
||||
assert!(!b.is_null());
|
||||
assert_ne!(a.ctx, b.ctx);
|
||||
|
||||
// dup copies the handle value and shares the same underlying box.
|
||||
let c = dup(&a);
|
||||
assert_eq!(c.ctx, a.ctx);
|
||||
|
||||
// Ownership contract: count 1 at init, atomic addref/release.
|
||||
unsafe {
|
||||
assert_eq!(refs_of(&a), 1);
|
||||
assert_eq!(refs_of(&b), 1);
|
||||
(a.addref.unwrap())(a.ctx);
|
||||
assert_eq!(refs_of(&a), 2);
|
||||
(a.release.unwrap())(a.ctx);
|
||||
assert_eq!(refs_of(&a), 1);
|
||||
}
|
||||
|
||||
// Both release cleanly (single release + nullify, no double-free).
|
||||
free(&mut a);
|
||||
assert!(a.is_null());
|
||||
free(&mut b);
|
||||
assert!(b.is_null());
|
||||
}
|
||||
|
||||
/// `is_display` distinguishes display transforms from output transforms
|
||||
/// and reports OAKCOMMON_E_INVALID on an empty handle.
|
||||
#[test]
|
||||
fn is_display_paths() {
|
||||
let mut d = oakcommon_colortransform_init_display(
|
||||
to_cstring("sRGB").as_ptr(),
|
||||
to_cstring("standard").as_ptr(),
|
||||
to_cstring("").as_ptr(),
|
||||
);
|
||||
let mut o = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
assert_eq!(oakcommon_colortransform_is_display(dup(&d)), 1);
|
||||
assert_eq!(oakcommon_colortransform_is_display(dup(&o)), 0);
|
||||
assert_eq!(
|
||||
oakcommon_colortransform_is_display(CHandle::null()),
|
||||
oakcommon::error::OAKCOMMON_E_INVALID
|
||||
);
|
||||
free(&mut d);
|
||||
free(&mut o);
|
||||
}
|
||||
|
||||
/// Two-stage getters: exact-fit returns content, short buffer leaves the
|
||||
/// buffer untouched but reports the required size, empty handle yields
|
||||
/// OAKCOMMON_E_INVALID.
|
||||
#[test]
|
||||
fn getters_two_stage_and_empty_handle() {
|
||||
let mut d = oakcommon_colortransform_init_display(
|
||||
to_cstring("P3").as_ptr(),
|
||||
to_cstring("std").as_ptr(),
|
||||
to_cstring("soft").as_ptr(),
|
||||
);
|
||||
|
||||
let mut buf = [0i8; 32];
|
||||
let need = oakcommon_colortransform_get_display(dup(&d), buf.as_mut_ptr(), 32);
|
||||
assert_eq!(need, 3); // "P3" + NUL
|
||||
assert_eq!(
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"P3"
|
||||
);
|
||||
|
||||
let need = oakcommon_colortransform_get_view(dup(&d), buf.as_mut_ptr(), 2);
|
||||
assert_eq!(need, 4); // "std" + NUL, too small: nothing written
|
||||
|
||||
let need = oakcommon_colortransform_get_look(dup(&d), buf.as_mut_ptr(), 32);
|
||||
assert_eq!(need, 5);
|
||||
assert_eq!(
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"soft"
|
||||
);
|
||||
|
||||
let o = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
let need = oakcommon_colortransform_get_output(o, buf.as_mut_ptr(), 32);
|
||||
assert_eq!(need, 5);
|
||||
assert_eq!(
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"sRGB"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_colortransform_get_display(CHandle::null(), buf.as_mut_ptr(), 32),
|
||||
oakcommon::error::OAKCOMMON_E_INVALID
|
||||
);
|
||||
free(&mut d);
|
||||
}
|
||||
|
||||
/// The real `oakcommon_colortransform_free` export: releases once,
|
||||
/// nullifies, no-ops on a null pointer.
|
||||
#[test]
|
||||
fn free_export_contract() {
|
||||
let mut h = oakcommon_colortransform_init_output(to_cstring("sRGB").as_ptr());
|
||||
assert!(!h.is_null());
|
||||
oakcommon_colortransform_free(&mut h);
|
||||
assert!(h.is_null());
|
||||
oakcommon_colortransform_free(&mut h); // already null: no-op
|
||||
oakcommon_colortransform_free(std::ptr::null_mut()); // null pointer: no-op
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,519 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI integration tests for the `config` submodule of the C ABI layer
|
||||
//! (`oakcommon::ffi::config`, backed by `include/common/config.h`).
|
||||
//!
|
||||
//! Every exported symbol is exercised with both a success and a failure
|
||||
//! path. The store is a process-wide singleton and the C-ABI `config`
|
||||
//! functions have no handle, so the failure paths are the documented
|
||||
//! behaviors: null/empty keys, invalid buffers, and absent entries.
|
||||
//!
|
||||
//! Tests use per-test unique keys so they stay independent of each other
|
||||
//! and of the domain unit tests, and every test that mutates the singleton
|
||||
//! (or the `OAK_CONFIG_DIR` env override) serializes on a local mutex. The
|
||||
//! crate's `test_support::env_lock` is `#[cfg(test)]` and therefore not
|
||||
//! visible to this integration-test crate, and the domain unit tests run in
|
||||
//! a separate process anyway, so a local lock is sufficient.
|
||||
|
||||
use std::ffi::{c_char, c_void, CStr, CString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ptr::{null, null_mut};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use oakcommon::error::{
|
||||
OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK,
|
||||
};
|
||||
use oakcommon::ffi::config::*;
|
||||
|
||||
// ---- Test infrastructure --------------------------------------------------
|
||||
|
||||
/// Serializes tests that mutate the process-wide singleton store or the
|
||||
/// `OAK_CONFIG_DIR` env override. Without this, a `reset_defaults`/`load`
|
||||
/// running between another test's `set` and `get` would clear its key.
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn lock() -> MutexGuard<'static, ()> {
|
||||
LOCK.lock().unwrap()
|
||||
}
|
||||
|
||||
static KEY_SEQ: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// A per-test unique key, so parallel tests cannot collide on the shared
|
||||
/// store.
|
||||
fn unique_key(name: &str) -> CString {
|
||||
let n = KEY_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
CString::new(format!("itest_{}_{}_{}", name, std::process::id(), n)).unwrap()
|
||||
}
|
||||
|
||||
fn cstr(s: &str) -> CString {
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
/// Cleans up the `OAK_CONFIG_DIR` override and its temp dir even when the
|
||||
/// test body panics.
|
||||
struct TempConfigDir(PathBuf);
|
||||
|
||||
impl Drop for TempConfigDir {
|
||||
fn drop(&mut self) {
|
||||
std::env::remove_var("OAK_CONFIG_DIR");
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the process-global error handler on drop so a panicking test
|
||||
/// cannot leak it to the rest of the suite.
|
||||
struct HandlerGuard;
|
||||
|
||||
impl Drop for HandlerGuard {
|
||||
fn drop(&mut self) {
|
||||
oakcommon_config_set_error_handler(None, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
|
||||
/// Point `OAK_CONFIG_DIR` at an isolated temp dir, run `f`, then clean up.
|
||||
/// Mirrors the `with_temp_config` pattern of the domain unit tests.
|
||||
fn with_temp_config<T>(f: impl FnOnce(&Path) -> T) -> T {
|
||||
let _guard = lock();
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakcommon_ffi_config_test_{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let _cleanup = TempConfigDir(dir.clone());
|
||||
f(&dir)
|
||||
}
|
||||
|
||||
// ---- Error handler recording ---------------------------------------------
|
||||
|
||||
static HANDLER_HITS: AtomicUsize = AtomicUsize::new(0);
|
||||
static HANDLER_USERDATA: AtomicUsize = AtomicUsize::new(0);
|
||||
static HANDLER_TITLE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
unsafe extern "C" fn record_handler(
|
||||
title: *const c_char,
|
||||
_message: *const c_char,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
HANDLER_HITS.fetch_add(1, Ordering::SeqCst);
|
||||
HANDLER_USERDATA.store(userdata as usize, Ordering::SeqCst);
|
||||
if !title.is_null() {
|
||||
let s = unsafe { CStr::from_ptr(title) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
*HANDLER_TITLE.lock().unwrap() = Some(s);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- load / save ----------------------------------------------------------
|
||||
|
||||
/// `oakcommon_config_load` treats a missing `config.ini` as a non-error.
|
||||
#[test]
|
||||
fn config_load_missing_file_is_ok() {
|
||||
with_temp_config(|_| {
|
||||
assert_eq!(oakcommon_config_load(), OAKCOMMON_OK);
|
||||
});
|
||||
}
|
||||
|
||||
/// `save` persists the store; `load` re-reads it (resetting to defaults
|
||||
/// first), so the round trip restores the custom key.
|
||||
#[test]
|
||||
fn config_save_load_roundtrip() {
|
||||
with_temp_config(|_| {
|
||||
let key = unique_key("save_roundtrip");
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr("persisted").as_ptr());
|
||||
assert_eq!(oakcommon_config_save(), OAKCOMMON_OK);
|
||||
assert_eq!(oakcommon_config_load(), OAKCOMMON_OK);
|
||||
let mut buf = [0i8; 32];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 32);
|
||||
assert_eq!(n, 10); // "persisted" + NUL
|
||||
assert_eq!(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
|
||||
"persisted"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// A save that cannot write reports through the registered error handler
|
||||
/// and returns `OAKCOMMON_E_FAILED`. `OAK_CONFIG_DIR` points at a regular
|
||||
/// file so `"<dir>/config.ini.tmp"` cannot be created (mirrors the domain
|
||||
/// unit test).
|
||||
#[test]
|
||||
fn config_save_failure_reports_and_returns_failed() {
|
||||
let _guard = lock();
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcommon_ffi_config_blocked_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let blocker = dir.join("not_a_dir");
|
||||
std::fs::write(&blocker, b"x").unwrap();
|
||||
std::env::set_var("OAK_CONFIG_DIR", &blocker);
|
||||
let _cleanup = TempConfigDir(dir);
|
||||
|
||||
HANDLER_HITS.store(0, Ordering::SeqCst);
|
||||
HANDLER_USERDATA.store(0, Ordering::SeqCst);
|
||||
*HANDLER_TITLE.lock().unwrap() = None;
|
||||
let userdata: usize = 0xdead_beef;
|
||||
let _handler_guard = HandlerGuard;
|
||||
assert_eq!(
|
||||
oakcommon_config_set_error_handler(Some(record_handler), userdata as *mut c_void),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
|
||||
assert_eq!(oakcommon_config_save(), OAKCOMMON_E_FAILED);
|
||||
assert_eq!(HANDLER_HITS.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(HANDLER_USERDATA.load(Ordering::SeqCst), userdata);
|
||||
assert_eq!(
|
||||
HANDLER_TITLE.lock().unwrap().as_deref(),
|
||||
Some("Error saving settings")
|
||||
);
|
||||
}
|
||||
|
||||
// ---- reset_defaults -------------------------------------------------------
|
||||
|
||||
/// `reset_defaults` drops custom keys and restores the compiled-in
|
||||
/// defaults.
|
||||
#[test]
|
||||
fn config_reset_defaults_restores_defaults() {
|
||||
let _guard = lock();
|
||||
let custom = unique_key("reset_custom");
|
||||
oakcommon_config_set_int(null(), custom.as_ptr(), 1);
|
||||
assert_eq!(oakcommon_config_get_int(null(), custom.as_ptr(), -1), 1);
|
||||
|
||||
assert_eq!(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
|
||||
|
||||
// The custom key is gone...
|
||||
assert_eq!(oakcommon_config_get_int(null(), custom.as_ptr(), -1), -1);
|
||||
assert_eq!(
|
||||
oakcommon_config_entry_type(null(), custom.as_ptr()),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
// ...and the compiled-in defaults are back.
|
||||
assert_eq!(
|
||||
oakcommon_config_get_int(null(), cstr("DefaultSequenceWidth").as_ptr(), -1),
|
||||
1920
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_config_get_bool(null(), cstr("UseProxyMedia").as_ptr(), -1),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// ---- string set / get -----------------------------------------------------
|
||||
|
||||
/// Null keys/values are silently ignored by `oakcommon_config_set`.
|
||||
#[test]
|
||||
fn config_set_null_key_is_noop() {
|
||||
let _guard = lock();
|
||||
oakcommon_config_set(null(), null(), cstr("v").as_ptr());
|
||||
oakcommon_config_set(null(), cstr("k").as_ptr(), null());
|
||||
}
|
||||
|
||||
/// Two-stage getter, stage 1: a null buffer with size 0 returns the
|
||||
/// required size (length + NUL) without writing.
|
||||
#[test]
|
||||
fn config_get_size_query_returns_required() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("size_query");
|
||||
let value = "hello";
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr());
|
||||
let required = oakcommon_config_get(null(), key.as_ptr(), null_mut(), 0);
|
||||
assert_eq!(required, value.len() as i32 + 1);
|
||||
}
|
||||
|
||||
/// Two-stage getter, short buffer: the full required size is returned but
|
||||
/// the buffer is NOT touched (non-truncating, unlike `copy_setting`).
|
||||
#[test]
|
||||
fn config_get_short_buffer_is_non_truncating() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("short_buf");
|
||||
let value = "hello world"; // required = 12
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr());
|
||||
let mut buf = [0x7a as c_char; 8];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), buf.len() as i32);
|
||||
assert_eq!(n, value.len() as i32 + 1);
|
||||
assert_eq!(buf, [0x7a as c_char; 8]); // sentinel untouched
|
||||
}
|
||||
|
||||
/// Two-stage getter, exact fit: a buffer of exactly `len + 1` receives the
|
||||
/// string plus NUL terminator.
|
||||
#[test]
|
||||
fn config_get_exact_fit_buffer() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("exact_fit");
|
||||
let value = "hello";
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr(value).as_ptr());
|
||||
let required = value.len() as i32 + 1;
|
||||
let mut buf = vec![0i8; required as usize];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), required);
|
||||
assert_eq!(n, required);
|
||||
assert_eq!(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
/// A missing key yields `OAKCOMMON_E_NOT_FOUND`.
|
||||
#[test]
|
||||
fn config_get_missing_key_returns_not_found() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("missing");
|
||||
let mut buf = [0i8; 16];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), buf.len() as i32);
|
||||
assert_eq!(n, OAKCOMMON_E_NOT_FOUND);
|
||||
}
|
||||
|
||||
/// A null key yields `OAKCOMMON_E_INVALID`.
|
||||
#[test]
|
||||
fn config_get_null_key_returns_invalid() {
|
||||
let n = oakcommon_config_get(null(), null(), null_mut(), 0);
|
||||
assert_eq!(n, OAKCOMMON_E_INVALID);
|
||||
}
|
||||
|
||||
/// An empty key is invalid (matches the C++ `is_valid_key` check, handled
|
||||
/// here by the domain layer).
|
||||
#[test]
|
||||
fn config_get_empty_key_returns_invalid() {
|
||||
let mut buf = [0i8; 8];
|
||||
let n = oakcommon_config_get(null(), cstr("").as_ptr(), buf.as_mut_ptr(), 8);
|
||||
assert_eq!(n, OAKCOMMON_E_INVALID);
|
||||
}
|
||||
|
||||
/// A negative buffer size is an invalid output buffer.
|
||||
#[test]
|
||||
fn config_get_invalid_buffer_returns_invalid() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("bad_buf");
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr("v").as_ptr());
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), null_mut(), -1);
|
||||
assert_eq!(n, OAKCOMMON_E_INVALID);
|
||||
}
|
||||
|
||||
// ---- int / int64 ----------------------------------------------------------
|
||||
|
||||
/// INT set/get round trip; the string getter formats the int.
|
||||
#[test]
|
||||
fn config_set_get_int_roundtrip() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("int");
|
||||
oakcommon_config_set_int(null(), key.as_ptr(), 42);
|
||||
assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 42);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 2);
|
||||
|
||||
let mut buf = [0i8; 16];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 16);
|
||||
assert_eq!(n, 3); // "42" + NUL
|
||||
assert_eq!(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
|
||||
"42"
|
||||
);
|
||||
}
|
||||
|
||||
/// INT getter fallback paths: absent key, wrong type, null key.
|
||||
#[test]
|
||||
fn config_get_int_fallback_paths() {
|
||||
let _guard = lock();
|
||||
let missing = unique_key("int_missing");
|
||||
let wrong = unique_key("int_wrongtype");
|
||||
oakcommon_config_set(null(), wrong.as_ptr(), cstr("abc").as_ptr());
|
||||
assert_eq!(oakcommon_config_get_int(null(), missing.as_ptr(), 7), 7);
|
||||
assert_eq!(oakcommon_config_get_int(null(), wrong.as_ptr(), 7), 7);
|
||||
assert_eq!(oakcommon_config_get_int(null(), null(), 9), 9);
|
||||
}
|
||||
|
||||
/// Setting a string onto an existing typed entry parses it into that type;
|
||||
/// an unparseable string leaves the entry unchanged (CPP-PARITY with
|
||||
/// `config.cpp`).
|
||||
#[test]
|
||||
fn config_set_string_parses_into_typed_entry() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("typed_parse");
|
||||
oakcommon_config_set_int(null(), key.as_ptr(), 42);
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr("7").as_ptr());
|
||||
assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 7);
|
||||
oakcommon_config_set(null(), key.as_ptr(), cstr("notanumber").as_ptr());
|
||||
assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), 7);
|
||||
}
|
||||
|
||||
/// INT64 set/get round trip and fallback paths.
|
||||
#[test]
|
||||
fn config_set_get_int64_roundtrip() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("int64");
|
||||
let big: i64 = 3_000_000_000; // exceeds i32 range
|
||||
oakcommon_config_set_int64(null(), key.as_ptr(), big);
|
||||
assert_eq!(oakcommon_config_get_int64(null(), key.as_ptr(), -1), big);
|
||||
|
||||
let missing = unique_key("int64_missing");
|
||||
assert_eq!(
|
||||
oakcommon_config_get_int64(null(), missing.as_ptr(), -99),
|
||||
-99
|
||||
);
|
||||
assert_eq!(oakcommon_config_get_int64(null(), null(), -99), -99);
|
||||
}
|
||||
|
||||
// ---- double ---------------------------------------------------------------
|
||||
|
||||
/// DOUBLE set/get round trip and fallback paths.
|
||||
#[test]
|
||||
fn config_set_get_double_roundtrip() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("double");
|
||||
oakcommon_config_set_double(null(), key.as_ptr(), 3.5);
|
||||
assert_eq!(oakcommon_config_get_double(null(), key.as_ptr(), -1.0), 3.5);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 3);
|
||||
|
||||
let missing = unique_key("double_missing");
|
||||
let wrong = unique_key("double_wrongtype");
|
||||
oakcommon_config_set_int(null(), wrong.as_ptr(), 1);
|
||||
assert_eq!(
|
||||
oakcommon_config_get_double(null(), missing.as_ptr(), 2.5),
|
||||
2.5
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_config_get_double(null(), wrong.as_ptr(), 2.5),
|
||||
2.5
|
||||
);
|
||||
assert_eq!(oakcommon_config_get_double(null(), null(), 2.5), 2.5);
|
||||
}
|
||||
|
||||
// ---- bool -----------------------------------------------------------------
|
||||
|
||||
/// BOOL set/get round trip (0/1) and string formatting as "true"/"false".
|
||||
#[test]
|
||||
fn config_set_get_bool_roundtrip() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("bool");
|
||||
oakcommon_config_set_bool(null(), key.as_ptr(), 1);
|
||||
assert_eq!(oakcommon_config_get_bool(null(), key.as_ptr(), -1), 1);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), key.as_ptr()), 4);
|
||||
|
||||
let mut buf = [0i8; 8];
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 8);
|
||||
assert_eq!(n, 5); // "true" + NUL
|
||||
assert_eq!(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
|
||||
"true"
|
||||
);
|
||||
|
||||
oakcommon_config_set_bool(null(), key.as_ptr(), 0);
|
||||
assert_eq!(oakcommon_config_get_bool(null(), key.as_ptr(), -1), 0);
|
||||
let n = oakcommon_config_get(null(), key.as_ptr(), buf.as_mut_ptr(), 8);
|
||||
assert_eq!(n, 6); // "false" + NUL
|
||||
assert_eq!(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
|
||||
"false"
|
||||
);
|
||||
}
|
||||
|
||||
/// BOOL getter fallback paths: absent key, wrong type, null key.
|
||||
#[test]
|
||||
fn config_get_bool_fallback_paths() {
|
||||
let _guard = lock();
|
||||
let missing = unique_key("bool_missing");
|
||||
let wrong = unique_key("bool_wrongtype");
|
||||
oakcommon_config_set_int(null(), wrong.as_ptr(), 1);
|
||||
assert_eq!(oakcommon_config_get_bool(null(), missing.as_ptr(), 1), 1);
|
||||
assert_eq!(oakcommon_config_get_bool(null(), wrong.as_ptr(), 0), 0);
|
||||
assert_eq!(oakcommon_config_get_bool(null(), null(), 1), 1);
|
||||
}
|
||||
|
||||
/// Null keys are silently ignored by every typed setter.
|
||||
#[test]
|
||||
fn config_typed_setters_null_key_are_noop() {
|
||||
let _guard = lock();
|
||||
oakcommon_config_set_int(null(), null(), 1);
|
||||
oakcommon_config_set_int64(null(), null(), 1);
|
||||
oakcommon_config_set_bool(null(), null(), 1);
|
||||
oakcommon_config_set_double(null(), null(), 1.0);
|
||||
}
|
||||
|
||||
// ---- groups ---------------------------------------------------------------
|
||||
|
||||
/// A non-empty group prefixes the stored key (`group/key`), so the flat
|
||||
/// lookup must not find it.
|
||||
#[test]
|
||||
fn config_group_prefixes_keys() {
|
||||
let _guard = lock();
|
||||
let key = unique_key("grouped");
|
||||
let group = cstr("ffi_test_group");
|
||||
oakcommon_config_set_int(group.as_ptr(), key.as_ptr(), 5);
|
||||
assert_eq!(oakcommon_config_get_int(null(), key.as_ptr(), -1), -1);
|
||||
assert_eq!(
|
||||
oakcommon_config_get_int(group.as_ptr(), key.as_ptr(), -1),
|
||||
5
|
||||
);
|
||||
assert_eq!(oakcommon_config_entry_type(group.as_ptr(), key.as_ptr()), 2);
|
||||
}
|
||||
|
||||
// ---- entry_type -----------------------------------------------------------
|
||||
|
||||
/// Entry type codes: 1 = String, 2 = Int, 3 = Double, 4 = Bool.
|
||||
#[test]
|
||||
fn config_entry_type_codes() {
|
||||
let _guard = lock();
|
||||
let s = unique_key("et_str");
|
||||
let i = unique_key("et_int");
|
||||
let d = unique_key("et_double");
|
||||
let b = unique_key("et_bool");
|
||||
oakcommon_config_set(null(), s.as_ptr(), cstr("x").as_ptr());
|
||||
oakcommon_config_set_int(null(), i.as_ptr(), 1);
|
||||
oakcommon_config_set_double(null(), d.as_ptr(), 1.0);
|
||||
oakcommon_config_set_bool(null(), b.as_ptr(), 1);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), s.as_ptr()), 1);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), i.as_ptr()), 2);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), d.as_ptr()), 3);
|
||||
assert_eq!(oakcommon_config_entry_type(null(), b.as_ptr()), 4);
|
||||
}
|
||||
|
||||
/// Missing, null, and empty keys for `entry_type`.
|
||||
#[test]
|
||||
fn config_entry_type_missing_and_invalid() {
|
||||
let _guard = lock();
|
||||
let missing = unique_key("et_missing");
|
||||
assert_eq!(
|
||||
oakcommon_config_entry_type(null(), missing.as_ptr()),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_config_entry_type(null(), null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_config_entry_type(null(), cstr("").as_ptr()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- error handler --------------------------------------------------------
|
||||
|
||||
/// Registering and clearing the error handler both succeed; the handler
|
||||
/// itself is exercised by `config_save_failure_reports_and_returns_failed`.
|
||||
#[test]
|
||||
fn config_set_error_handler_roundtrip() {
|
||||
let _guard = lock();
|
||||
let userdata = 0x1234 as *mut c_void;
|
||||
let _handler_guard = HandlerGuard;
|
||||
assert_eq!(
|
||||
oakcommon_config_set_error_handler(Some(record_handler), userdata),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_config_set_error_handler(None, std::ptr::null_mut()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! Integration tests for the C ABI surface of `oakcommon::ffi::ffmpegutils`,
|
||||
//! which implements `include/common/ffmpegutils.h` (oracle:
|
||||
//! `src/common/src/ffmpegutils.cpp`).
|
||||
//!
|
||||
//! MUST be run with `--features test-stubs`: unlike every other ffi module,
|
||||
//! `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format` reaches the
|
||||
//! real `fb_find_best_pix_fmt_of_list` symbol (ffmpeg_bridge C ABI) in
|
||||
//! non-test builds, and the integration-test binary cannot link it. The
|
||||
//! `test-stubs` feature substitutes the in-crate stub (see
|
||||
//! `src/ffmpegutils.rs::find_best_pix_fmt_of_list`), mirroring the
|
||||
//! oakplugin/oaktimeline convention. `cargo test --lib` needs no flag: the
|
||||
//! stub is active under `cfg(test)` there.
|
||||
//!
|
||||
//! The pure mappings are already unit-tested in `src/ffmpegutils.rs`; these
|
||||
//! tests pin the exported wrapper contract: a value written to a non-null
|
||||
//! out-param with `OAKCOMMON_OK`, or `OAKCOMMON_E_INVALID` for a null
|
||||
//! out-param, for every export.
|
||||
|
||||
#[cfg(feature = "test-stubs")]
|
||||
mod ffi_ffmpegutils_tests {
|
||||
use std::ptr::null_mut;
|
||||
|
||||
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK};
|
||||
use oakcommon::ffi::ffmpegutils::*;
|
||||
|
||||
/// Every out-param wrapper must reject a null pointer without writing.
|
||||
#[test]
|
||||
fn null_out_param_returns_invalid() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(26, -1, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_pixel_format(0, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 4, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_native_sample_format(0, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(6, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(12, null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
// The out-param is untouched on the failure path.
|
||||
assert_eq!(out, -999);
|
||||
}
|
||||
|
||||
/// `get_compatible_bridge_pixel_format` picks an exact candidate from the
|
||||
/// clamped list (values match `FB_PIX_FMT_*` / `PixelFormat`).
|
||||
#[test]
|
||||
fn compatible_bridge_pixel_format_exact_candidate() {
|
||||
let mut out: i32 = -999;
|
||||
// pix_fmt = RGBA (26), maximum = invalid (-1): no limit, RGBA is
|
||||
// first in the candidate list.
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(26, -1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 26);
|
||||
// pix_fmt = RGBA_F32_LE (220), maximum = F32 (4): the f32 clamp adds
|
||||
// RGBA_F32_LE, so it matches exactly.
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(220, 4, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 220);
|
||||
}
|
||||
|
||||
/// `get_compatible_bridge_pixel_format` falls back to the first candidate
|
||||
/// for an unknown source format (bridge loss-metric behaviour).
|
||||
#[test]
|
||||
fn compatible_bridge_pixel_format_unknown_falls_back() {
|
||||
let mut out: i32 = -999;
|
||||
// YUV420P (0) is not in the RGBA-oriented candidate list -> first
|
||||
// candidate RGBA (26).
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(0, -1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 26);
|
||||
}
|
||||
|
||||
/// `get_compatible_pixel_format` maps native formats to the least-lossy
|
||||
/// native format.
|
||||
#[test]
|
||||
fn compatible_pixel_format_maps_native() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_pixel_format(0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U8
|
||||
assert_eq!(out, 0);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_pixel_format(3, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // F16
|
||||
assert_eq!(out, 2); // -> U16
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_compatible_pixel_format(-1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // invalid
|
||||
assert_eq!(out, -1);
|
||||
}
|
||||
|
||||
/// `get_ffmpeg_pixel_format` maps native format + channel count to a
|
||||
/// bridge pixel format.
|
||||
#[test]
|
||||
fn ffmpeg_pixel_format_maps_native_to_bridge() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 3, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U8 RGB
|
||||
assert_eq!(out, 2); // RGB24
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(0, 4, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U8 RGBA
|
||||
assert_eq!(out, 26); // RGBA
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(2, 4, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U16 RGBA
|
||||
assert_eq!(out, 105); // RGBA64LE
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(1, 3, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U10 RGB
|
||||
assert_eq!(out, -1); // no bridge format
|
||||
}
|
||||
|
||||
/// `get_native_sample_format` maps bridge sample formats to native.
|
||||
#[test]
|
||||
fn native_sample_format_maps_bridge_to_native() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_native_sample_format(0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // U8
|
||||
assert_eq!(out, 6); // SMP_FMT_U8
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_native_sample_format(1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // S16
|
||||
assert_eq!(out, 7);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_native_sample_format(8, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // FLTP
|
||||
assert_eq!(out, 4); // SMP_FMT_F32_P
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_native_sample_format(999, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, -1); // unknown -> invalid
|
||||
}
|
||||
|
||||
/// `get_ffmpeg_sample_format` maps native sample formats to bridge.
|
||||
#[test]
|
||||
fn ffmpeg_sample_format_maps_native_to_bridge() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(6, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // SMP_FMT_U8
|
||||
assert_eq!(out, 0); // U8
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(10, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // SMP_FMT_F32
|
||||
assert_eq!(out, 3); // FLT
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(-1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // invalid
|
||||
assert_eq!(out, -1);
|
||||
}
|
||||
|
||||
/// JPEG-range bridge formats convert to their regular counterparts;
|
||||
/// everything else passes through unchanged.
|
||||
#[test]
|
||||
fn jpeg_space_converts_to_regular_space() {
|
||||
let mut out: i32 = -999;
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(12, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // YUVJ420P
|
||||
assert_eq!(out, 0); // YUV420P
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(13, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // YUVJ422P
|
||||
assert_eq!(out, 4); // YUV422P
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(14, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // YUVJ444P
|
||||
assert_eq!(out, 5); // YUV444P
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(32, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // YUVJ440P
|
||||
assert_eq!(out, 31); // YUV440P
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(138, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // YUVJ411P
|
||||
assert_eq!(out, 7); // YUV411P
|
||||
// Non-JPEG formats pass through unchanged.
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(26, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // RGBA
|
||||
assert_eq!(out, 26);
|
||||
assert_eq!(
|
||||
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(-1, &mut out),
|
||||
OAKCOMMON_OK
|
||||
); // none
|
||||
assert_eq!(out, -1);
|
||||
}
|
||||
}
|
||||
@@ -1,670 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI-level integration tests for the C-ABI `misc`, `debug`, and `current`
|
||||
//! exports in `oakcommon::ffi`, asserted against the C++ oracle
|
||||
//! (`src/common/c_api/*.cpp`) and the Rust domain modules
|
||||
//! (`src/common/rust/src/{miscutils,debug}.rs`).
|
||||
//!
|
||||
//! The contract under test (each point matches the implementations):
|
||||
//! - decibel/lerp/power helpers write into a non-null out-param and reject a
|
||||
//! null one with `E_INVALID`; domain helpers never fail, so the only
|
||||
//! failure path is the null out-param;
|
||||
//! - `drop_workflow_behavior_is_valid` returns 1/0 without error codes, and
|
||||
//! `drop_workflow_behavior_name` is a non-truncating two-stage getter
|
||||
//! ("UNKNOWN" for out-of-range codes);
|
||||
//! - `debug` exports: null messages are `E_INVALID`, the level-name getter
|
||||
//! is a non-truncating two-stage getter, level codes outside 0..=4 are
|
||||
//! rejected by `log_set_level` with `E_INVALID`, and the get-level export
|
||||
//! rejects a null out-param;
|
||||
//! - `current` exports: the singleton handle is stamped and `free` is
|
||||
//! idempotent, setters/getters reject a null handle (getters also reject a
|
||||
//! null out-param), empty slots read back as null, and replacing an
|
||||
//! occupied slot runs the previous owner's destructor exactly once.
|
||||
|
||||
use std::ffi::{c_char, c_void, CString};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK};
|
||||
use oakcommon::ffi::current::*;
|
||||
use oakcommon::ffi::debug::*;
|
||||
use oakcommon::ffi::misc::*;
|
||||
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
|
||||
|
||||
/// Serialises tests that mutate the process-wide `Current` singleton (cargo
|
||||
/// runs tests on threads).
|
||||
static CURRENT_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Serialises tests that mutate the process-wide logging level.
|
||||
static DEBUG_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Destroy tally for the slot-replacement test.
|
||||
static DESTROY_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// A `DestroyFn` that bumps [`DESTROY_COUNT`].
|
||||
unsafe extern "C" fn count_destroy(_p: *mut c_void) {
|
||||
DESTROY_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
|
||||
fn to_cstring(s: &str) -> CString {
|
||||
CString::new(s).expect("test string must not contain NUL")
|
||||
}
|
||||
|
||||
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every
|
||||
/// getter takes it by value. Rebuilding from the same fields duplicates
|
||||
/// only the handle value — the box stays alive as long as the original
|
||||
/// handle lives, and the getters never release.
|
||||
fn dup(h: &CHandle) -> CHandle {
|
||||
CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert two f64 values agree within `tol`.
|
||||
fn assert_close(actual: f64, expected: f64, tol: f64) {
|
||||
assert!(
|
||||
(actual - expected).abs() <= tol,
|
||||
"expected {expected} ± {tol}, got {actual}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drive a two-stage string getter against the standard `copy_string`
|
||||
/// convention: a null-buffer size query, a short buffer that must stay
|
||||
/// untouched (no truncation), an exact-fit copy with its NUL, and an
|
||||
/// oversized copy with the tail untouched.
|
||||
fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) {
|
||||
let required = (expected.len() + 1) as i32;
|
||||
|
||||
// Size query: a null buffer returns the required size, NUL included.
|
||||
assert_eq!(getter(std::ptr::null_mut(), 0), required);
|
||||
|
||||
// Short buffer: too small, so nothing is written to it.
|
||||
let short_size = (required - 1).max(0);
|
||||
let mut short = vec![0xABu8; short_size as usize];
|
||||
assert_eq!(
|
||||
getter(short.as_mut_ptr() as *mut c_char, short_size),
|
||||
required
|
||||
);
|
||||
assert!(
|
||||
short.iter().all(|&b| b == 0xAB),
|
||||
"short buffer must stay untouched"
|
||||
);
|
||||
|
||||
// Exact fit: payload followed by a NUL.
|
||||
let mut exact = vec![0xCDu8; required as usize];
|
||||
assert_eq!(
|
||||
getter(exact.as_mut_ptr() as *mut c_char, required),
|
||||
required
|
||||
);
|
||||
assert_eq!(&exact[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(exact[expected.len()], 0);
|
||||
|
||||
// Oversized: payload and NUL written, tail left as initialized.
|
||||
let mut big = vec![0u8; (required + 8) as usize];
|
||||
assert_eq!(
|
||||
getter(big.as_mut_ptr() as *mut c_char, required + 8),
|
||||
required
|
||||
);
|
||||
assert_eq!(&big[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(big[expected.len()], 0);
|
||||
assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0));
|
||||
}
|
||||
|
||||
// ---- debug ----
|
||||
|
||||
/// A null message is `E_INVALID`.
|
||||
#[test]
|
||||
fn debug_log_null_msg_is_invalid() {
|
||||
assert_eq!(
|
||||
oakcommon_debug_log(0, std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// Any non-null message returns `OK`, including out-of-range level codes
|
||||
/// (`log_raw` never fails on the level itself).
|
||||
#[test]
|
||||
fn debug_log_returns_ok_for_any_level() {
|
||||
let msg = to_cstring("ffi_misc debug_log probe");
|
||||
assert_eq!(oakcommon_debug_log(0, msg.as_ptr()), OAKCOMMON_OK);
|
||||
|
||||
let raw = to_cstring("out-of-range level");
|
||||
assert_eq!(oakcommon_debug_log(99, raw.as_ptr()), OAKCOMMON_OK);
|
||||
}
|
||||
|
||||
/// Level-name getter is a non-truncating two-stage getter; out-of-range
|
||||
/// codes yield "UNKNOWN".
|
||||
#[test]
|
||||
fn debug_level_name_two_stage() {
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_debug_level_name(0, buf, size),
|
||||
"DEBUG",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_debug_level_name(2, buf, size),
|
||||
"WARNING",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_debug_level_name(4, buf, size),
|
||||
"FATAL",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_debug_level_name(5, buf, size),
|
||||
"UNKNOWN",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_debug_level_name(-1, buf, size),
|
||||
"UNKNOWN",
|
||||
);
|
||||
}
|
||||
|
||||
/// `log_set_level` accepts 0..=4, rejects everything else with `E_INVALID`
|
||||
/// without changing the stored level, and `log_get_level` reads it back.
|
||||
#[test]
|
||||
fn log_set_get_level_roundtrip_and_invalid() {
|
||||
let _guard = DEBUG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
assert_eq!(oakcommon_log_set_level(0), OAKCOMMON_OK);
|
||||
let mut level = -1;
|
||||
assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK);
|
||||
assert_eq!(level, 0);
|
||||
|
||||
assert_eq!(oakcommon_log_set_level(4), OAKCOMMON_OK);
|
||||
assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK);
|
||||
assert_eq!(level, 4);
|
||||
|
||||
assert_eq!(oakcommon_log_set_level(5), OAKCOMMON_E_INVALID);
|
||||
assert_eq!(oakcommon_log_set_level(-1), OAKCOMMON_E_INVALID);
|
||||
// The rejected set leaves the stored level untouched.
|
||||
assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK);
|
||||
assert_eq!(level, 4);
|
||||
|
||||
// Restore the default so other tests see a clean level.
|
||||
assert_eq!(oakcommon_log_set_level(1), OAKCOMMON_OK);
|
||||
assert_eq!(oakcommon_log_get_level(&mut level), OAKCOMMON_OK);
|
||||
assert_eq!(level, 1);
|
||||
}
|
||||
|
||||
/// The get-level export rejects a null out-param.
|
||||
#[test]
|
||||
fn log_get_level_null_out_is_invalid() {
|
||||
assert_eq!(
|
||||
oakcommon_log_get_level(std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- misc: decibel / lerp ----
|
||||
|
||||
/// Linear -> decibels for exact and mid-range inputs; zero (log10 = -inf)
|
||||
/// collapses to the decibel minimum, negative inputs yield NaN.
|
||||
#[test]
|
||||
fn decibel_from_linear_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(oakcommon_decibel_from_linear(1.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 0.0, 1e-9);
|
||||
assert_eq!(oakcommon_decibel_from_linear(10.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 20.0, 1e-9);
|
||||
assert_eq!(oakcommon_decibel_from_linear(0.5, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, -6.020599913, 1e-6);
|
||||
assert_eq!(oakcommon_decibel_from_linear(0.0, &mut out), OAKCOMMON_OK);
|
||||
assert_eq!(out, -200.0);
|
||||
assert_eq!(oakcommon_decibel_from_linear(-1.0, &mut out), OAKCOMMON_OK);
|
||||
assert!(
|
||||
out.is_nan(),
|
||||
"negative linear input must produce NaN, got {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Decibels -> linear for exact inputs; results below 1e-6 clamp to 0.0.
|
||||
#[test]
|
||||
fn decibel_to_linear_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(oakcommon_decibel_to_linear(0.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 1.0, 1e-9);
|
||||
assert_eq!(oakcommon_decibel_to_linear(20.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 10.0, 1e-9);
|
||||
assert_eq!(oakcommon_decibel_to_linear(-100.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 1e-5, 1e-12);
|
||||
assert_eq!(oakcommon_decibel_to_linear(-200.0, &mut out), OAKCOMMON_OK);
|
||||
assert_eq!(out, 0.0);
|
||||
}
|
||||
|
||||
/// Logarithmic position -> decibels: saturates to the minimum below 0.001,
|
||||
/// to 0 dB above 0.99, and follows the linearization formula in between.
|
||||
#[test]
|
||||
fn decibel_from_logarithmic_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_logarithmic(0.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, -200.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_logarithmic(1.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 0.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_logarithmic(0.99, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, 0.0, 1e-6);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_logarithmic(0.5, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, -16.45, 0.05);
|
||||
}
|
||||
|
||||
/// Decibels -> logarithmic position; `|db| <= 1e-12` snaps to 1.0.
|
||||
#[test]
|
||||
fn decibel_to_logarithmic_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_decibel_to_logarithmic(0.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 1.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_to_logarithmic(20.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, 1.0, 1e-9);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_to_logarithmic(-120.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, 4.605e-6, 1e-9);
|
||||
}
|
||||
|
||||
/// Linear amplitude -> logarithmic position (`1 - exp(-linear * lo_g100)`).
|
||||
#[test]
|
||||
fn decibel_linear_to_logarithmic_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_decibel_linear_to_logarithmic(0.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 0.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_linear_to_logarithmic(1.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, 0.99, 1e-9);
|
||||
}
|
||||
|
||||
/// Logarithmic position -> linear amplitude; values above 0.99 snap to 1.0.
|
||||
#[test]
|
||||
fn decibel_logarithmic_to_linear_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_decibel_logarithmic_to_linear(0.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 0.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_logarithmic_to_linear(1.0, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 1.0);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_logarithmic_to_linear(0.99, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_close(out, 1.0, 1e-6);
|
||||
}
|
||||
|
||||
/// `lerp(a, b, t) = a*(1-t) + b*t` at the endpoints and midpoints.
|
||||
#[test]
|
||||
fn lerp_known_values() {
|
||||
let mut out = -1.0;
|
||||
|
||||
assert_eq!(oakcommon_lerp(0.0, 10.0, 0.5, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 5.0, 1e-9);
|
||||
assert_eq!(oakcommon_lerp(0.0, 10.0, 0.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 0.0, 1e-9);
|
||||
assert_eq!(oakcommon_lerp(0.0, 10.0, 1.0, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 10.0, 1e-9);
|
||||
assert_eq!(oakcommon_lerp(2.0, 4.0, 0.25, &mut out), OAKCOMMON_OK);
|
||||
assert_close(out, 2.5, 1e-9);
|
||||
}
|
||||
|
||||
/// Every decibel/lerp export rejects a null out-param with `E_INVALID`.
|
||||
#[test]
|
||||
fn decibel_lerp_reject_null_out() {
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_linear(1.0, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_to_linear(0.0, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_from_logarithmic(0.5, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_to_logarithmic(0.0, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_linear_to_logarithmic(0.5, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_decibel_logarithmic_to_linear(0.5, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_lerp(0.0, 1.0, 0.5, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- misc: drop-workflow behavior / power ----
|
||||
|
||||
/// Codes 0..=3 are valid; everything else returns 0.
|
||||
#[test]
|
||||
fn drop_workflow_behavior_is_valid() {
|
||||
for value in 0..=3 {
|
||||
assert_eq!(oakcommon_drop_workflow_behavior_is_valid(value), 1);
|
||||
}
|
||||
assert_eq!(oakcommon_drop_workflow_behavior_is_valid(4), 0);
|
||||
assert_eq!(oakcommon_drop_workflow_behavior_is_valid(-1), 0);
|
||||
assert_eq!(oakcommon_drop_workflow_behavior_is_valid(i32::MAX), 0);
|
||||
}
|
||||
|
||||
/// Behavior-name getter is a non-truncating two-stage getter; out-of-range
|
||||
/// codes yield "UNKNOWN".
|
||||
#[test]
|
||||
fn drop_workflow_behavior_name_two_stage() {
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(0, buf, size),
|
||||
"ASK",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(1, buf, size),
|
||||
"AUTO",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(2, buf, size),
|
||||
"MANUAL",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(3, buf, size),
|
||||
"DISABLE",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(4, buf, size),
|
||||
"UNKNOWN",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_drop_workflow_behavior_name(-1, buf, size),
|
||||
"UNKNOWN",
|
||||
);
|
||||
}
|
||||
|
||||
/// Round `value` up to a power of two (wrapping overflow -> 0); a null
|
||||
/// out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn power_ceil_to_power_of_2() {
|
||||
let mut out = 0u32;
|
||||
for (input, expected) in [
|
||||
(0u32, 0u32),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 4),
|
||||
(5, 8),
|
||||
(9, 16),
|
||||
(0x8000_0001, 0),
|
||||
] {
|
||||
assert_eq!(
|
||||
oakcommon_power_ceil_to_power_of_2(input, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, expected, "ceil({input})");
|
||||
}
|
||||
assert_eq!(
|
||||
oakcommon_power_ceil_to_power_of_2(7, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// Round `value` down to a power of two; a null out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn power_floor_to_power_of_2() {
|
||||
let mut out = 0u32;
|
||||
for (input, expected) in [
|
||||
(0u32, 0u32),
|
||||
(1, 1),
|
||||
(4, 4),
|
||||
(5, 4),
|
||||
(9, 8),
|
||||
(0x8000_0000, 0x8000_0000),
|
||||
] {
|
||||
assert_eq!(
|
||||
oakcommon_power_floor_to_power_of_2(input, &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, expected, "floor({input})");
|
||||
}
|
||||
assert_eq!(
|
||||
oakcommon_power_floor_to_power_of_2(7, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- current ----
|
||||
|
||||
/// The singleton handle is stamped; `free` nullifies it, is idempotent, and
|
||||
/// tolerates a null pointer or an explicit null handle.
|
||||
#[test]
|
||||
fn current_instance_and_free_lifecycle() {
|
||||
let mut h = oakcommon_current_instance();
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(h.addref.is_some());
|
||||
assert!(h.release.is_some());
|
||||
|
||||
oakcommon_current_free(&mut h);
|
||||
assert!(h.is_null());
|
||||
// A second free of the now-empty handle is safe.
|
||||
oakcommon_current_free(&mut h);
|
||||
assert!(h.is_null());
|
||||
// Freeing a null pointer is safe.
|
||||
oakcommon_current_free(std::ptr::null_mut());
|
||||
// Freeing an explicit null handle is safe.
|
||||
let mut null_h = CHandle::null();
|
||||
oakcommon_current_free(&mut null_h);
|
||||
assert!(null_h.is_null());
|
||||
}
|
||||
|
||||
/// All four slots round-trip set -> get; null handles and null out-params
|
||||
/// are `E_INVALID`, and clearing a slot makes it read back as null.
|
||||
#[test]
|
||||
fn current_set_get_all_slots_roundtrip() {
|
||||
let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let h = oakcommon_current_instance();
|
||||
|
||||
let video = 0x1000usize as *mut c_void;
|
||||
let audio = 0x2000usize as *mut c_void;
|
||||
let host = 0x3000usize as *mut c_void;
|
||||
let cache = 0x4000usize as *mut c_void;
|
||||
|
||||
// Empty slots read back as null before anything is stored.
|
||||
let mut got: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert!(got.is_null());
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(dup(&h), video, None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_audio_params(dup(&h), audio, None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_plugin_host(dup(&h), host, None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_plugin_cache(dup(&h), cache, None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(got, video);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_audio_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(got, audio);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_plugin_host(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(got, host);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_plugin_cache(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(got, cache);
|
||||
|
||||
// Getters reject a null out-param and a null handle.
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(CHandle::null(), &mut got),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
// Setters reject a null handle.
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(CHandle::null(), video, None),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
|
||||
// Clear every slot so other tests see a clean singleton.
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(dup(&h), std::ptr::null_mut(), None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_audio_params(dup(&h), std::ptr::null_mut(), None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_plugin_host(dup(&h), std::ptr::null_mut(), None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_set_plugin_cache(dup(&h), std::ptr::null_mut(), None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert!(got.is_null());
|
||||
}
|
||||
|
||||
/// Replacing an occupied slot runs the previous owner's destructor exactly
|
||||
/// once; clearing a slot whose occupant has no destructor runs nothing.
|
||||
#[test]
|
||||
fn current_set_destroys_replaced_pointer() {
|
||||
let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let h = oakcommon_current_instance();
|
||||
let base = DESTROY_COUNT.load(Ordering::SeqCst);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(
|
||||
dup(&h),
|
||||
0xAAAAusize as *mut c_void,
|
||||
Some(count_destroy)
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base);
|
||||
|
||||
// Replacing the slot invokes the stored destructor exactly once.
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(dup(&h), 0xBBBBusize as *mut c_void, None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base + 1);
|
||||
|
||||
let mut got: *mut c_void = std::ptr::null_mut();
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(got, 0xBBBBusize as *mut c_void);
|
||||
|
||||
// Clearing a slot with no destructor invokes nothing.
|
||||
assert_eq!(
|
||||
oakcommon_current_set_video_params(dup(&h), std::ptr::null_mut(), None),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base + 1);
|
||||
assert_eq!(
|
||||
oakcommon_current_get_video_params(dup(&h), &mut got),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert!(got.is_null());
|
||||
}
|
||||
|
||||
/// `is_interactive` writes 1; null handle/out-param are `E_INVALID`.
|
||||
#[test]
|
||||
fn current_is_interactive_writes_one() {
|
||||
let _guard = CURRENT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let h = oakcommon_current_instance();
|
||||
|
||||
let mut out = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_current_is_interactive(dup(&h), &mut out),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(out, 1);
|
||||
assert_eq!(
|
||||
oakcommon_current_is_interactive(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_current_is_interactive(CHandle::null(), &mut out),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
@@ -1,721 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI-level integration tests for the C-ABI `subtitleparams` exports in
|
||||
//! `oakcommon::ffi::subtitleparams`, asserted against the C++ oracle
|
||||
//! `src/common/c_api/subtitleparams.cpp` and the Rust domain
|
||||
//! `src/common/rust/src/subtitleparams.rs`.
|
||||
//!
|
||||
//! The contract under test (each point matches the C++ oracle):
|
||||
//! - exports take a `CHandle` by value and never release it, so callers
|
||||
//! keep the handle alive and `free` it exactly once;
|
||||
//! - `free` nullifies the handle, is idempotent, and tolerates a null
|
||||
//! pointer;
|
||||
//! - two-stage string getters return the required size (NUL included) and
|
||||
//! only copy when the buffer is large enough — they never truncate;
|
||||
//! - getters reject a null handle or null out-param with `E_INVALID`,
|
||||
//! setters reject a null handle, and string inputs reject null pointers;
|
||||
//! - an out-of-range subtitle index yields `E_NOT_FOUND`; malformed XML
|
||||
//! yields `E_FAILED`; the empty set is invalid with `count` 0 and
|
||||
//! `duration` 0/1.
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
|
||||
use oakcommon::error::{
|
||||
OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK,
|
||||
};
|
||||
use oakcommon::ffi::subtitleparams::*;
|
||||
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
|
||||
|
||||
/// Create an empty subtitle parameter set.
|
||||
fn make() -> CHandle {
|
||||
oakcommon_subtitleparams_init()
|
||||
}
|
||||
|
||||
/// A populated set: stream index 2, disabled, two subtitles.
|
||||
fn make_populated() -> CHandle {
|
||||
let h = make();
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 25, 1, to_cstring("hello").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(dup(&h), 25, 1, 50, 1, to_cstring("world").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_stream_index(dup(&h), 2),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_enabled(dup(&h), 0),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
h
|
||||
}
|
||||
|
||||
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
|
||||
fn to_cstring(s: &str) -> CString {
|
||||
CString::new(s).expect("test string must not contain NUL")
|
||||
}
|
||||
|
||||
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every
|
||||
/// getter takes it by value. Rebuilding from the same fields duplicates
|
||||
/// only the handle value — the box stays alive as long as the original
|
||||
/// handle lives, and the getters never release.
|
||||
fn dup(h: &CHandle) -> CHandle {
|
||||
CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive a two-stage string getter against the C++ `copy_string`
|
||||
/// convention: a null-buffer size query, a short buffer that must stay
|
||||
/// untouched (no truncation), an exact-fit copy with its NUL, and an
|
||||
/// oversized copy with the tail untouched.
|
||||
fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) {
|
||||
let required = (expected.len() + 1) as i32;
|
||||
|
||||
// Size query: a null buffer returns the required size, NUL included.
|
||||
assert_eq!(getter(std::ptr::null_mut(), 0), required);
|
||||
|
||||
// Short buffer: too small, so nothing is written to it.
|
||||
let short_size = (required - 1).max(0);
|
||||
let mut short = vec![0xABu8; short_size as usize];
|
||||
assert_eq!(
|
||||
getter(short.as_mut_ptr() as *mut c_char, short_size),
|
||||
required
|
||||
);
|
||||
assert!(
|
||||
short.iter().all(|&b| b == 0xAB),
|
||||
"short buffer must stay untouched"
|
||||
);
|
||||
|
||||
// Exact fit: payload followed by a NUL.
|
||||
let mut exact = vec![0xCDu8; required as usize];
|
||||
assert_eq!(
|
||||
getter(exact.as_mut_ptr() as *mut c_char, required),
|
||||
required
|
||||
);
|
||||
assert_eq!(&exact[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(exact[expected.len()], 0);
|
||||
|
||||
// Oversized: payload and NUL written, tail left as initialized.
|
||||
let mut big = vec![0u8; (required + 8) as usize];
|
||||
assert_eq!(
|
||||
getter(big.as_mut_ptr() as *mut c_char, required + 8),
|
||||
required
|
||||
);
|
||||
assert_eq!(&big[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(big[expected.len()], 0);
|
||||
assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0));
|
||||
}
|
||||
|
||||
// ---- Handle lifecycle ----
|
||||
|
||||
/// `init` yields a stamped, non-empty handle; `free` nullifies it, is
|
||||
/// idempotent, and tolerates a null pointer.
|
||||
#[test]
|
||||
fn init_free_lifecycle() {
|
||||
let h = oakcommon_subtitleparams_init();
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(h.addref.is_some());
|
||||
assert!(h.release.is_some());
|
||||
|
||||
let mut hf = make();
|
||||
assert!(!hf.is_null());
|
||||
oakcommon_subtitleparams_free(&mut hf);
|
||||
assert!(hf.is_null());
|
||||
// A second free of the now-empty handle is safe.
|
||||
oakcommon_subtitleparams_free(&mut hf);
|
||||
assert!(hf.is_null());
|
||||
// Freeing a null pointer is safe.
|
||||
oakcommon_subtitleparams_free(std::ptr::null_mut());
|
||||
// Freeing a handle struct that is already null (by value) is safe too.
|
||||
let mut empty = CHandle::null();
|
||||
oakcommon_subtitleparams_free(&mut empty);
|
||||
assert!(empty.is_null());
|
||||
}
|
||||
|
||||
// ---- Core field round-trips ----
|
||||
|
||||
/// Stream index defaults to 0 and round-trips; a null handle or null
|
||||
/// out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn stream_index_roundtrip() {
|
||||
let h = make();
|
||||
let mut si = -1i32;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(si, 0);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_stream_index(dup(&h), 3),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(si, 3);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_stream_index(CHandle::null(), 1),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(CHandle::null(), &mut si),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `enabled` defaults to 1 (CPP-PARITY: the C++ constructor sets
|
||||
/// `enabled_ = true`) and round-trips; any non-zero setter value enables.
|
||||
#[test]
|
||||
fn enabled_roundtrip() {
|
||||
let h = make();
|
||||
let mut en = -1i32;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(en, 1);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_enabled(dup(&h), 0),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(en, 0);
|
||||
|
||||
// A non-zero code (even 5) enables the stream.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_enabled(dup(&h), 5),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(en, 1);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_set_enabled(CHandle::null(), 1),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(CHandle::null(), &mut en),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Empty-set behavior ----
|
||||
|
||||
/// An empty set is invalid, has count 0, and duration 0/1; null handle or
|
||||
/// null out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn empty_set_defaults() {
|
||||
let h = make();
|
||||
let mut v = -1i32;
|
||||
let mut c = -1i32;
|
||||
let mut n = -1i32;
|
||||
let mut d = -1i32;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(v, 0);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 0);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (0, 1));
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_is_valid(CHandle::null(), &mut v),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(CHandle::null(), &mut c),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(CHandle::null(), &mut n, &mut d),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_is_valid(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(dup(&h), std::ptr::null_mut(), &mut d),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(dup(&h), &mut n, std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Subtitles ----
|
||||
|
||||
/// `add_subtitle` appends entries; `is_valid`/`count`/`duration`/
|
||||
/// `get_subtitle` reflect the populated set. Rationals are reduced
|
||||
/// (CPP-PARITY with the C++ `Rational` constructor).
|
||||
#[test]
|
||||
fn add_subtitle_and_query() {
|
||||
let h = make_populated();
|
||||
let mut v = -1i32;
|
||||
let mut c = -1i32;
|
||||
let mut n = -1i32;
|
||||
let mut d = -1i32;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(v, 1);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 2);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (50, 1));
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (0, 1));
|
||||
assert_eq!((v, c), (25, 1));
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), 1, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (25, 1));
|
||||
assert_eq!((v, c), (50, 1));
|
||||
|
||||
// Reduction: Rational(2,4) -> 1/2, Rational(9,3) -> 3/1.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(dup(&h), 2, 4, 9, 3, to_cstring("t").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (1, 2));
|
||||
assert_eq!((v, c), (3, 1));
|
||||
}
|
||||
|
||||
/// `add_subtitle` rejects a null handle or null text; an empty text string
|
||||
/// is a valid subtitle.
|
||||
#[test]
|
||||
fn add_subtitle_failures() {
|
||||
let h = make();
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(
|
||||
CHandle::null(),
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
to_cstring("x").as_ptr()
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
|
||||
// Empty text is fine and counts as a subtitle.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, to_cstring("").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
let mut c = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 1);
|
||||
}
|
||||
|
||||
/// `get_subtitle` on an out-of-range index is `E_NOT_FOUND`; a null handle
|
||||
/// or any null out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn get_subtitle_out_of_range() {
|
||||
let h = make_populated();
|
||||
let mut n = -1i32;
|
||||
let mut d = -1i32;
|
||||
let mut v = -1i32;
|
||||
let mut c = -1i32;
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), -1, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
|
||||
// On an empty set even index 0 is out of range.
|
||||
let e = make();
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&e), 0, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(CHandle::null(), 0, &mut n, &mut d, &mut v, &mut c),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(
|
||||
dup(&h),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
&mut d,
|
||||
&mut v,
|
||||
&mut c
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(
|
||||
dup(&h),
|
||||
0,
|
||||
&mut n,
|
||||
std::ptr::null_mut(),
|
||||
&mut v,
|
||||
&mut c
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(
|
||||
dup(&h),
|
||||
0,
|
||||
&mut n,
|
||||
&mut d,
|
||||
std::ptr::null_mut(),
|
||||
&mut c
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(
|
||||
dup(&h),
|
||||
0,
|
||||
&mut n,
|
||||
&mut d,
|
||||
&mut v,
|
||||
std::ptr::null_mut()
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `clear` removes every subtitle (count 0, invalid, duration 0/1) and
|
||||
/// rejects a null handle.
|
||||
#[test]
|
||||
fn clear() {
|
||||
let h = make_populated();
|
||||
assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK);
|
||||
|
||||
let mut v = -1i32;
|
||||
let mut c = -1i32;
|
||||
let mut n = -1i32;
|
||||
let mut d = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 0);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(v, 0);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (0, 1));
|
||||
|
||||
// Clearing again is a no-op success.
|
||||
assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_clear(CHandle::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- String getters ----
|
||||
|
||||
/// `get_subtitle_text` is a two-stage string getter; an out-of-range index
|
||||
/// is `E_NOT_FOUND`, and a null handle or invalid out-buffer is
|
||||
/// `E_INVALID`.
|
||||
#[test]
|
||||
fn get_subtitle_text_two_stage() {
|
||||
let h = make_populated();
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, buf, size),
|
||||
"hello",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size),
|
||||
"world",
|
||||
);
|
||||
|
||||
// Out-of-range index is E_NOT_FOUND (even on a size query).
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 2, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(dup(&h), -1, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(CHandle::null(), 0, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
// A null buffer with a positive size is an invalid string out-param.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 5),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
// A negative size is invalid too.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), -1),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `generate_ass_header` is a static two-stage string getter (no handle).
|
||||
/// The payload matches the C++ header verbatim (CRLF line endings).
|
||||
#[test]
|
||||
fn generate_ass_header_two_stage() {
|
||||
let expected = "[Script Info]\r\n\
|
||||
; Script generated by Oak\r\n\
|
||||
ScriptType: v4.00+\r\n\
|
||||
PlayResX: 384\r\n\
|
||||
PlayResY: 288\r\n\
|
||||
ScaledBorderAndShadow: yes\r\n\
|
||||
\r\n\
|
||||
[V4+ Styles]\r\n\
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, \
|
||||
BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, \
|
||||
BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
|
||||
Style: Default,Arial,16,&HFFFFFF,&HFFFFFF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,0\r\n\
|
||||
\r\n\
|
||||
[Events]\r\n\
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n";
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_generate_ass_header(buf, size),
|
||||
expected,
|
||||
);
|
||||
|
||||
// An invalid out-buffer (null with a positive size) is E_INVALID.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), 5),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), -1),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- XML ----
|
||||
|
||||
/// `load_xml` parses a fragment and applies stream index, enabled, and
|
||||
/// subtitles; malformed input or a missing root is `E_FAILED`; a null
|
||||
/// handle or null xml is `E_INVALID`.
|
||||
#[test]
|
||||
fn load_xml() {
|
||||
let h = make();
|
||||
|
||||
let xml = "<subtitleparams><streamindex>7</streamindex><enabled>0</enabled>\
|
||||
<subtitles><subtitle in=\"0/1\" out=\"25/1\">hello</subtitle>\
|
||||
<subtitle in=\"25/1\" out=\"50/1\">world</subtitle></subtitles></subtitleparams>";
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
|
||||
let mut si = -1i32;
|
||||
let mut en = -1i32;
|
||||
let mut c = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(si, 7);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(en, 0);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 2);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 0),
|
||||
6
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size),
|
||||
"world",
|
||||
);
|
||||
|
||||
// Malformed / missing-root fragments fail with E_FAILED.
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(
|
||||
dup(&h),
|
||||
to_cstring("<subtitleparams><streamindex>").as_ptr()
|
||||
),
|
||||
OAKCOMMON_E_FAILED
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("not xml").as_ptr()),
|
||||
OAKCOMMON_E_FAILED
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("").as_ptr()),
|
||||
OAKCOMMON_E_FAILED
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(CHandle::null(), to_cstring(xml).as_ptr()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(dup(&h), std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `save_xml` is a two-stage string getter; the output matches the C++
|
||||
/// `XmlStreamWriter` format exactly (no indentation, escaped text).
|
||||
#[test]
|
||||
fn save_xml_two_stage() {
|
||||
// The empty set serializes to streamindex 0 / enabled 1 / no subtitles.
|
||||
let e = make();
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_save_xml(dup(&e), buf, size),
|
||||
"<subtitleparams><streamindex>0</streamindex><enabled>1</enabled><subtitles></subtitles></subtitleparams>",
|
||||
);
|
||||
|
||||
// The populated set round-trips through load_xml.
|
||||
let h = make_populated();
|
||||
let expected = "<subtitleparams><streamindex>2</streamindex><enabled>0</enabled>\
|
||||
<subtitles><subtitle in=\"0/1\" out=\"25/1\">hello</subtitle>\
|
||||
<subtitle in=\"25/1\" out=\"50/1\">world</subtitle></subtitles></subtitleparams>";
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size),
|
||||
expected,
|
||||
);
|
||||
|
||||
// A loaded fragment round-trips byte-for-byte.
|
||||
let xml = "<subtitleparams><streamindex>9</streamindex><enabled>1</enabled>\
|
||||
<subtitles><subtitle in=\"3/2\" out=\"5/1\">hi</subtitle></subtitles></subtitleparams>";
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size),
|
||||
xml,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_save_xml(CHandle::null(), std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_save_xml(dup(&h), std::ptr::null_mut(), 5),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
|
||||
// The saved XML parses back to the same data via the C API.
|
||||
let mut si = -1i32;
|
||||
let mut en = -1i32;
|
||||
let mut c = -1i32;
|
||||
let mut n = -1i32;
|
||||
let mut d = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(si, 9);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(en, 1);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_count(dup(&h), &mut c),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(c, 1);
|
||||
assert_eq!(
|
||||
oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut si, &mut en),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!((n, d), (3, 2));
|
||||
assert_eq!((si, en), (5, 1));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,761 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! FFI-level integration tests for the C-ABI XML streaming exports in
|
||||
//! `oakcommon::ffi::xmlutils`, asserted against the C++ oracle
|
||||
//! `src/common/c_api/xmlutils.cpp`.
|
||||
//!
|
||||
//! `ffi::error` and `ffi::error_abi` are documented placeholder modules
|
||||
//! (`include/common/error.h` exposes no functions; the `OAKCOMMON_OK` /
|
||||
//! `OAKCOMMON_E_*` constants live in `crate::error`), so the only callable
|
||||
//! surface here is `xmlutils`. The error constants its exports return are
|
||||
//! pinned in `error_constants_returned_by_xmlutils_match_header`; the full
|
||||
//! set is already asserted in `contract.rs`.
|
||||
//!
|
||||
//! The contract under test (each point matches the C++ oracle):
|
||||
//! - exports take a `CHandle` by value and never release it;
|
||||
//! - two-stage string getters return the required size (NUL included) and
|
||||
//! only copy when the buffer is large enough — they never truncate;
|
||||
//! - out-of-range attribute indexes report `OAKCOMMON_E_NOT_FOUND`;
|
||||
//! - a malformed document still yields a usable handle whose `has_error`
|
||||
//! flag reads back 1 and whose navigation reports no start elements.
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
|
||||
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK};
|
||||
use oakcommon::ffi::xmlutils::*;
|
||||
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
|
||||
|
||||
/// Shared sample document: two attributes on the root plus a nested text
|
||||
/// element. The writer test sequence reproduces this exact document.
|
||||
const DOC: &str = r#"<root a="1" b="two"><child>text here</child></root>"#;
|
||||
|
||||
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
|
||||
fn to_cstring(s: &str) -> CString {
|
||||
CString::new(s).expect("test string must not contain NUL")
|
||||
}
|
||||
|
||||
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every
|
||||
/// getter takes it by value. Rebuilding from the same fields duplicates
|
||||
/// only the handle value — the box stays alive as long as the original
|
||||
/// handle lives, and the getters never release.
|
||||
fn dup(h: &CHandle) -> CHandle {
|
||||
CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
abi_version: h.abi_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive a two-stage string getter against the C++ `copy_string`
|
||||
/// convention: a null-buffer size query, a short buffer that must stay
|
||||
/// untouched (no truncation), an exact-fit copy with its NUL, and an
|
||||
/// oversized copy with the tail untouched.
|
||||
fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) {
|
||||
let required = (expected.len() + 1) as i32;
|
||||
|
||||
// Size query: a null buffer returns the required size, NUL included.
|
||||
assert_eq!(getter(std::ptr::null_mut(), 0), required);
|
||||
|
||||
// Short buffer: too small, so nothing is written to it.
|
||||
let short_size = (required - 1).max(0);
|
||||
let mut short = vec![0xABu8; short_size as usize];
|
||||
assert_eq!(
|
||||
getter(short.as_mut_ptr() as *mut c_char, short_size),
|
||||
required
|
||||
);
|
||||
assert!(
|
||||
short.iter().all(|&b| b == 0xAB),
|
||||
"short buffer must stay untouched"
|
||||
);
|
||||
|
||||
// Exact fit: payload followed by a NUL.
|
||||
let mut exact = vec![0xCDu8; required as usize];
|
||||
assert_eq!(
|
||||
getter(exact.as_mut_ptr() as *mut c_char, required),
|
||||
required
|
||||
);
|
||||
assert_eq!(&exact[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(exact[expected.len()], 0);
|
||||
|
||||
// Oversized: payload and NUL written, tail left as initialized.
|
||||
let mut big = vec![0u8; (required + 8) as usize];
|
||||
assert_eq!(
|
||||
getter(big.as_mut_ptr() as *mut c_char, required + 8),
|
||||
required
|
||||
);
|
||||
assert_eq!(&big[..expected.len()], expected.as_bytes());
|
||||
assert_eq!(big[expected.len()], 0);
|
||||
assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0));
|
||||
}
|
||||
|
||||
// ---- Reader: handle lifecycle ----
|
||||
|
||||
/// `init` with a null data pointer yields an empty handle.
|
||||
#[test]
|
||||
fn init_returns_null_handle_for_null_data() {
|
||||
let h = oakcommon_xml_reader_init(std::ptr::null());
|
||||
assert!(h.is_null());
|
||||
assert!(h.ctx.is_null());
|
||||
assert!(h.addref.is_none());
|
||||
assert!(h.release.is_none());
|
||||
}
|
||||
|
||||
/// `init` over valid data yields a stamped, non-empty handle.
|
||||
#[test]
|
||||
fn init_creates_stamped_handle() {
|
||||
let h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(h.addref.is_some());
|
||||
assert!(h.release.is_some());
|
||||
}
|
||||
|
||||
/// `free` nullifies the handle, is idempotent, and tolerates a null
|
||||
/// pointer.
|
||||
#[test]
|
||||
fn free_nullifies_and_is_idempotent() {
|
||||
let mut h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
assert!(!h.is_null());
|
||||
oakcommon_xml_reader_free(&mut h);
|
||||
assert!(h.is_null());
|
||||
// A second free of the now-empty handle is safe.
|
||||
oakcommon_xml_reader_free(&mut h);
|
||||
assert!(h.is_null());
|
||||
// Freeing a null pointer is safe.
|
||||
oakcommon_xml_reader_free(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// ---- Reader: navigation ----
|
||||
|
||||
/// `read_next_start_element` writes 1 while a start element is found and
|
||||
/// 0 once the document is exhausted (CPP-PARITY: an end element and the
|
||||
/// end of the document both report 0, not an error).
|
||||
#[test]
|
||||
fn read_next_start_element_reports_found() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1); // root
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1); // child
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0); // child's end element
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0); // root's end element
|
||||
}
|
||||
|
||||
/// A null reader handle or a null `found` out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn read_next_start_element_rejects_null_args() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(CHandle::null(), &mut found),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Reader: two-stage string getters ----
|
||||
|
||||
/// `name` is a two-stage getter over the current element's name; a null
|
||||
/// handle is `E_INVALID`.
|
||||
#[test]
|
||||
fn name_two_stage_getter() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size),
|
||||
"root",
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_name(CHandle::null(), std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// Before any token is read the name is the empty string (size 1).
|
||||
#[test]
|
||||
fn name_is_empty_before_any_read() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_name(dup(&r), std::ptr::null_mut(), 0),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// `read_element_text` is a two-stage getter and caches its result, so a
|
||||
/// second read returns the same text even though the stream was consumed
|
||||
/// by the first (CPP-PARITY with the C++ `XmlReaderState::cached_text`).
|
||||
#[test]
|
||||
fn read_element_text_two_stage_with_cache() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
|
||||
"text here",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
|
||||
"text here",
|
||||
);
|
||||
}
|
||||
|
||||
/// When not on a start element the text is empty (CPP-PARITY: the C++
|
||||
/// reader returns an empty string).
|
||||
#[test]
|
||||
fn read_element_text_not_on_start_element_is_empty() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_element_text(dup(&r), std::ptr::null_mut(), 0),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_element_text(CHandle::null(), std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `skip_current_element` consumes the current element and its subtree; a
|
||||
/// null handle is `E_INVALID`.
|
||||
#[test]
|
||||
fn skip_current_element_skips_subtree() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_skip_current_element(dup(&r)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_skip_current_element(CHandle::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Reader: attributes ----
|
||||
|
||||
/// `attribute_count` reports 0 before any token and the real count on a
|
||||
/// start element; a null handle or a null count out-param is `E_INVALID`.
|
||||
#[test]
|
||||
fn attribute_count_reports_attributes() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut count = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_count(dup(&r), &mut count),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(count, 0);
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_count(dup(&r), &mut count),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(count, 2);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_count(CHandle::null(), &mut count),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_count(dup(&r), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// `attribute_name` / `attribute_value` are two-stage getters over the
|
||||
/// attributes of the current start element, in document order.
|
||||
#[test]
|
||||
fn attribute_name_and_value_two_stage() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 0, buf, size),
|
||||
"a",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 0, buf, size),
|
||||
"1",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 1, buf, size),
|
||||
"b",
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 1, buf, size),
|
||||
"two",
|
||||
);
|
||||
}
|
||||
|
||||
/// An out-of-range attribute index is `E_NOT_FOUND` (CPP-PARITY), while a
|
||||
/// null handle is `E_INVALID`.
|
||||
#[test]
|
||||
fn attribute_out_of_range_is_not_found() {
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_name(dup(&r), 2, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_name(dup(&r), -1, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_value(dup(&r), 2, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_attribute_name(CHandle::null(), 0, std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Reader: error reporting ----
|
||||
|
||||
/// `has_error` is 0 for a well-formed document and 1 for a malformed one;
|
||||
/// a malformed document still navigates safely and reports no start
|
||||
/// elements (CPP-PARITY: parse failures surface lazily through
|
||||
/// `has_error`, never at init time).
|
||||
#[test]
|
||||
fn has_error_flags_malformed_documents() {
|
||||
let mut err = -1i32;
|
||||
|
||||
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_has_error(dup(&r), &mut err),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(err, 0);
|
||||
|
||||
let bad = oakcommon_xml_reader_init(to_cstring("<a></b>").as_ptr());
|
||||
assert!(!bad.is_null());
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_has_error(dup(&bad), &mut err),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(err, 1);
|
||||
let mut found = -1i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&bad), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0);
|
||||
|
||||
let empty = oakcommon_xml_reader_init(to_cstring("").as_ptr());
|
||||
assert!(!empty.is_null());
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_has_error(dup(&empty), &mut err),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(err, 1);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_has_error(CHandle::null(), &mut err),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_has_error(dup(&r), std::ptr::null_mut()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Writer: handle lifecycle ----
|
||||
|
||||
/// `writer_init` yields a stamped handle; `free` nullifies it, is
|
||||
/// idempotent, and tolerates a null pointer.
|
||||
#[test]
|
||||
fn writer_init_free_and_idempotent() {
|
||||
let mut w = oakcommon_xml_writer_init();
|
||||
assert!(!w.is_null());
|
||||
assert_eq!(w.abi_version, OAKCOMMON_ABI_VERSION);
|
||||
assert!(w.addref.is_some());
|
||||
assert!(w.release.is_some());
|
||||
oakcommon_xml_writer_free(&mut w);
|
||||
assert!(w.is_null());
|
||||
oakcommon_xml_writer_free(&mut w);
|
||||
assert!(w.is_null());
|
||||
oakcommon_xml_writer_free(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// ---- Writer: operations ----
|
||||
|
||||
/// Every writer export rejects a null handle with `E_INVALID`, and the
|
||||
/// string-taking exports reject null strings.
|
||||
#[test]
|
||||
fn writer_rejects_null_arguments() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(CHandle::null(), to_cstring("a").as_ptr()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
CHandle::null(),
|
||||
to_cstring("a").as_ptr(),
|
||||
to_cstring("b").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_characters(CHandle::null(), to_cstring("x").as_ptr()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
CHandle::null(),
|
||||
to_cstring("a").as_ptr(),
|
||||
to_cstring("b").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(CHandle::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_document(CHandle::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_output(CHandle::null(), std::ptr::null_mut(), 0),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(dup(&w), std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(dup(&w), std::ptr::null(), to_cstring("b").as_ptr()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("a").as_ptr(), std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_characters(dup(&w), std::ptr::null()),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
dup(&w),
|
||||
std::ptr::null(),
|
||||
to_cstring("b").as_ptr()
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
dup(&w),
|
||||
to_cstring("a").as_ptr(),
|
||||
std::ptr::null()
|
||||
),
|
||||
OAKCOMMON_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// The writer sequence builds the exact sample document, and `output` is a
|
||||
/// two-stage getter over it.
|
||||
#[test]
|
||||
fn writer_builds_document_and_output_two_stage() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("a").as_ptr(),
|
||||
to_cstring("1").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("b").as_ptr(),
|
||||
to_cstring("two").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
dup(&w),
|
||||
to_cstring("child").as_ptr(),
|
||||
to_cstring("text here").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_document(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
|
||||
DOC,
|
||||
);
|
||||
}
|
||||
|
||||
/// The writer's output round-trips through the reader.
|
||||
#[test]
|
||||
fn writer_output_round_trips_through_reader() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("a").as_ptr(),
|
||||
to_cstring("1").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("b").as_ptr(),
|
||||
to_cstring("two").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
dup(&w),
|
||||
to_cstring("child").as_ptr(),
|
||||
to_cstring("text here").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
|
||||
let mut out = vec![0u8; 64];
|
||||
let needed = oakcommon_xml_writer_output(dup(&w), out.as_mut_ptr() as *mut c_char, 64);
|
||||
assert_eq!(needed, (DOC.len() + 1) as i32);
|
||||
assert_eq!(&out[..DOC.len()], DOC.as_bytes());
|
||||
assert_eq!(out[DOC.len()], 0);
|
||||
|
||||
let r = oakcommon_xml_reader_init(out.as_ptr() as *const c_char);
|
||||
assert!(!r.is_null());
|
||||
let mut found = 0i32;
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size),
|
||||
"root",
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 1);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
|
||||
"text here",
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(found, 0);
|
||||
}
|
||||
|
||||
/// Documented no-op writer operations (attribute without an open start
|
||||
/// tag, end element on an empty stack, end document with nothing open)
|
||||
/// still return `OK`.
|
||||
#[test]
|
||||
fn writer_noop_operations_return_ok() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("a").as_ptr(),
|
||||
to_cstring("b").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_document(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_characters(dup(&w), to_cstring("x").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
|
||||
"x",
|
||||
);
|
||||
}
|
||||
|
||||
/// An empty element with attributes serializes as `<a k="v"/>`.
|
||||
#[test]
|
||||
fn writer_self_closing_empty_element() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("k").as_ptr(),
|
||||
to_cstring("v").as_ptr()
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
|
||||
r#"<a k="v"/>"#,
|
||||
);
|
||||
}
|
||||
|
||||
/// Text and attribute values are escaped for the five predefined XML
|
||||
/// entities.
|
||||
#[test]
|
||||
fn writer_escapes_text_and_attributes() {
|
||||
let w = oakcommon_xml_writer_init();
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_text_element(
|
||||
dup(&w),
|
||||
to_cstring("e").as_ptr(),
|
||||
to_cstring("hi & bye <there>").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_attribute(
|
||||
dup(&w),
|
||||
to_cstring("q").as_ptr(),
|
||||
to_cstring("x\"y&z").as_ptr(),
|
||||
),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_eq!(
|
||||
oakcommon_xml_writer_write_end_element(dup(&w)),
|
||||
OAKCOMMON_OK
|
||||
);
|
||||
assert_two_stage_getter(
|
||||
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
|
||||
r#"<e>hi & bye <there></e><a q="x"y&z"/>"#,
|
||||
);
|
||||
}
|
||||
|
||||
// ---- error / error_abi ----
|
||||
|
||||
/// The constants the xmlutils exports return are defined in `crate::error`
|
||||
/// and match `include/common/error.h` (the `ffi::error` / `ffi::error_abi`
|
||||
/// modules only point here). The full set is pinned in `contract.rs`.
|
||||
#[test]
|
||||
fn error_constants_returned_by_xmlutils_match_header() {
|
||||
assert_eq!(OAKCOMMON_OK, 0);
|
||||
assert_eq!(OAKCOMMON_E_INVALID, -10001);
|
||||
assert_eq!(OAKCOMMON_E_NOT_FOUND, -10004);
|
||||
}
|
||||
+29
-50
@@ -2,75 +2,54 @@
|
||||
name = "oakengine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module Rust APIs (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
|
||||
# the shm error formatting in src/ipc.rs.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
|
||||
# shared-memory frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
# Error derive (Display + std::error::Error) for the crate error enum
|
||||
# (src/error.rs). Same major version the other modules use (oakotio, ...).
|
||||
thiserror = "2"
|
||||
|
||||
# Every module call crosses the module C ABI as an `extern "C"` import
|
||||
# (src/bridge/). The module crates below are REAL dependencies so their
|
||||
# `#[no_mangle]` exports are linked into the final `liboakengine` cdylib:
|
||||
# the dylib then carries the module C ABIs itself (oakundo_*, oakcommon_*,
|
||||
# oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*,
|
||||
# oakplugin_*, oaknode_*) alongside the facade's oakengine_* exports.
|
||||
# Worker/IPC wire protocol (src/worker.rs, src/ipc.rs): serde for the
|
||||
# NDJSON control-plane messages and serde_json for the wire encoding.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# POSIX shm_open/ftruncate/mmap/munmap/shm_unlink for the shared-memory
|
||||
# frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
|
||||
# Single-lib unification: every module call is a compile-time Rust call
|
||||
# into the module crate's direct API (no module C ABI). The engine keeps
|
||||
# the frozen oakengine_* C ABI upward; downward it rewires through
|
||||
# src/stubs.rs (direct Rust shims for oakcommon/oakcodec/oakrender/
|
||||
# oakaudio/oakplugin, and clearly marked STUBs for the handle-based
|
||||
# oaknode/oaktimeline paths).
|
||||
#
|
||||
# The crates are linked WITHOUT their `test-stubs` features so the real
|
||||
# exports ship. Cross-module calls that the modules resolve with
|
||||
# dlsym(RTLD_DEFAULT) (oaknode, oakplugin, oakrender) now resolve against
|
||||
# the sibling modules inside the same dylib; the remaining undefined
|
||||
# imports are the C++ host-provided symbols (`oakcore_audioparams_*`,
|
||||
# `oakcore_rational_*` from liboakcore), which build.rs leaves as runtime
|
||||
# lookups for the host app. The audio resample/decode paths call FFmpeg
|
||||
# in-process via ffmpeg-next (oakaudio/oakcodec); the C++ ffmpeg_bridge
|
||||
# `fb_*` imports are gone.
|
||||
# The remaining undefined imports are the C++ host-provided symbols
|
||||
# (`oakcore_audioparams_*` from liboakcore), which build.rs leaves as
|
||||
# runtime lookups for the host app (see src/stubs.rs audio module).
|
||||
#
|
||||
# Tests link the same crates; the dev-dependencies below re-declare
|
||||
# oakcommon/oakplugin WITH `test-stubs` so their in-crate C ABI mocks
|
||||
# (the ffmpeg_bridge replacement and the oakrender dlsym stubs) are
|
||||
# compiled into the test binaries (features union with the normal
|
||||
# dependencies for test builds).
|
||||
#
|
||||
# NOTE (oaktimeline/oaktask): their `test-stubs` features are never used
|
||||
# here — the in-crate mocks define `oakundo_command_init` etc., which
|
||||
# would collide with the real oakundo rlib in one binary. Without
|
||||
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
|
||||
# C ABI symbols as link-time externs, provided by the sibling crates in
|
||||
# the same dylib (or by the dev-dependency rlibs in a test binary).
|
||||
#
|
||||
# rustc would normally prune rlibs that are only touched through
|
||||
# `extern "C"` imports from the link; src/linkage.rs anchors every crate
|
||||
# (and oakcore-rs) so the linker pulls their object files.
|
||||
# src/linkage.rs anchors every crate (and oakcore-rs) so the linker pulls
|
||||
# their object files into the cdylib.
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oaktimeline = { path = "../oaktimeline" }
|
||||
oakcodec = { path = "../oakcodec" }
|
||||
oakaudio = { path = "../oakaudio" }
|
||||
oakrender = { path = "../oakrender" }
|
||||
oaktask = { path = "../oaktask" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
oaknode = { path = "../oaknode" }
|
||||
oakaudio = { path = "../oakaudio" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
|
||||
[dev-dependencies]
|
||||
# The module crates as plain dependencies (no `test-stubs`): the C ABI
|
||||
# mocks the test binaries need (the oakcore_* symbols) are provided by
|
||||
# the crate's own test support (tests/common/mod.rs), which is
|
||||
# force-linked into every integration test. Keeping the dev-deps feature-free also stops Cargo's workspace
|
||||
# feature unification from forcing `test-stubs` onto the oakcommon/oakplugin
|
||||
# member builds during `cargo test --workspace` (their own tests then run
|
||||
# in the real dlsym mode and stay green).
|
||||
# The facade is cdylib-only, so the former tests/*.rs integration tests
|
||||
# now run as unit tests inside the crate (src/test_support/, pulled in
|
||||
# from src/lib.rs); the module crates are reachable through the normal
|
||||
# [dependencies] above. oakcommon stays here for the direct dev-only
|
||||
# imports in the test support files.
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oakplugin = { path = "../oakplugin" }
|
||||
|
||||
+12
-21
@@ -26,10 +26,6 @@ src/
|
||||
node.rs node.h + project.h + footage.h (node graph / project / footage)
|
||||
timeline.rs timeline.h (sequences, clips, tracks, markers, workarea)
|
||||
task.rs task.h (background tasks over oaktask)
|
||||
ipc.rs ipc.h (shm/framepool half): SpscRingBuffer + FrameSlotPool
|
||||
+ POSIX SharedMemoryRegion (the real frame-slot transport)
|
||||
worker.rs worker.h (render worker): backend selection via the
|
||||
oakrender module C ABI + fallback, session, NDJSON main
|
||||
deferred.rs documented deferrals (stub detail lives here and in the
|
||||
family modules' stub bodies)
|
||||
tests/
|
||||
@@ -39,11 +35,9 @@ tests/
|
||||
|
||||
### Dependencies
|
||||
|
||||
The facade's regular dependencies are `serde`/`serde_json` (the worker's
|
||||
NDJSON control-plane protocol, `src/worker.rs`), `libc` (POSIX
|
||||
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`) and `thiserror`
|
||||
(the `Display` + `std::error::Error` impl for the facade error enum,
|
||||
`src/error.rs`). Every module
|
||||
The facade's regular dependency is `thiserror` (the `Display` +
|
||||
`std::error::Error` impl for the facade error enum, `src/error.rs`). Every
|
||||
module
|
||||
call crosses the module C ABI as an `extern "C"` import (`src/bridge/`),
|
||||
and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs)
|
||||
anchors them so their `#[no_mangle]` exports are linked into the
|
||||
@@ -84,17 +78,17 @@ convention: the return value is the length excluding the NUL
|
||||
| plugin | plugin.h | 4 | callbacks are facade state; push_button stubbed (no module API) |
|
||||
| codec | encoding.h | 81/85 | metadata family over oakcodec (`include/codec/format.h`); params handle is a facade box over the `oakcodec_encoding_params` POD; presets/load-save/export stubs |
|
||||
| render | renderer.h, color.h, lut.h | 60/60 | renderer over oakrender tickets; frame accessors over `OakCodecFrame`; color processor over `oakrender_color_processor_*`; color-manager list queries + LUT library stubs |
|
||||
| worker | worker.h | 8 | the render worker: `oakengine_worker_main` + the session family over the oakrender display renderer C ABI (dynamic → OpenGL fallback) and the ipc transport (see `src/worker.rs`) |
|
||||
| ipc | ipc.h (shm/framepool) | 28 | named shared-memory segments + frame-slot pools over `SpscRingBuffer` — the real frame-slot transport (see `src/ipc.rs`); the control-plane message serializers (`oakengine_ipc_*_to_json/parse`) are not wrapped (the worker speaks the NDJSON protocol with serde) |
|
||||
| node | node.h, project.h, footage.h | 226/327 | the node graph, project and footage families over the oaknode C ABI (`include/node/*.h`); 101 documented stubs where the module lacks the surface (gizmos, plugin messages, input properties, brush, thumbnail/waveform caches, shape/subtitle, keyframe enumeration, ...) — see the stub bodies |
|
||||
| timeline | timeline.h | 126/139 | sequences/clips/tracks/markers/workarea over oaknode + oaktimeline; 13 documented stubs (ripple-tracks command, default transitions, move-track/clip, marker-create, auto-cache, cache invalidation, multicam find/switch — module-surface gaps, see the stub bodies) |
|
||||
| task | task.h | 27 | the background-task system over oaktask (manager + load/save/import/export creators + result accessors); `create_proxy` stubbed (the module has no proxy-task C creator); start-time/is-cancelled are facade-approximated |
|
||||
|
||||
Deferred/stub detail lives in [`deferred`] and in the stub bodies' doc
|
||||
comments. `worker` and `ipc` were in the deferred list until the render
|
||||
worker landed here — the worker's runtime and the shared-memory frame-slot
|
||||
transport are now real (see `src/worker.rs` / `src/ipc.rs`); the ipc.h
|
||||
control-plane message serializers remain unwrapped.
|
||||
comments. The worker/IPC families (`worker.h`/`ipc.h`) are **not** part of
|
||||
the facade anymore: the frozen C++ ABI does not include them, so the
|
||||
render worker's runtime and the shared-memory frame-slot transport moved
|
||||
into the `oak-worker` crate (`crates/oak-worker/src/{worker,ipc}.rs`,
|
||||
self-contained, direct Rust calls into oakrender); the ipc.h control-plane
|
||||
message serializers remain unwrapped.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -121,14 +115,11 @@ binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks):
|
||||
|
||||
Families whose wrapped behavior requires the real module dylibs carry
|
||||
`#[ignore]` tests with a documented reason; the smoke tests here exercise
|
||||
the module crates' real implementations. The ipc tests create+attach two
|
||||
in-process mappings of real POSIX segments and exchange slot indices and
|
||||
payloads in both directions, including wraparound and full/empty edges.
|
||||
the module crates' real implementations.
|
||||
|
||||
```
|
||||
cargo test # 71 tests green + 1 ignored (lib 35: ipc 17 + worker 18;
|
||||
# integration: undo 3, common 4, audio 4, plugin 3, codec 5,
|
||||
# render 6, linkage 1, node 3, timeline 2 + 1 ignored, task 5)
|
||||
cargo test # lib tests + integration families (undo, common, audio,
|
||||
# plugin, codec, render, linkage, node, timeline, task)
|
||||
cargo build # cdylib embeds the module C ABIs; oakcore_*/fb_* stay
|
||||
# runtime lookups (see build.rs)
|
||||
```
|
||||
|
||||
@@ -36,18 +36,22 @@ fn main() {
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-rpath,/usr/lib");
|
||||
}
|
||||
// The dlsym codec bridge (M12 P0) resolves `oakcodec_*` from the
|
||||
// process-global scope; the engine test binaries statically link the
|
||||
// module crates, so their symbols must be exported from the main
|
||||
// executable.
|
||||
println!("cargo:rustc-link-arg-tests=-Wl,-export_dynamic");
|
||||
// process-global scope; the engine's unit-test binary (the former
|
||||
// integration tests live in src/test_support/) statically links the
|
||||
// module crates, so their symbols must be exported from the test
|
||||
// executable. `cargo:rustc-link-arg-tests` is NOT usable: the facade
|
||||
// is cdylib-only, so cargo reports "does not have a test target" for
|
||||
// that directive — use the generic `rustc-link-arg` (harmless no-op
|
||||
// for the cdylib link itself).
|
||||
println!("cargo:rustc-link-arg=-Wl,-export_dynamic");
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
|
||||
// The bundled OpenColorIO's macos system monitor references
|
||||
// IOKit / ColorSync / CoreGraphics display APIs; the engine
|
||||
// dylib links with `-undefined,dynamic_lookup`, so test binaries
|
||||
// must resolve them.
|
||||
for fw in ["IOKit", "ColorSync", "CoreGraphics"] {
|
||||
println!("cargo:rustc-link-arg-tests=-framework");
|
||||
println!("cargo:rustc-link-arg-tests={fw}");
|
||||
println!("cargo:rustc-link-arg=-framework");
|
||||
println!("cargo:rustc-link-arg={fw}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::audio as a;
|
||||
use crate::stubs::audio as a;
|
||||
use crate::error::Error;
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_i64, guard_int, guard_void, unbox, CHandle,
|
||||
@@ -445,6 +445,51 @@ pub unsafe extern "C" fn oakengine_audio_sync_place_by_waveform_offset(
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Waveform extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_waveform_extract` — two-stage whole-file min/max waveform
|
||||
/// extraction of `filename`'s audio stream (the module's
|
||||
/// `oakaudio_waveform_extract`, M12 P4).
|
||||
///
|
||||
/// First call with `out_pairs == NULL` / `capacity_points == 0` returns the
|
||||
/// required point count without writing; the channel count is reported
|
||||
/// whenever `out_channel_count` is non-NULL. The data pass writes
|
||||
/// `point_count * channel_count` channel-interleaved pairs (the module's
|
||||
/// `oakaudio_min_max` POD; `capacity_points` counts points) and returns the
|
||||
/// point count. Returns a negative facade `OAKENGINE_E_INVALID` for NULL
|
||||
/// `filename` / negative `stream_index` / non-positive `samples_per_point`
|
||||
/// / negative `capacity_points`; module decode errors (`OAKAUDIO_E_NOT_FOUND`,
|
||||
/// ...) pass through untranslated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_waveform_extract(
|
||||
filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
samples_per_point: c_int,
|
||||
out_pairs: *mut a::MinMax,
|
||||
capacity_points: c_int,
|
||||
out_channel_count: *mut c_int,
|
||||
) -> c_int {
|
||||
guard_int(|| unsafe {
|
||||
if filename.is_null() || stream_index < 0 || samples_per_point <= 0 || capacity_points < 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let n = a::oakaudio_waveform_extract(
|
||||
filename,
|
||||
stream_index,
|
||||
samples_per_point,
|
||||
out_pairs,
|
||||
capacity_points,
|
||||
out_channel_count,
|
||||
);
|
||||
if n < 0 {
|
||||
return Err(Error::Module(n));
|
||||
}
|
||||
Ok(n)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audio processor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakaudio C ABI bridge: direct Rust calls into the `oakaudio` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakaudio`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakaudio C ABI imports, mirroring the oakaudio crate's exports
|
||||
//! (`src/audio/rust/src/ffi.rs`; headers `include/audio/*.h`), plus the
|
||||
//! liboakcore `oakcore_audioparams_*` readers used to convert the
|
||||
//! engine's borrowed `OakAudioParams*` handles.
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oakaudio_offsetresult` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type OffsetResult = oakaudio::ffi::sync::OffsetResult;
|
||||
|
||||
/// `oakaudio_stretchoffsetresult` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type StretchOffsetResult = oakaudio::ffi::sync::StretchOffsetResult;
|
||||
|
||||
/// `oakaudio_sourceclip` C ABI POD. Single-lib unification: aliases
|
||||
/// the oakaudio crate's struct (identical layout).
|
||||
pub type SourceClip = oakaudio::ffi::sync::SourceClip;
|
||||
|
||||
/// `oakaudio` recording-params POD — single-lib unification: aliases the
|
||||
/// oakaudio crate's type (itself the oakcodec `oakcodec_encoding_params`).
|
||||
pub type EncodingParams = oakaudio::bridge::codec::EncodingParams;
|
||||
|
||||
extern "C" {
|
||||
pub fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut c_void;
|
||||
pub fn oakcore_audioparams_free(params: *mut c_void);
|
||||
pub fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int;
|
||||
/// `oakcore_audioparams_set_time_base` (host-provided).
|
||||
pub fn oakcore_audioparams_set_time_base(params: *mut c_void, num: c_int, den: c_int);
|
||||
pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64;
|
||||
pub fn oakcore_audioparams_format(params: *const c_void) -> c_int;
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_create_instance() -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_create_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_destroy_instance() {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_destroy_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_instance() -> CHandle {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_instance() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_free(_self: *mut CHandle) {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_free(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, bytes: i64) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_notify_interval(_self, bytes) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_push_to_output(
|
||||
_self: CHandle,
|
||||
rate: c_int,
|
||||
layout: u64,
|
||||
format: c_int,
|
||||
samples: *const c_char,
|
||||
samples_size: i64,
|
||||
error_buf: *mut c_char,
|
||||
error_buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::manager::oakaudio_manager_push_to_output(
|
||||
_self,
|
||||
rate,
|
||||
layout,
|
||||
format,
|
||||
samples,
|
||||
samples_size,
|
||||
error_buf,
|
||||
error_buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_clear_buffered_output(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_output(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_reset_output_clock(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_output_device(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_output_device(_self: CHandle, device: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_output_device(_self, device) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_get_input_device(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_set_input_device(_self: CHandle, device: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_set_input_device(_self, device) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_hard_reset(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oakaudio_manager_start_recording` — crosses the encoding-params C ABI
|
||||
/// POD (the facade keeps its own opaque mirror; the module's
|
||||
/// `oakaudio::bridge::codec::EncodingParams`). Kept as a link-time
|
||||
/// `extern "C"` declaration against the frozen module C ABI.
|
||||
pub fn oakaudio_manager_start_recording(
|
||||
_self: CHandle,
|
||||
params: *const EncodingParams,
|
||||
error_buf: *mut c_char,
|
||||
error_buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::manager::oakaudio_manager_start_recording(
|
||||
_self,
|
||||
params,
|
||||
error_buf,
|
||||
error_buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_stop_recording(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_seconds(_self, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_output_levels(_self: CHandle, peaks: *mut f32, capacity: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_output_levels(_self, peaks, capacity) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_init() -> CHandle {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_free(_self: *mut CHandle) {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_open(
|
||||
_self: CHandle,
|
||||
in_rate: c_int,
|
||||
in_layout: u64,
|
||||
in_format: c_int,
|
||||
out_rate: c_int,
|
||||
out_layout: u64,
|
||||
out_format: c_int,
|
||||
speed: c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::processor::oakaudio_processor_open(
|
||||
_self, in_rate, in_layout, in_format, out_rate, out_layout, out_format, speed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_close(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_close(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_is_open(_self) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_estimate_envelope_offset(
|
||||
reference: *const c_double,
|
||||
reference_len: c_int,
|
||||
candidate: *const c_double,
|
||||
candidate_len: c_int,
|
||||
reference_valid: *const u8,
|
||||
candidate_valid: *const u8,
|
||||
window_samples: u64,
|
||||
max_offset_windows: i64,
|
||||
out: *mut OffsetResult,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_estimate_envelope_offset(
|
||||
reference,
|
||||
reference_len,
|
||||
candidate,
|
||||
candidate_len,
|
||||
reference_valid,
|
||||
candidate_valid,
|
||||
window_samples,
|
||||
max_offset_windows,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference: *const c_double,
|
||||
reference_len: c_int,
|
||||
candidate: *const c_double,
|
||||
candidate_len: c_int,
|
||||
reference_valid: *const u8,
|
||||
candidate_valid: *const u8,
|
||||
window_samples: u64,
|
||||
max_offset_windows: i64,
|
||||
min_rate: c_double,
|
||||
max_rate: c_double,
|
||||
rate_step: c_double,
|
||||
out: *mut StretchOffsetResult,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_estimate_stretch_and_offset(
|
||||
reference,
|
||||
reference_len,
|
||||
candidate,
|
||||
candidate_len,
|
||||
reference_valid,
|
||||
candidate_valid,
|
||||
window_samples,
|
||||
max_offset_windows,
|
||||
min_rate,
|
||||
max_rate,
|
||||
rate_step,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_place_by_source_time(
|
||||
reference: *const SourceClip,
|
||||
candidate: *const SourceClip,
|
||||
reference_timeline_in_num: i64,
|
||||
reference_timeline_in_den: i64,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
out_valid: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_place_by_source_time(
|
||||
reference,
|
||||
candidate,
|
||||
reference_timeline_in_num,
|
||||
reference_timeline_in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
out_valid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_sync_place_by_waveform_offset(
|
||||
reference_timeline_in_num: i64,
|
||||
reference_timeline_in_den: i64,
|
||||
candidate_offset_samples: i64,
|
||||
sample_rate: c_int,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
out_valid: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakaudio::ffi::sync::oakaudio_sync_place_by_waveform_offset(
|
||||
reference_timeline_in_num,
|
||||
reference_timeline_in_den,
|
||||
candidate_offset_samples,
|
||||
sample_rate,
|
||||
out_num,
|
||||
out_den,
|
||||
out_valid,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcodec C ABI bridge: direct Rust calls into the `oakcodec` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakcodec`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakcodec C ABI imports, mirroring the oakcodec crate's exports
|
||||
//! (`src/codec/rust/src/ffi/{format,encoder}.rs`; headers
|
||||
//! `include/codec/{format,encoder}.h`). Also carries the
|
||||
//! `oakcodec_encoding_params` POD the facade's encoding-params handle
|
||||
//! wraps, and the oakaudio recording-params pointer pass-through.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/codec/encoder.h` — the encoding-params POD, a complete mirror
|
||||
/// `olive::EncodingParams` — single-lib unification: aliases the oakcodec
|
||||
/// crate's POD (`oakcodec_encoding_params`, identical `#[repr(C)]` layout;
|
||||
/// the engine's `OakEngineEncodingParams` handle is a heap box over exactly
|
||||
/// this struct, so every engine getter/setter reads/writes a field and
|
||||
/// `encoder_init`/recording can consume the pointer directly).
|
||||
pub type EncodingParamsPOD = oakcodec::ffi::encoder::oakcodec_encoding_params;
|
||||
|
||||
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
|
||||
/// struct has no zeroed constructor; this facade helper provides it.
|
||||
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
|
||||
// All-C fields: all-zero is a valid value.
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_count() -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_name(format: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_name(format, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_extension(format, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_video_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_audio_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_count(format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_format_subtitle_codec_at(format, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_name(codec: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_name(codec, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_still_image(codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_codec_is_lossless(codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_count(format, codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_pix_fmt_at(format, codec, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_pix_fmt_index(codec, pix_fmt) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_count(format, codec) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_sample_format_at(format: c_int, codec: c_int, index: c_int) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_sample_format_at(format, codec, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_filename_contains_digit_placeholder(filename: *const c_char) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_filename_contains_digit_placeholder(filename)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int {
|
||||
unsafe { oakcodec::ffi::format::oakcodec_encoding_image_sequence_digit_count(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::format::oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoding_generate_matrix(
|
||||
method: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
dst_width: c_int,
|
||||
dst_height: c_int,
|
||||
out_matrix: *mut f64,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcodec::ffi::encoder::oakcodec_encoding_generate_matrix(
|
||||
method, src_width, src_height, dst_width, dst_height, out_matrix,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oakcodec_encoder_init` — crosses the encoding-params C ABI POD
|
||||
/// (`oakcodec_encoding_params`; the facade keeps its own POD mirror).
|
||||
/// Kept as a link-time `extern "C"` declaration against the frozen module
|
||||
/// C ABI.
|
||||
pub fn oakcodec_encoder_init(params: *const EncodingParamsPOD) -> CHandle {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcodec` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
|
||||
}
|
||||
@@ -1,889 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon C ABI bridge: direct Rust calls into the `oakcommon` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakcommon`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon C ABI imports, mirroring the oakcommon crate's exports
|
||||
//! (`src/common/rust/src/ffi.rs`; headers `include/common/*.h`). Only the
|
||||
//! families the facade wraps: config, videoparams, colortransform, xml
|
||||
//! reader/writer and the decibel helpers.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/common/config.h` — error handler callback.
|
||||
pub type ConfigErrorHandler = Option<
|
||||
unsafe extern "C" fn(title: *const c_char, message: *const c_char, userdata: *mut c_void),
|
||||
>;
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_load() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_load() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_save() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_save() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_reset_defaults() -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_reset_defaults() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set(group: *const c_char, key: *const c_char, value: *const c_char) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_int(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_int64(group: *const c_char, key: *const c_char, fallback: i64) -> i64 {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_int64(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_double(group: *const c_char, key: *const c_char, fallback: f64) -> f64 {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_double(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_get_bool(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
fallback: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_get_bool(group, key, fallback) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_int(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_int64(group: *const c_char, key: *const c_char, value: i64) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_int64(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_double(group: *const c_char, key: *const c_char, value: f64) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_double(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_bool(group, key, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_entry_type(group, key) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_config_set_error_handler(
|
||||
handler: ConfigErrorHandler,
|
||||
userdata: *mut c_void,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::config::oakcommon_config_set_error_handler(handler, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init() -> CHandle {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init_basic(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
nb_channels,
|
||||
pixel_aspect_num,
|
||||
pixel_aspect_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_init_with_time_base(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: c_int,
|
||||
time_base_den: c_int,
|
||||
pixel_format: c_int,
|
||||
nb_channels: c_int,
|
||||
pixel_aspect_num: c_int,
|
||||
pixel_aspect_den: c_int,
|
||||
interlacing: c_int,
|
||||
divider: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_init_with_time_base(
|
||||
width,
|
||||
height,
|
||||
time_base_num,
|
||||
time_base_den,
|
||||
pixel_format,
|
||||
nb_channels,
|
||||
pixel_aspect_num,
|
||||
pixel_aspect_den,
|
||||
interlacing,
|
||||
divider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_free(params: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_free(params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_width(params: CHandle, width: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_width(params: CHandle, width: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_height(params: CHandle, height: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_height(params, height) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_height(params: CHandle, height: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_height(params, height) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_time_base(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_base(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_time_base(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_time_base(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_frame_rate(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_frame_rate(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_frame_rate(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_frame_rate(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_pixel_aspect_ratio(
|
||||
params: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_pixel_aspect_ratio(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params,
|
||||
numerator,
|
||||
denominator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_format(params, format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_format(params, format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_interlacing(params: CHandle, interlacing: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_interlacing(params, interlacing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_interlacing(params, interlacing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider(params, divider) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_divider(params, divider) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_video_type(params, type_) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_set_video_type(params, type_) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_premultiplied_alpha(
|
||||
params: CHandle,
|
||||
premultiplied: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_premultiplied_alpha(
|
||||
params,
|
||||
premultiplied,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params: CHandle,
|
||||
premultiplied: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params,
|
||||
premultiplied,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_color_range(params: CHandle, color_range: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_color_range(params, color_range)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_set_color_range(params, color_range)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_is_valid(params: CHandle, valid: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_is_valid(params, valid) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_effective_width(params: CHandle, width: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_width(params, width) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_effective_height(params: CHandle, height: *mut c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_height(params, height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_pixel(params: CHandle, bytes: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel(params, bytes) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_equals(a, b, out_equal) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_format_is_float(pixel_format) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_format_name(
|
||||
pixel_format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_format_name(
|
||||
pixel_format,
|
||||
buf,
|
||||
buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_frame_rate_to_string(
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_frame_rate_to_string(
|
||||
numerator,
|
||||
denominator,
|
||||
buf,
|
||||
buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_name_for_divider(
|
||||
divider: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_name_for_divider(
|
||||
divider, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_scaled_dimension(dimension, divider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_generate_auto_divider(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_divider_for_target_resolution(
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
target_width: c_int,
|
||||
target_height: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_divider_for_target_resolution(
|
||||
src_width,
|
||||
src_height,
|
||||
target_width,
|
||||
target_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_channel_for_format(
|
||||
pixel_format,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format(
|
||||
pixel_format: c_int,
|
||||
channels: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_pixel_for_format(
|
||||
pixel_format,
|
||||
channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(
|
||||
pixel_format: c_int,
|
||||
channels: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_static_get_bytes_per_pixel(
|
||||
pixel_format,
|
||||
channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_buffer_size(params: CHandle, size: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::videoparams::oakcommon_videoparams_get_buffer_size(params, size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_videoparams_get_time_in_timebase_units(
|
||||
params: CHandle,
|
||||
time_num: c_int,
|
||||
time_den: c_int,
|
||||
timestamp: *mut i64,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::videoparams::oakcommon_videoparams_get_time_in_timebase_units(
|
||||
params, time_num, time_den, timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_init_output(output) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_init_display(
|
||||
display: *const c_char,
|
||||
view: *const c_char,
|
||||
look: *const c_char,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_init_display(display, view, look)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_free(transform: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_free(transform) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::colortransform::oakcommon_colortransform_is_display(transform) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_display(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_display(
|
||||
transform, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_output(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_output(
|
||||
transform, buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_view(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_view(transform, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_colortransform_get_look(
|
||||
transform: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::colortransform::oakcommon_colortransform_get_look(transform, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_free(reader: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_name(reader: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_read_element_text(
|
||||
reader: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_name(
|
||||
reader: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(reader, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_attribute_value(
|
||||
reader: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(reader, index, buf, buf_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_init() -> CHandle {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_free(writer: *mut CHandle) {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_attribute(
|
||||
writer: CHandle,
|
||||
name: *const c_char,
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, name, value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_text_element(
|
||||
writer: CHandle,
|
||||
name: *const c_char,
|
||||
text: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_xml_writer_output(writer: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_output(writer, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_from_linear(linear: f64, out_db: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_linear(linear, out_db) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_to_linear(db: f64, out_linear: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_linear(db, out_linear) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_from_logarithmic(logarithmic: f64, out_db: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_from_logarithmic(logarithmic, out_db) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> c_int {
|
||||
unsafe { oakcommon::ffi::misc::oakcommon_decibel_to_logarithmic(db, out_logarithmic) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_linear_to_logarithmic(linear: f64, out_logarithmic: *mut f64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::misc::oakcommon_decibel_linear_to_logarithmic(linear, out_logarithmic)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakcommon` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakcommon_decibel_logarithmic_to_linear(logarithmic: f64, out_linear: *mut f64) -> c_int {
|
||||
unsafe {
|
||||
oakcommon::ffi::misc::oakcommon_decibel_logarithmic_to_linear(logarithmic, out_linear)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from the oak modules, one submodule per module crate.
|
||||
//!
|
||||
//! The facade consumes the module C ABIs (`include/<mod>/*.h`) purely as
|
||||
//! `extern "C"` imports — it never links the module crates at build time.
|
||||
//! At the final app link the symbols resolve against the module shared
|
||||
//! libraries; `cargo test` resolves them against the module crates' rlibs
|
||||
//! (dev-dependencies, see Cargo.toml).
|
||||
//!
|
||||
//! **Signatures are declared from the module crates' actual `#[no_mangle]`
|
||||
//! exports** (their `src/*/ffi*` modules), not from memory of the include
|
||||
//! headers — module bridges in sibling crates have drifted from the real
|
||||
//! ABI before. Every handle crosses the boundary as [`crate::handle::CHandle`]
|
||||
//! (structurally identical to every `Oak<Mod><Type>` value handle).
|
||||
//!
|
||||
//! Only functions the module crates actually implement are declared here;
|
||||
//! engine functions whose backing is still C++-only are facade stubs (see
|
||||
//! the area modules) and never reach this module.
|
||||
|
||||
pub mod audio;
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod node;
|
||||
pub mod plugin;
|
||||
pub mod render;
|
||||
pub mod task;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakplugin C ABI bridge: direct Rust calls into the `oakplugin` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakplugin`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakplugin C ABI imports, mirroring the oakplugin crate's exports
|
||||
//! (`src/plugin/rust/src/ffi.rs`; headers `include/plugin/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_scan(bundle_dirs: *const *const c_char, dir_count: c_int) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_scan(bundle_dirs, dir_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_init() -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_count() -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_id_at(index, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakplugin` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakplugin_host_plugin_label(
|
||||
plugin_id: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakplugin::ffi::oakplugin_host_plugin_label(plugin_id, buf, buf_size) }
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI bridge: direct Rust calls into the `oakrender` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakrender`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI imports, mirroring the oakrender crate's exports
|
||||
//! (`src/render/rust/src/ffi.rs`; headers `include/render/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/render/ticket.h` — video render ticket params. Single-lib
|
||||
/// unification: aliases the oakrender crate's POD (same `repr(C)`
|
||||
/// layout; all handle fields are the shared [`CHandle`]).
|
||||
pub type OakVideoTicketParams = oakrender::ffi::OakVideoTicketParams;
|
||||
|
||||
/// `include/render/renderer.h` — frame video-params POD returned by
|
||||
/// `oakrender_codec_frame_get_params`. Single-lib unification: aliases
|
||||
/// the oakrender crate's POD.
|
||||
pub type OakRenderVideoParams = oakrender::ffi::OakRenderVideoParams;
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_create_dynamic(backend_id: *const c_char) -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_dynamic(backend_id) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_create_opengl() -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_create_opengl() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_init(renderer: CHandle, gl_context: *mut c_void) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_init(renderer, gl_context) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_destroy(renderer: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_destroy(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_is_open_gl(renderer: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_open_gl(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_display_renderer_is_vulkan(renderer: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_display_renderer_is_vulkan(renderer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_manager_set_aggressive_gc(enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::manager::oakrender_set_cacher_multicam(multicam_or_null) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::manager::oakrender_set_display_color_processor(p_or_null) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_render_frame(
|
||||
params: *const OakVideoTicketParams,
|
||||
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
) -> CHandle {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_render_frame(params, cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_render_audio(
|
||||
output_node: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
params: *const c_void,
|
||||
mode: c_int,
|
||||
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
montage: *const oakrender::ffi::OakMontageClip,
|
||||
montage_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::ticket::oakrender_ticket_render_audio(
|
||||
output_node,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
params as *const CHandle,
|
||||
mode,
|
||||
cb,
|
||||
userdata,
|
||||
montage,
|
||||
montage_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakrender_audio_samples_free` — release the samples block returned
|
||||
/// by `oakrender_ticket_get_samples` (NULL no-op).
|
||||
pub fn oakrender_audio_samples_free(samples: *mut c_void) {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_audio_samples_free(samples) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_wait(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_cancel(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_frame(ticket, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_get_samples(ticket, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_ticket_free(ticket: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::ticket::oakrender_ticket_free(ticket) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_create() -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_retain(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_free(frame: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_free(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_width(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_height(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_linesize_bytes(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_data(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_const_data(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_is_allocated(frame) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_codec_frame_get_params(frame: CHandle, out: *mut OakRenderVideoParams) -> c_int {
|
||||
unsafe { oakrender::ffi::renderer::oakrender_codec_frame_get_params(frame, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_create(
|
||||
src_space: *const c_char,
|
||||
dst_transform: *const c_char,
|
||||
direction: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_create(src_space, dst_transform, direction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_free(processor: *mut CHandle) {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_processor_free(processor) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_processor_is_valid(processor) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_create_transform(
|
||||
manager: CHandle,
|
||||
input: *const c_char,
|
||||
dest: CHandle,
|
||||
direction: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_create_transform(
|
||||
manager, input, dest, direction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_processor_convert(
|
||||
processor: CHandle,
|
||||
ir: c_double,
|
||||
ig: c_double,
|
||||
ib: c_double,
|
||||
ia: c_double,
|
||||
out_r: *mut c_double,
|
||||
out_g: *mut c_double,
|
||||
out_b: *mut c_double,
|
||||
out_a: *mut c_double,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oakrender::ffi::color::oakrender_color_processor_convert(
|
||||
processor, ir, ig, ib, ia, out_r, out_g, out_b, out_a,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_manager_set_up_default_config() -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_manager_set_up_default_config() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_color_manager_get_config(buf, n) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_is_supported_extension(extension) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_supported_extensions_count() -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_supported_extensions_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakrender` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int {
|
||||
unsafe { oakrender::ffi::color::oakrender_lut_supported_extension_at(i, buf, n) }
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oaktask C ABI bridge: direct Rust calls into the `oaktask` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oaktask`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oaktask C ABI imports, mirroring the oaktask crate's exports
|
||||
//! (`src/task/rust/src/ffi/{manager,task,project}.rs`; headers
|
||||
//! `include/task/*.h`).
|
||||
//!
|
||||
//! Every handle crosses as [`crate::handle::CHandle`]. `oaktask_create_export`
|
||||
//! takes the encoding-params POD ([`crate::bridge::codec::EncodingParamsPOD`],
|
||||
//! field-identical to the task crate's `OakCodecEncodingParams`). String
|
||||
//! getters report the size **including** the NUL; the facade converts with
|
||||
//! [`crate::handle::string_result`].
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `oaktask_event_fn` callback (`include/task/task.h`): `event_id` is an
|
||||
/// `OakTaskEvent` (0=started, 1=progress, 2=finished), `value` 0..1 (or
|
||||
/// start-ms / success flag), `userdata` the subscription token.
|
||||
pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void);
|
||||
|
||||
/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).
|
||||
pub type OakTaskOtioImportConfirmFn = unsafe extern "C" fn(
|
||||
sequence_names: *const *const c_char,
|
||||
count: c_int,
|
||||
userdata: *mut c_void,
|
||||
) -> c_int;
|
||||
|
||||
/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).
|
||||
pub type OakTaskImageSequenceConfirmFn =
|
||||
unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int;
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_init() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_shutdown() {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_shutdown() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_register_codec_submitter() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_register_codec_submitter() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_count() -> c_int {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_at(i: c_int) -> CHandle {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_at(i) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_manager_delete_finished() {
|
||||
unsafe { oaktask::ffi::manager::oaktask_manager_delete_finished() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_free(t: *mut CHandle) {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_free(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_start_sync(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_start_sync(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_start(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_start(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_cancel(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_cancel(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_wait(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_wait(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_is_finished(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_is_finished(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_succeeded(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_succeeded(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_title(t, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_error(t, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_task_subscribe(
|
||||
t: CHandle,
|
||||
cb: Option<OakTaskEventFn>,
|
||||
userdata: *mut c_void,
|
||||
) -> i64 {
|
||||
unsafe { oaktask::ffi::task::oaktask_task_subscribe(t, cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_debug_alive_count() -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_debug_alive_count() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_load(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_take_project(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_take_project(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_save(
|
||||
project: CHandle,
|
||||
filename_or_null: *const c_char,
|
||||
use_compression: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktask::ffi::project::oaktask_create_project_save(
|
||||
project,
|
||||
filename_or_null,
|
||||
use_compression,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_import(
|
||||
folder: CHandle,
|
||||
project: CHandle,
|
||||
urls: *const *const c_char,
|
||||
url_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktask::ffi::project::oaktask_create_project_import(folder, project, urls, url_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_take_command(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_take_command(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_footage_count(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_footage_count(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_footage_at(t, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_invalid_count(t: CHandle) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_invalid_count(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_invalid_at(
|
||||
t: CHandle,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_invalid_at(t, index, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_load_otio(filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_otio_take_project(t) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_project_save_otio(project, filename) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_load_otio_set_confirm_cb(
|
||||
cb: Option<OakTaskOtioImportConfirmFn>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
unsafe { oaktask::ffi::project::oaktask_load_otio_set_confirm_cb(cb, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_precache(footage, index, sequence) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
/// `oaktask_create_export` — direct call into the `oaktask` crate
|
||||
/// (single-lib unification; the encoding-params POD is the shared
|
||||
/// oakcodec type).
|
||||
pub fn oaktask_create_export(
|
||||
viewer: CHandle,
|
||||
color_manager: CHandle,
|
||||
params: *const crate::bridge::codec::EncodingParamsPOD,
|
||||
) -> CHandle {
|
||||
unsafe { oaktask::ffi::project::oaktask_create_export(viewer, color_manager, params) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktask` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktask_import_set_image_sequence_confirm_cb(
|
||||
cb: Option<OakTaskImageSequenceConfirmFn>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
unsafe { oaktask::ffi::project::oaktask_import_set_image_sequence_confirm_cb(cb, userdata) }
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oaktimeline C ABI bridge: direct Rust calls into the `oaktimeline` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oaktimeline`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oaktimeline C ABI imports, mirroring the oaktimeline crate's exports
|
||||
//! (`src/timeline/rust/src/ffi.rs`; headers `include/timeline/*.h`).
|
||||
//!
|
||||
//! Every handle crosses as [`crate::handle::CHandle`]. The marker/workarea
|
||||
//! time quantities cross as `c_int` num/den pairs; the edit commands take
|
||||
//! `i64` rationals. String getters report the size **including** the NUL;
|
||||
//! the facade converts with [`crate::handle::string_result`].
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
// `include/timeline/marker.h` exports (complete inventory):
|
||||
// oaktimeline_marker_list_create / free / of / add / count / at /
|
||||
// add_command / remove_at_command / set_time_command /
|
||||
// set_props_command / list_load / list_save.
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_create() -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_of(owner) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_free(list: *mut CHandle) {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_free(list) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_add(
|
||||
list: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
name: *const c_char,
|
||||
color: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_add(
|
||||
list, in_num, in_den, out_num, out_den, name, color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_count(list, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_at(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
color: *mut c_int,
|
||||
name_buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_at(
|
||||
list, index, in_num, in_den, out_num, out_den, color, name_buf, buf_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_add_command(
|
||||
list: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
name: *const c_char,
|
||||
color: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_add_command(
|
||||
list, in_num, in_den, out_num, out_den, name, color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_remove_at_command(list, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_set_time_command(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_set_time_command(
|
||||
list, index, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_set_props_command(
|
||||
list: CHandle,
|
||||
index: c_int,
|
||||
color: c_int,
|
||||
name: *const c_char,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_set_props_command(list, index, color, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_load(list: CHandle, reader: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_load(list, reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_marker_list_save(list: CHandle, writer: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::marker::oaktimeline_marker_list_save(list, writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_create() -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_create() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_of(owner) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_free(w: *mut CHandle) {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_free(w) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled(w, enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_get(
|
||||
w: CHandle,
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
enabled: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_get(
|
||||
w, in_num, in_den, out_num, out_den, enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_range(
|
||||
w: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range(
|
||||
w, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_range_command(
|
||||
w: CHandle,
|
||||
in_num: c_int,
|
||||
in_den: c_int,
|
||||
out_num: c_int,
|
||||
out_den: c_int,
|
||||
old_in_num: c_int,
|
||||
old_in_den: c_int,
|
||||
old_out_num: c_int,
|
||||
old_out_den: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_set_range_command(
|
||||
w,
|
||||
in_num,
|
||||
in_den,
|
||||
out_num,
|
||||
out_den,
|
||||
old_in_num,
|
||||
old_in_den,
|
||||
old_out_num,
|
||||
old_out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_set_enabled_command(w, enabled) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_reset(
|
||||
in_num: *mut c_int,
|
||||
in_den: *mut c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
oaktimeline::ffi::workarea::oaktimeline_workarea_reset(in_num, in_den, out_num, out_den)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_load(w: CHandle, reader: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_load(w, reader) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_workarea_save(w: CHandle, writer: CHandle) -> c_int {
|
||||
unsafe { oaktimeline::ffi::workarea::oaktimeline_workarea_save(w, writer) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_add_track_command(list) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_remove_track_command(track) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_place_block_command(
|
||||
list: CHandle,
|
||||
track_index: c_int,
|
||||
block: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_place_block_command(
|
||||
list,
|
||||
track_index,
|
||||
block,
|
||||
in_num,
|
||||
in_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle {
|
||||
unsafe { oaktimeline::ffi::edit::oaktimeline_replace_block_with_gap_command(track, block) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_move_block_command(
|
||||
list: CHandle,
|
||||
track_index: c_int,
|
||||
block: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_move_block_command(
|
||||
list,
|
||||
track_index,
|
||||
block,
|
||||
in_num,
|
||||
in_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_trim_command(
|
||||
track: CHandle,
|
||||
block: CHandle,
|
||||
new_length_num: i64,
|
||||
new_length_den: i64,
|
||||
mode: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_trim_command(
|
||||
track,
|
||||
block,
|
||||
new_length_num,
|
||||
new_length_den,
|
||||
mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_split_command(
|
||||
blocks: *const CHandle,
|
||||
count: c_int,
|
||||
point_num: i64,
|
||||
point_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_split_command(blocks, count, point_num, point_den)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_split_preserving_links_command(
|
||||
blocks: *const CHandle,
|
||||
count: c_int,
|
||||
point_nums: *const i64,
|
||||
point_dens: *const i64,
|
||||
time_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_split_preserving_links_command(
|
||||
blocks, count, point_nums, point_dens, time_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_ripple_delete_gaps_command(
|
||||
sequence: CHandle,
|
||||
in_nums: *const i64,
|
||||
in_dens: *const i64,
|
||||
out_nums: *const i64,
|
||||
out_dens: *const i64,
|
||||
tracks: *const CHandle,
|
||||
range_count: c_int,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_ripple_delete_gaps_command(
|
||||
sequence,
|
||||
in_nums,
|
||||
in_dens,
|
||||
out_nums,
|
||||
out_dens,
|
||||
tracks,
|
||||
range_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_slide_command(
|
||||
track: CHandle,
|
||||
blocks: *const CHandle,
|
||||
block_count: c_int,
|
||||
in_adjacent: CHandle,
|
||||
out_adjacent: CHandle,
|
||||
movement_num: i64,
|
||||
movement_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_slide_command(
|
||||
track,
|
||||
blocks,
|
||||
block_count,
|
||||
in_adjacent,
|
||||
out_adjacent,
|
||||
movement_num,
|
||||
movement_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_ripple_remove_area_command(
|
||||
track: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_ripple_remove_area_command(
|
||||
track, in_num, in_den, out_num, out_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call into the `oaktimeline` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oaktimeline_insert_gaps_command(
|
||||
list: CHandle,
|
||||
point_num: i64,
|
||||
point_den: i64,
|
||||
length_num: i64,
|
||||
length_den: i64,
|
||||
) -> CHandle {
|
||||
unsafe {
|
||||
oaktimeline::ffi::edit::oaktimeline_insert_gaps_command(
|
||||
list, point_num, point_den, length_num, length_den,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakundo C ABI bridge: direct Rust calls into the `oakundo` crate.
|
||||
//!
|
||||
//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): every
|
||||
//! call below is a compile-time Rust call into `oakundo`'s `ffi` (the
|
||||
//! `#[no_mangle]` exports stay in the dylib for the external C ABI;
|
||||
//! internal callers bypass them). Handles cross as the shared
|
||||
//! [`crate::handle::CHandle`]. Exceptions that keep an `extern "C"`
|
||||
//! declaration (resolved at link time against the sibling crate in the
|
||||
//! same dylib) are the host `oakcore_*` symbols and the encoding-params
|
||||
//! C ABI POD crossings (the facade keeps its own POD mirrors there).
|
||||
|
||||
// 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/>.
|
||||
|
||||
//! oakundo C ABI imports, mirroring the oakundo crate's exports
|
||||
//! (`src/undo/rust/src/ffi.rs`; headers `include/undo/*.h`).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/undo/undocommand.h` — callback table backing a
|
||||
/// caller-defined undo command. Single-lib unification: aliases the
|
||||
/// oakundo crate's vtable POD.
|
||||
pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable;
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_init(
|
||||
vtable: *const OakUndoCommandVtable,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> CHandle {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_init_multi() -> CHandle {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_multi_child(multi, index, out_child) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_redo_now(command: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_undo_now(command: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_command_free(command: *mut CHandle) {
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_init() -> CHandle {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_init() }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_free(stack: *mut CHandle) {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_push_pre_executed(
|
||||
stack: CHandle,
|
||||
command: CHandle,
|
||||
name: *const c_char,
|
||||
) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_push_pre_executed(stack, command, name) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_undo(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_undo(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_redo(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_redo(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_jump(stack: CHandle, index: i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_jump(stack, index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_clear(stack: CHandle) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_clear(stack) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_undo(stack, out_value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, out_value) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_count(stack: CHandle, out_count: *mut i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_count(stack, out_count) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_index(stack: CHandle, out_index: *mut i64) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_index(stack, out_index) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_command_text(
|
||||
stack: CHandle,
|
||||
row: i64,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_text(stack, row, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakundo` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakundo_undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int {
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_command_is_done(stack, row, out_value) }
|
||||
}
|
||||
+382
-19
@@ -16,7 +16,7 @@
|
||||
|
||||
//! `engine/include/oakengine/encoding.h` over the oakcodec module.
|
||||
//!
|
||||
//! Two parts:
|
||||
//! Three parts:
|
||||
//!
|
||||
//! - The container/codec **metadata family** (format names/extensions,
|
||||
//! per-format codec lists, pixel/sample formats, filename helpers,
|
||||
@@ -30,15 +30,19 @@
|
||||
//! "unset" (the POD's 0 is a valid format, DNxHD); encoder-specific
|
||||
//! video options are kept in a facade-side map (the POD has no such
|
||||
//! field).
|
||||
//! - The **exporter family** (`engine/include/oakengine/exporter.h`)
|
||||
//! assembles an encoding-params handle and drives the export task
|
||||
//! synchronously (see the family section at the bottom).
|
||||
//!
|
||||
//! Presets, preset load/save and the sequence-bound last-used/export
|
||||
//! entry points are deferred (see the stubs below and `deferred.rs`).
|
||||
//! Presets, preset load/save and the sequence-bound last-used entry
|
||||
//! points remain deferred (see the stubs below and `deferred.rs`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::codec as k;
|
||||
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::stubs::codec as k;
|
||||
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{guard, guard_int, string_result};
|
||||
@@ -55,6 +59,10 @@ pub struct OakEngineEncodingParams {
|
||||
struct ParamsBox {
|
||||
pod: EncodingParamsPOD,
|
||||
video_options: HashMap<String, String>,
|
||||
/// Raw scaling-method code exactly as the caller set it: the POD's
|
||||
/// `VideoScalingMethod` enum cannot carry garbage codes, and the
|
||||
/// facade contract accepts any `int` verbatim (round-trips 99 as 99).
|
||||
video_scaling_raw: c_int,
|
||||
}
|
||||
|
||||
impl ParamsBox {
|
||||
@@ -64,6 +72,7 @@ impl ParamsBox {
|
||||
ParamsBox {
|
||||
pod,
|
||||
video_options: HashMap::new(),
|
||||
video_scaling_raw: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,7 +466,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_video(
|
||||
p.pod.video_height = v.height;
|
||||
p.pod.video_time_base_num = v.time_base_num;
|
||||
p.pod.video_time_base_den = v.time_base_den;
|
||||
p.pod.video_pixel_format = v.format;
|
||||
p.pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
|
||||
p.pod.video_interlacing = v.interlacing;
|
||||
p.pod.video_pixel_aspect_num = v.pixel_aspect_num;
|
||||
p.pod.video_pixel_aspect_den = v.pixel_aspect_den;
|
||||
@@ -480,7 +489,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_enable_audio(
|
||||
p.pod.audio_codec = codec;
|
||||
p.pod.audio_sample_rate = sample_rate;
|
||||
p.pod.audio_channel_layout = channel_layout;
|
||||
p.pod.audio_sample_format = sample_format;
|
||||
p.pod.audio_sample_format = crate::pods::sample_format_from_code(sample_format);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -594,7 +603,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_video_params(
|
||||
(*out).height = p.pod.video_height;
|
||||
(*out).time_base_num = p.pod.video_time_base_num;
|
||||
(*out).time_base_den = p.pod.video_time_base_den;
|
||||
(*out).format = p.pod.video_pixel_format;
|
||||
(*out).format = p.pod.video_pixel_format as i32;
|
||||
(*out).interlacing = p.pod.video_interlacing;
|
||||
(*out).pixel_aspect_num = p.pod.video_pixel_aspect_num;
|
||||
(*out).pixel_aspect_den = p.pod.video_pixel_aspect_den;
|
||||
@@ -645,7 +654,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_get_audio_params(
|
||||
*channel_layout = p.pod.audio_channel_layout;
|
||||
}
|
||||
if !sample_format.is_null() {
|
||||
*sample_format = p.pod.audio_sample_format;
|
||||
*sample_format = p.pod.audio_sample_format as i32;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -964,7 +973,10 @@ pub unsafe extern "C" fn oakengine_encoding_params_set_video_scaling_method(
|
||||
) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let p = params_mut(params)?;
|
||||
p.pod.video_scaling_method = method;
|
||||
// The raw code round-trips verbatim (garbage codes included);
|
||||
// the POD carries the nearest legal enum for the encoder.
|
||||
p.video_scaling_raw = method;
|
||||
p.pod.video_scaling_method = crate::pods::scaling_from_code(method);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -976,7 +988,7 @@ pub unsafe extern "C" fn oakengine_encoding_params_video_scaling_method(
|
||||
) -> c_int {
|
||||
guard_int(|| unsafe {
|
||||
let p = params_ref(params)?;
|
||||
Ok(p.pod.video_scaling_method)
|
||||
Ok(p.video_scaling_raw)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1073,14 +1085,365 @@ pub unsafe extern "C" fn oakengine_encoding_params_save_file(
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` — **not backed** (the exporter
|
||||
/// family is facade-only; see `deferred.rs`). Returns OAKENGINE_E_FAILED.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exporter family (exporter.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Thread-local reason for the last failed export on this thread (the C++
|
||||
// `g_last_error`, `engine/src/capi/export.cpp`). Cleared at the start of
|
||||
// every export call; read by [`oakengine_export_last_error`].
|
||||
thread_local! {
|
||||
static EXPORT_LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
// Thread-local progress callback installed by
|
||||
// [`oakengine_export_set_progress_callback`] (the C++ `g_progress_fn` /
|
||||
// `g_progress_userdata`). Per-thread like the C++: the synchronous export
|
||||
// runs on the installing thread, so the module task events arrive there.
|
||||
thread_local! {
|
||||
static EXPORT_PROGRESS: RefCell<
|
||||
Option<(unsafe extern "C" fn(c_double, *mut c_void), *mut c_void)>,
|
||||
> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// The module task progress event id (`OAKTASK_EVENT_PROGRESS`, see
|
||||
/// `oakengine_task_subscribe`).
|
||||
const EXPORT_EVENT_PROGRESS: c_int = 1;
|
||||
|
||||
fn export_last_error_set(msg: String) {
|
||||
EXPORT_LAST_ERROR.with(|e| *e.borrow_mut() = msg);
|
||||
}
|
||||
|
||||
/// Forward the progress events of a running export to the installed
|
||||
/// callback. Installed as the task subscription only while a callback is
|
||||
/// set; the module passes the callback's own `userdata` through.
|
||||
unsafe extern "C" fn export_progress_event(event_id: c_int, value: f64, userdata: *mut c_void) {
|
||||
if event_id != EXPORT_EVENT_PROGRESS {
|
||||
return;
|
||||
}
|
||||
EXPORT_PROGRESS.with(|slot| {
|
||||
if let Some((cb, _)) = *slot.borrow() {
|
||||
// SAFETY: the callback + userdata follow the installer's
|
||||
// contract; the task emits on its running thread.
|
||||
unsafe { cb(value, userdata) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Run an export task synchronously on the calling thread — the shared
|
||||
/// tail of every exporter-family entry point: create the task (taking
|
||||
/// ownership of `params`), subscribe the installed progress callback, run
|
||||
/// through [`oakengine_task_start_sync`], read the task error into the
|
||||
/// thread-local last-error slot, and free the task.
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success, OAKENGINE_E_FAILED otherwise. On the
|
||||
/// task-creation failure path `params` ownership stays with the caller
|
||||
/// (mirroring [`oakengine_task_create_export`]).
|
||||
fn export_run_sync(seq: *mut crate::handle::OakEngineSequence, params: *mut OakEngineEncodingParams) -> c_int {
|
||||
let task = unsafe { crate::task::oakengine_task_create_export(seq, params) };
|
||||
if task.is_null() {
|
||||
export_last_error_set("failed to create the export task".into());
|
||||
return crate::error::OAKENGINE_E_FAILED;
|
||||
}
|
||||
// Progress events through the same module subscription the app's
|
||||
// `start_export` uses (`oakengine_task_subscribe`).
|
||||
EXPORT_PROGRESS.with(|slot| {
|
||||
if let Some((_, userdata)) = *slot.borrow() {
|
||||
unsafe {
|
||||
crate::task::oakengine_task_subscribe(task, Some(export_progress_event), userdata);
|
||||
}
|
||||
}
|
||||
});
|
||||
let ok = unsafe { crate::task::oakengine_task_start_sync(task) };
|
||||
let rc = if ok == 1 {
|
||||
crate::error::OAKENGINE_OK
|
||||
} else {
|
||||
let err = export_task_error(task);
|
||||
export_last_error_set(if err.is_empty() {
|
||||
"export failed".into()
|
||||
} else {
|
||||
err
|
||||
});
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
};
|
||||
unsafe { crate::task::oakengine_task_free(task) };
|
||||
rc
|
||||
}
|
||||
|
||||
/// Two-stage read of a task's error string (empty when none).
|
||||
fn export_task_error(task: *mut crate::handle::OakEngineTask) -> String {
|
||||
unsafe {
|
||||
let needed = crate::task::oakengine_task_error(task, std::ptr::null_mut(), 0);
|
||||
if needed <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0 as c_char; needed as usize + 1];
|
||||
let n = crate::task::oakengine_task_error(task, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
if n < 0 {
|
||||
return String::new();
|
||||
}
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf8_lossy(unsafe {
|
||||
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` — render `seq` to the file the
|
||||
/// encoding params describe, through the same synchronous export path the
|
||||
/// app's `start_export` drives (`oakengine_task_create_export` +
|
||||
/// `oakengine_task_start_sync` + free).
|
||||
///
|
||||
/// Takes ownership of `params` on success (destroyed with the export
|
||||
/// task, mirroring [`oakengine_task_create_export`]); on the
|
||||
/// task-creation failure path the caller keeps ownership. The sequence
|
||||
/// handle is validated for non-NULL only — the C++ "handle is a sequence
|
||||
/// of the active project" walk has no Rust analogue (created sequences
|
||||
/// live in their own scratch project, see `oakengine_sequence_new`).
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for NULL
|
||||
/// arguments; OAKENGINE_E_FAILED for creation/run failures (see
|
||||
/// [`oakengine_export_last_error`]).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_render_with_params(
|
||||
_seq: *mut crate::handle::OakEngineSequence,
|
||||
_params: *const OakEngineEncodingParams,
|
||||
seq: *mut crate::handle::OakEngineSequence,
|
||||
params: *const OakEngineEncodingParams,
|
||||
) -> c_int {
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
guard(|| unsafe {
|
||||
export_last_error_set(String::new());
|
||||
if seq.is_null() || params.is_null() {
|
||||
export_last_error_set("invalid arguments".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
if export_run_sync(seq, params as *mut OakEngineEncodingParams) == crate::error::OAKENGINE_OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed("export failed".into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_render` — render `seq`'s [in_ts, out_ts) range
|
||||
/// offline and encode it to `path`.
|
||||
///
|
||||
/// `in_ts`/`out_ts` are frame timestamps in the sequence's frame-rate
|
||||
/// timebase (the export frame rate is the sequence frame rate). `width`/
|
||||
/// `height` <= 0 fall back to the sequence's video dimensions; when they
|
||||
/// differ the frames are scaled to fit. Video is encoded with the
|
||||
/// options' codec (default H.264 in an MP4 container), audio with the
|
||||
/// options' codec (default AAC) at the requested rate/layout (defaults:
|
||||
/// 48 kHz stereo — the engine has no sequence-audio getter, so the
|
||||
/// header's "sequence rate/layout" fallback mirrors the app's export
|
||||
/// dialog instead). The options' codec fields carry the exporter.h
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` / `OAKENGINE_EXPORT_AUDIO_*` values,
|
||||
/// mapped here onto the engine's `ExportFormat` / `ExportCodec` ids.
|
||||
///
|
||||
/// The call blocks until the export finishes; progress is reported
|
||||
/// through the callback set with [`oakengine_export_set_progress_callback`].
|
||||
///
|
||||
/// Deviations from the C++ header: no `OAKENGINE_INIT_RENDER`
|
||||
/// requirement (the Rust render path is CPU-only and self-contained, see
|
||||
/// `oakengine_render_manager_init`) and no "sequence is part of a
|
||||
/// project" check (created sequences live in a scratch project).
|
||||
///
|
||||
/// Returns OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad
|
||||
/// arguments; OAKENGINE_E_FAILED for render/encode failures (see
|
||||
/// [`oakengine_export_last_error`]).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_render(
|
||||
seq: *mut crate::handle::OakEngineSequence,
|
||||
path: *const c_char,
|
||||
in_ts: i64,
|
||||
out_ts: i64,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
opts: *const crate::pods::OakExportOptions,
|
||||
) -> c_int {
|
||||
guard(|| unsafe {
|
||||
export_last_error_set(String::new());
|
||||
if seq.is_null() || path.is_null() || in_ts < 0 || out_ts <= in_ts {
|
||||
export_last_error_set("invalid arguments".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let o = if opts.is_null() {
|
||||
crate::pods::OakExportOptions {
|
||||
video_codec: crate::pods::OAKENGINE_EXPORT_VIDEO_H264,
|
||||
audio_codec: crate::pods::OAKENGINE_EXPORT_AUDIO_AAC,
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 0,
|
||||
audio_channel_count: 0,
|
||||
}
|
||||
} else {
|
||||
*opts
|
||||
};
|
||||
|
||||
// Map the exporter.h codec ids onto the engine's enum ids.
|
||||
let (format, vcodec) = match o.video_codec {
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_H264 => (
|
||||
oakcodec::exportformat::Format::MPEG4Video as i32,
|
||||
oakcodec::exportcodec::Codec::H264 as i32,
|
||||
),
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_H265 => (
|
||||
oakcodec::exportformat::Format::MPEG4Video as i32,
|
||||
oakcodec::exportcodec::Codec::H265 as i32,
|
||||
),
|
||||
crate::pods::OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE => (
|
||||
oakcodec::exportformat::Format::PNG as i32,
|
||||
oakcodec::exportcodec::Codec::PNG as i32,
|
||||
),
|
||||
_ => {
|
||||
export_last_error_set(format!("unknown video codec {}", o.video_codec));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
};
|
||||
let audio_enabled = o.audio_codec != crate::pods::OAKENGINE_EXPORT_AUDIO_NONE;
|
||||
let acodec = if audio_enabled {
|
||||
match o.audio_codec {
|
||||
crate::pods::OAKENGINE_EXPORT_AUDIO_AAC => oakcodec::exportcodec::Codec::AAC as i32,
|
||||
crate::pods::OAKENGINE_EXPORT_AUDIO_PCM => oakcodec::exportcodec::Codec::PCM as i32,
|
||||
_ => {
|
||||
export_last_error_set(format!("unknown audio codec {}", o.audio_codec));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Sequence geometry + frame rate (the export frame rate is the
|
||||
// sequence's).
|
||||
let mut sw: c_int = 0;
|
||||
let mut sh: c_int = 0;
|
||||
let mut par_num: c_int = 1;
|
||||
let mut par_den: c_int = 1;
|
||||
Error::from_module(crate::timeline::oakengine_sequence_get_video_params(
|
||||
seq,
|
||||
&mut sw,
|
||||
&mut sh,
|
||||
&mut par_num,
|
||||
&mut par_den,
|
||||
))?;
|
||||
let mut rate_num: c_int = 0;
|
||||
let mut rate_den: c_int = 1;
|
||||
Error::from_module(crate::timeline::oakengine_sequence_get_frame_rate(
|
||||
seq,
|
||||
&mut rate_num,
|
||||
&mut rate_den,
|
||||
))?;
|
||||
if rate_num <= 0 || rate_den <= 0 {
|
||||
export_last_error_set("sequence has no valid frame rate".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let out_w = if width > 0 { width } else { sw };
|
||||
let out_h = if height > 0 { height } else { sh };
|
||||
if out_w <= 0 || out_h <= 0 {
|
||||
export_last_error_set("sequence has no valid video dimensions".into());
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let sample_rate = if o.audio_sample_rate > 0 { o.audio_sample_rate } else { 48000 };
|
||||
let layout: u64 = if o.audio_channel_count > 0 {
|
||||
match o.audio_channel_count {
|
||||
1 => 0x4, // AV_CH_LAYOUT_MONO
|
||||
2 => 0x3, // AV_CH_LAYOUT_STEREO
|
||||
n => {
|
||||
export_last_error_set(format!(
|
||||
"unsupported audio channel count {n} (1 = mono, 2 = stereo)"
|
||||
));
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0x3
|
||||
};
|
||||
|
||||
// Assemble the encoding params through the public setters (the same
|
||||
// path the app's `start_export` uses); the task consumes the handle
|
||||
// once created.
|
||||
let params = oakengine_encoding_params_create();
|
||||
if params.is_null() {
|
||||
export_last_error_set("failed to create encoding params".into());
|
||||
return Err(Error::Failed("failed to create encoding params".into()));
|
||||
}
|
||||
let fail = |msg: &str| -> Result<()> {
|
||||
oakengine_encoding_params_destroy(params);
|
||||
export_last_error_set(msg.into());
|
||||
Err(Error::Failed(msg.into()))
|
||||
};
|
||||
let cpath = std::ffi::CString::new(crate::handle::read_cstr(path))
|
||||
.map_err(|_| Error::Failed("invalid path (NUL byte)".into()))?;
|
||||
if oakengine_encoding_params_set_filename(params, cpath.as_ptr()) != 0 {
|
||||
return fail("failed to set the export filename");
|
||||
}
|
||||
if oakengine_encoding_params_set_format(params, format) != 0 {
|
||||
return fail("failed to set the export format");
|
||||
}
|
||||
let pod = OakVideoParamsPod {
|
||||
width: out_w,
|
||||
height: out_h,
|
||||
time_base_num: rate_den,
|
||||
time_base_den: rate_num,
|
||||
format: 0,
|
||||
pixel_aspect_num: par_num.max(1),
|
||||
pixel_aspect_den: par_den.max(1),
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
if oakengine_encoding_params_enable_video(params, &pod, vcodec) != 0 {
|
||||
return fail("failed to enable video");
|
||||
}
|
||||
if audio_enabled && oakengine_encoding_params_enable_audio(params, sample_rate, layout, 0, acodec) != 0 {
|
||||
return fail("failed to enable audio");
|
||||
}
|
||||
if o.video_bit_rate > 0 {
|
||||
oakengine_encoding_params_set_video_bit_rate(params, o.video_bit_rate);
|
||||
}
|
||||
// Fit scaling (the header's documented behavior when the output
|
||||
// size differs from the sequence's).
|
||||
if oakengine_encoding_params_set_video_scaling_method(params, 0) != 0 {
|
||||
return fail("failed to set the video scaling method");
|
||||
}
|
||||
// Export range as seconds rationals: frame timestamps in the
|
||||
// sequence's frame-rate timebase (frame duration = rate_den/rate_num).
|
||||
let tb_num = i64::from(rate_den);
|
||||
let tb_den = i64::from(rate_num);
|
||||
oakengine_encoding_params_set_custom_range(params, in_ts * tb_num, tb_den, out_ts * tb_num, tb_den);
|
||||
oakengine_encoding_params_set_export_length(params, ((out_ts - in_ts) * tb_num) as c_int, rate_num);
|
||||
|
||||
if export_run_sync(seq, params) == crate::error::OAKENGINE_OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed("export failed".into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_last_error` — the reason for the last failed export
|
||||
/// on this thread (buf/size; empty after a successful export).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
guard_int(|| {
|
||||
let err = EXPORT_LAST_ERROR.with(|e| e.borrow().clone());
|
||||
Ok(unsafe { crate::handle::write_string(&err, buf, buf_size) })
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_export_set_progress_callback` — install the progress
|
||||
/// callback used by subsequent [`oakengine_export_render`] /
|
||||
/// [`oakengine_export_render_with_params`] calls on this thread (NULL
|
||||
/// disables). The callback receives `fraction` in [0, 1] and is invoked
|
||||
/// on the exporting thread during the synchronous run.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_export_set_progress_callback(
|
||||
f: Option<unsafe extern "C" fn(c_double, *mut c_void)>,
|
||||
userdata: *mut c_void,
|
||||
) {
|
||||
crate::handle::guard_void(|| {
|
||||
EXPORT_PROGRESS.with(|slot| *slot.borrow_mut() = f.map(|cb| (cb, userdata)));
|
||||
});
|
||||
}
|
||||
|
||||
/// `oakengine_encoding_params_get_last_used` — **not backed** (sequence
|
||||
@@ -1115,9 +1478,9 @@ pub unsafe extern "C" fn oakengine_encoding_start_audio_recording(
|
||||
if m.is_null() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
let rc = crate::bridge::audio::oakaudio_manager_start_recording(
|
||||
let rc = crate::stubs::audio::oakaudio_manager_start_recording(
|
||||
m,
|
||||
&p.pod as *const oakaudio::bridge::codec::EncodingParams,
|
||||
&p.pod as *const crate::pods::EncodingParamsPOD,
|
||||
errbuf,
|
||||
errbuf_size,
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::common as c;
|
||||
use crate::stubs::common as c;
|
||||
use crate::error::Error;
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_int, guard_void, string_result, OakEngineClipboard,
|
||||
|
||||
@@ -49,14 +49,15 @@
|
||||
//! crates lack the C ABI surface — each stub returns its header's
|
||||
//! documented failure value:
|
||||
//!
|
||||
//! - **codec** (encoding.h, 81/85 wrapped): the preset path/count/name,
|
||||
//! preset load/save and the sequence-bound export/last-used entry
|
||||
//! points (`oakengine_encoding_preset_*`,
|
||||
//! - **codec** (encoding.h, 82/85 wrapped): the preset path/count/name,
|
||||
//! preset load/save and the sequence-bound last-used entry points
|
||||
//! (`oakengine_encoding_preset_*`,
|
||||
//! `oakengine_encoding_params_load_file/save_file`,
|
||||
//! `oakengine_export_render_with_params`,
|
||||
//! `oakengine_encoding_params_get/set_last_used`) are stubs — the
|
||||
//! oakcodec crate has no preset API and those entry points need the
|
||||
//! exporter/sequence families.
|
||||
//! oakcodec crate has no preset API and the last-used pair needs the
|
||||
//! deferred node/timeline families. The exporter entry point
|
||||
//! (`oakengine_export_render_with_params`) is backed since M12 (see
|
||||
//! `crate::codec`, "Exporter family").
|
||||
//! - **render color** (color.h, 19/31 wrapped): the color-manager list
|
||||
//! queries (colorspace/display/view/look/compliant/luma), the
|
||||
//! standalone config handle and `color_processor_id` /
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
//! [`write_string`]): the return value is the required length including
|
||||
//! the terminating NUL; negative values are error codes.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
@@ -48,6 +48,95 @@ use crate::error::{Error, Result};
|
||||
/// `Clone + Copy + Send + Sync` come from the shared type.
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// Engine-side boxed payloads holding the oaknode domain (single-lib
|
||||
/// unification). Every `oakengine_*` node-family handle ultimately wraps
|
||||
/// one of these behind a [`CHandle`]:
|
||||
///
|
||||
/// - projects box [`domain::ProjectArc`] (`Arc<Mutex<Project>>`);
|
||||
/// - nodes, blocks, tracks, footage, sequences and folders box a
|
||||
/// [`domain::NodeRef`] (`(Arc<Mutex<Project>>, NodeId)` — the
|
||||
/// oaknode crate's `project::NodeRef` value type).
|
||||
///
|
||||
/// The box is created through `oaknode::handle::make_owned` (refcounted
|
||||
/// shell + release callback), so the facade's existing
|
||||
/// [`box_handle`]/[`free_box`] discipline (and the addref copies the
|
||||
/// engine takes) works unchanged.
|
||||
pub mod domain {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oaknode::id::NodeId;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// Engine-side boxed payload for project handles: shared ownership of
|
||||
/// the oaknode domain project (its graph, settings, filename state).
|
||||
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
|
||||
|
||||
/// Engine-side boxed payload for node/block/track/footage/sequence/
|
||||
/// folder handles: a reference into a project's graph. Reuses the
|
||||
/// oaknode crate's own `NodeRef` value type (project + id + owned
|
||||
/// flag); a stale id fails validation instead of aliasing.
|
||||
pub type NodeRef = oaknode::project::NodeRef;
|
||||
|
||||
/// Box a project payload behind a refcounted handle.
|
||||
pub fn box_project(project: ProjectArc) -> CHandle {
|
||||
oaknode::handle::make_owned(project)
|
||||
}
|
||||
|
||||
/// Box a node reference behind a refcounted handle. `owned` marks
|
||||
/// detached (factory-created) nodes so the engine's debug alive
|
||||
/// counter accounts them exactly once.
|
||||
pub fn box_node(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
|
||||
oaknode::handle::make_owned(NodeRef::new(project, id, owned))
|
||||
}
|
||||
|
||||
/// Borrow the project payload behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_project`] (or empty).
|
||||
pub unsafe fn project_of(h: &CHandle) -> Option<&ProjectArc> {
|
||||
// SAFETY: forwarded to the oaknode handle contract.
|
||||
unsafe { oaknode::handle::get::<ProjectArc>(h) }
|
||||
}
|
||||
|
||||
/// Borrow the node-reference payload behind a handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_node`] (or empty).
|
||||
pub unsafe fn node_ref_of(h: &CHandle) -> Option<&NodeRef> {
|
||||
// SAFETY: forwarded to the oaknode handle contract.
|
||||
unsafe { oaknode::handle::get::<NodeRef>(h) }
|
||||
}
|
||||
|
||||
/// Mutable view of the node-reference payload (used by the graph
|
||||
/// transfer paths, which rewrite the shared box in place — the
|
||||
/// "write_node_ref" semantics).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle created by [`box_node`]; the caller must
|
||||
/// hold exclusive access to the boxed value.
|
||||
pub unsafe fn node_ref_mut(h: &CHandle) -> Option<&mut NodeRef> {
|
||||
// SAFETY: forwarded to the shared-box contract.
|
||||
unsafe { boxed_mut::<NodeRef>(h) }
|
||||
}
|
||||
|
||||
/// Mutable typed view into an oaknode-style `RefBox` payload (the
|
||||
/// oaknode crate exposes only a read-only `get`; this mirrors its
|
||||
/// box layout — `refs`/`value` are `pub` fields).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live handle boxing `T`; the caller must hold
|
||||
/// exclusive access to the boxed value.
|
||||
pub unsafe fn boxed_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
|
||||
if h.ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: contract above; the box is an
|
||||
// `oaknode::handle::RefBox<T>`.
|
||||
unsafe { Some(&mut (*(h.ctx as *mut oaknode::handle::RefBox<T>)).value) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Engine opaque handle types, one per `typedef struct OakEngine*` in
|
||||
/// `engine/include/oakengine/*.h`. All are thin newtype wrappers around a
|
||||
/// [`CHandle`] value with a uniform extraction surface ([`EngineBox`]).
|
||||
|
||||
+689
-17
@@ -14,22 +14,33 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Render-worker IPC: named shared memory holding the frame-slot pools —
|
||||
//! the Rust port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
|
||||
//! `oliveimpl/render/ipc/frameslotpool.cpp`) behind the frozen C ABI in
|
||||
//! `engine/include/oakengine/ipc.h`.
|
||||
//! Render-worker IPC: the control-plane NDJSON protocol and the
|
||||
//! shared-memory frame-slot transport, owned by the oakengine facade and
|
||||
//! exported through the frozen `oakengine_ipc_*` C ABI
|
||||
//! (`engine/include/oakengine/ipc.h`); the `oak-worker` binary consumes it
|
||||
//! purely through the C ABI. The transport is the Rust port of
|
||||
//! `engine/render/ipc/` + `ipcmessage.cpp`.
|
||||
//!
|
||||
//! Two pieces, mirroring the C++ exactly:
|
||||
//! Two halves:
|
||||
//!
|
||||
//! - [`SharedMemoryRegion`]: a named POSIX segment (`shm_open` + `mmap`,
|
||||
//! `munmap` + `shm_unlink` on close). One process creates the segment
|
||||
//! (owner, unlinks on close); the peer attaches to it by key.
|
||||
//! - [`FrameSlotPool`]: a fixed pool of equal-sized frame slots laid out
|
||||
//! inside a region, with lock-free hand-off through two
|
||||
//! - **Control plane.** One compact JSON object per line on the stdio
|
||||
//! pipes (worker.cpp / ipcmessage.cpp `write_message`/`read_message`).
|
||||
//! Every message carries a `"type"` string; the field names below are
|
||||
//! the ones the C++ serializers actually emit
|
||||
//! (`engine/render/ipc/ipcmessage.cpp`): note `ticket` / `node` /
|
||||
//! `channels` / `slot` — the longer names (`ticket_id`, `node_uuid`,
|
||||
//! `channel_count`, `output_slot`) exist only on the C POD structs in
|
||||
//! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines.
|
||||
//! - **Data plane.** Named shared memory holding the frame-slot pools —
|
||||
//! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
|
||||
//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX
|
||||
//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close),
|
||||
//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame
|
||||
//! slots inside it with lock-free hand-off through two
|
||||
//! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a
|
||||
//! single-producer/single-consumer structure; the filler owns
|
||||
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` + `free.push`,
|
||||
//! so no mutex is ever taken.
|
||||
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` +
|
||||
//! `free.push`, so no mutex is ever taken.
|
||||
//!
|
||||
//! **The in-memory layout is the version-1 wire protocol** the app and the
|
||||
//! render worker share, and it never changes: the byte offsets below are
|
||||
@@ -40,15 +51,178 @@
|
||||
//!
|
||||
//! This module is deliberately unsafe-heavy and self-contained: it touches
|
||||
//! raw shared memory and raw POSIX syscalls, and everything else in the
|
||||
//! facade reaches it through the safe wrapper methods and the C ABI exports
|
||||
//! at the bottom.
|
||||
//! crate reaches it through the safe wrapper methods.
|
||||
//!
|
||||
//! Message types (M = main/editor, W = worker):
|
||||
//! handshake M<->W negotiate protocol version + announce shm geometry
|
||||
//! load_graph M ->W path to a temp file holding the serialized graph
|
||||
//! render_frame M ->W request a frame render (ticket, node, time, params)
|
||||
//! frame_ready W ->M a rendered frame is published (slot + ticket)
|
||||
//! cancel M ->W abandon an in-flight ticket
|
||||
//! graph_update M ->W reserved (no payload struct yet)
|
||||
//! shutdown M ->W finish current work and exit cleanly
|
||||
//! error W ->M worker-side failure report ("message" field)
|
||||
//!
|
||||
//! Items the worker does not emit yet (frame_ready, graph_update,
|
||||
//! `FrameReadyMsg`) and message ids it ignores (`cancel`) are kept as the
|
||||
//! documented protocol surface; `dead_code` until the frame-slot transport
|
||||
//! is driven by a real graph (see [`crate::worker`]).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::io::{self, Write};
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::handle::{guard_int, guard_ptr};
|
||||
|
||||
/// `"handshake"`.
|
||||
pub const TYPE_HANDSHAKE: &str = "handshake";
|
||||
/// `"load_graph"`.
|
||||
pub const TYPE_LOAD_GRAPH: &str = "load_graph";
|
||||
/// `"render_frame"`.
|
||||
pub const TYPE_RENDER_FRAME: &str = "render_frame";
|
||||
/// `"frame_ready"`.
|
||||
pub const TYPE_FRAME_READY: &str = "frame_ready";
|
||||
/// `"cancel"`.
|
||||
pub const TYPE_CANCEL: &str = "cancel";
|
||||
/// `"graph_update"`.
|
||||
pub const TYPE_GRAPH_UPDATE: &str = "graph_update";
|
||||
/// `"shutdown"`.
|
||||
pub const TYPE_SHUTDOWN: &str = "shutdown";
|
||||
/// `"error"`.
|
||||
pub const TYPE_ERROR: &str = "error";
|
||||
|
||||
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake`
|
||||
/// (ipc.h). Wire field names match the C++ serializer.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct HandshakeMsg {
|
||||
/// Protocol version.
|
||||
pub protocol_version: i32,
|
||||
/// Worker->main output shared-memory segment key.
|
||||
pub shm_key: String,
|
||||
/// Main->worker input shared-memory segment key (optional).
|
||||
pub input_shm_key: String,
|
||||
/// Number of main->worker input frame slots.
|
||||
pub input_slots: i32,
|
||||
/// Number of worker->main output frame slots.
|
||||
pub output_slots: i32,
|
||||
/// Per-output-slot pixel block size.
|
||||
pub slot_data_bytes: i64,
|
||||
/// Per-input-slot pixel block size.
|
||||
pub input_slot_data_bytes: i64,
|
||||
}
|
||||
|
||||
impl HandshakeMsg {
|
||||
/// The worker's startup handshake (`worker.cpp startup_handshake()`).
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"type": TYPE_HANDSHAKE,
|
||||
"protocol_version": self.protocol_version,
|
||||
"shm_key": self.shm_key,
|
||||
"input_shm_key": self.input_shm_key,
|
||||
"input_slots": self.input_slots,
|
||||
"output_slots": self.output_slots,
|
||||
"slot_data_bytes": self.slot_data_bytes,
|
||||
"input_slot_data_bytes": self.input_slot_data_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `render_frame` — request a frame render. Wire names per ipcmessage.cpp:
|
||||
/// `ticket`, `node`, `channels` (not the ipc.h POD names).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct RenderFrameMsg {
|
||||
/// Correlates with the eventual frame_ready.
|
||||
pub ticket: i64,
|
||||
/// Viewer node stable uuid in the loaded graph.
|
||||
pub node: String,
|
||||
/// Frame timestamp numerator.
|
||||
pub time_num: i64,
|
||||
/// Frame timestamp denominator.
|
||||
pub time_den: i64,
|
||||
/// Forced output size (0 = graph default).
|
||||
pub width: i32,
|
||||
/// Forced output height (0 = graph default).
|
||||
pub height: i32,
|
||||
/// Forced PixelFormat (-1 = default).
|
||||
pub format: i32,
|
||||
/// Channel count (0 = default).
|
||||
pub channels: i32,
|
||||
/// RenderMode.
|
||||
pub mode: i32,
|
||||
/// Optional decoded input slot (-1 = none).
|
||||
pub input_slot: i32,
|
||||
/// Ordered decoded input slots.
|
||||
pub input_slots: Vec<i32>,
|
||||
/// Output color transform present?
|
||||
pub has_color_transform: bool,
|
||||
/// Color transform targets the display space.
|
||||
pub color_is_display: bool,
|
||||
/// Output color space name.
|
||||
pub color_output: String,
|
||||
/// Output color view name.
|
||||
pub color_view: String,
|
||||
/// Output color look name.
|
||||
pub color_look: String,
|
||||
}
|
||||
|
||||
/// `frame_ready` — a rendered frame is published (wire names `ticket`/
|
||||
/// `slot`).
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct FrameReadyMsg {
|
||||
/// Correlates with the render_frame request.
|
||||
pub ticket: i64,
|
||||
/// Index into the worker->main output FrameSlotPool.
|
||||
pub slot: i32,
|
||||
}
|
||||
|
||||
/// `cancel` — abandon an in-flight ticket by id.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct CancelMsg {
|
||||
/// The in-flight ticket id to abandon.
|
||||
pub ticket: i64,
|
||||
}
|
||||
|
||||
/// `load_graph` — path to a temporary file holding the serialized graph.
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct LoadGraphMsg {
|
||||
/// Path to the temporary file holding the serialized graph.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Build a worker-side error report, mirroring `error_message()` in
|
||||
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
|
||||
/// non-zero.
|
||||
pub fn error_message(message: &str, ticket: Option<i64>) -> Value {
|
||||
match ticket.filter(|t| *t != 0) {
|
||||
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
|
||||
None => json!({ "type": TYPE_ERROR, "message": message }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
|
||||
/// `ipcmessage.cpp write_message()`.
|
||||
pub fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
|
||||
let line =
|
||||
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
w.write_all(line.as_bytes())?;
|
||||
w.write_all(b"\n")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared-memory frame-slot transport
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `OAK_IPC_SHM_KEY_CAP` — capacity of shm key strings (ipc.h), incl. NUL.
|
||||
pub const OAK_IPC_SHM_KEY_CAP: usize = 128;
|
||||
/// `OAK_IPC_COLORSPACE_CAP` — capacity of `oak_frame_slot_meta::colorspace`.
|
||||
@@ -77,6 +251,8 @@ pub enum ShmMode {
|
||||
}
|
||||
|
||||
impl ShmMode {
|
||||
/// Map the C ABI mode integer (`OAK_IPC_SHM_MODE_CREATE` = 0,
|
||||
/// `OAK_IPC_SHM_MODE_ATTACH` = 1) back to the enum.
|
||||
fn from_c(v: c_int) -> ShmMode {
|
||||
match v {
|
||||
0 => ShmMode::Create,
|
||||
@@ -748,7 +924,7 @@ impl SharedMemoryRegion {
|
||||
/// Unmap and (if owner) unlink the segment. Also called by `Drop`.
|
||||
pub fn close(&mut self) {
|
||||
if !self.data.is_null() {
|
||||
unsafe { libc::munmap(self.data as *mut c_void, self.size) };
|
||||
unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) };
|
||||
self.data = ptr::null_mut();
|
||||
}
|
||||
if self.fd >= 0 {
|
||||
@@ -804,7 +980,503 @@ impl Drop for SharedMemoryRegion {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- Control-plane protocol ------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn handshake_wire_format_matches_cpp_field_names() {
|
||||
let hs = HandshakeMsg {
|
||||
protocol_version: 1,
|
||||
shm_key: "olive-rw-1234-0-out".into(),
|
||||
input_shm_key: "".into(),
|
||||
input_slots: 0,
|
||||
output_slots: 6,
|
||||
slot_data_bytes: 4096,
|
||||
input_slot_data_bytes: 0,
|
||||
};
|
||||
let value = hs.to_json();
|
||||
// Key order is not part of the contract (JSON objects; the C++
|
||||
// QJsonObject is hash-ordered too), but the names must match the
|
||||
// C++ serializer exactly.
|
||||
assert_eq!(value["type"], "handshake");
|
||||
assert_eq!(value["protocol_version"], 1);
|
||||
assert_eq!(value["shm_key"], "olive-rw-1234-0-out");
|
||||
assert_eq!(value["input_shm_key"], "");
|
||||
assert_eq!(value["input_slots"], 0);
|
||||
assert_eq!(value["output_slots"], 6);
|
||||
assert_eq!(value["slot_data_bytes"], 4096);
|
||||
assert_eq!(value["input_slot_data_bytes"], 0);
|
||||
// And the serialized line must parse back to the same object.
|
||||
let round: serde_json::Value =
|
||||
serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap();
|
||||
assert_eq!(round, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_parse_accepts_cpp_field_names() {
|
||||
let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#;
|
||||
let m: RenderFrameMsg = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(m.ticket, 42);
|
||||
assert_eq!(m.node, "abcd");
|
||||
assert_eq!(m.time_num, 1);
|
||||
assert_eq!(m.time_den, 24);
|
||||
assert_eq!(m.width, 1920);
|
||||
assert_eq!(m.input_slot, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_defaults_on_missing_fields() {
|
||||
// The C++ parser defaults missing fields (QJsonValue defaults);
|
||||
// serde(default) mirrors that.
|
||||
let m: RenderFrameMsg =
|
||||
serde_json::from_str(r#"{"type":"render_frame","ticket":7}"#).unwrap();
|
||||
assert_eq!(m.ticket, 7);
|
||||
assert_eq!(m.time_den, 0);
|
||||
assert!(m.node.is_empty());
|
||||
assert!(!m.has_color_transform);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_message_carries_ticket_only_when_nonzero() {
|
||||
assert_eq!(
|
||||
error_message("boom", None),
|
||||
json!({ "type": "error", "message": "boom" })
|
||||
);
|
||||
assert_eq!(
|
||||
error_message("boom", Some(0)),
|
||||
json!({ "type": "error", "message": "boom" })
|
||||
);
|
||||
assert_eq!(
|
||||
error_message("boom", Some(9)),
|
||||
json!({ "type": "error", "message": "boom", "ticket": 9 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_message_emits_one_json_line() {
|
||||
let mut buf = Vec::new();
|
||||
write_message(&mut buf, &json!({ "type": "shutdown" })).unwrap();
|
||||
assert_eq!(String::from_utf8(buf).unwrap(), "{\"type\":\"shutdown\"}\n");
|
||||
}
|
||||
|
||||
// ---- Shared-memory transport -----------------------------------------
|
||||
|
||||
/// A unique, temporary POSIX segment key for a test (pid + counter), so
|
||||
/// parallel test runs never collide.
|
||||
fn test_key(name: &str) -> String {
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
|
||||
+ &format!("-{name}")
|
||||
}
|
||||
|
||||
/// Create one segment and map it a second time — the in-process
|
||||
/// equivalent of two processes sharing a segment. Returns
|
||||
/// `(owner_region, peer_region)`; both must be kept alive for the
|
||||
/// whole test (the peer is an attach that does not unlink).
|
||||
fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) {
|
||||
let mut owner = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
owner.open(key, size, ShmMode::Create),
|
||||
"create failed: {}",
|
||||
owner.error()
|
||||
);
|
||||
let mut peer = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
peer.open(key, size, ShmMode::Attach),
|
||||
"attach failed: {}",
|
||||
peer.error()
|
||||
);
|
||||
(owner, peer)
|
||||
}
|
||||
|
||||
// ---- SpscRingBuffer -------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ring_bytes_needed_matches_cpp_layout() {
|
||||
// 12 header bytes + capacity * 4.
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16);
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20);
|
||||
assert_eq!(SpscRingBuffer::bytes_needed(0), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_empty_full_and_single_entry() {
|
||||
let key = test_key("ring-empty");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: both mappings are live and at least `size` bytes.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
assert!(unsafe { cons.is_empty_approx() });
|
||||
let mut v = 99;
|
||||
assert!(!unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, 99);
|
||||
|
||||
assert!(unsafe { prod.push(7) });
|
||||
assert!(!unsafe { cons.is_empty_approx() });
|
||||
assert_eq!(unsafe { cons.size_approx() }, 1);
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, 7);
|
||||
assert!(unsafe { cons.is_empty_approx() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_capacity_minus_one_live_entries() {
|
||||
// A ring of capacity N holds at most N-1 entries (one slot is
|
||||
// always left empty to tell full from empty).
|
||||
let key = test_key("ring-cap");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
for i in 0..3 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
// The 4th push must fail: head would collide with tail.
|
||||
assert!(!unsafe { prod.push(99) });
|
||||
|
||||
let mut v = 0;
|
||||
for expected in 0..3 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, expected);
|
||||
}
|
||||
assert!(!unsafe { cons.pop(&mut v) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_wraparound_preserves_order() {
|
||||
// Fill, drain, then wrap past the end of the slot array: cursors
|
||||
// are modulo-capacity, order must be preserved across the wrap.
|
||||
let key = test_key("ring-wrap");
|
||||
let size = SpscRingBuffer::bytes_needed(4);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
|
||||
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
|
||||
|
||||
for i in 0..3 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
let mut v = 0;
|
||||
for _ in 0..3 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
}
|
||||
// Ring is empty again; push past the wrap point.
|
||||
for i in 3..6 {
|
||||
assert!(unsafe { prod.push(i) });
|
||||
}
|
||||
for expected in 3..6 {
|
||||
assert!(unsafe { cons.pop(&mut v) });
|
||||
assert_eq!(v, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- FrameSlotPool --------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn framepool_bytes_needed_matches_cpp_offsets() {
|
||||
// Recompute by hand with the C++ layout: header 64, each ring
|
||||
// align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data
|
||||
// align_up(slot_bytes, 64) * n.
|
||||
let check = |n: u32, slot: usize| {
|
||||
let ring = align_up(12 + 4 * (n as usize + 1), 64);
|
||||
let expected =
|
||||
64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize;
|
||||
assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected);
|
||||
};
|
||||
check(4, 4096);
|
||||
check(6, 1_000_000);
|
||||
check(1, 64);
|
||||
check(3, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_create_attach_two_processes_both_directions() {
|
||||
// "Two processes": two mappings of the same segment. Owner creates
|
||||
// the pool; the peer attaches. A filler on one side and a drainer
|
||||
// on the other exchange slots in both directions.
|
||||
let key = test_key("pool-bidi");
|
||||
let slots = 4u32;
|
||||
let slot_bytes = 64usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
|
||||
// SAFETY: both mappings are live and sized by bytes_needed.
|
||||
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
|
||||
assert!(filler.is_valid());
|
||||
assert!(drainer.is_valid());
|
||||
assert_eq!(drainer.slot_count(), slots);
|
||||
assert_eq!(drainer.slot_data_bytes(), slot_bytes);
|
||||
|
||||
// Filler acquires every slot exactly once (seeded free ring), then
|
||||
// the free ring is empty.
|
||||
let mut got = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) });
|
||||
got.push(s);
|
||||
}
|
||||
got.sort_unstable();
|
||||
assert_eq!(got, vec![0, 1, 2, 3]);
|
||||
let mut extra = 0;
|
||||
assert!(!unsafe { filler.acquire(&mut extra) });
|
||||
// Drainer sees nothing ready yet.
|
||||
assert!(!unsafe { drainer.consume(&mut extra) });
|
||||
|
||||
// Filler writes pixels + meta into two slots and publishes them.
|
||||
for (i, slot) in [0u32, 2u32].iter().enumerate() {
|
||||
// SAFETY: `slot` was acquired above.
|
||||
let data = unsafe { filler.slot_data(*slot) };
|
||||
unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) };
|
||||
// SAFETY: slot in range.
|
||||
let meta = unsafe { &mut *filler.meta(*slot) };
|
||||
meta.id = 100 + *slot as i64;
|
||||
meta.width = 8;
|
||||
meta.height = 8;
|
||||
meta.data_size = slot_bytes as i32;
|
||||
assert!(unsafe { filler.publish(*slot) });
|
||||
}
|
||||
|
||||
// Drainer consumes them through its own mapping and sees the same
|
||||
// payloads and metadata.
|
||||
let mut consumed = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { drainer.consume(&mut s) });
|
||||
// SAFETY: s was consumed.
|
||||
let data = unsafe { drainer.slot_data_const(s) };
|
||||
let meta = unsafe { &*drainer.meta_const(s) };
|
||||
assert_eq!(meta.id, 100 + s as i64);
|
||||
assert_eq!(meta.width, 8);
|
||||
assert_eq!(meta.data_size, slot_bytes as i32);
|
||||
// SAFETY: slot_bytes readable in the slot block.
|
||||
let first = unsafe { *data };
|
||||
assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8);
|
||||
consumed.push(s);
|
||||
}
|
||||
consumed.sort_unstable();
|
||||
assert_eq!(consumed, vec![0, 2]);
|
||||
assert!(!unsafe { drainer.consume(&mut extra) });
|
||||
|
||||
// Drainer releases the slots back; the filler can acquire them
|
||||
// again — the full round trip through both rings.
|
||||
for s in consumed {
|
||||
assert!(unsafe { drainer.release(s) });
|
||||
}
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) });
|
||||
assert_eq!(s, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_wraparound_and_full_edges() {
|
||||
// Small pool: cycle every slot many times, verifying the rings'
|
||||
// modulo behavior end to end.
|
||||
let key = test_key("pool-wrap");
|
||||
let slots = 3u32;
|
||||
let slot_bytes = 32usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
|
||||
// SAFETY: live mappings.
|
||||
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
|
||||
for cycle in 0..4u32 {
|
||||
let mut published = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}");
|
||||
// SAFETY: acquired slot.
|
||||
unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) };
|
||||
// SAFETY: slot in range.
|
||||
let meta = unsafe { &mut *filler.meta(s) };
|
||||
meta.id = i64::from(cycle * 100 + s);
|
||||
assert!(unsafe { filler.publish(s) });
|
||||
published.push(s);
|
||||
}
|
||||
// Pool is full on the filler side.
|
||||
let mut x = 0;
|
||||
assert!(!unsafe { filler.acquire(&mut x) });
|
||||
|
||||
// Drain everything on the drainer side.
|
||||
let mut consumed = Vec::new();
|
||||
for _ in 0..slots {
|
||||
let mut s = 0;
|
||||
assert!(unsafe { drainer.consume(&mut s) });
|
||||
// SAFETY: consumed slot.
|
||||
let meta = unsafe { &*drainer.meta_const(s) };
|
||||
assert_eq!(meta.id, i64::from(cycle * 100 + s));
|
||||
// SAFETY: 1 byte readable.
|
||||
assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8);
|
||||
consumed.push(s);
|
||||
}
|
||||
assert!(!unsafe { drainer.consume(&mut x) });
|
||||
consumed.sort_unstable();
|
||||
assert_eq!(consumed, vec![0, 1, 2]);
|
||||
|
||||
for s in consumed {
|
||||
assert!(unsafe { drainer.release(s) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_attach_rejects_wrong_magic() {
|
||||
let key = test_key("pool-badmagic");
|
||||
let size = FrameSlotPool::bytes_needed(2, 16);
|
||||
let (owner, _peer) = two_mappings(&key, size);
|
||||
// Overwrite the header area with garbage — no pool magic.
|
||||
// SAFETY: owner mapping is live.
|
||||
unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) };
|
||||
// SAFETY: buffer is live.
|
||||
let pool = unsafe { FrameSlotPool::attach(owner.data()) };
|
||||
assert!(!pool.is_valid());
|
||||
assert_eq!(pool.slot_count(), 0);
|
||||
assert_eq!(pool.slot_data_bytes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framepool_pool_over_reused_segment_is_consistent() {
|
||||
// A pool that has been cycled fully and then attached fresh reports
|
||||
// the same geometry as bytes_needed computed it.
|
||||
let key = test_key("pool-geometry");
|
||||
let slots = 5u32;
|
||||
let slot_bytes = 1000usize;
|
||||
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
|
||||
let (owner, peer) = two_mappings(&key, size);
|
||||
// SAFETY: live mappings.
|
||||
let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
|
||||
let attached = unsafe { FrameSlotPool::attach(peer.data()) };
|
||||
assert!(attached.is_valid());
|
||||
assert_eq!(attached.slot_count(), slots);
|
||||
assert_eq!(attached.slot_data_bytes(), slot_bytes);
|
||||
// Slot stride is 64-aligned (matches the C++ data layout).
|
||||
// SAFETY: valid pool.
|
||||
let s0 = unsafe { attached.slot_data(0) };
|
||||
let s1 = unsafe { attached.slot_data(1) };
|
||||
assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN));
|
||||
}
|
||||
|
||||
// ---- SharedMemoryRegion ---------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn region_create_attach_write_visibility() {
|
||||
let key = test_key("region-vis");
|
||||
let size = 4096usize;
|
||||
let (mut owner, mut peer) = two_mappings(&key, size);
|
||||
assert!(owner.is_valid());
|
||||
assert!(peer.is_valid());
|
||||
assert_eq!(owner.size(), size);
|
||||
assert_eq!(peer.size(), size);
|
||||
assert_eq!(owner.key(), key);
|
||||
assert_eq!(peer.key(), key);
|
||||
|
||||
// Owner writes; peer sees it through its own mapping.
|
||||
// SAFETY: both mappings are live with `size` bytes.
|
||||
unsafe {
|
||||
let dst = owner.data() as *mut u32;
|
||||
*dst = 0xDEADBEEF;
|
||||
}
|
||||
// SAFETY: peer mapping live.
|
||||
let seen = unsafe { *(peer.data() as *const u32) };
|
||||
assert_eq!(seen, 0xDEADBEEF);
|
||||
|
||||
// Peer writes back; owner sees it.
|
||||
// SAFETY: peer mapping live.
|
||||
unsafe {
|
||||
let dst = peer.data() as *mut u32;
|
||||
*dst = 0x12345678;
|
||||
}
|
||||
// SAFETY: owner mapping live.
|
||||
assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678);
|
||||
|
||||
// Closing the ATTACH side does not unlink: while the owner lives,
|
||||
// a third mapping can still open the name.
|
||||
peer.close();
|
||||
assert!(!peer.is_valid());
|
||||
let mut third = SharedMemoryRegion::new();
|
||||
assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error());
|
||||
assert!(third.is_valid());
|
||||
third.close();
|
||||
|
||||
// Closing the OWNER unlinks the segment; further attaches fail.
|
||||
owner.close();
|
||||
assert!(!owner.is_valid());
|
||||
let mut fourth = SharedMemoryRegion::new();
|
||||
assert!(!fourth.open(&key, size, ShmMode::Attach));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_create_replaces_stale_segment() {
|
||||
// Mirrors the C++: Create unlinks any stale segment with the same
|
||||
// name first (crash cleanup), so a second Create SUCCEEDS and owns
|
||||
// a fresh, zeroed segment.
|
||||
let key = test_key("region-exists");
|
||||
let size = 128usize;
|
||||
let (mut owner, _peer) = two_mappings(&key, size);
|
||||
assert!(owner.is_valid());
|
||||
// SAFETY: owner mapping live.
|
||||
unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE };
|
||||
|
||||
let mut second = SharedMemoryRegion::new();
|
||||
assert!(
|
||||
second.open(&key, size, ShmMode::Create),
|
||||
"{}",
|
||||
second.error()
|
||||
);
|
||||
assert!(second.is_valid());
|
||||
// The replacement segment is fresh (zeroed by create).
|
||||
// SAFETY: second mapping live.
|
||||
assert_eq!(unsafe { *(second.data() as *const u32) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_attach_fails_when_segment_too_small() {
|
||||
// macOS rounds shm segment sizes up to a 16 KiB minimum, so use
|
||||
// sizes above that to exercise the size check.
|
||||
let key = test_key("region-small");
|
||||
let (owner, _peer) = two_mappings(&key, 4096);
|
||||
assert!(owner.is_valid());
|
||||
|
||||
// Attaching with a larger size than the segment must fail (the
|
||||
// fstat check, mirroring the C++).
|
||||
let mut big = SharedMemoryRegion::new();
|
||||
assert!(!big.open(&key, 65536, ShmMode::Attach));
|
||||
assert!(!big.is_valid());
|
||||
assert!(!big.error().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_make_key_format() {
|
||||
assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3");
|
||||
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_keys_are_isolation_safe() {
|
||||
// Keys with slashes are flattened to a single-slash POSIX name.
|
||||
let key = "a/b/c";
|
||||
let size = 64usize;
|
||||
let (mut owner, mut peer) = two_mappings(key, size);
|
||||
assert!(owner.is_valid());
|
||||
assert!(peer.is_valid());
|
||||
// The actual POSIX name is "/a_b_c".
|
||||
// SAFETY: mapping live.
|
||||
unsafe { *(owner.data() as *mut u32) = 7 };
|
||||
// SAFETY: peer mapping live.
|
||||
assert_eq!(unsafe { *(peer.data() as *const u32) }, 7);
|
||||
}
|
||||
}
|
||||
// C ABI exports (engine/include/oakengine/ipc.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1219,7 +1891,7 @@ pub unsafe extern "C" fn oakengine_ipc_framepool_release(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod cabi_tests {
|
||||
use super::*;
|
||||
|
||||
/// A unique, temporary POSIX segment key for a test (pid + counter), so
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
pub use oaknode;
|
||||
|
||||
pub mod audio;
|
||||
pub mod bridge;
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod deferred;
|
||||
@@ -77,28 +76,35 @@ pub mod ipc;
|
||||
pub mod linkage;
|
||||
pub mod node;
|
||||
pub mod plugin;
|
||||
pub mod pods;
|
||||
pub mod render;
|
||||
pub mod stubs;
|
||||
pub mod task;
|
||||
pub mod testmedia;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
pub mod worker;
|
||||
|
||||
/// The former `tests/*.rs` integration tests, now unit tests (the facade
|
||||
/// is cdylib-only, so integration tests cannot link it as an rlib crate;
|
||||
/// see `test_support/mod.rs`).
|
||||
#[cfg(test)]
|
||||
#[path = "test_support/mod.rs"]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod test_link {
|
||||
// The lib's own unit-test binary must link the module crates' rlibs to
|
||||
// satisfy the facade's `extern "C"` imports that the unit tests compile
|
||||
// in — e.g. the worker session's oakrender display renderer (src/worker.rs).
|
||||
// satisfy the facade's imports that the unit tests compile in — e.g.
|
||||
// the render family's oakrender display renderer (src/render.rs).
|
||||
// The integration tests do the same through tests/common/mod.rs
|
||||
// `force_link()`; this covers the `cargo test` unit-test binary.
|
||||
#![allow(dead_code)]
|
||||
fn force_link() -> usize {
|
||||
let fns: [usize; 4] = [
|
||||
oakrender::ffi::renderer::oakrender_display_renderer_create_opengl as *const ()
|
||||
as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as *const () as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as *const () as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as *const () as usize,
|
||||
oakrender::backend::DisplayRenderer::new as *const () as usize,
|
||||
oaknode::project::Project::new as *const () as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as *const () as usize,
|
||||
oaktask::manager::TaskManager::init as *const () as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
@@ -39,23 +39,24 @@
|
||||
/// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib
|
||||
/// for that lookup to succeed.
|
||||
fn force_link() -> usize {
|
||||
let fns: [usize; 13] = [
|
||||
let fns: [usize; 11] = [
|
||||
// oakcore-rs (pure value types; referenced so its rlib is linked).
|
||||
oakcore_rs::Rational::new(1, 2).numerator() as usize,
|
||||
// One exported C ABI symbol per module crate.
|
||||
oakundo::ffi::undostack::oakundo_undostack_init as usize,
|
||||
oakcommon::ffi::config::oakcommon_config_get_int as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
|
||||
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
|
||||
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as usize,
|
||||
oakplugin::ffi::oakplugin_host_plugin_count as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as usize,
|
||||
// oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs).
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as usize,
|
||||
// One public direct-Rust symbol per module crate. oakundo/oakcommon
|
||||
// no longer export a C ABI; their handle-level Rust API functions
|
||||
// serve as the link anchors.
|
||||
oakundo::undostack::undostack_init as usize,
|
||||
oakcommon::configstore::ConfigStore::instance as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as usize,
|
||||
oakcodec::exportformat::Format::get_name as usize,
|
||||
oakrender::manager::RenderManager::init as usize,
|
||||
oaktask::manager::TaskManager::init as usize,
|
||||
oaknode::project::Project::new as usize,
|
||||
// oaknode's serializer resolves oakcommon XML/undo symbols at
|
||||
// runtime; anchors for the dylib.
|
||||
oakcommon::xmlutils::XmlWriter::new as usize,
|
||||
oakcommon::xmlutils::XmlReader::new as usize,
|
||||
oakundo::undocommand::command_init as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
+520
-166
@@ -19,8 +19,8 @@
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use crate::bridge::common as c;
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::common as c;
|
||||
use crate::stubs::node as n;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{
|
||||
@@ -2518,13 +2518,16 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no keyframing-enabled query.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(0)
|
||||
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2534,13 +2537,16 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no keyframe enumeration C ABI.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(0)
|
||||
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2553,16 +2559,20 @@ pub unsafe extern "C" fn oakengine_node_keyframe_at(
|
||||
time_ts: *mut i64,
|
||||
value: *mut OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || time_ts.is_null() || value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = index;
|
||||
let _ = time_ts;
|
||||
let _ = value;
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, value);
|
||||
Error::from_module(rc)?;
|
||||
// Rational seconds -> frame timestamp in the project time base.
|
||||
let ts = num as i128 * tb.1 as i128 / den as i128 / tb.0 as i128;
|
||||
*time_ts = ts as i64;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2578,14 +2588,38 @@ pub unsafe extern "C" fn oakengine_node_keyframe_get_easing(
|
||||
y2: *mut f32,
|
||||
type_: *mut c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || type_.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (index, x1, y1, x2, y2, type_);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
// Locate the key's time through the count/at pair.
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
Error::from_module(n::oaknode_node_keyframe_at(
|
||||
h, input_id, index, &mut num, &mut den, &mut dummy,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_type_at(
|
||||
h, input_id, num, den, type_,
|
||||
))?;
|
||||
if !x1.is_null() {
|
||||
let mut bx: f64 = 0.0;
|
||||
let mut by: f64 = 0.0;
|
||||
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 0, &mut bx, &mut by) == 0 {
|
||||
*x1 = bx as f32;
|
||||
*y1 = by as f32;
|
||||
}
|
||||
}
|
||||
if !x2.is_null() {
|
||||
let mut bx: f64 = 0.0;
|
||||
let mut by: f64 = 0.0;
|
||||
if n::oaknode_node_keyframe_bezier_at(h, input_id, num, den, 1, &mut bx, &mut by) == 0 {
|
||||
*x2 = bx as f32;
|
||||
*y2 = by as f32;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2634,14 +2668,14 @@ pub unsafe extern "C" fn oakengine_node_keyframe_remove(
|
||||
input_id: *const c_char,
|
||||
time_ts: i64,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no remove-keyframe C ABI.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = time_ts;
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2685,11 +2719,31 @@ pub unsafe extern "C" fn oakengine_node_insert_keyframe_command(
|
||||
pub unsafe extern "C" fn oakengine_node_remove_keyframe_command(
|
||||
keyframe: *mut OakEngineKeyframe,
|
||||
) -> *mut c_void {
|
||||
// Stub: the module has no remove-keyframe command creator (the module
|
||||
// keyframe handles are detached values, not track members).
|
||||
guard_ptr(|| unsafe {
|
||||
let _ = unbox(keyframe);
|
||||
Ok(std::ptr::null_mut())
|
||||
let h = unbox(keyframe)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
Error::from_module(n::oaknode_keyframe_get_time(h, &mut num, &mut den))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(h, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let mut parent = CHandle::null();
|
||||
Error::from_module(n::oaknode_keyframe_get_parent(h, &mut parent))?;
|
||||
// Build the undo command as a closure over the remove path (the
|
||||
// module has no remove-key command creator; the closure carries
|
||||
// the same semantics).
|
||||
let cmd = crate::stubs::node::box_keyframe_remove_command(
|
||||
parent,
|
||||
input_buf.as_ptr(),
|
||||
num,
|
||||
den,
|
||||
);
|
||||
if cmd.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(command_box(cmd)?.cast())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2746,17 +2800,52 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_easing(
|
||||
x2: f32,
|
||||
y2: f32,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count` (no easing accessors).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (time_ts, type_, x1, y1, x2, y2);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_type(
|
||||
h, input_id, t_num, t_den, type_,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 0, x1 as f64, y1 as f64,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 1, x2 as f64, y2 as f64,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Apply a value-at-time write WITHOUT pushing an undo row (the
|
||||
/// `*_many` keyframe editors apply live writes; documented deviation
|
||||
/// from the C++ multi commands).
|
||||
///
|
||||
/// # Safety
|
||||
/// `h` must be a live module node handle; `input_id` a valid
|
||||
/// NUL-terminated string; `v` a live POD.
|
||||
unsafe fn live_set_value_at_time(
|
||||
h: CHandle,
|
||||
input_id: *const c_char,
|
||||
t_num: i64,
|
||||
t_den: i64,
|
||||
v: *const OakNodeValue,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
|
||||
h, input_id, t_num, t_den, v, 0, &mut cmd,
|
||||
))?;
|
||||
let rc = oakundo::undocommand::command_redo_now(cmd);
|
||||
let mut cmd_h = cmd;
|
||||
oakundo::undocommand::command_free(&mut cmd_h);
|
||||
Error::from_module(rc)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframes_set_type_many`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
|
||||
@@ -2768,14 +2857,23 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_type_many(
|
||||
count: c_int,
|
||||
type_: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key type writes (the C++ grouped them into one command;
|
||||
// documented deviation — no undo row is created).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, type_);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_type(
|
||||
h, input_id, t_num, t_den, type_,
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2790,14 +2888,29 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_time_many(
|
||||
count: c_int,
|
||||
new_time_ts: i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key re-times: each key is removed from its old time and
|
||||
// re-inserted at the new one with its value (no undo row; documented).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && old_times_ts.is_null())
|
||||
{
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, old_times_ts, tracks, count, new_time_ts);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (new_num, new_den) = ts_to_time(new_time_ts, tb);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let old_ts = *old_times_ts.add(i);
|
||||
let (old_num, old_den) = ts_to_time(old_ts, tb);
|
||||
let mut v = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_get_input_at_time(h, input_id, old_num, old_den, &mut v);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, old_num, old_den))?;
|
||||
live_set_value_at_time(h, input_id, new_num, new_den, &v)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2813,14 +2926,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_value_many(
|
||||
values: *const OakNodeValue,
|
||||
old_values: *const OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key value writes (no undo row; the C++ grouped them —
|
||||
// documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null()
|
||||
|| input_id.is_null()
|
||||
|| count < 0
|
||||
|| (count > 0 && (times_ts.is_null() || values.is_null()))
|
||||
{
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, values, old_values);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks, old_values);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
live_set_value_at_time(h, input_id, t_num, t_den, values.add(i))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2838,14 +2962,25 @@ pub unsafe extern "C" fn oakengine_node_keyframes_set_bezier_many(
|
||||
out_x: f64,
|
||||
out_y: f64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
// Live per-key bezier writes (no undo row; documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || count < 0 || (count > 0 && times_ts.is_null()) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, times_ts, tracks, count, in_x, in_y, out_x, out_y);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let _ = (element, tracks);
|
||||
for i in 0..count as usize {
|
||||
let ts = *times_ts.add(i);
|
||||
let (t_num, t_den) = ts_to_time(ts, tb);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 0, in_x, in_y,
|
||||
))?;
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, 1, out_x, out_y,
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2863,14 +2998,17 @@ pub unsafe extern "C" fn oakengine_node_keyframe_set_bezier_point(
|
||||
old_x: f64,
|
||||
old_y: f64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
if self_.is_null() || input_id.is_null() || (point_index != 0 && point_index != 1) {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track, point_index, x, y, old_x, old_y);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let _ = (element, track, old_x, old_y);
|
||||
Error::from_module(n::oaknode_node_keyframe_set_bezier(
|
||||
h, input_id, t_num, t_den, point_index, x, y,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2881,14 +3019,11 @@ pub unsafe extern "C" fn oakengine_node_keyframes_clear(
|
||||
self_: *mut OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the module has no clear-keyframes command; since it also has
|
||||
// no keyframes, the documented no-op result is returned.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
Ok(())
|
||||
Error::from_module(n::oaknode_node_clear_keyframes(unbox(self_)?, input_id))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3040,14 +3175,19 @@ pub unsafe extern "C" fn oakengine_node_input_is_keyframed_ex(
|
||||
input_id: *const c_char,
|
||||
element: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_input_is_keyframed`.
|
||||
// Whole-value tracks: the element selector is recorded but the track
|
||||
// query covers the (input, -1) track (documented deviation).
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let rc = n::oaknode_node_is_input_keyframing(unbox(self_)?, input_id);
|
||||
let _ = element;
|
||||
Ok(0)
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3101,14 +3241,29 @@ pub unsafe extern "C" fn oakengine_node_input_get_default_value(
|
||||
track: c_int,
|
||||
out: *mut OakNodeValue,
|
||||
) -> c_int {
|
||||
// Stub: the oaknode module has no default-value export.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || out.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = track;
|
||||
Err(Error::NotFound)
|
||||
// The declared type's default maps back into the POD (same
|
||||
// conversion as the getter family).
|
||||
let mut ty: c_int = 0;
|
||||
Error::from_module(n::oaknode_node_input_get_type(
|
||||
unbox(self_)?,
|
||||
input_id,
|
||||
&mut ty,
|
||||
))?;
|
||||
// Best-effort: the default of the enabled input is Boolean(true);
|
||||
// other inputs report the None POD (documented deviation).
|
||||
let default = OakNodeValue {
|
||||
kind: ty,
|
||||
num: if ty == value_type::BOOL { 1 } else { 0 },
|
||||
den: 0,
|
||||
f: [0.0; 4],
|
||||
};
|
||||
*out = default;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5271,14 +5426,19 @@ pub unsafe extern "C" fn oakengine_node_keyframe_count_on_track(
|
||||
element: c_int,
|
||||
track: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
// Whole-value tracks: the element/track selectors are recorded but
|
||||
// the count covers the (input, -1) track (documented deviation).
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let rc = n::oaknode_node_keyframe_count(unbox(self_)?, input_id);
|
||||
let _ = (element, track);
|
||||
Ok(0)
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5293,14 +5453,35 @@ pub unsafe extern "C" fn oakengine_node_keyframes_toggle_at_time(
|
||||
on: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track, on, undo_name);
|
||||
Err(Error::NotFound)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let has = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
|
||||
if has < 0 {
|
||||
return Err(Error::Module(has));
|
||||
}
|
||||
let _ = (element, track);
|
||||
if on != 0 && has == 0 {
|
||||
// Insert a key at the time carrying the current value.
|
||||
let mut v = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_get_input_at_time(h, input_id, t_num, t_den, &mut v);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
Error::from_module(n::oaknode_node_set_input_at_time_undoable(
|
||||
h, input_id, t_num, t_den, &v, 0, &mut cmd,
|
||||
))?;
|
||||
push_command(cmd, if undo_name.is_null() { "Toggle Keyframe" } else { "" })
|
||||
} else if on == 0 && has == 1 {
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(h, input_id, t_num, t_den))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5313,17 +5494,70 @@ pub unsafe extern "C" fn oakengine_node_has_keyframe_at_time(
|
||||
time_ts: i64,
|
||||
track: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
Ok(0)
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let _ = (element, track);
|
||||
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, t_num, t_den);
|
||||
if rc < 0 {
|
||||
Err(Error::Module(rc))
|
||||
} else {
|
||||
Ok(rc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// First (earliest) or last (latest) keyframe time of an input, written
|
||||
/// as a rational-seconds pair (0/1 when the input has no keys).
|
||||
///
|
||||
/// # Safety
|
||||
/// `self_` must be a live engine node handle; `input_id` a valid
|
||||
/// NUL-terminated string.
|
||||
unsafe fn keyframe_extreme_time(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
earliest: bool,
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> Result<c_int> {
|
||||
unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let count = n::oaknode_node_keyframe_count(h, input_id);
|
||||
if count <= 0 {
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
return Ok(0);
|
||||
}
|
||||
let index = if earliest { 0 } else { count - 1 };
|
||||
let mut k_num: i64 = 0;
|
||||
let mut k_den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut k_num, &mut k_den, &mut dummy);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
if !num.is_null() {
|
||||
*num = k_num;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = k_den;
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframe_earliest_time`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
|
||||
@@ -5333,21 +5567,9 @@ pub unsafe extern "C" fn oakengine_node_keyframe_earliest_time(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = element;
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
keyframe_extreme_time(self_, input_id, true, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5360,22 +5582,85 @@ pub unsafe extern "C" fn oakengine_node_keyframe_latest_time(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
let _ = element;
|
||||
keyframe_extreme_time(self_, input_id, false, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Closest keyframe time strictly before (or at/after) a frame timestamp,
|
||||
/// written as rational seconds (0/1 when none — the documented neutral
|
||||
/// result).
|
||||
///
|
||||
/// # Safety
|
||||
/// `self_` must be a live engine node handle; `input_id` a valid
|
||||
/// NUL-terminated string.
|
||||
unsafe fn closest_keyframe_time(
|
||||
self_: *const OakEngineNode,
|
||||
input_id: *const c_char,
|
||||
element: c_int,
|
||||
time_ts: i64,
|
||||
track: c_int,
|
||||
before: bool,
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> Result<c_int> {
|
||||
unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = element;
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
let h = unbox(self_)?;
|
||||
let tb = time_base_for(h);
|
||||
let (t_num, t_den) = ts_to_time(time_ts, tb);
|
||||
let target = t_num as f64 / t_den as f64;
|
||||
let _ = (element, track);
|
||||
let count = n::oaknode_node_keyframe_count(h, input_id);
|
||||
let mut best: Option<(i64, i64)> = None;
|
||||
for i in 0..count {
|
||||
let mut k_num: i64 = 0;
|
||||
let mut k_den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
if n::oaknode_node_keyframe_at(h, input_id, i, &mut k_num, &mut k_den, &mut dummy) != 0 {
|
||||
continue;
|
||||
}
|
||||
let k = k_num as f64 / k_den as f64;
|
||||
let matches = if before { k < target } else { k >= target };
|
||||
if !matches {
|
||||
continue;
|
||||
}
|
||||
match best {
|
||||
Some((bn, bd)) => {
|
||||
let b = bn as f64 / bd as f64;
|
||||
let closer = if before { k > b } else { k < b };
|
||||
if closer {
|
||||
best = Some((k_num, k_den));
|
||||
}
|
||||
}
|
||||
None => best = Some((k_num, k_den)),
|
||||
}
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
match best {
|
||||
Some((n_, d_)) => {
|
||||
if !num.is_null() {
|
||||
*num = n_;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = d_;
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
None => {
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_node_keyframe_closest_time_before`.
|
||||
@@ -5389,21 +5674,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_before(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
closest_keyframe_time(self_, input_id, element, time_ts, track, true, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5418,21 +5690,8 @@ pub unsafe extern "C" fn oakengine_node_keyframe_closest_time_after(
|
||||
num: *mut i64,
|
||||
den: *mut i64,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
set_node_error("invalid arguments");
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, time_ts, track);
|
||||
if !num.is_null() {
|
||||
*num = 0;
|
||||
}
|
||||
if !den.is_null() {
|
||||
*den = 1;
|
||||
}
|
||||
Ok(0)
|
||||
closest_keyframe_time(self_, input_id, element, time_ts, track, false, num, den)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5445,10 +5704,23 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_on_track(
|
||||
track: c_int,
|
||||
index: c_int,
|
||||
) -> *mut OakEngineKeyframe {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, track, index);
|
||||
Ok(std::ptr::null_mut())
|
||||
guard_ptr(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || index < 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut dummy = OakNodeValue::none();
|
||||
let rc = n::oaknode_node_keyframe_at(h, input_id, index, &mut num, &mut den, &mut dummy);
|
||||
if rc != 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let kf = n::oaknode_keyframe_create(num, den, &dummy, 0, track, element, input_id, h);
|
||||
if kf.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineKeyframe>(kf))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5462,10 +5734,24 @@ pub unsafe extern "C" fn oakengine_node_keyframe_handle_at_time(
|
||||
time_num: i64,
|
||||
time_den: i64,
|
||||
) -> *mut OakEngineKeyframe {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, track, time_num, time_den);
|
||||
Ok(std::ptr::null_mut())
|
||||
guard_ptr(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || time_den == 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
let rc = n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den);
|
||||
if rc != 1 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let mut dummy = OakNodeValue::none();
|
||||
if n::oaknode_node_get_input_at_time(h, input_id, time_num, time_den, &mut dummy) != 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let kf = n::oaknode_keyframe_create(time_num, time_den, &dummy, 0, track, element, input_id, h);
|
||||
if kf.ctx.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineKeyframe>(kf))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5480,17 +5766,26 @@ pub unsafe extern "C" fn oakengine_node_keyframes_at_time(
|
||||
out_handles: *mut *mut OakEngineKeyframe,
|
||||
max_handles: c_int,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count`.
|
||||
guard_int(|| {
|
||||
let _ = (
|
||||
self_,
|
||||
input_id,
|
||||
element,
|
||||
time_num,
|
||||
time_den,
|
||||
out_handles,
|
||||
max_handles,
|
||||
);
|
||||
guard_int(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() || max_handles < 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
if max_handles == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let h = unbox(self_)?;
|
||||
if n::oaknode_node_has_keyframe_at_time(h, input_id, time_num, time_den) != 1 {
|
||||
return Ok(0);
|
||||
}
|
||||
if !out_handles.is_null() {
|
||||
let kf =
|
||||
n::oaknode_keyframe_create(time_num, time_den, &OakNodeValue::none(), 0, 0, element, input_id, h);
|
||||
if !kf.ctx.is_null() {
|
||||
// SAFETY: the caller guarantees `max_handles` slots.
|
||||
*out_handles = box_handle::<OakEngineKeyframe>(kf);
|
||||
return Ok(1);
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
@@ -5506,15 +5801,20 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing(
|
||||
enable_all_tracks: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: the module has no keyframing-enable command; its keyframe
|
||||
// tracks are never populated through the C ABI.
|
||||
// In the Rust model an input "is keyframed" when its track holds
|
||||
// keys; disabling therefore clears the keys, enabling is a no-op
|
||||
// (documented deviation from the C++ enable flag).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || input_id.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let _ = (element, keyframing, track, enable_all_tracks, undo_name);
|
||||
Err(Error::Invalid)
|
||||
let h = unbox(self_)?;
|
||||
let _ = (element, track, enable_all_tracks, undo_name);
|
||||
if keyframing != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Error::from_module(n::oaknode_node_clear_keyframes(h, input_id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5526,9 +5826,19 @@ pub unsafe extern "C" fn oakengine_node_set_input_keyframing_command(
|
||||
element: c_int,
|
||||
keyframing: c_int,
|
||||
) -> *mut c_void {
|
||||
// Stub: see `oakengine_node_set_input_keyframing`.
|
||||
guard_ptr(|| {
|
||||
let _ = (self_, input_id, element, keyframing);
|
||||
guard_ptr(|| unsafe {
|
||||
let rc = oakengine_node_set_input_keyframing(
|
||||
self_,
|
||||
input_id,
|
||||
element,
|
||||
keyframing,
|
||||
0,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
);
|
||||
Error::from_module(rc)?;
|
||||
// The live write has no command counterpart (see the export
|
||||
// notes); return NULL like the other non-undoable creators.
|
||||
Ok(std::ptr::null_mut())
|
||||
})
|
||||
}
|
||||
@@ -5541,14 +5851,33 @@ pub unsafe extern "C" fn oakengine_node_keyframes_paste(
|
||||
count: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_set_input_keyframing` (no insert path).
|
||||
// Live key inserts at each source key's time (no undo row; the C++
|
||||
// built one command — documented deviation).
|
||||
guard(|| unsafe {
|
||||
if self_.is_null() || keyframes.is_null() || count <= 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
let h = unbox(self_)?;
|
||||
let _ = undo_name;
|
||||
Err(Error::Invalid)
|
||||
for i in 0..count as usize {
|
||||
let kf = *keyframes.add(i);
|
||||
if kf.is_null() {
|
||||
continue;
|
||||
}
|
||||
let kh = unbox(kf)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
let mut v = OakNodeValue::none();
|
||||
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
|
||||
Error::from_module(n::oaknode_keyframe_get_value(kh, &mut v))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
live_set_value_at_time(h, input_buf.as_ptr(), num, den, &v)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5816,13 +6145,38 @@ pub unsafe extern "C" fn oakengine_keyframes_remove_many(
|
||||
count: c_int,
|
||||
undo_name: *const c_char,
|
||||
) -> c_int {
|
||||
// Stub: see `oakengine_node_keyframe_count` (no remove-key path).
|
||||
guard(|| {
|
||||
// Live per-key removals through each keyframe's track reference (no
|
||||
// undo row; documented deviation).
|
||||
guard(|| unsafe {
|
||||
if keyframes.is_null() || count <= 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let _ = undo_name;
|
||||
Err(Error::Invalid)
|
||||
for i in 0..count as usize {
|
||||
let kf = *keyframes.add(i);
|
||||
if kf.is_null() {
|
||||
continue;
|
||||
}
|
||||
let kh = unbox(kf)?;
|
||||
let mut num: i64 = 0;
|
||||
let mut den: i64 = 0;
|
||||
Error::from_module(n::oaknode_keyframe_get_time(kh, &mut num, &mut den))?;
|
||||
let mut input_buf = [0 as c_char; 256];
|
||||
let rc = n::oaknode_keyframe_get_input(kh, input_buf.as_mut_ptr(), 256);
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let mut parent = CHandle::null();
|
||||
Error::from_module(n::oaknode_keyframe_get_parent(kh, &mut parent))?;
|
||||
Error::from_module(n::oaknode_node_remove_keyframe(
|
||||
parent,
|
||||
input_buf.as_ptr(),
|
||||
num,
|
||||
den,
|
||||
))?;
|
||||
release_handle(parent);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::plugin as p;
|
||||
use crate::stubs::plugin as p;
|
||||
use crate::error::Error;
|
||||
use crate::handle::guard;
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// 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/>.
|
||||
|
||||
//! POD mirrors for the deleted `src/bridge/` (single-lib unification).
|
||||
//!
|
||||
//! The facade still exchanges plain-`repr(C)` PODs with the module crates
|
||||
//! (and, upward, with the host app through the frozen `oakengine_*` C
|
||||
//! ABI). The deleted bridge aliased these types to the module crates'
|
||||
//! `ffi` declarations; those ffi modules are gone, so the engine keeps
|
||||
//! its own mirrors here. Where a module crate still owns the canonical
|
||||
//! POD (oakcodec's [`EncodingParamsPOD`]) the facade aliases it directly.
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
/// `oakcodec_encoding_params` (`include/codec/encoder.h`) — single-lib
|
||||
/// unification: aliases the oakcodec crate's POD
|
||||
/// ([`oakcodec::encodingparams::EncodingParams`], identical `#[repr(C)]`
|
||||
/// layout), so the facade's encoding-params handle reads/writes fields of
|
||||
/// exactly the struct the oakcodec/oakaudio creators consume.
|
||||
pub type EncodingParamsPOD = oakcodec::encodingparams::EncodingParams;
|
||||
|
||||
/// `oak_export_options` (`engine/include/oakengine/exporter.h`) — POD
|
||||
/// export parameters for [`crate::codec::oakengine_export_render`]. 0 (or
|
||||
/// negative) fields select the documented per-field default; the codec
|
||||
/// fields carry the exporter.h `OAKENGINE_EXPORT_VIDEO_*` /
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` values (NOT the engine's `ExportCodec`
|
||||
/// ids — see the mapping notes on `oakengine_export_render`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakExportOptions {
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` value; default H264.
|
||||
pub video_codec: c_int,
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` value; default AAC;
|
||||
/// [`OAKENGINE_EXPORT_AUDIO_NONE`] disables the audio track.
|
||||
pub audio_codec: c_int,
|
||||
/// Video bit rate in bit/s; <= 0 lets the encoder choose (FFmpeg
|
||||
/// defaults).
|
||||
pub video_bit_rate: i64,
|
||||
/// Audio sample rate in Hz; <= 0 uses the engine's default (48 kHz).
|
||||
pub audio_sample_rate: c_int,
|
||||
/// Audio channel count (1 = mono, 2 = stereo); <= 0 uses the engine's
|
||||
/// default (stereo).
|
||||
pub audio_channel_count: c_int,
|
||||
}
|
||||
|
||||
/// `OAKENGINE_EXPORT_VIDEO_*` — video codecs for
|
||||
/// [`OakExportOptions::video_codec`].
|
||||
pub const OAKENGINE_EXPORT_VIDEO_H264: c_int = 0;
|
||||
/// H.265/HEVC in an MP4 container.
|
||||
pub const OAKENGINE_EXPORT_VIDEO_H265: c_int = 1;
|
||||
/// PNG still-image sequence.
|
||||
pub const OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE: c_int = 2;
|
||||
|
||||
/// `OAKENGINE_EXPORT_AUDIO_*` — audio codecs for
|
||||
/// [`OakExportOptions::audio_codec`].
|
||||
pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0;
|
||||
/// Uncompressed PCM.
|
||||
pub const OAKENGINE_EXPORT_AUDIO_PCM: c_int = 1;
|
||||
/// Disable the audio track entirely (not a codec).
|
||||
pub const OAKENGINE_EXPORT_AUDIO_NONE: c_int = -1;
|
||||
|
||||
/// The oakaudio recording-params POD is the same shared codec POD.
|
||||
pub type AudioEncodingParams = EncodingParamsPOD;
|
||||
|
||||
/// Zeroed encoding-params POD (all fields 0 / NUL). The codec crate's
|
||||
/// struct has no zeroed constructor; this facade helper provides it.
|
||||
pub fn zeroed_encoding_params() -> EncodingParamsPOD {
|
||||
// All-field zero is a valid value (enums carry their 0 variants).
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
/// `oakrender_video_params` (`include/render/renderer.h`) — identical
|
||||
/// layout to the engine's own video-params POD.
|
||||
pub type OakRenderVideoParams = crate::common::OakVideoParamsPod;
|
||||
|
||||
/// `oakrender_video_ticket_params` (`include/render/ticket.h`), the POD
|
||||
/// the deleted bridge aliased from `oakrender::ffi`. The oakrender crate
|
||||
/// now exposes value-typed `ticket::VideoTicketParams`; the facade keeps
|
||||
/// this mirror for its synchronous render path (see [`crate::render`]).
|
||||
/// All handle fields are the shared [`crate::handle::CHandle`].
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakVideoTicketParams {
|
||||
/// Connected texture output node (borrowed).
|
||||
pub output_node: crate::handle::CHandle,
|
||||
/// By-value oakcommon handle.
|
||||
pub video_params: crate::handle::CHandle,
|
||||
/// Borrowed oakcore audio-params handle, may be null.
|
||||
pub audio_params: *const std::ffi::c_void,
|
||||
/// Frame timestamp as rational.
|
||||
pub time_num: i64,
|
||||
/// Frame timestamp as rational.
|
||||
pub time_den: i64,
|
||||
/// Borrowed, empty ctx = null.
|
||||
pub color_manager: crate::handle::CHandle,
|
||||
/// RenderMode::Mode as int.
|
||||
pub mode: c_int,
|
||||
/// 0/0 = off.
|
||||
pub force_width: c_int,
|
||||
/// 0/0 = off.
|
||||
pub force_height: c_int,
|
||||
/// Used when has_force_matrix != 0.
|
||||
pub force_matrix: [f64; 16],
|
||||
/// 0/1.
|
||||
pub has_force_matrix: c_int,
|
||||
/// PixelFormat as int, -1 = off.
|
||||
pub force_format: c_int,
|
||||
/// 0 = off.
|
||||
pub force_channel_count: c_int,
|
||||
/// Borrowed; empty ctx = none.
|
||||
pub force_color_output: crate::handle::CHandle,
|
||||
/// By value; empty ctx = default.
|
||||
pub force_color_transform: crate::handle::CHandle,
|
||||
/// Borrowed frame cache; empty ctx = none.
|
||||
pub cache: crate::handle::CHandle,
|
||||
/// Single-footage decode filename (null = off; M12 P0).
|
||||
pub footage_filename: *const std::ffi::c_char,
|
||||
/// Media stream index for `footage_filename`.
|
||||
pub footage_stream: c_int,
|
||||
/// Sequence montage clip array (null = off; M12 P0). Clips are
|
||||
/// ordered bottom-to-top; the last element is the topmost.
|
||||
pub montage: *const MontagePod,
|
||||
/// `montage` element count.
|
||||
pub montage_count: c_int,
|
||||
}
|
||||
|
||||
/// One sequence-montage clip (`oakrender::ffi::OakMontageClip`, M12 P0):
|
||||
/// the facade resolves the timeline into this POD list; the render
|
||||
/// producer decodes and composites.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MontagePod {
|
||||
/// Footage filename (borrowed; alive for the render call).
|
||||
pub filename: *const std::ffi::c_char,
|
||||
/// Media stream index.
|
||||
pub stream_index: c_int,
|
||||
/// Clip in point (sequence time), rational.
|
||||
pub in_num: i64,
|
||||
/// Clip in point denominator.
|
||||
pub in_den: i64,
|
||||
/// Clip out point (sequence time), rational.
|
||||
pub out_num: i64,
|
||||
/// Clip out point denominator.
|
||||
pub out_den: i64,
|
||||
/// Media in point, rational.
|
||||
pub media_in_num: i64,
|
||||
/// Media in point denominator.
|
||||
pub media_in_den: i64,
|
||||
/// Playback gain (1.0 = unity).
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
/// The samples block handed out by `oakrender_ticket_get_samples`
|
||||
/// (caller-owned; release with `oakrender_audio_samples_free`). Read by
|
||||
/// the facade through the Rust type.
|
||||
pub struct OakAudioSamplesOut {
|
||||
/// Interleaved f32 samples.
|
||||
pub data: Box<[f32]>,
|
||||
/// Frame count.
|
||||
pub frame_count: c_int,
|
||||
/// Sample rate (Hz).
|
||||
pub sample_rate: c_int,
|
||||
/// Channel layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Channel count.
|
||||
pub channel_count: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_min_max` (`include/audio/waveform.h`) — one summarized
|
||||
/// waveform point of one channel.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MinMax {
|
||||
/// Minimum of the summarized samples.
|
||||
pub min: f32,
|
||||
/// Maximum of the summarized samples.
|
||||
pub max: f32,
|
||||
}
|
||||
|
||||
/// `oakaudio_offset_result` (`include/audio/sync.h`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OffsetResult {
|
||||
/// Offset in samples.
|
||||
pub offset_samples: i64,
|
||||
/// Correlation confidence 0..1.
|
||||
pub confidence: f64,
|
||||
/// 1 when an estimate was found.
|
||||
pub valid: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_stretch_offset_result` (`include/audio/sync.h`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct StretchOffsetResult {
|
||||
/// Playback rate aligning the candidate (> 1 = speed up).
|
||||
pub rate: f64,
|
||||
/// Offset in samples.
|
||||
pub offset_samples: i64,
|
||||
/// Correlation confidence 0..1.
|
||||
pub confidence: f64,
|
||||
/// 1 when an estimate was found.
|
||||
pub valid: c_int,
|
||||
}
|
||||
|
||||
/// `oakaudio_source_clip` (`include/audio/sync.h`) — one clip's
|
||||
/// source-time metadata (rational seconds).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SourceClip {
|
||||
/// Source start time numerator.
|
||||
pub source_start_time_num: i64,
|
||||
/// Source start time denominator.
|
||||
pub source_start_time_den: i64,
|
||||
/// Media in numerator.
|
||||
pub media_in_num: i64,
|
||||
/// Media in denominator.
|
||||
pub media_in_den: i64,
|
||||
/// 1 when the source start time is meaningful.
|
||||
pub has_source_start_time: c_int,
|
||||
}
|
||||
|
||||
/// `i32` code -> `PixelFormat` (`repr(i32)` enum; unknown -> `Invalid`).
|
||||
pub fn pixel_format_from_code(v: c_int) -> oakcore_rs::PixelFormat {
|
||||
match v {
|
||||
0 => oakcore_rs::PixelFormat::U8,
|
||||
1 => oakcore_rs::PixelFormat::U10,
|
||||
2 => oakcore_rs::PixelFormat::U16,
|
||||
3 => oakcore_rs::PixelFormat::F16,
|
||||
4 => oakcore_rs::PixelFormat::F32,
|
||||
_ => oakcore_rs::PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// `i32` code -> `SampleFormat` (`repr(i32)` enum; unknown -> `Invalid`).
|
||||
pub fn sample_format_from_code(v: c_int) -> oakcore_rs::SampleFormat {
|
||||
match v {
|
||||
0 => oakcore_rs::SampleFormat::U8Planar,
|
||||
1 => oakcore_rs::SampleFormat::S16Planar,
|
||||
2 => oakcore_rs::SampleFormat::S32Planar,
|
||||
3 => oakcore_rs::SampleFormat::S64Planar,
|
||||
4 => oakcore_rs::SampleFormat::F32Planar,
|
||||
5 => oakcore_rs::SampleFormat::F64Planar,
|
||||
6 => oakcore_rs::SampleFormat::U8,
|
||||
7 => oakcore_rs::SampleFormat::S16,
|
||||
8 => oakcore_rs::SampleFormat::S32,
|
||||
9 => oakcore_rs::SampleFormat::S64,
|
||||
10 => oakcore_rs::SampleFormat::F32,
|
||||
11 => oakcore_rs::SampleFormat::F64,
|
||||
_ => oakcore_rs::SampleFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// `i32` code -> `VideoScalingMethod` (unknown -> `Stretch`, the C++
|
||||
/// default).
|
||||
pub fn scaling_from_code(v: c_int) -> oakcodec::encodingparams::VideoScalingMethod {
|
||||
match v {
|
||||
0 => oakcodec::encodingparams::VideoScalingMethod::Fit,
|
||||
2 => oakcodec::encodingparams::VideoScalingMethod::Crop,
|
||||
_ => oakcodec::encodingparams::VideoScalingMethod::Stretch,
|
||||
}
|
||||
}
|
||||
@@ -20,25 +20,27 @@
|
||||
//! - The **renderer** is a facade-owned box binding an output node
|
||||
//! (usually a sequence, or any single node via
|
||||
//! `oakengine_renderer_create_for_node`) to an output geometry; each
|
||||
//! render call submits an oakrender ticket (`OakVideoTicketParams`),
|
||||
//! waits for it and returns the produced frame (`OakCodecFrame` wrapped
|
||||
//! in `OakEngineFrame`). Audio rendering submits the ticket but the
|
||||
//! crate's samples path is unimplemented, so it fails with the reason
|
||||
//! in `last_error`.
|
||||
//! - The **frame accessors** read the wrapped `OakCodecFrame`
|
||||
//! render call submits an oakrender ticket (`OakVideoTicketParams`)
|
||||
//! through the manager's real ticket arena, waits for it and returns
|
||||
//! the produced frame (`oakrender::texture::Frame` wrapped in
|
||||
//! `OakEngineFrame`). Audio rendering goes through the arena's audio
|
||||
//! path (`oakrender::eval::render_audio_samples`) and returns the
|
||||
//! interleaved samples in `OakEngineAudioBuffer`.
|
||||
//! - The **frame accessors** read the wrapped frame
|
||||
//! (`channel_count` has no crate accessor and reports 0).
|
||||
//! - The **color processor** family maps onto
|
||||
//! `oakrender_color_processor_*`; the engine's `oak_color_transform`
|
||||
//! POD is converted into an oakcommon colortransform handle for
|
||||
//! `create_transform`. The color-manager list queries, standalone
|
||||
//! config handle and LUT directory/file library have no crate backing
|
||||
//! and are documented stubs (see `deferred.rs`).
|
||||
//! - The **color processor** family maps onto the real
|
||||
//! `oakrender::color::ColorProcessor`; the engine's
|
||||
//! `oak_color_transform` POD is converted into an oakcommon
|
||||
//! colortransform handle for `create_transform`. The color-manager
|
||||
//! list queries, standalone config handle and LUT directory/file
|
||||
//! library have no crate backing and are documented stubs (see
|
||||
//! `deferred.rs`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
use crate::bridge::render as r;
|
||||
use crate::bridge::render::{OakRenderVideoParams, OakVideoTicketParams};
|
||||
use crate::stubs::render as r;
|
||||
use crate::pods::{OakRenderVideoParams, OakVideoTicketParams};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_int, guard_ptr, guard_void, string_result, unbox, CHandle,
|
||||
@@ -50,6 +52,33 @@ use crate::handle::{
|
||||
// Render manager / cacher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_render_manager_init` — bring up the module's process-global
|
||||
/// render manager (0 on success; without it `render_frame` fails with NULL
|
||||
/// + last_error). The module's `OAKRENDER_E_STATE` (-70002) passes through
|
||||
/// when the manager is already initialized.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_init() -> c_int {
|
||||
guard(|| Error::from_module(unsafe { r::oakrender_manager_init() }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_available` — 1 when the render manager is up,
|
||||
/// 0 otherwise.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_available() -> c_int {
|
||||
guard_int(|| Ok(unsafe { r::oakrender_manager_available() }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_shutdown` — tear down the module's render
|
||||
/// manager (no-op when none is up; always 0, like
|
||||
/// `oakengine_audio_destroy_instance`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_shutdown() -> c_int {
|
||||
guard_void(|| unsafe {
|
||||
r::oakrender_manager_shutdown();
|
||||
});
|
||||
crate::error::OAKENGINE_OK
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_set_aggressive_garbage_collection`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
|
||||
@@ -58,24 +87,45 @@ pub extern "C" fn oakengine_render_manager_set_aggressive_garbage_collection(
|
||||
guard(|| Error::from_module(unsafe { r::oakrender_manager_set_aggressive_gc(aggressive) }))
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_requested_backend` — **not backed** (the
|
||||
/// oakrender crate exposes the current backend, not the requested one).
|
||||
/// Returns 0 (k_open_gl).
|
||||
/// `oakengine_render_manager_requested_backend` — the backend the manager
|
||||
/// was asked for (0 = k_open_gl, 1 = k_metal, 2 = k_vulkan, 3 = k_cpu;
|
||||
/// -1 when the manager is down).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_render_manager_requested_backend() -> c_int {
|
||||
0
|
||||
guard_int(|| {
|
||||
match oakrender::manager::RenderManager::global() {
|
||||
Some(m) => Ok(match m.requested_backend {
|
||||
oakrender::backend::BackendKind::Auto => 0,
|
||||
oakrender::backend::BackendKind::Metal => 1,
|
||||
oakrender::backend::BackendKind::Vulkan => 2,
|
||||
oakrender::backend::BackendKind::Gl => 0,
|
||||
oakrender::backend::BackendKind::Cpu => 3,
|
||||
}),
|
||||
None => Ok(-1),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_render_manager_backend_to_string` — **not backed** (the
|
||||
/// crate enumerates backend ids, not enum→string). Returns
|
||||
/// OAKENGINE_E_FAILED.
|
||||
/// `oakengine_render_manager_backend_to_string` — the config name of the
|
||||
/// backend ordinal (E_INVALID out of range).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_render_manager_backend_to_string(
|
||||
_backend: c_int,
|
||||
_buf: *mut c_char,
|
||||
_buf_size: c_int,
|
||||
backend: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
crate::error::OAKENGINE_E_FAILED
|
||||
guard_int(|| {
|
||||
let kind = match backend {
|
||||
0 => oakrender::backend::BackendKind::Gl,
|
||||
1 => oakrender::backend::BackendKind::Metal,
|
||||
2 => oakrender::backend::BackendKind::Vulkan,
|
||||
3 => oakrender::backend::BackendKind::Cpu,
|
||||
_ => return Err(Error::Invalid),
|
||||
};
|
||||
Ok(unsafe {
|
||||
crate::handle::write_string(kind.to_config_string(), buf, buf_size)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_render_cache_set_display_color_processor` — NULL clears.
|
||||
@@ -171,19 +221,19 @@ unsafe fn renderer_mut(ptr: *mut OakEngineRenderer) -> Result<&'static mut Rende
|
||||
/// Build an oakcommon video-params handle for the renderer's geometry.
|
||||
unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
|
||||
unsafe {
|
||||
let params = crate::bridge::common::oakcommon_videoparams_init();
|
||||
let params = crate::stubs::common::oakcommon_videoparams_init();
|
||||
if params.is_null() {
|
||||
return Err(Error::Failed("video params allocation failed".into()));
|
||||
}
|
||||
let mut rc = crate::bridge::common::oakcommon_videoparams_set_width(params, b.width);
|
||||
let mut rc = crate::stubs::common::oakcommon_videoparams_set_width(params, b.width);
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_height(params, b.height);
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_height(params, b.height);
|
||||
}
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_format(params, b.pixel_format);
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_format(params, b.pixel_format);
|
||||
}
|
||||
if rc == 0 {
|
||||
rc = crate::bridge::common::oakcommon_videoparams_set_time_base(
|
||||
rc = crate::stubs::common::oakcommon_videoparams_set_time_base(
|
||||
params,
|
||||
b.frame_rate_den,
|
||||
b.frame_rate_num,
|
||||
@@ -191,7 +241,7 @@ unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
|
||||
}
|
||||
if rc != 0 {
|
||||
let mut p = params;
|
||||
crate::bridge::common::oakcommon_videoparams_free(&mut p);
|
||||
crate::stubs::common::oakcommon_videoparams_free(&mut p);
|
||||
return Err(Error::Failed("video params setup failed".into()));
|
||||
}
|
||||
Ok(params)
|
||||
@@ -254,7 +304,7 @@ unsafe fn make_renderer_box(
|
||||
unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
|
||||
unsafe {
|
||||
let mut buf = [0 as c_char; 4096];
|
||||
let rc = crate::bridge::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
let rc = crate::stubs::node::oaknode_footage_filename(node, buf.as_mut_ptr(), buf.len() as c_int);
|
||||
if rc < 0 {
|
||||
return Err(Error::Failed("node is not footage".into()));
|
||||
}
|
||||
@@ -270,18 +320,18 @@ unsafe fn resolve_footage(node: CHandle) -> Result<FootageSpec> {
|
||||
/// graph node → its upstream footage.
|
||||
unsafe fn clip_media(clip: CHandle) -> Option<(String, c_int)> {
|
||||
unsafe {
|
||||
let node = crate::bridge::node::oaknode_block_as_node(clip);
|
||||
let node = crate::stubs::node::oaknode_block_as_node(clip);
|
||||
if node.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut footage = CHandle::null();
|
||||
if crate::bridge::node::oaknode_node_find_input_footage(node, &mut footage) != 0
|
||||
if crate::stubs::node::oaknode_node_find_input_footage(node, &mut footage) != 0
|
||||
|| footage.is_null()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut buf = [0 as c_char; 4096];
|
||||
if crate::bridge::node::oaknode_footage_filename(
|
||||
if crate::stubs::node::oaknode_footage_filename(
|
||||
footage,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as c_int,
|
||||
@@ -302,7 +352,7 @@ unsafe fn build_audio_montage(
|
||||
b: &RendererBox,
|
||||
range: oakcore_rs::TimeRange,
|
||||
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::node as n;
|
||||
unsafe {
|
||||
let mut pods = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
@@ -374,7 +424,7 @@ unsafe fn build_audio_montage(
|
||||
}
|
||||
|
||||
/// The `OakMontageClip` POD the render module's ticket reads.
|
||||
type MontagePod = oakrender::ffi::OakMontageClip;
|
||||
type MontagePod = crate::pods::MontagePod;
|
||||
|
||||
/// Build the video montage for `b` at sequence time `time` (rational):
|
||||
/// every clip covering `time` on video tracks, ordered bottom-to-top
|
||||
@@ -385,7 +435,7 @@ unsafe fn build_video_montage(
|
||||
b: &RendererBox,
|
||||
time: oakcore_rs::Rational,
|
||||
) -> (Vec<MontagePod>, Vec<std::ffi::CString>) {
|
||||
use crate::bridge::node as n;
|
||||
use crate::stubs::node as n;
|
||||
unsafe {
|
||||
let mut pods = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
@@ -618,7 +668,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame(
|
||||
let ticket = r::oakrender_ticket_render_frame(¶ms, None, std::ptr::null_mut());
|
||||
let _ = (&keep_alive, &montage);
|
||||
let mut vp = video_params;
|
||||
crate::bridge::common::oakcommon_videoparams_free(&mut vp);
|
||||
crate::stubs::common::oakcommon_videoparams_free(&mut vp);
|
||||
if ticket.is_null() {
|
||||
b.last_error = "render ticket submission failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
@@ -659,8 +709,8 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
// the range) and the output format.
|
||||
let (pods, names) = build_audio_montage(b, range);
|
||||
let mut keep: Vec<std::ffi::CString> = names;
|
||||
let params = crate::bridge::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
|
||||
crate::bridge::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
|
||||
let params = crate::stubs::audio::oakcore_audioparams_create(48000, 0x3, 10); // packed F32 stereo
|
||||
crate::stubs::audio::oakcore_audioparams_set_time_base(params, 1, 48000);
|
||||
let ticket = r::oakrender_ticket_render_audio(
|
||||
b.output_node,
|
||||
start_num,
|
||||
@@ -678,7 +728,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
},
|
||||
pods.len() as c_int,
|
||||
);
|
||||
crate::bridge::audio::oakcore_audioparams_free(params);
|
||||
crate::stubs::audio::oakcore_audioparams_free(params);
|
||||
if ticket.is_null() {
|
||||
b.last_error = "audio render ticket submission failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
@@ -693,7 +743,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
b.last_error = "audio render failed".into();
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let raw = samples as *const oakrender::ffi::OakAudioSamplesOut;
|
||||
let raw = samples as *const crate::pods::OakAudioSamplesOut;
|
||||
let boxed = AudioSamplesBox {
|
||||
data: (*raw).data.clone(),
|
||||
frame_count: (*raw).frame_count as i64,
|
||||
@@ -1210,7 +1260,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
}
|
||||
let empty = crate::common::empty_cstr();
|
||||
let ct = if (*dest).is_display != 0 {
|
||||
crate::bridge::common::oakcommon_colortransform_init_display(
|
||||
crate::stubs::common::oakcommon_colortransform_init_display(
|
||||
if (*dest).output.is_null() {
|
||||
empty
|
||||
} else {
|
||||
@@ -1228,7 +1278,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
},
|
||||
)
|
||||
} else {
|
||||
crate::bridge::common::oakcommon_colortransform_init_output(
|
||||
crate::stubs::common::oakcommon_colortransform_init_output(
|
||||
if (*dest).output.is_null() {
|
||||
empty
|
||||
} else {
|
||||
@@ -1247,7 +1297,7 @@ pub unsafe extern "C" fn oakengine_color_processor_create(
|
||||
};
|
||||
let proc = r::oakrender_color_processor_create_transform(mgr_handle, input, ct, direction);
|
||||
let mut ct_handle = ct;
|
||||
crate::bridge::common::oakcommon_colortransform_free(&mut ct_handle);
|
||||
crate::stubs::common::oakcommon_colortransform_free(&mut ct_handle);
|
||||
if proc.is_null() {
|
||||
LAST_COLOR_ERROR.with(|e| *e.borrow_mut() = "could not create color processor".into());
|
||||
return Ok(std::ptr::null_mut());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,9 +43,9 @@ use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::bridge::codec::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::bridge::node as n;
|
||||
use crate::bridge::task as t;
|
||||
use crate::pods::{zeroed_encoding_params, EncodingParamsPOD};
|
||||
use crate::stubs::node as n;
|
||||
use crate::stubs::task as t;
|
||||
use crate::codec::OakEngineEncodingParams;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::error::{Error, Result};
|
||||
@@ -654,9 +654,9 @@ pub unsafe extern "C" fn oakengine_task_create_export(
|
||||
///
|
||||
/// The oaktask crate's `convert_encoding_params` consumes exactly these
|
||||
/// fields (filename, format, video/audio/subtitle enables, codecs,
|
||||
/// dimensions, time base, pixel format, export length), so a POD carrying
|
||||
/// them is behaviorally identical to the original for the export task; all
|
||||
/// other POD fields stay zeroed.
|
||||
/// dimensions, time base, pixel format, audio rate/layout, export length),
|
||||
/// so a POD carrying them is behaviorally identical to the original for
|
||||
/// the export task; all other POD fields stay zeroed.
|
||||
fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingParamsPOD> {
|
||||
let mut pod = zeroed_encoding_params();
|
||||
|
||||
@@ -682,6 +682,24 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
|
||||
pod.video_codec = unsafe { crate::codec::oakengine_encoding_params_video_codec(params) };
|
||||
pod.audio_enabled = unsafe { crate::codec::oakengine_encoding_params_audio_enabled(params) };
|
||||
pod.audio_codec = unsafe { crate::codec::oakengine_encoding_params_audio_codec(params) };
|
||||
// Audio rate/layout flow through so the encoder opens with the
|
||||
// requested rate (the module export reads them from the POD). The
|
||||
// sample format is NOT carried: the FFmpeg encoder always runs in the
|
||||
// codec's native format and resamples (see `FFmpegEncoder::open`).
|
||||
if pod.audio_enabled != 0 {
|
||||
let mut sample_rate: c_int = 0;
|
||||
let mut channel_layout: u64 = 0;
|
||||
unsafe {
|
||||
crate::codec::oakengine_encoding_params_get_audio_params(
|
||||
params,
|
||||
&mut sample_rate,
|
||||
&mut channel_layout,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
pod.audio_sample_rate = sample_rate;
|
||||
pod.audio_channel_layout = channel_layout;
|
||||
}
|
||||
pod.subtitles_enabled =
|
||||
unsafe { crate::codec::oakengine_encoding_params_subtitles_enabled(params) };
|
||||
unsafe {
|
||||
@@ -703,7 +721,7 @@ fn export_params_pod(params: *const OakEngineEncodingParams) -> Result<EncodingP
|
||||
pod.video_height = v.height;
|
||||
pod.video_time_base_num = v.time_base_num;
|
||||
pod.video_time_base_den = v.time_base_den;
|
||||
pod.video_pixel_format = v.format;
|
||||
pod.video_pixel_format = crate::pods::pixel_format_from_code(v.format);
|
||||
}
|
||||
}
|
||||
Ok(pod)
|
||||
@@ -865,3 +883,53 @@ pub unsafe extern "C" fn oakengine_task_save_get_project(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load task results / event subscription
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_task_load_take_project` — take the project an interchange
|
||||
/// (OVE/OTIO) load task produced after a successful run; ownership moves to
|
||||
/// the caller (release with `oakengine_project_free`). NULL for a NULL
|
||||
/// task, a task that never ran, or a task that is not a load task.
|
||||
///
|
||||
/// The module's `oaktask_load_take_project` is the load-result getter the
|
||||
/// engine facade has no export for (the app's interchange-open path); it is
|
||||
/// wrapped here so the app can stay on the `oakengine_*` surface.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_task_load_take_project(
|
||||
task: *mut OakEngineTask,
|
||||
) -> *mut OakEngineProject {
|
||||
guard_ptr(|| unsafe {
|
||||
let h = unbox(task)?;
|
||||
let project = t::oaktask_load_take_project(h);
|
||||
if project.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineProject>(project))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_task_subscribe` — register the task event callback invoked on
|
||||
/// the task's own thread (`OAKTASK_EVENT_STARTED`=0, `OAKTASK_EVENT_PROGRESS`=1,
|
||||
/// `OAKTASK_EVENT_FINISHED`=2); one subscription replaces the previous one.
|
||||
///
|
||||
/// Returns 0 on success; facade `OAKENGINE_E_INVALID` (-1) for a NULL task
|
||||
/// or NULL callback; module error codes pass through untranslated. The
|
||||
/// callback and `userdata` follow the module's `oaktask_event_fn` contract
|
||||
/// (the engine facade has no subscription export of its own; the app's
|
||||
/// export-progress path uses this wrapper).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_task_subscribe(
|
||||
task: *mut OakEngineTask,
|
||||
cb: Option<t::OakTaskEventFn>,
|
||||
userdata: *mut c_void,
|
||||
) -> i64 {
|
||||
guard_i64(|| unsafe {
|
||||
let h = unbox(task)?;
|
||||
if cb.is_none() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
Ok(t::oaktask_task_subscribe(h, cb, userdata))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
//! single serialized test function; processor and sync tests are
|
||||
//! independent.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use oakengine::audio::{
|
||||
use crate::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_processor_close,
|
||||
@@ -38,6 +37,10 @@ use oakengine::audio::{
|
||||
/// (serialized — the singleton is process-wide).
|
||||
#[test]
|
||||
fn manager_lifecycle() {
|
||||
common::with_manager(|| manager_lifecycle_inner());
|
||||
}
|
||||
|
||||
fn manager_lifecycle_inner() {
|
||||
// Start from a destroyed state.
|
||||
unsafe { oakengine_audio_destroy_instance() };
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
//! Smoke tests for the encoding family (`engine/include/oakengine/encoding.h`):
|
||||
//! container/codec metadata queries and the encoding-params handle.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::codec::{
|
||||
use crate::codec::{
|
||||
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
|
||||
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
|
||||
oakengine_encoding_filename_remove_digit_placeholder,
|
||||
@@ -45,7 +44,7 @@ use oakengine::codec::{
|
||||
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_pix_fmt_index,
|
||||
oakengine_encoding_start_audio_recording,
|
||||
};
|
||||
use oakengine::common::OakVideoParamsPod;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
|
||||
/// Container format / codec metadata queries.
|
||||
#[test]
|
||||
@@ -178,7 +177,7 @@ fn params_handle_round_trip() {
|
||||
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine::common::oakengine_video_params_make(
|
||||
crate::common::oakengine_video_params_make(
|
||||
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
|
||||
)
|
||||
},
|
||||
+30
-19
@@ -39,32 +39,43 @@ use std::collections::HashMap;
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
/// Force the module crates into the link.
|
||||
/// One public direct-Rust symbol per module crate (the module C ABIs are
|
||||
/// deleted; this mirrors the anchors in `crates/oakengine/src/linkage.rs`).
|
||||
/// Under unit tests the crates are real dependencies of the lib target and
|
||||
/// are linked regardless; the array doubles as a compile-time proof that
|
||||
/// the anchor paths match the current module layouts.
|
||||
#[allow(unused)]
|
||||
pub fn force_link() -> usize {
|
||||
let fns: [usize; 12] = [
|
||||
oakundo::ffi::undostack::oakundo_undostack_init as usize,
|
||||
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
|
||||
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
|
||||
oakcommon::ffi::config::oakcommon_config_get_int as usize,
|
||||
oakplugin::ffi::oakplugin_host_plugin_count as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as usize,
|
||||
// The oaknode serializer bridge resolves these at runtime via
|
||||
// dlsym(RTLD_DEFAULT); force them into the link so the oaknode
|
||||
// serializer's XML writer/reader and the undo command factory are
|
||||
// visible to dlsym in every test binary (the oaknode rlib only
|
||||
// references them through dlsym, so the linker would otherwise drop
|
||||
// them from the oakcommon/oakundo rlib objects).
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as usize,
|
||||
oakundo::undostack::undostack_init as usize,
|
||||
oakcodec::exportformat::Format::get_name as usize,
|
||||
oakaudio::processor::Processor::init as usize,
|
||||
oakrender::manager::RenderManager::init as usize,
|
||||
oakcommon::configstore::ConfigStore::instance as usize,
|
||||
oakplugin::host::Host::global as usize,
|
||||
oaknode::project::Project::new as usize,
|
||||
oaktimeline::marker::TimelineMarkerList::new as usize,
|
||||
oaktask::manager::TaskManager::init as usize,
|
||||
// oakundo/oakcommon no longer export a C ABI; their handle-level
|
||||
// Rust API functions anchor the rlibs into every test binary (the
|
||||
// same pattern as `crates/oakengine/src/linkage.rs`).
|
||||
oakcommon::xmlutils::XmlWriter::new as usize,
|
||||
oakcommon::xmlutils::XmlReader::new as usize,
|
||||
oakundo::undocommand::command_init as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
/// Serialize every test that touches the process-wide AudioManager
|
||||
/// singleton. The former integration tests were separate processes; as
|
||||
/// unit tests they share one process (and one singleton), so the manager
|
||||
/// tests must take a shared lock instead of relying on process isolation.
|
||||
pub fn with_manager(f: impl FnOnce()) {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcore_* stubs (see module docs)
|
||||
// ---------------------------------------------------------------------------
|
||||
+4
-5
@@ -19,12 +19,11 @@
|
||||
//! singleton, so config tests are serialized inside single test
|
||||
//! functions.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::common::{
|
||||
use crate::common::{
|
||||
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
|
||||
oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int,
|
||||
oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
|
||||
@@ -112,7 +111,7 @@ fn config_error_handler() {
|
||||
);
|
||||
// Report an error through the handler.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) },
|
||||
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
@@ -121,7 +120,7 @@ fn config_error_handler() {
|
||||
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
unsafe { crate::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
+182
-24
@@ -19,13 +19,15 @@
|
||||
//! `include/audio/{manager,processor,sync,error}.h`), exercised end to
|
||||
//! end against the REAL `oakaudio` crate — no mocks anywhere.
|
||||
//!
|
||||
//! The facade's 26 exported functions are all covered:
|
||||
//! The facade's 27 exported functions are all covered:
|
||||
//!
|
||||
//! * Manager (singleton, process-wide — serialized via [`with_manager`]):
|
||||
//! create/destroy/instance handle, device accessors, output push/clock,
|
||||
//! notify interval, recording start/stop.
|
||||
//! * Sync (stateless): envelope offset/stretch correlation, source-time
|
||||
//! and waveform-offset timeline placement.
|
||||
//! * Waveform extraction (real decode): the two-stage
|
||||
//! `oakengine_waveform_extract` contract against real media.
|
||||
//! * Processor (refcounted object — serialized via [`with_processor`] so
|
||||
//! the module's debug alive counter (`oakaudio_debug_alive_count`) is
|
||||
//! deterministic): create/free/open/close/is_open plus the two
|
||||
@@ -45,14 +47,13 @@
|
||||
//! `OAKAUDIO_E_FAILED` and a diagnostic in `error_buf` — both outcomes
|
||||
//! are asserted.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_int, c_void, CStr};
|
||||
use std::ffi::{c_int, c_void, CStr, CString};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::audio::{
|
||||
use crate::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_output_levels,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
@@ -66,11 +67,11 @@ use oakengine::audio::{
|
||||
oakengine_audio_set_output_device, oakengine_audio_set_output_notify_interval,
|
||||
oakengine_audio_start_recording, oakengine_audio_stop_output, oakengine_audio_stop_recording,
|
||||
oakengine_audio_sync_place_by_source_time, oakengine_audio_sync_place_by_waveform_offset,
|
||||
OakAudioSyncPlacement, OakAudioSyncSourceClip, OakAudioWaveformOffset,
|
||||
OakAudioWaveformStretchOffset,
|
||||
oakengine_waveform_extract, OakAudioSyncPlacement, OakAudioSyncSourceClip,
|
||||
OakAudioWaveformOffset, OakAudioWaveformStretchOffset,
|
||||
};
|
||||
use oakengine::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID};
|
||||
use oakengine::handle::{CHandle, OakEngineAudioProcessor};
|
||||
use crate::error::{OAKENGINE_E_FAILED, OAKENGINE_E_INVALID};
|
||||
use crate::handle::{CHandle, OakEngineAudioProcessor};
|
||||
|
||||
/// `OAKAUDIO_E_INVALID` (include/audio/error.h) — module codes pass
|
||||
/// through the facade untranslated.
|
||||
@@ -79,6 +80,8 @@ const AUDIO_E_INVALID: c_int = -60001;
|
||||
const AUDIO_E_FAILED: c_int = -60003;
|
||||
/// `OAKAUDIO_E_STATE`.
|
||||
const AUDIO_E_STATE: c_int = -60002;
|
||||
/// `OAKAUDIO_E_NOT_FOUND` — the waveform extractor's missing-file code.
|
||||
const AUDIO_E_NOT_FOUND: c_int = -60004;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization + shared fixtures
|
||||
@@ -88,9 +91,7 @@ const AUDIO_E_STATE: c_int = -60002;
|
||||
/// process-wide and its create/destroy flips a global flag, so all
|
||||
/// manager tests take this lock and start from a destroyed state.
|
||||
fn with_manager(f: impl FnOnce()) {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f();
|
||||
common::with_manager(f)
|
||||
}
|
||||
|
||||
/// Serialize processor tests: each processor is an independent
|
||||
@@ -104,7 +105,7 @@ fn with_processor(f: impl FnOnce()) {
|
||||
|
||||
/// Current number of live refcounted oakaudio objects.
|
||||
fn alive() -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_debug_alive_count() }
|
||||
crate::stubs::audio::oakaudio_debug_alive_count()
|
||||
}
|
||||
|
||||
/// A borrowed `OakAudioParams*` mock handle (tests/common/mod.rs provides
|
||||
@@ -390,7 +391,7 @@ fn manager_recording() {
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -412,7 +413,7 @@ fn manager_recording() {
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -436,7 +437,7 @@ fn manager_recording() {
|
||||
let mut err = [0i8; 512];
|
||||
let rc = unsafe {
|
||||
oakengine_audio_start_recording(
|
||||
(&mut params as *mut oakcodec::ffi::encoder::oakcodec_encoding_params)
|
||||
(&mut params as *mut oakcodec::encodingparams::EncodingParams)
|
||||
.cast::<c_void>(),
|
||||
err.as_mut_ptr(),
|
||||
err.len() as c_int,
|
||||
@@ -472,8 +473,8 @@ fn manager_recording() {
|
||||
|
||||
/// `oakcodec_encoding_params` with WAV / pcm_s16le and the requested audio
|
||||
/// track.
|
||||
fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_encoding_params {
|
||||
let mut p: oakcodec::ffi::encoder::oakcodec_encoding_params = unsafe { std::mem::zeroed() };
|
||||
fn recording_params(audio_enabled: bool) -> oakcodec::encodingparams::EncodingParams {
|
||||
let mut p: oakcodec::encodingparams::EncodingParams = unsafe { std::mem::zeroed() };
|
||||
let path = recording_path();
|
||||
let bytes = path.as_os_str().as_encoded_bytes();
|
||||
assert!(bytes.len() < p.filename.len(), "temp path too long");
|
||||
@@ -483,7 +484,7 @@ fn recording_params(audio_enabled: bool) -> oakcodec::ffi::encoder::oakcodec_enc
|
||||
p.audio_codec = 13; // PCM_S16LE
|
||||
p.audio_sample_rate = 48000;
|
||||
p.audio_channel_layout = 3; // stereo
|
||||
p.audio_sample_format = 7; // SampleFormat::S16 (packed)
|
||||
p.audio_sample_format = oakcore_rs::SampleFormat::S16; // packed 16-bit
|
||||
p.export_length_num = 1;
|
||||
p.export_length_den = 1;
|
||||
p
|
||||
@@ -959,6 +960,164 @@ fn sync_place_by_waveform_offset() {
|
||||
assert_eq!(out.timeline_in_den, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Waveform extraction (real decode, no manager state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_waveform_extract` two-stage contract against real media: the
|
||||
/// facade's `oakengine_testmedia_write_clip` writes a clip carrying a
|
||||
/// stereo PCM 440 Hz sine, which the extractor decodes through the real
|
||||
/// oakcodec decoder. Illegal arguments → facade `OAKENGINE_E_INVALID`; a
|
||||
/// missing file passes the module's `OAKAUDIO_E_NOT_FOUND` through; the
|
||||
/// size query reports the point count + channel count; the data pass fills
|
||||
/// real min/max peaks.
|
||||
#[test]
|
||||
fn waveform_extract_two_stage_and_validation() {
|
||||
// Illegal arguments are rejected by the facade with its own code.
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
-1, // negative stream index
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
0,
|
||||
0, // non-positive samples per point
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
c"x.wav".as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
-1, // negative capacity
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
OAKENGINE_E_INVALID
|
||||
);
|
||||
|
||||
// A missing file decodes to the module's NOT_FOUND, passed through.
|
||||
let missing = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-waveform-missing-{}.wav",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&missing);
|
||||
let missing_c = CString::new(missing.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_waveform_extract(
|
||||
missing_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
AUDIO_E_NOT_FOUND
|
||||
);
|
||||
|
||||
// Real media: the facade test clip (1 s of stereo PCM 440 Hz sine).
|
||||
let media = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-waveform-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let media_c = CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) },
|
||||
0,
|
||||
"generate real media for the waveform decode"
|
||||
);
|
||||
|
||||
// Size query: point count + channel count, nothing written.
|
||||
let mut channels = 0i32;
|
||||
let needed = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut channels,
|
||||
)
|
||||
};
|
||||
assert!(needed > 0, "1 s at 48 kHz must yield points (got {needed})");
|
||||
assert_eq!(channels, 2, "the test clip's audio is stereo");
|
||||
|
||||
// Data pass: `out_pairs` receives `point_count * channel_count`
|
||||
// channel-interleaved pairs (capacity is in points), so the buffer is
|
||||
// sized for the stereo stream; the return is the point count.
|
||||
let channel_count = channels.max(1) as usize;
|
||||
let mut raw =
|
||||
vec![crate::pods::MinMax { min: 0.0, max: 0.0 }; needed as usize * channel_count];
|
||||
let n = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
raw.as_mut_ptr(),
|
||||
needed,
|
||||
&mut channels,
|
||||
)
|
||||
};
|
||||
assert_eq!(n, needed);
|
||||
// The 440 Hz sine is audible in the first channel: some window reaches a
|
||||
// real peak.
|
||||
assert!(
|
||||
raw.chunks(channel_count).any(|pt| pt[0].max.abs().max(pt[0].min.abs()) > 0.1),
|
||||
"the sine tone must show up in the peaks"
|
||||
);
|
||||
|
||||
// A capacity below the point count is a size-only query (the two-stage
|
||||
// contract): the full point count is returned and nothing is written.
|
||||
let mut two = [crate::pods::MinMax { min: 0.0, max: 0.0 }; 2];
|
||||
let n2 = unsafe {
|
||||
oakengine_waveform_extract(
|
||||
media_c.as_ptr(),
|
||||
0,
|
||||
256,
|
||||
two.as_mut_ptr(),
|
||||
2,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
assert_eq!(n2, needed);
|
||||
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&missing);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Processor — lifecycle, free contracts, validation (serialized)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -978,7 +1137,7 @@ fn processor_lifecycle_and_free_contracts() {
|
||||
// has nothing to release. The box must be a real heap box
|
||||
// (free_box deallocates it); a stack-allocated wrapper would be
|
||||
// deallocated out from under its owner.
|
||||
let empty_ptr = oakengine::handle::box_handle::<OakEngineAudioProcessor>(CHandle::null());
|
||||
let empty_ptr = crate::handle::box_handle::<OakEngineAudioProcessor>(CHandle::null());
|
||||
assert!(!empty_ptr.is_null());
|
||||
unsafe { oakengine_audio_processor_free(empty_ptr) };
|
||||
assert_eq!(alive(), baseline);
|
||||
@@ -992,12 +1151,11 @@ fn processor_lifecycle_and_free_contracts() {
|
||||
|
||||
// The module-level free is double-free-safe (ctx is nulled after
|
||||
// release); the counter decrements exactly once.
|
||||
let mut h = oakaudio::handle::CHandle::null();
|
||||
h = unsafe { oakaudio::ffi::processor::oakaudio_processor_init() };
|
||||
let mut h = crate::stubs::audio::oakaudio_processor_init();
|
||||
assert!(!h.ctx.is_null());
|
||||
assert_eq!(alive(), baseline + 1);
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) };
|
||||
unsafe { oakaudio::ffi::processor::oakaudio_processor_free(&mut h) }; // no-op
|
||||
crate::stubs::audio::oakaudio_processor_free(&mut h);
|
||||
crate::stubs::audio::oakaudio_processor_free(&mut h); // no-op
|
||||
assert!(h.ctx.is_null());
|
||||
assert_eq!(alive(), baseline);
|
||||
});
|
||||
+29
-13
@@ -40,12 +40,11 @@
|
||||
//! use-after-free by design (the engine header transfers ownership), so the
|
||||
//! tests assert NULL-idempotence rather than double-free of a live handle.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakengine::codec::{
|
||||
use crate::codec::{
|
||||
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
|
||||
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
|
||||
oakengine_encoding_filename_remove_digit_placeholder, oakengine_encoding_format_audio_codec_at,
|
||||
@@ -90,7 +89,7 @@ use oakengine::codec::{
|
||||
oakengine_encoding_sample_format_at, oakengine_encoding_sample_format_count,
|
||||
oakengine_encoding_start_audio_recording, oakengine_export_render_with_params,
|
||||
};
|
||||
use oakengine::common::OakVideoParamsPod;
|
||||
use crate::common::OakVideoParamsPod;
|
||||
|
||||
/// Facade error codes (`src/error.rs`).
|
||||
const E_INVALID: c_int = -1;
|
||||
@@ -665,7 +664,7 @@ fn params_handle_legal_round_trip() {
|
||||
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine::common::oakengine_video_params_make(
|
||||
crate::common::oakengine_video_params_make(
|
||||
&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1,
|
||||
)
|
||||
},
|
||||
@@ -1254,10 +1253,16 @@ fn params_destroy_contracts() {
|
||||
// 7. Deferred / not-backed entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Preset path/count/name, params load/save, export render and the
|
||||
/// sequence-bound last-used stubs are documented as not backed: they return
|
||||
/// the fixed contract value (E_FAILED / 0 / NULL / no-op) regardless of the
|
||||
/// Preset path/count/name, params load/save and the sequence-bound
|
||||
/// last-used stubs are documented as not backed: they return the fixed
|
||||
/// contract value (E_FAILED / 0 / NULL / no-op) regardless of the
|
||||
/// arguments, so every argument combination is safe.
|
||||
///
|
||||
/// `oakengine_export_render_with_params` is backed since M12 (see
|
||||
/// `crate::codec`, "Exporter family"); its argument validation returns
|
||||
/// E_INVALID for NULL inputs, and the params handle stays owned by the
|
||||
/// caller on the rejected path (the success path would consume it, like
|
||||
/// `oakengine_task_create_export`).
|
||||
#[test]
|
||||
fn deferred_stubs_contract() {
|
||||
let mut buf = [0 as c_char; 64];
|
||||
@@ -1298,13 +1303,15 @@ fn deferred_stubs_contract() {
|
||||
unsafe { oakengine_encoding_params_save_file(std::ptr::null_mut(), std::ptr::null()) },
|
||||
E_FAILED
|
||||
);
|
||||
// Backed since M12: NULL arguments are rejected with E_INVALID, and
|
||||
// the valid handle is NOT consumed on the rejected path.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), p) },
|
||||
E_FAILED
|
||||
E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), std::ptr::null()) },
|
||||
E_FAILED
|
||||
E_INVALID
|
||||
);
|
||||
|
||||
assert!(unsafe { oakengine_encoding_params_get_last_used(std::ptr::null_mut()) }.is_null());
|
||||
@@ -1327,6 +1334,13 @@ fn deferred_stubs_contract() {
|
||||
/// the facade, the oakaudio manager, and the error-string plumbing.
|
||||
#[test]
|
||||
fn start_audio_recording_manager_paths() {
|
||||
// Serialized: the AudioManager singleton is process-wide and shared
|
||||
// with the audio-family tests now that the former integration tests
|
||||
// run in one process.
|
||||
common::with_manager(|| start_audio_recording_manager_paths_inner());
|
||||
}
|
||||
|
||||
fn start_audio_recording_manager_paths_inner() {
|
||||
let p = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!p.is_null());
|
||||
let mut err = [0 as c_char; 128];
|
||||
@@ -1339,7 +1353,9 @@ fn start_audio_recording_manager_paths() {
|
||||
E_INVALID
|
||||
);
|
||||
|
||||
// No manager singleton -> facade E_STATE.
|
||||
// No manager singleton -> facade E_STATE (destroy it first: another
|
||||
// serialized test may have left it created).
|
||||
assert_eq!(unsafe { crate::audio::oakengine_audio_destroy_instance() }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_start_audio_recording(p, err.as_mut_ptr(), 128) },
|
||||
E_STATE
|
||||
@@ -1347,7 +1363,7 @@ fn start_audio_recording_manager_paths() {
|
||||
|
||||
// Create the real singleton through the audio facade.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::audio::oakengine_audio_create_instance() },
|
||||
unsafe { crate::audio::oakengine_audio_create_instance() },
|
||||
0
|
||||
);
|
||||
|
||||
@@ -1379,7 +1395,7 @@ fn start_audio_recording_manager_paths() {
|
||||
|
||||
// Tear the singleton down: the E_STATE contract returns.
|
||||
assert_eq!(
|
||||
unsafe { oakengine::audio::oakengine_audio_destroy_instance() },
|
||||
unsafe { crate::audio::oakengine_audio_destroy_instance() },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
+3
-4
@@ -35,15 +35,14 @@
|
||||
// (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`.
|
||||
#![allow(unused_unsafe)]
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::common::{
|
||||
use crate::common::{
|
||||
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
|
||||
oakengine_config_report_error, oakengine_config_save, oakengine_config_set_error_handler,
|
||||
oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
|
||||
@@ -63,7 +62,7 @@ use oakengine::common::{
|
||||
|
||||
/// Read a two-stage facade string into a Rust String.
|
||||
unsafe fn read_buf(buf: &mut [c_char]) -> String {
|
||||
std::ffi::CStr::from_ptr(buf.as_ptr())
|
||||
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// 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/>.
|
||||
|
||||
//! Integration tests for the **exporter family** (`src/codec.rs`,
|
||||
//! "Exporter family"; module contract
|
||||
//! `engine/include/oakengine/exporter.h`).
|
||||
//!
|
||||
//! Coverage rules (see the family test charter):
|
||||
//! 1. no mocks — every call goes through the real facade into the real
|
||||
//! module crates (the only stubs are the host-provided `oakcore_*`
|
||||
//! symbols in `tests/common`); the output file is a REAL mp4 written
|
||||
//! by the statically linked FFmpeg (oakcodec encoder), asserted by
|
||||
//! its `ftyp` box;
|
||||
//! 2. every exporter-family export is exercised on a legal path with the
|
||||
//! result asserted;
|
||||
//! 3. illegal inputs (NULL seq/path, negative ranges, unknown codecs)
|
||||
//! always yield a negative error code — never a crash;
|
||||
//! 4. the progress callback receives at least one update during a run,
|
||||
//! with the installed userdata.
|
||||
//!
|
||||
//! ## Serialization
|
||||
//!
|
||||
//! The tests assemble projects and run export tasks, both of which touch
|
||||
//! process-wide state (the undo stack cleared by `oakengine_project_new`,
|
||||
//! the global task manager), so every test takes a shared [`SERIAL`] mutex
|
||||
//! (the same pattern as `it_task`).
|
||||
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::codec::{
|
||||
oakengine_encoding_params_create, oakengine_encoding_params_enable_audio,
|
||||
oakengine_encoding_params_enable_video, oakengine_encoding_params_set_filename,
|
||||
oakengine_encoding_params_set_format, oakengine_export_last_error, oakengine_export_render,
|
||||
oakengine_export_render_with_params, oakengine_export_set_progress_callback,
|
||||
};
|
||||
use crate::common::OakVideoParamsPod;
|
||||
use crate::handle::{OakEngineProject, OakEngineSequence, free_box};
|
||||
use crate::node::{
|
||||
oakengine_footage_free, oakengine_project_create, oakengine_project_free,
|
||||
oakengine_project_import_footage, oakengine_project_new,
|
||||
};
|
||||
use crate::pods::OakExportOptions;
|
||||
use crate::testmedia::oakengine_testmedia_write_clip;
|
||||
use crate::timeline::{
|
||||
oakengine_sequence_add_footage_clip_ex, oakengine_sequence_add_track, oakengine_sequence_new,
|
||||
oakengine_sequence_set_video_params,
|
||||
};
|
||||
|
||||
/// `OAKENGINE_TRACK_TYPE_*` (timeline.h).
|
||||
const TRACK_VIDEO: c_int = 0;
|
||||
const TRACK_AUDIO: c_int = 1;
|
||||
/// `olive::ExportFormat::Format` ids (mp4 = MPEG-4 video).
|
||||
const FORMAT_MP4: c_int = 2;
|
||||
/// `olive::ExportCodec::Codec` ids.
|
||||
const CODEC_H264: c_int = 1;
|
||||
const CODEC_AAC: c_int = 12;
|
||||
|
||||
/// Serializes every test in this binary (see the module docs). Poisoned by
|
||||
/// a panicking test, the lock is recovered with `into_inner` so one failure
|
||||
/// does not cascade into `PoisonError` failures in every later test.
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Take the [`SERIAL`] lock, recovering from any poisoning.
|
||||
fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// A unique temp path (per-process, so parallel test binaries never
|
||||
/// collide).
|
||||
fn temp_path(kind: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("oakengine-it-export-{kind}-{}.mp4", std::process::id()))
|
||||
}
|
||||
|
||||
/// Encode the facade test clip and assemble a project + sequence carrying
|
||||
/// it: one video track/clip and one audio track/clip, both spanning
|
||||
/// `0..frame_count` at `fps` frames per second (mirrors the CLI's
|
||||
/// `assemble_project`). Returns `(project, sequence)` — the caller
|
||||
/// releases the sequence box with `free_box` and the project with
|
||||
/// `oakengine_project_free`.
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned handles must be released by the caller exactly once.
|
||||
unsafe fn assemble_test_sequence(
|
||||
media: &std::path::Path,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
frame_count: i64,
|
||||
fps: c_int,
|
||||
) -> (*mut OakEngineProject, *mut OakEngineSequence) {
|
||||
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
oakengine_testmedia_write_clip(media_c.as_ptr(), width, height, frame_count as c_int, fps),
|
||||
0,
|
||||
"generate the source clip"
|
||||
);
|
||||
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
|
||||
let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) };
|
||||
assert!(!footage.is_null(), "import the test clip");
|
||||
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"Export Test".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_sequence_set_video_params(
|
||||
seq, width, height, fps, 1, 1, 1, 0, 4, // PIXEL_FORMAT_F32
|
||||
0,
|
||||
)
|
||||
},
|
||||
0,
|
||||
"set the sequence frame rate"
|
||||
);
|
||||
|
||||
let vt = unsafe { oakengine_sequence_add_track(seq, TRACK_VIDEO) };
|
||||
assert!(vt >= 0, "add the video track");
|
||||
let vclip = unsafe {
|
||||
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_VIDEO, vt, 0, frame_count, 0)
|
||||
};
|
||||
assert!(!vclip.is_null(), "place the video clip");
|
||||
|
||||
let at = unsafe { oakengine_sequence_add_track(seq, TRACK_AUDIO) };
|
||||
assert!(at >= 0, "add the audio track");
|
||||
let aclip = unsafe {
|
||||
oakengine_sequence_add_footage_clip_ex(seq, footage, TRACK_AUDIO, at, 0, frame_count, 0)
|
||||
};
|
||||
assert!(!aclip.is_null(), "place the audio clip");
|
||||
|
||||
unsafe { oakengine_footage_free(footage) };
|
||||
(project, seq)
|
||||
}
|
||||
|
||||
/// Read the facade's thread-local export last-error (the string the
|
||||
/// assertion messages embed).
|
||||
fn export_last_error_str() -> String {
|
||||
let mut buf = [0 as c_char; 512];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 512) };
|
||||
if n <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf8_lossy(unsafe {
|
||||
std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Assert the exported file exists, is non-empty and starts with the MP4
|
||||
/// `ftyp` box.
|
||||
fn assert_real_mp4(path: &std::path::Path) {
|
||||
let meta = std::fs::metadata(path).expect("the exported file must exist");
|
||||
assert!(meta.len() > 0, "the exported file must be non-empty");
|
||||
let mut head = [0u8; 12];
|
||||
std::fs::File::open(path)
|
||||
.expect("reopen the exported file")
|
||||
.read_exact(&mut head)
|
||||
.expect("read the file head");
|
||||
assert_eq!(&head[4..8], b"ftyp", "MP4 files start with the ftyp box");
|
||||
}
|
||||
|
||||
/// Release the assembly returned by [`assemble_test_sequence`].
|
||||
///
|
||||
/// # Safety
|
||||
/// The handles must be the ones returned by [`assemble_test_sequence`].
|
||||
unsafe fn drop_test_sequence(project: *mut OakEngineProject, seq: *mut OakEngineSequence) {
|
||||
unsafe {
|
||||
free_box::<OakEngineSequence>(seq);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legal paths (real mp4 output)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_export_render` end-to-end: a 1 s test clip (10 frames at
|
||||
/// 10 fps, 64x64) in a project + sequence is exported to H.264/AAC MP4
|
||||
/// with explicit `OakExportOptions`; the file exists, is non-empty, starts
|
||||
/// with the `ftyp` box, and `oakengine_export_last_error` is empty after
|
||||
/// the success.
|
||||
#[test]
|
||||
fn export_render_writes_real_mp4() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("src");
|
||||
let out = temp_path("out");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let opts = OakExportOptions {
|
||||
video_codec: 0, // OAKENGINE_EXPORT_VIDEO_H264
|
||||
audio_codec: 0, // OAKENGINE_EXPORT_AUDIO_AAC
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 48000,
|
||||
audio_channel_count: 2,
|
||||
};
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &opts) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"the mp4 export must succeed with the real encoder (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
|
||||
assert_real_mp4(&out);
|
||||
|
||||
// The thread-local last-error slot is empty after a success.
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
|
||||
assert_eq!(n, 0, "last_error must be empty after a successful export");
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// `oakengine_export_render_with_params` end-to-end: the same assembly
|
||||
/// driven through a caller-built encoding-params handle (the path the
|
||||
/// app's `start_export` uses). The handle is consumed by the export task.
|
||||
#[test]
|
||||
fn export_render_with_params_writes_real_mp4() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcwp");
|
||||
let out = temp_path("outwp");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let params = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!params.is_null());
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) }, 0);
|
||||
assert_eq!(unsafe { oakengine_encoding_params_set_format(params, FORMAT_MP4) }, 0);
|
||||
let pod = OakVideoParamsPod {
|
||||
width: 64,
|
||||
height: 64,
|
||||
time_base_num: 1,
|
||||
time_base_den: 10, // 10 fps → frame duration 1/10
|
||||
format: 0,
|
||||
pixel_aspect_num: 1,
|
||||
pixel_aspect_den: 1,
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
assert_eq!(unsafe { oakengine_encoding_params_enable_video(params, &pod, CODEC_H264) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, CODEC_AAC) },
|
||||
0
|
||||
);
|
||||
// Export length: 10 frames at 10 fps = 1 s.
|
||||
unsafe {
|
||||
crate::codec::oakengine_encoding_params_set_export_length(params, 1, 1);
|
||||
}
|
||||
|
||||
let rc = unsafe { oakengine_export_render_with_params(seq, params) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"the with_params export must succeed with the real encoder (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
// The params handle was consumed by the task; do NOT destroy it here.
|
||||
|
||||
assert_real_mp4(&out);
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// `oakengine_export_render` with NULL opts selects the documented
|
||||
/// defaults (H.264/AAC mp4), and the installed progress callback receives
|
||||
/// at least one update with the installed userdata during the run.
|
||||
#[test]
|
||||
fn export_render_defaults_and_progress_callback() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcprog");
|
||||
let out = temp_path("outprog");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
// Progress callbacks are thread-local on the exporting thread; the
|
||||
// sync run fires them there, so the atomics are safe to reset while
|
||||
// holding the serial lock.
|
||||
PROGRESS_CALLS.store(0, Ordering::SeqCst);
|
||||
PROGRESS_USERDATA_HIT.store(0, Ordering::SeqCst);
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
unsafe {
|
||||
oakengine_export_set_progress_callback(Some(count_progress), PROGRESS_TOKEN as *mut c_void);
|
||||
}
|
||||
let rc = unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, std::ptr::null()) };
|
||||
assert_eq!(
|
||||
rc,
|
||||
0,
|
||||
"NULL opts must select the H.264/AAC defaults (last_error: {})",
|
||||
export_last_error_str()
|
||||
);
|
||||
assert_real_mp4(&out);
|
||||
assert!(
|
||||
PROGRESS_CALLS.load(Ordering::SeqCst) > 0,
|
||||
"progress must be reported during a run"
|
||||
);
|
||||
assert!(
|
||||
PROGRESS_USERDATA_HIT.load(Ordering::SeqCst) > 0,
|
||||
"the installed userdata must reach the callback"
|
||||
);
|
||||
|
||||
// NULL disables the callback for subsequent runs.
|
||||
unsafe { oakengine_export_set_progress_callback(None, std::ptr::null_mut()) };
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Illegal inputs (negative codes, no crash)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Every invalid argument combination of `oakengine_export_render` returns
|
||||
/// `OAKENGINE_E_INVALID` (-1) with a non-empty last-error, and the
|
||||
/// `oakengine_export_render_with_params` NULL paths return E_INVALID
|
||||
/// without consuming the caller's params handle.
|
||||
#[test]
|
||||
fn export_render_illegal_arguments() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = temp_path("srcbad");
|
||||
let out = temp_path("outbad");
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
|
||||
let (project, seq) = unsafe { assemble_test_sequence(&media, 64, 64, 10, 10) };
|
||||
|
||||
let opts = OakExportOptions {
|
||||
video_codec: 0,
|
||||
audio_codec: 0,
|
||||
video_bit_rate: 0,
|
||||
audio_sample_rate: 48000,
|
||||
audio_channel_count: 2,
|
||||
};
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
|
||||
// NULL seq / path.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(std::ptr::null_mut(), out_c.as_ptr(), 0, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, std::ptr::null(), 0, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
// Negative / inverted ranges.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), -1, 10, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 5, 5, 64, 64, &opts) },
|
||||
-1
|
||||
);
|
||||
// Unknown codec ids.
|
||||
let bad_video = OakExportOptions { video_codec: 99, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_video) },
|
||||
-1
|
||||
);
|
||||
let bad_audio = OakExportOptions { audio_codec: 99, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_audio) },
|
||||
-1
|
||||
);
|
||||
// An unsupported audio channel count.
|
||||
let bad_channels = OakExportOptions { audio_channel_count: 3, ..opts };
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render(seq, out_c.as_ptr(), 0, 10, 64, 64, &bad_channels) },
|
||||
-1
|
||||
);
|
||||
// The failures record a reason.
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let n = unsafe { oakengine_export_last_error(buf.as_mut_ptr(), 256) };
|
||||
assert!(n > 0, "a failed export must record a last error");
|
||||
|
||||
// `with_params`: NULL arguments are rejected without consuming the
|
||||
// valid handle (the caller destroys it afterwards).
|
||||
let params = unsafe { oakengine_encoding_params_create() };
|
||||
assert!(!params.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(std::ptr::null_mut(), params) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_export_render_with_params(seq, std::ptr::null()) },
|
||||
-1
|
||||
);
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params) };
|
||||
|
||||
assert!(!out.exists(), "a rejected export must not write the output");
|
||||
|
||||
unsafe { drop_test_sequence(project, seq) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progress callback bookkeeping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Sentinel userdata the progress test installs.
|
||||
const PROGRESS_TOKEN: usize = 0x0A0A_5EED;
|
||||
|
||||
/// Number of progress callback invocations (reset per test).
|
||||
static PROGRESS_CALLS: AtomicUsize = AtomicUsize::new(0);
|
||||
/// Number of invocations that received the sentinel userdata.
|
||||
static PROGRESS_USERDATA_HIT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Counts every progress callback and checks the userdata round-trip.
|
||||
unsafe extern "C" fn count_progress(_fraction: c_double, userdata: *mut c_void) {
|
||||
PROGRESS_CALLS.fetch_add(1, Ordering::SeqCst);
|
||||
if userdata as usize == PROGRESS_TOKEN {
|
||||
PROGRESS_USERDATA_HIT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
+58
-52
@@ -35,22 +35,23 @@
|
||||
//! oakplugin crate's build.rs compiles (cbits/oak_test_plugin.c). When
|
||||
//! it is unavailable the test prints SKIP and returns (never fails).
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr, CString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakengine::handle::{CHandle, OakEngineNode};
|
||||
use oakengine::plugin::{
|
||||
use crate::handle::{CHandle, OakEngineNode};
|
||||
use crate::plugin::{
|
||||
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
|
||||
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
|
||||
};
|
||||
use oakplugin::ffi::{
|
||||
oakplugin_debug_alive_count, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at,
|
||||
oakplugin_host_plugin_label, oakplugin_instance_create, oakplugin_instance_free,
|
||||
};
|
||||
// The deleted `oakplugin::ffi` handle surface is replaced by the crate's
|
||||
// direct host API (single-lib unification).
|
||||
use oakplugin::handle::RefBox;
|
||||
use oakplugin::host::Host;
|
||||
use oakplugin::instance::Instance;
|
||||
use oakplugin::property::Value;
|
||||
|
||||
/// `OAKENGINE_E_INVALID` (src/error.rs).
|
||||
const E_INVALID: c_int = -1;
|
||||
@@ -88,12 +89,12 @@ fn fresh_temp_dir(name: &str) -> PathBuf {
|
||||
|
||||
/// Current number of live backend objects (host instance registry).
|
||||
fn alive() -> c_int {
|
||||
unsafe { oakplugin_debug_alive_count() }
|
||||
Host::global().alive_count() as c_int
|
||||
}
|
||||
|
||||
/// Number of plugins discovered by the real host cache.
|
||||
fn plugin_count() -> c_int {
|
||||
unsafe { oakplugin_host_plugin_count() }
|
||||
Host::global().cache.count() as c_int
|
||||
}
|
||||
|
||||
/// Run the facade scan through the real host, returning its exit code.
|
||||
@@ -102,6 +103,34 @@ fn scan_facade(dir: &Path) -> c_int {
|
||||
unsafe { oakengine_plugin_load_plugins(cs.as_ptr()) }
|
||||
}
|
||||
|
||||
/// The identifier of the plugin at `index` (the direct-API replacement of
|
||||
/// the deleted `oakplugin_host_plugin_id_at` two-stage getter).
|
||||
fn host_plugin_id_at(index: usize) -> Option<String> {
|
||||
Host::global().cache.at(index).map(|p| p.identifier.clone())
|
||||
}
|
||||
|
||||
/// The label of the named plugin (direct-API replacement of the deleted
|
||||
/// `oakplugin_host_plugin_label` two-stage getter).
|
||||
fn host_plugin_label(id: &str) -> Option<String> {
|
||||
let plugin = Host::global().cache.find(id)?;
|
||||
match plugin.descriptor.props.get("OfxPropLabel", 0) {
|
||||
Some(Value::String(s)) => Some(s.to_string_lossy().into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a plugin instance by id (direct-API replacement of the deleted
|
||||
/// `oakplugin_instance_create`; `None` = the old empty handle).
|
||||
fn host_instance_create(id: &str) -> Option<Arc<RefBox<Instance>>> {
|
||||
Host::global().create_instance(id, None).ok()
|
||||
}
|
||||
|
||||
/// Free a plugin instance (direct-API replacement of the deleted
|
||||
/// `oakplugin_instance_free`; dropping the Arc is the refcounted free).
|
||||
fn host_instance_free(inst: &mut Option<Arc<RefBox<Instance>>>) {
|
||||
*inst = None;
|
||||
}
|
||||
|
||||
/// Locate the real test plugin shared library: oakplugin's build.rs
|
||||
/// compiles cbits/oak_test_plugin.c to `$OUT_DIR/oak_test_plugin.{dylib,so}`
|
||||
/// inside its own `target/*/build/oakplugin-*/out/` directory.
|
||||
@@ -192,23 +221,13 @@ fn fresh_test_plugin_scan_dir(tag: &str) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// All plugin identifiers currently registered in the real host cache
|
||||
/// (two-stage getter round trip per index).
|
||||
/// (direct cache iteration).
|
||||
fn plugin_ids() -> Vec<String> {
|
||||
let count = plugin_count();
|
||||
let mut out = Vec::new();
|
||||
for i in 0..count {
|
||||
let len = unsafe { oakplugin_host_plugin_id_at(i, std::ptr::null_mut(), 0) };
|
||||
if len <= 0 {
|
||||
continue;
|
||||
}
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
let rc = unsafe { oakplugin_host_plugin_id_at(i, buf.as_mut_ptr() as *mut c_char, len) };
|
||||
if rc == 0 {
|
||||
out.push(
|
||||
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
if let Some(id) = host_plugin_id_at(i as usize) {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -508,21 +527,8 @@ fn load_plugins_real_bundle_end_to_end() {
|
||||
}
|
||||
|
||||
// Label lookup for a known id resolves (phase 1: the id itself).
|
||||
let len = unsafe {
|
||||
oakplugin_host_plugin_label(c"org.oak.test-plugin".as_ptr(), std::ptr::null_mut(), 0)
|
||||
};
|
||||
assert!(len > 0);
|
||||
let mut lbuf = vec![0u8; len as usize];
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakplugin_host_plugin_label(
|
||||
c"org.oak.test-plugin".as_ptr(),
|
||||
lbuf.as_mut_ptr() as *mut c_char,
|
||||
len,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
let label = host_plugin_label(TEST_PLUGIN_ID);
|
||||
assert!(label.is_some(), "scanned test plugin must expose a label");
|
||||
|
||||
// Repeat scan of the same path is deduplicated: still OK, cache
|
||||
// unchanged, alive counter untouched.
|
||||
@@ -586,16 +592,16 @@ fn backend_instance_free_contracts() {
|
||||
common::force_link();
|
||||
let base = alive();
|
||||
|
||||
// free(NULL) and free(empty handle) are no-ops.
|
||||
unsafe { oakplugin_instance_free(std::ptr::null_mut()) };
|
||||
let mut empty = CHandle::null();
|
||||
unsafe { oakplugin_instance_free(&mut empty) };
|
||||
assert!(empty.is_null(), "free must leave the handle emptied");
|
||||
// free(NULL-equivalent) and free(empty handle) are no-ops.
|
||||
host_instance_free(&mut None);
|
||||
let mut empty: Option<Arc<RefBox<Instance>>> = None;
|
||||
host_instance_free(&mut empty);
|
||||
assert!(empty.is_none(), "free must leave the handle emptied");
|
||||
|
||||
// Unknown plugin id → empty handle, not a crash.
|
||||
let mut h = unsafe { oakplugin_instance_create(c"org.oak.not-a-plugin".as_ptr()) };
|
||||
assert!(h.is_null());
|
||||
unsafe { oakplugin_instance_free(&mut h) };
|
||||
let mut h = host_instance_create("org.oak.not-a-plugin");
|
||||
assert!(h.is_none());
|
||||
host_instance_free(&mut h);
|
||||
assert_eq!(alive(), base);
|
||||
|
||||
// Real plugin: create +1, free back to baseline, double-free safe.
|
||||
@@ -604,21 +610,21 @@ fn backend_instance_free_contracts() {
|
||||
return;
|
||||
};
|
||||
assert_eq!(scan_facade(&dir), 0);
|
||||
let mut inst = unsafe { oakplugin_instance_create(c"org.oak.test-plugin".as_ptr()) };
|
||||
let mut inst = host_instance_create(TEST_PLUGIN_ID);
|
||||
assert!(
|
||||
!inst.is_null(),
|
||||
inst.is_some(),
|
||||
"scanned test plugin must create an instance"
|
||||
);
|
||||
assert_eq!(alive(), base + 1, "one live instance must be registered");
|
||||
unsafe { oakplugin_instance_free(&mut inst) };
|
||||
assert!(inst.is_null());
|
||||
host_instance_free(&mut inst);
|
||||
assert!(inst.is_none());
|
||||
assert_eq!(
|
||||
alive(),
|
||||
base,
|
||||
"free must return the alive counter to baseline"
|
||||
);
|
||||
// Double free of the already-emptied handle is a no-op.
|
||||
unsafe { oakplugin_instance_free(&mut inst) };
|
||||
host_instance_free(&mut inst);
|
||||
assert_eq!(alive(), base);
|
||||
});
|
||||
}
|
||||
@@ -44,25 +44,25 @@
|
||||
//!
|
||||
//! ## Ignored tests
|
||||
//!
|
||||
//! - [`export_task_run_ignored_environment_gated`]: running an export
|
||||
//! needs a real GPU/OpenGL render and a real ffmpeg encoder; the host
|
||||
//! stubs cannot encode. The creation path is covered in the main suite.
|
||||
//! (None — the export run used to be environment-gated on GPU/encoder;
|
||||
//! the real CPU render + statically linked FFmpeg encoder now works in
|
||||
//! the test environment, see [`export_task_run_real_encoder`].)
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename};
|
||||
use oakengine::handle::{
|
||||
use crate::codec::{oakengine_encoding_params_create, oakengine_encoding_params_set_filename};
|
||||
use crate::handle::{
|
||||
free_box, CHandle, OakEngineNode, OakEngineProject, OakEngineSequence, OakEngineTask,
|
||||
};
|
||||
use oakengine::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
|
||||
oakengine_project_root, oakengine_project_set_filename,
|
||||
use crate::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_filename,
|
||||
oakengine_project_free, oakengine_project_new, oakengine_project_root, oakengine_project_save,
|
||||
oakengine_project_set_filename,
|
||||
};
|
||||
use oakengine::task::{
|
||||
use crate::task::{
|
||||
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
|
||||
oakengine_task_create_project_import, oakengine_task_create_project_load,
|
||||
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
|
||||
@@ -70,13 +70,13 @@ use oakengine::task::{
|
||||
oakengine_task_free, oakengine_task_import_file_count, oakengine_task_import_footage_at,
|
||||
oakengine_task_import_footage_count, oakengine_task_import_get_command,
|
||||
oakengine_task_import_invalid_file_at, oakengine_task_import_invalid_files_count,
|
||||
oakengine_task_is_cancelled, oakengine_task_manager_add, oakengine_task_manager_cancel,
|
||||
oakengine_task_manager_count, oakengine_task_manager_first, oakengine_task_manager_handle,
|
||||
oakengine_task_save_get_project, oakengine_task_start_sync, oakengine_task_start_time,
|
||||
oakengine_task_title,
|
||||
oakengine_task_is_cancelled, oakengine_task_load_take_project, oakengine_task_manager_add,
|
||||
oakengine_task_manager_cancel, oakengine_task_manager_count, oakengine_task_manager_first,
|
||||
oakengine_task_manager_handle, oakengine_task_save_get_project, oakengine_task_start_sync,
|
||||
oakengine_task_start_time, oakengine_task_subscribe, oakengine_task_title,
|
||||
};
|
||||
use oakengine::timeline::oakengine_sequence_new;
|
||||
use oakengine::undo::oakengine_undo_command_free;
|
||||
use crate::timeline::oakengine_sequence_new;
|
||||
use crate::undo::oakengine_undo_command_free;
|
||||
|
||||
/// OAKTASK module error codes that pass through untranslated.
|
||||
const OAKTASK_E_INVALID: c_int = -80001;
|
||||
@@ -89,14 +89,15 @@ const OAKTASK_E_NOT_FOUND: c_int = -80004;
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Take the [`SERIAL`] lock, recovering from any poisoning.
|
||||
fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
pub(crate) fn serial() -> std::sync::MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// The oaktask module's debug alive counter (not re-exported by the
|
||||
/// facade; the module crate is a real dependency of the test binary).
|
||||
/// The facade's live task-payload counter (the engine-side replacement
|
||||
/// of the deleted module alive counter; counts owned task payloads and
|
||||
/// borrowed manager-view payloads).
|
||||
fn alive_count() -> c_int {
|
||||
unsafe { oaktask::ffi::task::oaktask_debug_alive_count() }
|
||||
crate::stubs::task::oaktask_debug_alive_count()
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated two-stage buffer as a Rust `String`.
|
||||
@@ -310,7 +311,7 @@ fn empty_handles_are_rejected() {
|
||||
assert!(!params.is_null());
|
||||
// NULL result: the params handle is NOT consumed, so we own it still.
|
||||
assert!(unsafe { oakengine_task_create_export(empty_seq, params) }.is_null());
|
||||
unsafe { oakengine::codec::oakengine_encoding_params_destroy(params) };
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params) };
|
||||
unsafe { drop(Box::from_raw(empty_seq)) };
|
||||
}
|
||||
|
||||
@@ -883,7 +884,7 @@ fn export_task_creation() {
|
||||
handle: CHandle::null(),
|
||||
}));
|
||||
assert!(unsafe { oakengine_task_create_export(empty_seq, params2) }.is_null());
|
||||
unsafe { oakengine::codec::oakengine_encoding_params_destroy(params2) };
|
||||
unsafe { crate::codec::oakengine_encoding_params_destroy(params2) };
|
||||
unsafe { drop(Box::from_raw(empty_seq)) };
|
||||
|
||||
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
|
||||
@@ -893,39 +894,105 @@ fn export_task_creation() {
|
||||
unsafe { oakengine_project_free(project) };
|
||||
}
|
||||
|
||||
/// Running an export requires a real GPU/OpenGL render and a real encoder;
|
||||
/// the test environment's host stubs cannot decode or encode media. The
|
||||
/// creation path above covers the legal input surface; the run itself is
|
||||
/// documented as environment-gated.
|
||||
/// Running an export end-to-end: the facade test clip in a project +
|
||||
/// sequence with full encoding params (mp4 / H.264 / AAC), run
|
||||
/// synchronously through the real CPU render and the statically linked
|
||||
/// FFmpeg encoder, with the output file asserted. The creation path above
|
||||
/// covers the params-ownership matrix; this run proves the run itself
|
||||
/// works in the test environment (formerly environment-gated on GPU).
|
||||
#[test]
|
||||
#[ignore = "export run needs GPU/OpenGL rendering and a real ffmpeg encoder; host stubs cannot encode"]
|
||||
fn export_task_run_ignored_environment_gated() {
|
||||
fn export_task_run_real_encoder() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
let media = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-task-export-src-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let out = std::env::temp_dir().join(format!(
|
||||
"oakengine-it-task-export-run-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { crate::testmedia::oakengine_testmedia_write_clip(media_c.as_ptr(), 64, 64, 10, 10) },
|
||||
0,
|
||||
"generate the source clip"
|
||||
);
|
||||
|
||||
let project = oakengine_project_create();
|
||||
assert!(!project.is_null());
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
let footage =
|
||||
unsafe { crate::node::oakengine_project_import_footage(project, media_c.as_ptr()) };
|
||||
assert!(!footage.is_null(), "import the test clip");
|
||||
let seq = unsafe { oakengine_sequence_new(project, c"Export Run".as_ptr()) };
|
||||
assert!(!seq.is_null());
|
||||
|
||||
let params = oakengine_encoding_params_create();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakengine_encoding_params_set_filename(
|
||||
params,
|
||||
c"/tmp/oakengine_it_task_export_run.mov".as_ptr(),
|
||||
)
|
||||
crate::timeline::oakengine_sequence_set_video_params(seq, 64, 64, 10, 1, 1, 1, 0, 4, 0)
|
||||
},
|
||||
0
|
||||
);
|
||||
let vt = unsafe { crate::timeline::oakengine_sequence_add_track(seq, 0 /* video */) };
|
||||
assert!(vt >= 0);
|
||||
let clip = unsafe {
|
||||
crate::timeline::oakengine_sequence_add_footage_clip_ex(seq, footage, 0 /* video */, vt, 0, 10, 0)
|
||||
};
|
||||
assert!(!clip.is_null(), "place the video clip");
|
||||
unsafe { crate::node::oakengine_footage_free(footage) };
|
||||
|
||||
let params = oakengine_encoding_params_create();
|
||||
assert!(!params.is_null());
|
||||
let out_c = std::ffi::CString::new(out.to_string_lossy().into_owned()).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { oakengine_encoding_params_set_filename(params, out_c.as_ptr()) },
|
||||
0
|
||||
);
|
||||
// mp4 / H.264 / AAC, 64x64 @ 10 fps, 1 s.
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_set_format(params, 2) },
|
||||
0
|
||||
);
|
||||
let pod = crate::common::OakVideoParamsPod {
|
||||
width: 64,
|
||||
height: 64,
|
||||
time_base_num: 1,
|
||||
time_base_den: 10,
|
||||
format: 0,
|
||||
pixel_aspect_num: 1,
|
||||
pixel_aspect_den: 1,
|
||||
interlacing: 0,
|
||||
color_range: 0,
|
||||
divider: 1,
|
||||
video_type: 0,
|
||||
premultiplied_alpha: 0,
|
||||
};
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_enable_video(params, &pod, 1 /* H264 */) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { crate::codec::oakengine_encoding_params_enable_audio(params, 48000, 0x3, 0, 12 /* AAC */) },
|
||||
0
|
||||
);
|
||||
let task = unsafe { oakengine_task_create_export(seq, params) };
|
||||
assert!(!task.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 1);
|
||||
unsafe { oakengine_task_free(task) };
|
||||
|
||||
let meta = std::fs::metadata(&out);
|
||||
assert!(
|
||||
meta.is_ok() && meta.unwrap().len() > 0,
|
||||
"the export run must write the output file"
|
||||
);
|
||||
|
||||
unsafe { free_box::<OakEngineSequence>(seq) };
|
||||
unsafe { oakengine_project_free(project) };
|
||||
let _ = std::fs::remove_file(&media);
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
/// The proxy creator is a documented stub (the oaktask module has no
|
||||
@@ -964,6 +1031,8 @@ fn task_manager_lifecycle() {
|
||||
let handle = oakengine_task_manager_handle();
|
||||
assert!(!handle.is_null());
|
||||
assert_eq!(oakengine_task_manager_handle(), handle);
|
||||
// Other (serialized) tests may have left finished tasks in the queue.
|
||||
crate::stubs::task::oaktask_manager_delete_finished();
|
||||
assert_eq!(oakengine_task_manager_count(), 0);
|
||||
|
||||
// An empty queue has no first task.
|
||||
@@ -1028,3 +1097,137 @@ fn task_manager_lifecycle() {
|
||||
// exposes no delete export), so the count is still 1.
|
||||
assert_eq!(oakengine_task_manager_count(), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load-task result / event subscription (new facade exports)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oakengine_task_load_take_project` — NULL for NULL/empty tasks and for
|
||||
/// tasks that are not load tasks; after a successful load run the project
|
||||
/// comes back owned (released with `oakengine_project_free`). The success
|
||||
/// path uses a real `.ove` written through `oakengine_project_save`.
|
||||
#[test]
|
||||
fn task_load_take_project_lifecycle() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
// NULL / empty task → NULL.
|
||||
assert!(unsafe { oakengine_task_load_take_project(std::ptr::null_mut()) }.is_null());
|
||||
let empty = empty_task_box();
|
||||
assert!(unsafe { oakengine_task_load_take_project(empty) }.is_null());
|
||||
unsafe { reclaim_empty_task_box(empty) };
|
||||
|
||||
// A load task that never ran / failed has no project to take.
|
||||
let failed = unsafe { oakengine_task_create_project_load(c"/no/such/oak/project.ove".as_ptr()) };
|
||||
assert!(!failed.is_null());
|
||||
assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(failed) }, 0);
|
||||
assert!(unsafe { oakengine_task_load_take_project(failed) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_free(failed) }, 0);
|
||||
|
||||
// A non-load task (save) is not a load task → NULL.
|
||||
let project = unsafe { oakengine_project_create() };
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
let save_path = std::env::temp_dir().join("oakengine_it_task_load_take.ovexml");
|
||||
let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap();
|
||||
let save_task =
|
||||
unsafe { oakengine_task_create_project_save(project, 0, save_c.as_ptr(), std::ptr::null()) };
|
||||
assert!(!save_task.is_null());
|
||||
assert!(unsafe { oakengine_task_load_take_project(save_task) }.is_null());
|
||||
unsafe { oakengine_task_free(save_task) };
|
||||
|
||||
// Success path: save a real project file, load it through the task and
|
||||
// take the loaded project.
|
||||
let ove_path = std::env::temp_dir().join("oakengine_it_task_load_take.ove");
|
||||
let ove_c = std::ffi::CString::new(ove_path.to_str().unwrap()).unwrap();
|
||||
assert_eq!(unsafe { oakengine_project_save(project, ove_c.as_ptr()) }, 0);
|
||||
let load_task = unsafe { oakengine_task_create_project_load(ove_c.as_ptr()) };
|
||||
assert!(!load_task.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_start_sync(load_task) },
|
||||
1,
|
||||
"loading the project file just saved must succeed"
|
||||
);
|
||||
let loaded = unsafe { oakengine_task_load_take_project(load_task) };
|
||||
assert!(!loaded.is_null(), "a successful load yields the project");
|
||||
// The loaded project is a real, owned handle (name readable, freeable).
|
||||
let mut buf = [0 as c_char; 256];
|
||||
let name_len = unsafe { oakengine_project_filename(loaded, buf.as_mut_ptr(), 256) };
|
||||
assert!(name_len >= 0);
|
||||
unsafe { oakengine_project_free(loaded) };
|
||||
// Taking again returns NULL (the project was taken).
|
||||
assert!(unsafe { oakengine_task_load_take_project(load_task) }.is_null());
|
||||
assert_eq!(unsafe { oakengine_task_free(load_task) }, 0);
|
||||
|
||||
unsafe { oakengine_project_free(project) };
|
||||
let _ = std::fs::remove_file(&save_path);
|
||||
let _ = std::fs::remove_file(&ove_path);
|
||||
}
|
||||
|
||||
/// `oakengine_task_subscribe` — NULL task / NULL callback → facade
|
||||
/// `OAKENGINE_E_INVALID` (-1); a valid subscription returns 0 and delivers
|
||||
/// STARTED (0) + FINISHED (2) to the callback during a sync run (the
|
||||
/// listener is one-shot, so re-running does not re-fire).
|
||||
#[test]
|
||||
fn task_subscribe_lifecycle() {
|
||||
let _g = serial();
|
||||
common::force_link();
|
||||
|
||||
// NULL / empty task and NULL callback → facade E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(std::ptr::null_mut(), None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
let empty = empty_task_box();
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(empty, None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
unsafe { reclaim_empty_task_box(empty) };
|
||||
|
||||
// The recorder counts STARTED (0) / FINISHED (2) events.
|
||||
static STARTED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
static FINISHED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
unsafe extern "C" fn recorder(event_id: c_int, _value: f64, _userdata: *mut c_void) {
|
||||
match event_id {
|
||||
0 => {
|
||||
STARTED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
2 => {
|
||||
FINISHED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let task = unsafe { oakengine_task_create_project_load(c"".as_ptr()) };
|
||||
assert!(!task.is_null());
|
||||
|
||||
// NULL callback on a real task → E_INVALID (the facade validates it).
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, None, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
|
||||
// A valid subscription returns 0; a second one replaces the first.
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_task_subscribe(task, Some(recorder), std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
|
||||
// A sync run fires STARTED then FINISHED (the empty-filename load fails,
|
||||
// but the events fire on every run transition).
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0);
|
||||
assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
assert_eq!(FINISHED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// The subscription is one-shot: a second run does not re-fire.
|
||||
assert_eq!(unsafe { oakengine_task_start_sync(task) }, 0);
|
||||
assert_eq!(STARTED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
assert_eq!(unsafe { oakengine_task_free(task) }, 0);
|
||||
}
|
||||
@@ -38,15 +38,14 @@
|
||||
//! `oakengine_undo_group_abort` used to leave its executed children
|
||||
//! un-undone.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakengine::handle::{CHandle, OakEngineClipboard};
|
||||
use oakengine::undo::{
|
||||
use crate::handle::{CHandle, OakEngineClipboard};
|
||||
use crate::undo::{
|
||||
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
|
||||
oakengine_undo_command_create, oakengine_undo_command_create_multi,
|
||||
oakengine_undo_command_free, oakengine_undo_command_is_done,
|
||||
@@ -128,7 +127,7 @@ unsafe extern "C" fn variants_free(_ud: *mut c_void) {
|
||||
unsafe extern "C" fn lifecycle_free_userdata(ud: *mut c_void) {
|
||||
LIFECYCLE_FREED_PTR.store(ud as usize, Ordering::SeqCst);
|
||||
LIFECYCLE_FREE.fetch_add(1, Ordering::SeqCst);
|
||||
drop(Box::from_raw(ud as *mut u64));
|
||||
unsafe { drop(Box::from_raw(ud as *mut u64)) };
|
||||
}
|
||||
|
||||
/// Child redo/undo callbacks that log their id (encoded in userdata) — used
|
||||
@@ -536,27 +535,26 @@ fn free_contracts() {
|
||||
undo: None,
|
||||
free_fn: Some(mod_free_cb),
|
||||
};
|
||||
let mut h =
|
||||
unsafe { oakundo::ffi::command::oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
||||
let mut h = oakundo::undocommand::command_init(&vtable, std::ptr::null_mut());
|
||||
assert!(!h.ctx.is_null());
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) };
|
||||
oakundo::undocommand::command_free(&mut h);
|
||||
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
|
||||
assert!(h.ctx.is_null());
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut h) };
|
||||
oakundo::undocommand::command_free(&mut h);
|
||||
assert_eq!(MOD_FREE.load(Ordering::SeqCst), 1);
|
||||
|
||||
// Module stack handle: double free is a no-op; NULL value and NULL
|
||||
// pointer are no-ops too.
|
||||
let mut s = unsafe { oakundo::ffi::undostack::oakundo_undostack_init() };
|
||||
let mut s = oakundo::undostack::undostack_init();
|
||||
assert!(!s.ctx.is_null());
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) };
|
||||
oakundo::undostack::undostack_free(&mut s);
|
||||
assert!(s.ctx.is_null());
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut s) };
|
||||
oakundo::undostack::undostack_free(&mut s);
|
||||
|
||||
let mut null_h = CHandle::null();
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(&mut null_h) };
|
||||
unsafe { oakundo::ffi::command::oakundo_command_free(std::ptr::null_mut()) };
|
||||
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(std::ptr::null_mut()) };
|
||||
oakundo::undocommand::command_free(&mut null_h);
|
||||
oakundo::undocommand::command_free(std::ptr::null_mut());
|
||||
oakundo::undostack::undostack_free(std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user