feat(engine): clip move, clip effect_input, mandatory static FFmpeg

- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
This commit is contained in:
2026-08-11 23:04:48 +08:00
parent a209f63d52
commit 18ff60f147
351 changed files with 32880 additions and 15939 deletions
+1
View File
@@ -113,3 +113,4 @@ otio-install/
# Rust
**/target/
tarpaulin-out/
.env
+5 -1
View File
@@ -60,7 +60,11 @@ fn main() {
let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
let profile_dir = target_dir.join(&profile);
let deps_dir = profile_dir.join("deps");
let dylib = if os == "macos" { "liboakengine.dylib" } else { "liboakengine.so" };
let dylib = if os == "macos" {
"liboakengine.dylib"
} else {
"liboakengine.so"
};
// The un-hashed dependency artifact is the normal case; the
// workspace-member copy is the fallback. If only the hashed artifact
+20 -20
View File
@@ -21,24 +21,24 @@ use crate::cmd::{port_not_wired, require_or, EXIT_ERROR};
/// Run `info`. `project` is the .ove path from the command line.
pub fn run(project: String) -> i32 {
if let Err(code) = require_or(
"info",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
],
EXIT_ERROR,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(OAKENGINE_INIT_HEADLESS)
// project_create + project_load(project, ...)
// name/filename/is_modified/sequence_count/sequence_at(...) +
// fmt::sequence() / fmt::footage_entry() for each
// project_free + oakengine_shutdown()
// The formatters already exist in crate::fmt and are golden-tested.
let _ = &project;
port_not_wired("info", EXIT_ERROR)
if let Err(code) = require_or(
"info",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
],
EXIT_ERROR,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(OAKENGINE_INIT_HEADLESS)
// project_create + project_load(project, ...)
// name/filename/is_modified/sequence_count/sequence_at(...) +
// fmt::sequence() / fmt::footage_entry() for each
// project_free + oakengine_shutdown()
// The formatters already exist in crate::fmt and are golden-tested.
let _ = &project;
port_not_wired("info", EXIT_ERROR)
}
+14 -14
View File
@@ -47,25 +47,25 @@ pub const EXIT_USAGE: i32 = 64;
/// code the C++ binary would exit with when that family's work is
/// impossible (1 for info/probe, 2 for render/transcode).
pub fn require_or(
cmd: &str,
families: &[&DeferredFamily],
unavailable_code: i32,
cmd: &str,
families: &[&DeferredFamily],
unavailable_code: i32,
) -> Result<(), i32> {
match crate::deferred::require(families) {
Ok(()) => Ok(()),
Err(msg) => {
eprintln!("error: {cmd}: {msg}");
Err(unavailable_code)
}
}
match crate::deferred::require(families) {
Ok(()) => Ok(()),
Err(msg) => {
eprintln!("error: {cmd}: {msg}");
Err(unavailable_code)
}
}
}
/// Fallback for the (today unreachable) success arm of `require_or`: the
/// gate reported the families available, but the call-through port is not
/// wired yet. Never panics; reports an internal error and returns `code`.
pub fn port_not_wired(cmd: &str, code: i32) -> i32 {
eprintln!(
"error: {cmd}: internal error: facade families reported available but no port is wired yet"
);
code
eprintln!(
"error: {cmd}: internal error: facade families reported available but no port is wired yet"
);
code
}
+14 -14
View File
@@ -22,18 +22,18 @@ use crate::cmd::{port_not_wired, require_or, EXIT_ERROR};
/// Run `probe`. `mediafile` is the media path from the command line.
pub fn run(mediafile: String) -> i32 {
if let Err(code) = require_or(
"probe",
&[&crate::deferred::INIT, &crate::deferred::NODE],
EXIT_ERROR,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(OAKENGINE_INIT_HEADLESS)
// footage_probe(mediafile) -> decoder_name/duration/stream infos,
// formatted with the fmt::* lines (golden-tested)
// footage_free + oakengine_shutdown()
let _ = &mediafile;
port_not_wired("probe", EXIT_ERROR)
if let Err(code) = require_or(
"probe",
&[&crate::deferred::INIT, &crate::deferred::NODE],
EXIT_ERROR,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(OAKENGINE_INIT_HEADLESS)
// footage_probe(mediafile) -> decoder_name/duration/stream infos,
// formatted with the fmt::* lines (golden-tested)
// footage_free + oakengine_shutdown()
let _ = &mediafile;
port_not_wired("probe", EXIT_ERROR)
}
+38 -38
View File
@@ -27,43 +27,43 @@ use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE
/// then [`crate::ppm::write_ppm`] / [`crate::wav::write_wav`] per frame) is
/// gated on the deferred families below.
pub fn run(project: String, start_seconds: &str, end_seconds: &str, out_dir: &str) -> i32 {
let start: f64 = match start_seconds.parse() {
Ok(v) => v,
Err(_) => {
eprintln!("error: invalid start seconds \"{start_seconds}\"");
return EXIT_USAGE;
}
};
let end: f64 = match end_seconds.parse() {
Ok(v) => v,
Err(_) => {
eprintln!("error: invalid end seconds \"{end_seconds}\"");
return EXIT_USAGE;
}
};
if end <= start {
eprintln!("error: invalid end seconds \"{end_seconds}\"");
return EXIT_USAGE;
}
let start: f64 = match start_seconds.parse() {
Ok(v) => v,
Err(_) => {
eprintln!("error: invalid start seconds \"{start_seconds}\"");
return EXIT_USAGE;
}
};
let end: f64 = match end_seconds.parse() {
Ok(v) => v,
Err(_) => {
eprintln!("error: invalid end seconds \"{end_seconds}\"");
return EXIT_USAGE;
}
};
if end <= start {
eprintln!("error: invalid end seconds \"{end_seconds}\"");
return EXIT_USAGE;
}
if let Err(code) = require_or(
"render",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
&crate::deferred::RENDER,
],
EXIT_RENDER_UNAVAILABLE,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(HEADLESS | RENDER), chdir to the project dir,
// project_load, sequence 0 frame rate -> start_ts/end_ts,
// renderer_create(f32, fr_num, fr_den), then for each timestamp
// render_frame -> ppm::write_ppm (progress on stderr), then
// render_audio -> wav::write_wav. Both writers are golden-tested.
let _ = (&project, &start, &end, &out_dir);
port_not_wired("render", EXIT_RENDER_UNAVAILABLE)
if let Err(code) = require_or(
"render",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
&crate::deferred::RENDER,
],
EXIT_RENDER_UNAVAILABLE,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// oakengine_init(HEADLESS | RENDER), chdir to the project dir,
// project_load, sequence 0 frame rate -> start_ts/end_ts,
// renderer_create(f32, fr_num, fr_den), then for each timestamp
// render_frame -> ppm::write_ppm (progress on stderr), then
// render_audio -> wav::write_wav. Both writers are golden-tested.
let _ = (&project, &start, &end, &out_dir);
port_not_wired("render", EXIT_RENDER_UNAVAILABLE)
}
+39 -44
View File
@@ -22,49 +22,44 @@ use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE
/// Run `transcode`. `width`/`format` are validated exactly like the C++ loop
/// over `argv[4..]`; the facade work is gated on the deferred families below.
pub fn run(
input_media: String,
out: String,
width: Option<String>,
format: Option<String>,
) -> i32 {
if let Some(w) = &width {
match w.parse::<i64>() {
Ok(n) if n > 0 => {}
_ => {
eprintln!("error: invalid width \"{w}\"");
return EXIT_USAGE;
}
}
}
if let Some(f) = &format {
if f != "ppm" && f != "mp4" {
eprintln!("error: unknown --format \"{f}\" (ppm|mp4)");
return EXIT_USAGE;
}
}
pub fn run(input_media: String, out: String, width: Option<String>, format: Option<String>) -> i32 {
if let Some(w) = &width {
match w.parse::<i64>() {
Ok(n) if n > 0 => {}
_ => {
eprintln!("error: invalid width \"{w}\"");
return EXIT_USAGE;
}
}
}
if let Some(f) = &format {
if f != "ppm" && f != "mp4" {
eprintln!("error: unknown --format \"{f}\" (ppm|mp4)");
return EXIT_USAGE;
}
}
if let Err(code) = require_or(
"transcode",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
&crate::deferred::RENDER,
&crate::deferred::EXPORT,
],
EXIT_RENDER_UNAVAILABLE,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// probe the source for geometry/fps/duration, build a temporary
// project (new + import_footage + sequence_new + add_track x2 +
// add_footage_clip x2), then either the ppm path (render_frame /
// render_audio -> ppm::write_ppm / wav::write_wav) or the mp4 path
// (oakengine_export_render with H.264/AAC options + progress
// callback). The C++ exits 2 when the render/export backend is
// unavailable, which is also the code used here.
let _ = (&input_media, &out, &width, &format);
port_not_wired("transcode", EXIT_RENDER_UNAVAILABLE)
if let Err(code) = require_or(
"transcode",
&[
&crate::deferred::INIT,
&crate::deferred::NODE,
&crate::deferred::TIMELINE,
&crate::deferred::RENDER,
&crate::deferred::EXPORT,
],
EXIT_RENDER_UNAVAILABLE,
) {
return code;
}
// Facade port (unreachable while the families above are deferred):
// probe the source for geometry/fps/duration, build a temporary
// project (new + import_footage + sequence_new + add_track x2 +
// add_footage_clip x2), then either the ppm path (render_frame /
// render_audio -> ppm::write_ppm / wav::write_wav) or the mp4 path
// (oakengine_export_render with H.264/AAC options + progress
// callback). The C++ exits 2 when the render/export backend is
// unavailable, which is also the code used here.
let _ = (&input_media, &out, &width, &format);
port_not_wired("transcode", EXIT_RENDER_UNAVAILABLE)
}
+39 -44
View File
@@ -32,12 +32,12 @@
/// One deferred facade family: what it covers, which engine headers define
/// it, and why the facade does not wrap it yet.
pub struct DeferredFamily {
/// Short family name, as used in messages.
pub name: &'static str,
/// Engine headers involved.
pub headers: &'static str,
/// Why the family is not wrapped yet (from the facade's deferred.rs).
pub reason: &'static str,
/// Short family name, as used in messages.
pub name: &'static str,
/// Engine headers involved.
pub headers: &'static str,
/// Why the family is not wrapped yet (from the facade's deferred.rs).
pub reason: &'static str,
}
/// `init.h` — engine process initialization/shutdown.
@@ -99,50 +99,45 @@ pub const EXPORT: DeferredFamily = DeferredFamily {
/// carries the composed "not yet available" message naming each deferred
/// family and its reason, for the subcommands to print and exit on.
pub fn require(families: &[&DeferredFamily]) -> Result<(), String> {
if families.is_empty() {
return Ok(());
}
let mut detail = String::new();
for f in families {
detail.push_str(&format!("\n - {} ({}): {}", f.name, f.headers, f.reason));
}
Err(format!(
"not yet available in the Rust facade (oakengine): these family(ies) are still deferred \
if families.is_empty() {
return Ok(());
}
let mut detail = String::new();
for f in families {
detail.push_str(&format!("\n - {} ({}): {}", f.name, f.headers, f.reason));
}
Err(format!(
"not yet available in the Rust facade (oakengine): these family(ies) are still deferred \
(see src/facade/rust/src/deferred.rs):{detail}"
))
))
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn empty_family_list_is_available() {
assert!(require(&[]).is_ok());
}
#[test]
fn empty_family_list_is_available() {
assert!(require(&[]).is_ok());
}
#[test]
fn deferred_family_lists_a_reason() {
let err = require(&[&INIT]).unwrap_err();
assert!(err.contains("not yet available"));
assert!(err.contains("init"));
assert!(err.contains("oakengine"));
}
#[test]
fn deferred_family_lists_a_reason() {
let err = require(&[&INIT]).unwrap_err();
assert!(err.contains("not yet available"));
assert!(err.contains("init"));
assert!(err.contains("oakengine"));
}
#[test]
fn all_cli_families_are_currently_deferred() {
// Keeps this file honest: if any family the CLI depends on flips to
// available, the subcommand ports in src/cmd/ become reachable and
// the tests asserting "not yet available" must be revisited.
let all: [&[&DeferredFamily]; 5] = [
&[&INIT],
&[&NODE],
&[&TIMELINE],
&[&RENDER],
&[&EXPORT],
];
for families in all {
assert!(require(families).is_err());
}
}
#[test]
fn all_cli_families_are_currently_deferred() {
// Keeps this file honest: if any family the CLI depends on flips to
// available, the subcommand ports in src/cmd/ become reachable and
// the tests asserting "not yet available" must be revisited.
let all: [&[&DeferredFamily]; 5] =
[&[&INIT], &[&NODE], &[&TIMELINE], &[&RENDER], &[&EXPORT]];
for families in all {
assert!(require(families).is_err());
}
}
}
+223 -246
View File
@@ -58,37 +58,37 @@ use std::ffi::{c_char, c_double, c_int, c_void};
#[repr(C)]
pub struct OakEngineProject {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineSequence {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineRenderer {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineFrame {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineAudioBuffer {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineFootage {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
#[repr(C)]
pub struct OakEngineClip {
_opaque: [u8; 0],
_opaque: [u8; 0],
}
// ---------------------------------------------------------------------------
@@ -99,46 +99,46 @@ pub struct OakEngineClip {
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakFootageVideoInfo {
pub stream_index: c_int,
pub width: c_int,
pub height: c_int,
pub frame_rate_num: c_int,
pub frame_rate_den: c_int,
pub duration_ts: i64,
pub time_base_num: c_int,
pub time_base_den: c_int,
pub color_primaries: c_int,
pub color_trc: c_int,
pub interlaced: c_int,
pub stream_index: c_int,
pub width: c_int,
pub height: c_int,
pub frame_rate_num: c_int,
pub frame_rate_den: c_int,
pub duration_ts: i64,
pub time_base_num: c_int,
pub time_base_den: c_int,
pub color_primaries: c_int,
pub color_trc: c_int,
pub interlaced: c_int,
}
/// `oak_footage_audio_info` (engine/include/oakengine/footage.h).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakFootageAudioInfo {
pub stream_index: c_int,
pub sample_rate: c_int,
pub channel_layout: u64,
pub channel_count: c_int,
pub duration_ts: i64,
pub time_base_num: c_int,
pub time_base_den: c_int,
pub stream_index: c_int,
pub sample_rate: c_int,
pub channel_layout: u64,
pub channel_count: c_int,
pub duration_ts: i64,
pub time_base_num: c_int,
pub time_base_den: c_int,
}
/// `oak_export_options` (engine/include/oakengine/exporter.h).
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakExportOptions {
pub video_codec: c_int,
pub audio_codec: c_int,
pub video_bit_rate: i64,
pub audio_sample_rate: c_int,
pub audio_channel_count: c_int,
pub video_codec: c_int,
pub audio_codec: c_int,
pub video_bit_rate: i64,
pub audio_sample_rate: c_int,
pub audio_channel_count: c_int,
}
/// `oakengine_export_progress_fn` (exporter.h).
pub type OakEngineExportProgressFn =
Option<unsafe extern "C" fn(fraction: c_double, userdata: *mut c_void)>;
Option<unsafe extern "C" fn(fraction: c_double, userdata: *mut c_void)>;
// ---------------------------------------------------------------------------
// Constants (verbatim values from the engine headers).
@@ -172,216 +172,193 @@ pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0;
// ---------------------------------------------------------------------------
extern "C" {
// ---- init.h ----------------------------------------------------------
pub fn oakengine_init(flags: c_int) -> c_int;
pub fn oakengine_shutdown() -> c_int;
// ---- init.h ----------------------------------------------------------
pub fn oakengine_init(flags: c_int) -> c_int;
pub fn oakengine_shutdown() -> c_int;
// ---- project.h -------------------------------------------------------
pub fn oakengine_project_create() -> *mut OakEngineProject;
pub fn oakengine_project_free(self_: *mut OakEngineProject);
pub fn oakengine_project_new(self_: *mut OakEngineProject) -> c_int;
pub fn oakengine_project_load(
self_: *mut OakEngineProject,
path: *const c_char,
err: *mut c_char,
err_size: c_int,
) -> c_int;
pub fn oakengine_project_name(
self_: *const OakEngineProject,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_filename(
self_: *const OakEngineProject,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_is_modified(self_: *const OakEngineProject) -> c_int;
pub fn oakengine_project_sequence_count(
self_: *const OakEngineProject,
) -> c_int;
pub fn oakengine_project_sequence_at(
self_: *const OakEngineProject,
index: c_int,
) -> *mut OakEngineSequence;
pub fn oakengine_project_footage_count(
self_: *const OakEngineProject,
) -> c_int;
pub fn oakengine_project_footage_filename(
self_: *const OakEngineProject,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_footage_is_online(
self_: *const OakEngineProject,
index: c_int,
) -> c_int;
// ---- project.h -------------------------------------------------------
pub fn oakengine_project_create() -> *mut OakEngineProject;
pub fn oakengine_project_free(self_: *mut OakEngineProject);
pub fn oakengine_project_new(self_: *mut OakEngineProject) -> c_int;
pub fn oakengine_project_load(
self_: *mut OakEngineProject,
path: *const c_char,
err: *mut c_char,
err_size: c_int,
) -> c_int;
pub fn oakengine_project_name(
self_: *const OakEngineProject,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_filename(
self_: *const OakEngineProject,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_is_modified(self_: *const OakEngineProject) -> c_int;
pub fn oakengine_project_sequence_count(self_: *const OakEngineProject) -> c_int;
pub fn oakengine_project_sequence_at(
self_: *const OakEngineProject,
index: c_int,
) -> *mut OakEngineSequence;
pub fn oakengine_project_footage_count(self_: *const OakEngineProject) -> c_int;
pub fn oakengine_project_footage_filename(
self_: *const OakEngineProject,
index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_project_footage_is_online(
self_: *const OakEngineProject,
index: c_int,
) -> c_int;
// ---- footage.h -------------------------------------------------------
pub fn oakengine_project_import_footage(
project: *mut OakEngineProject,
path: *const c_char,
) -> *mut OakEngineFootage;
pub fn oakengine_footage_probe(path: *const c_char) -> *mut OakEngineFootage;
pub fn oakengine_footage_free(self_: *mut OakEngineFootage);
pub fn oakengine_footage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
pub fn oakengine_footage_get_decoder_name(
self_: *mut OakEngineFootage,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_footage_get_duration(
self_: *mut OakEngineFootage,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_footage_get_video_stream_count(
self_: *const OakEngineFootage,
) -> c_int;
pub fn oakengine_footage_get_video_stream_info(
self_: *mut OakEngineFootage,
index: c_int,
out: *mut OakFootageVideoInfo,
) -> c_int;
pub fn oakengine_footage_get_audio_stream_count(
self_: *const OakEngineFootage,
) -> c_int;
pub fn oakengine_footage_get_audio_stream_info(
self_: *mut OakEngineFootage,
index: c_int,
out: *mut OakFootageAudioInfo,
) -> c_int;
pub fn oakengine_footage_get_subtitle_stream_count(
self_: *const OakEngineFootage,
) -> c_int;
// ---- footage.h -------------------------------------------------------
pub fn oakengine_project_import_footage(
project: *mut OakEngineProject,
path: *const c_char,
) -> *mut OakEngineFootage;
pub fn oakengine_footage_probe(path: *const c_char) -> *mut OakEngineFootage;
pub fn oakengine_footage_free(self_: *mut OakEngineFootage);
pub fn oakengine_footage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
pub fn oakengine_footage_get_decoder_name(
self_: *mut OakEngineFootage,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_footage_get_duration(
self_: *mut OakEngineFootage,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_footage_get_video_stream_count(self_: *const OakEngineFootage) -> c_int;
pub fn oakengine_footage_get_video_stream_info(
self_: *mut OakEngineFootage,
index: c_int,
out: *mut OakFootageVideoInfo,
) -> c_int;
pub fn oakengine_footage_get_audio_stream_count(self_: *const OakEngineFootage) -> c_int;
pub fn oakengine_footage_get_audio_stream_info(
self_: *mut OakEngineFootage,
index: c_int,
out: *mut OakFootageAudioInfo,
) -> c_int;
pub fn oakengine_footage_get_subtitle_stream_count(self_: *const OakEngineFootage) -> c_int;
// ---- timeline.h ------------------------------------------------------
pub fn oakengine_sequence_new(
project: *mut OakEngineProject,
name: *const c_char,
) -> *mut OakEngineSequence;
pub fn oakengine_sequence_name(
self_: *const OakEngineSequence,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_sequence_get_length(
self_: *const OakEngineSequence,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_sequence_get_length_rational(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_frame_rate(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_video_params(
self_: *const OakEngineSequence,
width: *mut c_int,
height: *mut c_int,
par_num: *mut c_int,
par_den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_track_count(
self_: *const OakEngineSequence,
video: *mut c_int,
audio: *mut c_int,
subtitle: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_playhead(
self_: *const OakEngineSequence,
timestamp: *mut i64,
) -> c_int;
pub fn oakengine_sequence_get_playhead_seconds(
self_: *const OakEngineSequence,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_sequence_add_track(
self_: *mut OakEngineSequence,
track_type: c_int,
) -> c_int;
pub fn oakengine_sequence_add_footage_clip(
seq: *mut OakEngineSequence,
footage: *mut OakEngineFootage,
track_type: c_int,
track_index: c_int,
in_ts: i64,
out_ts: i64,
media_in: i64,
) -> *mut OakEngineClip;
pub fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int)
-> c_int;
// ---- timeline.h ------------------------------------------------------
pub fn oakengine_sequence_new(
project: *mut OakEngineProject,
name: *const c_char,
) -> *mut OakEngineSequence;
pub fn oakengine_sequence_name(
self_: *const OakEngineSequence,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_sequence_get_length(
self_: *const OakEngineSequence,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_sequence_get_length_rational(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_frame_rate(
self_: *const OakEngineSequence,
num: *mut c_int,
den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_video_params(
self_: *const OakEngineSequence,
width: *mut c_int,
height: *mut c_int,
par_num: *mut c_int,
par_den: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_track_count(
self_: *const OakEngineSequence,
video: *mut c_int,
audio: *mut c_int,
subtitle: *mut c_int,
) -> c_int;
pub fn oakengine_sequence_get_playhead(
self_: *const OakEngineSequence,
timestamp: *mut i64,
) -> c_int;
pub fn oakengine_sequence_get_playhead_seconds(
self_: *const OakEngineSequence,
seconds: *mut c_double,
) -> c_int;
pub fn oakengine_sequence_add_track(self_: *mut OakEngineSequence, track_type: c_int) -> c_int;
pub fn oakengine_sequence_add_footage_clip(
seq: *mut OakEngineSequence,
footage: *mut OakEngineFootage,
track_type: c_int,
track_index: c_int,
in_ts: i64,
out_ts: i64,
media_in: i64,
) -> *mut OakEngineClip;
pub fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
// ---- renderer.h ------------------------------------------------------
pub fn oakengine_renderer_create(
seq: *mut OakEngineSequence,
width: c_int,
height: c_int,
pixel_format: c_int,
frame_rate_num: c_int,
frame_rate_den: c_int,
output_colorspace: *const c_char,
) -> *mut OakEngineRenderer;
pub fn oakengine_renderer_free(self_: *mut OakEngineRenderer);
pub fn oakengine_renderer_last_error(
self_: *const OakEngineRenderer,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_renderer_render_frame(
self_: *mut OakEngineRenderer,
timestamp: i64,
) -> *mut OakEngineFrame;
pub fn oakengine_renderer_render_audio(
self_: *mut OakEngineRenderer,
start_timestamp: i64,
length_timestamp: i64,
) -> *mut OakEngineAudioBuffer;
// ---- renderer.h ------------------------------------------------------
pub fn oakengine_renderer_create(
seq: *mut OakEngineSequence,
width: c_int,
height: c_int,
pixel_format: c_int,
frame_rate_num: c_int,
frame_rate_den: c_int,
output_colorspace: *const c_char,
) -> *mut OakEngineRenderer;
pub fn oakengine_renderer_free(self_: *mut OakEngineRenderer);
pub fn oakengine_renderer_last_error(
self_: *const OakEngineRenderer,
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakengine_renderer_render_frame(
self_: *mut OakEngineRenderer,
timestamp: i64,
) -> *mut OakEngineFrame;
pub fn oakengine_renderer_render_audio(
self_: *mut OakEngineRenderer,
start_timestamp: i64,
length_timestamp: i64,
) -> *mut OakEngineAudioBuffer;
// ---- renderer.h (OakEngineFrame) -------------------------------------
pub fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void;
pub fn oakengine_frame_free(self_: *mut OakEngineFrame);
// ---- renderer.h (OakEngineFrame) -------------------------------------
pub fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int;
pub fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void;
pub fn oakengine_frame_free(self_: *mut OakEngineFrame);
// ---- renderer.h (OakEngineAudioBuffer) --------------------------------
pub fn oakengine_audio_sample_rate(
self_: *const OakEngineAudioBuffer,
) -> c_int;
pub fn oakengine_audio_channel_count(
self_: *const OakEngineAudioBuffer,
) -> c_int;
pub fn oakengine_audio_sample_count(
self_: *const OakEngineAudioBuffer,
) -> i64;
pub fn oakengine_audio_data(
self_: *const OakEngineAudioBuffer,
channel: c_int,
) -> *const f32;
pub fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer);
// ---- renderer.h (OakEngineAudioBuffer) --------------------------------
pub fn oakengine_audio_sample_rate(self_: *const OakEngineAudioBuffer) -> c_int;
pub fn oakengine_audio_channel_count(self_: *const OakEngineAudioBuffer) -> c_int;
pub fn oakengine_audio_sample_count(self_: *const OakEngineAudioBuffer) -> i64;
pub fn oakengine_audio_data(self_: *const OakEngineAudioBuffer, channel: c_int) -> *const f32;
pub fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer);
// ---- exporter.h ------------------------------------------------------
pub fn oakengine_export_render(
seq: *mut OakEngineSequence,
path: *const c_char,
in_ts: i64,
out_ts: i64,
width: c_int,
height: c_int,
opts: *const OakExportOptions,
) -> c_int;
pub fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
pub fn oakengine_export_set_progress_callback(
f: OakEngineExportProgressFn,
userdata: *mut c_void,
);
// ---- exporter.h ------------------------------------------------------
pub fn oakengine_export_render(
seq: *mut OakEngineSequence,
path: *const c_char,
in_ts: i64,
out_ts: i64,
width: c_int,
height: c_int,
opts: *const OakExportOptions,
) -> c_int;
pub fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
pub fn oakengine_export_set_progress_callback(
f: OakEngineExportProgressFn,
userdata: *mut c_void,
);
}
/// Read a facade string (buf/size convention) into an owned `String`,
@@ -393,17 +370,17 @@ extern "C" {
/// `getter` must be one of the `oakengine_*` string getters declared above
/// and `handle` a live handle for it.
pub unsafe fn facade_string(
getter: unsafe extern "C" fn(*const c_void, *mut c_char, c_int) -> c_int,
handle: *const c_void,
getter: unsafe extern "C" fn(*const c_void, *mut c_char, c_int) -> c_int,
handle: *const c_void,
) -> String {
unsafe {
let size = getter(handle, std::ptr::null_mut(), 0);
if size < 0 {
return String::new();
}
let mut s = vec![0u8; size as usize + 1];
let n = getter(handle, s.as_mut_ptr() as *mut c_char, size + 1);
s.truncate(n.max(0) as usize);
String::from_utf8_lossy(&s).into_owned()
}
unsafe {
let size = getter(handle, std::ptr::null_mut(), 0);
if size < 0 {
return String::new();
}
let mut s = vec![0u8; size as usize + 1];
let n = getter(handle, s.as_mut_ptr() as *mut c_char, size + 1);
s.truncate(n.max(0) as usize);
String::from_utf8_lossy(&s).into_owned()
}
}
+157 -140
View File
@@ -31,27 +31,27 @@
/// `Project: <name>` (`cmd_info`).
pub fn project_line(name: &str) -> String {
format!("Project: {name}")
format!("Project: {name}")
}
/// `File: <filename>` (`cmd_info`).
pub fn file_line(filename: &str) -> String {
format!("File: {filename}")
format!("File: {filename}")
}
/// `Modified: yes|no` (`cmd_info`).
pub fn modified_line(modified: bool) -> String {
format!("Modified: {}", if modified { "yes" } else { "no" })
format!("Modified: {}", if modified { "yes" } else { "no" })
}
/// `Sequences: <n>` (`cmd_info`).
pub fn sequences_line(count: i64) -> String {
format!("Sequences: {count}")
format!("Sequences: {count}")
}
/// `Footage: <n>` (`cmd_info`).
pub fn footage_line(count: i64) -> String {
format!("Footage: {count}")
format!("Footage: {count}")
}
/// One sequence block (`print_sequence` in cli/main.cpp).
@@ -64,29 +64,29 @@ pub fn footage_line(count: i64) -> String {
/// playhead: 0 (0.000000 s)
/// ```
pub fn sequence(
index: i64,
name: &str,
length_seconds: f64,
len_num: i64,
len_den: i64,
fr_num: i64,
fr_den: i64,
video: i64,
audio: i64,
subtitle: i64,
playhead: i64,
playhead_seconds: f64,
index: i64,
name: &str,
length_seconds: f64,
len_num: i64,
len_den: i64,
fr_num: i64,
fr_den: i64,
video: i64,
audio: i64,
subtitle: i64,
playhead: i64,
playhead_seconds: f64,
) -> String {
let fps = if fr_den != 0 {
fr_num as f64 / fr_den as f64
} else {
0.0
};
format!(
" [{index}] \"{name}\"\n length: {length_seconds:.6} s ({len_num}/{len_den})\n \
let fps = if fr_den != 0 {
fr_num as f64 / fr_den as f64
} else {
0.0
};
format!(
" [{index}] \"{name}\"\n length: {length_seconds:.6} s ({len_num}/{len_den})\n \
frame rate: {fr_num}/{fr_den} ({fps:.3} fps)\n tracks: video={video} audio={audio} \
subtitle={subtitle}\n playhead: {playhead} ({playhead_seconds:.6} s)"
)
)
}
/// One footage entry (`cmd_info`).
@@ -95,25 +95,25 @@ pub fn sequence(
/// [0] "/abs/path/demo.mp4" online
/// ```
pub fn footage_entry(index: i64, filename: &str, online: bool) -> String {
format!(
" [{index}] \"{filename}\" {}",
if online { "online" } else { "offline" }
)
format!(
" [{index}] \"{filename}\" {}",
if online { "online" } else { "offline" }
)
}
/// `Decoder: <name>` (`cmd_probe`).
pub fn decoder_line(decoder: &str) -> String {
format!("Decoder: {decoder}")
format!("Decoder: {decoder}")
}
/// `Duration: <seconds> s` (`cmd_probe`).
pub fn duration_line(seconds: f64) -> String {
format!("Duration: {seconds:.6} s")
format!("Duration: {seconds:.6} s")
}
/// `Video streams: <n>` (`cmd_probe`).
pub fn video_streams_line(count: i64) -> String {
format!("Video streams: {count}")
format!("Video streams: {count}")
}
/// One video-stream line (`cmd_probe`).
@@ -122,39 +122,39 @@ pub fn video_streams_line(count: i64) -> String {
/// [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive
/// ```
pub fn video_stream(
index: i64,
stream_index: i64,
width: i64,
height: i64,
frame_rate_num: i64,
frame_rate_den: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
color_primaries: i64,
color_trc: i64,
interlaced: bool,
index: i64,
stream_index: i64,
width: i64,
height: i64,
frame_rate_num: i64,
frame_rate_den: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
color_primaries: i64,
color_trc: i64,
interlaced: bool,
) -> String {
let fps = if frame_rate_den != 0 {
frame_rate_num as f64 / frame_rate_den as f64
} else {
0.0
};
let interlace = if interlaced {
"interlaced"
} else {
"progressive"
};
format!(
" [{index}] stream {stream_index}: {width}x{height}, {frame_rate_num}/{frame_rate_den} \
let fps = if frame_rate_den != 0 {
frame_rate_num as f64 / frame_rate_den as f64
} else {
0.0
};
let interlace = if interlaced {
"interlaced"
} else {
"progressive"
};
format!(
" [{index}] stream {stream_index}: {width}x{height}, {frame_rate_num}/{frame_rate_den} \
fps ({fps:.3}), duration {duration_ts}/{time_base_den} ({seconds:.6} s), \
primaries={color_primaries} trc={color_trc}, {interlace}"
)
)
}
/// `Audio streams: <n>` (`cmd_probe`).
pub fn audio_streams_line(count: i64) -> String {
format!("Audio streams: {count}")
format!("Audio streams: {count}")
}
/// One audio-stream line (`cmd_probe`).
@@ -163,88 +163,105 @@ pub fn audio_streams_line(count: i64) -> String {
/// [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)
/// ```
pub fn audio_stream(
index: i64,
stream_index: i64,
sample_rate: i64,
channel_count: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
index: i64,
stream_index: i64,
sample_rate: i64,
channel_count: i64,
duration_ts: i64,
time_base_den: i64,
seconds: f64,
) -> String {
format!(
" [{index}] stream {stream_index}: {sample_rate} Hz, {channel_count} channels, \
format!(
" [{index}] stream {stream_index}: {sample_rate} Hz, {channel_count} channels, \
duration {duration_ts}/{time_base_den} ({seconds:.6} s)"
)
)
}
/// `Subtitle streams: <n>` (`cmd_probe`).
pub fn subtitle_streams_line(count: i64) -> String {
format!("Subtitle streams: {count}")
format!("Subtitle streams: {count}")
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
// Golden text captured from the C++ binary:
// cmake-build-debug/cli/oak-cli info tests/project_with_footage.ove
// cmake-build-debug/cli/oak-cli probe tests/demo.mp4
// Golden text captured from the C++ binary:
// cmake-build-debug/cli/oak-cli info tests/project_with_footage.ove
// cmake-build-debug/cli/oak-cli probe tests/demo.mp4
#[test]
fn golden_info_output() {
let mut out = String::new();
out.push_str(&project_line("project_with_footage"));
out.push('\n');
out.push_str(&file_line("/Users/sunyu/Projects/oak/tests/project_with_footage.ove"));
out.push('\n');
out.push_str(&modified_line(false));
out.push('\n');
out.push_str(&sequences_line(1));
out.push('\n');
out.push_str(&sequence(
0, "Fixture Sequence", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0,
));
out.push('\n');
out.push_str(&footage_line(1));
out.push('\n');
out.push_str(&footage_entry(0, "/Users/sunyu/Projects/oak/tests/demo.mp4", true));
#[test]
fn golden_info_output() {
let mut out = String::new();
out.push_str(&project_line("project_with_footage"));
out.push('\n');
out.push_str(&file_line(
"/Users/sunyu/Projects/oak/tests/project_with_footage.ove",
));
out.push('\n');
out.push_str(&modified_line(false));
out.push('\n');
out.push_str(&sequences_line(1));
out.push('\n');
out.push_str(&sequence(
0,
"Fixture Sequence",
0.0,
0,
1,
30000,
1001,
0,
0,
0,
0,
0.0,
));
out.push('\n');
out.push_str(&footage_line(1));
out.push('\n');
out.push_str(&footage_entry(
0,
"/Users/sunyu/Projects/oak/tests/demo.mp4",
true,
));
const GOLDEN: &str = concat!(
"Project: project_with_footage\n",
"File: /Users/sunyu/Projects/oak/tests/project_with_footage.ove\n",
"Modified: no\n",
"Sequences: 1\n",
" [0] \"Fixture Sequence\"\n",
" length: 0.000000 s (0/1)\n",
" frame rate: 30000/1001 (29.970 fps)\n",
" tracks: video=0 audio=0 subtitle=0\n",
" playhead: 0 (0.000000 s)\n",
"Footage: 1\n",
" [0] \"/Users/sunyu/Projects/oak/tests/demo.mp4\" online",
);
assert_eq!(out, GOLDEN);
}
const GOLDEN: &str = concat!(
"Project: project_with_footage\n",
"File: /Users/sunyu/Projects/oak/tests/project_with_footage.ove\n",
"Modified: no\n",
"Sequences: 1\n",
" [0] \"Fixture Sequence\"\n",
" length: 0.000000 s (0/1)\n",
" frame rate: 30000/1001 (29.970 fps)\n",
" tracks: video=0 audio=0 subtitle=0\n",
" playhead: 0 (0.000000 s)\n",
"Footage: 1\n",
" [0] \"/Users/sunyu/Projects/oak/tests/demo.mp4\" online",
);
assert_eq!(out, GOLDEN);
}
#[test]
fn golden_probe_output() {
let mut out = String::new();
out.push_str(&decoder_line("ffmpeg"));
out.push('\n');
out.push_str(&duration_line(17.0));
out.push('\n');
out.push_str(&video_streams_line(1));
out.push('\n');
out.push_str(&video_stream(
0, 0, 1920, 1080, 25, 1, 217600, 12800, 17.0, 1, 1, false,
));
out.push('\n');
out.push_str(&audio_streams_line(1));
out.push('\n');
out.push_str(&audio_stream(0, 1, 48000, 2, 816000, 48000, 17.0));
out.push('\n');
out.push_str(&subtitle_streams_line(0));
#[test]
fn golden_probe_output() {
let mut out = String::new();
out.push_str(&decoder_line("ffmpeg"));
out.push('\n');
out.push_str(&duration_line(17.0));
out.push('\n');
out.push_str(&video_streams_line(1));
out.push('\n');
out.push_str(&video_stream(
0, 0, 1920, 1080, 25, 1, 217600, 12800, 17.0, 1, 1, false,
));
out.push('\n');
out.push_str(&audio_streams_line(1));
out.push('\n');
out.push_str(&audio_stream(0, 1, 48000, 2, 816000, 48000, 17.0));
out.push('\n');
out.push_str(&subtitle_streams_line(0));
const GOLDEN: &str = concat!(
const GOLDEN: &str = concat!(
"Decoder: ffmpeg\n",
"Duration: 17.000000 s\n",
"Video streams: 1\n",
@@ -253,21 +270,21 @@ mod tests {
" [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)\n",
"Subtitle streams: 0",
);
assert_eq!(out, GOLDEN);
}
assert_eq!(out, GOLDEN);
}
#[test]
fn fps_rounding_matches_printf() {
// 30000/1001 = 29.970029... -> %.3f -> "29.970"
let s = sequence(0, "S", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0);
assert!(s.contains("frame rate: 30000/1001 (29.970 fps)"), "{s}");
}
#[test]
fn fps_rounding_matches_printf() {
// 30000/1001 = 29.970029... -> %.3f -> "29.970"
let s = sequence(0, "S", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0);
assert!(s.contains("frame rate: 30000/1001 (29.970 fps)"), "{s}");
}
#[test]
fn offline_footage_prints_offline() {
assert_eq!(
footage_entry(2, "gone.mp4", false),
" [2] \"gone.mp4\" offline"
);
}
#[test]
fn offline_footage_prints_offline() {
assert_eq!(
footage_entry(2, "gone.mp4", false),
" [2] \"gone.mp4\" offline"
);
}
}
+82 -82
View File
@@ -83,101 +83,101 @@ Exit codes:\n\
/// is reproduced exactly; clap still enforces the argument shapes.
#[derive(Parser, Debug)]
#[command(
name = "oak-cli",
disable_help_flag = true,
disable_version_flag = true,
subcommand_required = true
name = "oak-cli",
disable_help_flag = true,
disable_version_flag = true,
subcommand_required = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Print project name, sequences and footage.
Info {
/// Path to the project file (.ove).
project: String,
},
/// Render the first sequence to PPM frames (P6, 8-bit RGB) and the
/// audio range to a PCM s16 WAV file in <out_dir>.
Render {
/// Path to the project file (.ove).
project: String,
/// Start of the rendered range, in seconds.
start_seconds: String,
/// End of the rendered range, in seconds (must be > start).
end_seconds: String,
/// Directory the PPM frames and audio.wav are written into.
out_dir: String,
},
/// Probe a media file: decoder, duration, video and audio streams.
Probe {
/// Media file to probe.
mediafile: String,
},
/// Transcode a media file end to end.
Transcode {
/// Source media file.
input_media: String,
/// Output MP4 path, or the output directory with --format ppm.
out: String,
/// Output width (defaults to the source width).
width: Option<String>,
/// Output format: "mp4" (default) or "ppm".
#[arg(long = "format")]
format: Option<String>,
},
/// Print project name, sequences and footage.
Info {
/// Path to the project file (.ove).
project: String,
},
/// Render the first sequence to PPM frames (P6, 8-bit RGB) and the
/// audio range to a PCM s16 WAV file in <out_dir>.
Render {
/// Path to the project file (.ove).
project: String,
/// Start of the rendered range, in seconds.
start_seconds: String,
/// End of the rendered range, in seconds (must be > start).
end_seconds: String,
/// Directory the PPM frames and audio.wav are written into.
out_dir: String,
},
/// Probe a media file: decoder, duration, video and audio streams.
Probe {
/// Media file to probe.
mediafile: String,
},
/// Transcode a media file end to end.
Transcode {
/// Source media file.
input_media: String,
/// Output MP4 path, or the output directory with --format ppm.
out: String,
/// Output width (defaults to the source width).
width: Option<String>,
/// Output format: "mp4" (default) or "ppm".
#[arg(long = "format")]
format: Option<String>,
},
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let args: Vec<String> = std::env::args().skip(1).collect();
// argv[1] handling that mirrors the C++ main() exactly.
if let Some(first) = args.first() {
if first == "--help" || first == "-h" {
print!("{USAGE}");
exit(cmd::EXIT_OK);
}
}
if let Some(first) = args.first() {
if !matches!(first.as_str(), "info" | "render" | "probe" | "transcode") {
eprintln!("error: unknown command \"{first}\"");
eprint_usage();
exit(cmd::EXIT_USAGE);
}
}
// argv[1] handling that mirrors the C++ main() exactly.
if let Some(first) = args.first() {
if first == "--help" || first == "-h" {
print!("{USAGE}");
exit(cmd::EXIT_OK);
}
}
if let Some(first) = args.first() {
if !matches!(first.as_str(), "info" | "render" | "probe" | "transcode") {
eprintln!("error: unknown command \"{first}\"");
eprint_usage();
exit(cmd::EXIT_USAGE);
}
}
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
// clap's own arity/format message, then the C++ usage text.
let _ = e.print();
eprint_usage();
exit(cmd::EXIT_USAGE);
}
};
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
// clap's own arity/format message, then the C++ usage text.
let _ = e.print();
eprint_usage();
exit(cmd::EXIT_USAGE);
}
};
let code = match cli.command {
Command::Info { project } => cmd::info::run(project),
Command::Render {
project,
start_seconds,
end_seconds,
out_dir,
} => cmd::render::run(project, &start_seconds, &end_seconds, &out_dir),
Command::Probe { mediafile } => cmd::probe::run(mediafile),
Command::Transcode {
input_media,
out,
width,
format,
} => cmd::transcode::run(input_media, out, width, format),
};
exit(code);
let code = match cli.command {
Command::Info { project } => cmd::info::run(project),
Command::Render {
project,
start_seconds,
end_seconds,
out_dir,
} => cmd::render::run(project, &start_seconds, &end_seconds, &out_dir),
Command::Probe { mediafile } => cmd::probe::run(mediafile),
Command::Transcode {
input_media,
out,
width,
format,
} => cmd::transcode::run(input_media, out, width, format),
};
exit(code);
}
fn eprint_usage() {
eprint!("{USAGE}");
eprint!("{USAGE}");
}
+132 -131
View File
@@ -41,155 +41,156 @@ pub const PIXEL_FORMAT_F32: i32 = 4;
/// bytes apart (stride). `channels` is the per-pixel channel count in the
/// source data; only the first three channels are emitted.
pub fn write_ppm(
path: &Path,
width: i32,
height: i32,
format: i32,
channels: i32,
linesize: i32,
data: &[u8],
path: &Path,
width: i32,
height: i32,
format: i32,
channels: i32,
linesize: i32,
data: &[u8],
) -> io::Result<()> {
let width = usize::try_from(width).map_err(|_| invalid_data("negative width"))?;
let height = usize::try_from(height).map_err(|_| invalid_data("negative height"))?;
let linesize = usize::try_from(linesize).unwrap_or(0);
let channels = usize::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
if channels < 3 {
return Err(invalid_data("channel count below 3"));
}
let width = usize::try_from(width).map_err(|_| invalid_data("negative width"))?;
let height = usize::try_from(height).map_err(|_| invalid_data("negative height"))?;
let linesize = usize::try_from(linesize).unwrap_or(0);
let channels = usize::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
if channels < 3 {
return Err(invalid_data("channel count below 3"));
}
let mut out = Vec::with_capacity(
format!("P6\n{width} {height}\n255\n").len() + width * height * 3,
);
out.extend_from_slice(format!("P6\n{width} {height}\n255\n").as_bytes());
let mut out =
Vec::with_capacity(format!("P6\n{width} {height}\n255\n").len() + width * height * 3);
out.extend_from_slice(format!("P6\n{width} {height}\n255\n").as_bytes());
let mut row = vec![0u8; width * 3];
for y in 0..height {
let line_start = y * linesize;
let line_end = line_start.checked_add(linesize);
let line = match line_end {
Some(end) if end <= data.len() => &data[line_start..end],
_ => {
return Err(invalid_data("pixel data buffer is shorter than the frame geometry"));
}
};
for x in 0..width {
for c in 0..3 {
let v = if format == PIXEL_FORMAT_F32 {
// f32: 4 bytes per channel.
let off = (x * channels + c) * 4;
let px = f32::from_ne_bytes([
line[off],
line[off + 1],
line[off + 2],
line[off + 3],
]);
let clamped = if px < 0.0 {
0.0
} else if px > 1.0 {
1.0
} else {
px
};
// static_cast<unsigned char>(clamped * 255.0f + 0.5f):
// truncation toward zero, same as Rust `as u8`.
(clamped * 255.0 + 0.5) as u8
} else if format == 0 {
// u8: 1 byte per channel.
line[x * channels + c]
} else {
return Err(invalid_data(&format!(
"unsupported frame pixel format {format}"
)));
};
row[x * 3 + c] = v;
}
}
out.extend_from_slice(&row);
}
let mut row = vec![0u8; width * 3];
for y in 0..height {
let line_start = y * linesize;
let line_end = line_start.checked_add(linesize);
let line = match line_end {
Some(end) if end <= data.len() => &data[line_start..end],
_ => {
return Err(invalid_data(
"pixel data buffer is shorter than the frame geometry",
));
}
};
for x in 0..width {
for c in 0..3 {
let v = if format == PIXEL_FORMAT_F32 {
// f32: 4 bytes per channel.
let off = (x * channels + c) * 4;
let px = f32::from_ne_bytes([
line[off],
line[off + 1],
line[off + 2],
line[off + 3],
]);
let clamped = if px < 0.0 {
0.0
} else if px > 1.0 {
1.0
} else {
px
};
// static_cast<unsigned char>(clamped * 255.0f + 0.5f):
// truncation toward zero, same as Rust `as u8`.
(clamped * 255.0 + 0.5) as u8
} else if format == 0 {
// u8: 1 byte per channel.
line[x * channels + c]
} else {
return Err(invalid_data(&format!(
"unsupported frame pixel format {format}"
)));
};
row[x * 3 + c] = v;
}
}
out.extend_from_slice(&row);
}
let mut f = std::fs::File::create(path)?;
f.write_all(&out)
let mut f = std::fs::File::create(path)?;
f.write_all(&out)
}
fn invalid_data(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
fn bytes(hex: &str) -> Vec<u8> {
let mut v = Vec::new();
for pair in hex.as_bytes().chunks(2) {
let s = std::str::from_utf8(pair).unwrap();
v.push(u8::from_str_radix(s, 16).unwrap());
}
v
}
fn bytes(hex: &str) -> Vec<u8> {
let mut v = Vec::new();
for pair in hex.as_bytes().chunks(2) {
let s = std::str::from_utf8(pair).unwrap();
v.push(u8::from_str_radix(s, 16).unwrap());
}
v
}
#[test]
fn writes_p6_header_and_u8_rows() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_u8.ppm");
// 2x2, 3 channels, linesize 6, u8.
let data = vec![
1, 2, 3, 4, 5, 6, //
7, 8, 9, 10, 11, 12, //
];
write_ppm(&path, 2, 2, 0, 3, 6, &data).unwrap();
#[test]
fn writes_p6_header_and_u8_rows() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_u8.ppm");
// 2x2, 3 channels, linesize 6, u8.
let data = vec![
1, 2, 3, 4, 5, 6, //
7, 8, 9, 10, 11, 12, //
];
write_ppm(&path, 2, 2, 0, 3, 6, &data).unwrap();
let got = std::fs::read(&path).unwrap();
let mut expected = b"P6\n2 2\n255\n".to_vec();
expected.extend_from_slice(&data);
assert_eq!(got, expected);
let _ = std::fs::remove_file(&path);
}
let got = std::fs::read(&path).unwrap();
let mut expected = b"P6\n2 2\n255\n".to_vec();
expected.extend_from_slice(&data);
assert_eq!(got, expected);
let _ = std::fs::remove_file(&path);
}
#[test]
fn f32_rows_are_clamped_and_quantized() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_f32.ppm");
// 1x1, RGBA (4 channels), linesize 16, f32.
let data = bytes("0000803f0000803f0000803f00000000"); // 1.0, 1.0, 1.0, 0.0
write_ppm(&path, 1, 1, PIXEL_FORMAT_F32, 4, 16, &data).unwrap();
#[test]
fn f32_rows_are_clamped_and_quantized() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_f32.ppm");
// 1x1, RGBA (4 channels), linesize 16, f32.
let data = bytes("0000803f0000803f0000803f00000000"); // 1.0, 1.0, 1.0, 0.0
write_ppm(&path, 1, 1, PIXEL_FORMAT_F32, 4, 16, &data).unwrap();
let got = std::fs::read(&path).unwrap();
assert_eq!(&got[..11], b"P6\n1 1\n255\n");
assert_eq!(&got[11..], &[255, 255, 255]); // 1.0 -> 255 (clamped * 255 + 0.5, truncated)
let _ = std::fs::remove_file(&path);
}
let got = std::fs::read(&path).unwrap();
assert_eq!(&got[..11], b"P6\n1 1\n255\n");
assert_eq!(&got[11..], &[255, 255, 255]); // 1.0 -> 255 (clamped * 255 + 0.5, truncated)
let _ = std::fs::remove_file(&path);
}
#[test]
fn clamps_f32_negative_and_over_one() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_clamp.ppm");
// 2x1 RGB f32: (-0.5, 0.25, 2.0) | (0.0, 0.5, 1.0)
let mut data = Vec::new();
for v in [-0.5f32, 0.25, 2.0, 0.0, 0.5, 1.0] {
data.extend_from_slice(&v.to_ne_bytes());
}
write_ppm(&path, 2, 1, PIXEL_FORMAT_F32, 3, 24, &data).unwrap();
#[test]
fn clamps_f32_negative_and_over_one() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_clamp.ppm");
// 2x1 RGB f32: (-0.5, 0.25, 2.0) | (0.0, 0.5, 1.0)
let mut data = Vec::new();
for v in [-0.5f32, 0.25, 2.0, 0.0, 0.5, 1.0] {
data.extend_from_slice(&v.to_ne_bytes());
}
write_ppm(&path, 2, 1, PIXEL_FORMAT_F32, 3, 24, &data).unwrap();
let got = std::fs::read(&path).unwrap();
// 0.0 -> 0, 0.25*255+0.5=64.25 -> 64, 1.0 -> 255, 0.5*255+0.5=128.0 -> 128
assert_eq!(&got[11..], &[0, 64, 255, 0, 128, 255]);
let _ = std::fs::remove_file(&path);
}
let got = std::fs::read(&path).unwrap();
// 0.0 -> 0, 0.25*255+0.5=64.25 -> 64, 1.0 -> 255, 0.5*255+0.5=128.0 -> 128
assert_eq!(&got[11..], &[0, 64, 255, 0, 128, 255]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn unsupported_format_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_bad.ppm");
let err = write_ppm(&path, 1, 1, 7, 3, 3, &[0, 0, 0]).unwrap_err();
assert!(err.to_string().contains("unsupported frame pixel format 7"));
}
#[test]
fn unsupported_format_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_bad.ppm");
let err = write_ppm(&path, 1, 1, 7, 3, 3, &[0, 0, 0]).unwrap_err();
assert!(err.to_string().contains("unsupported frame pixel format 7"));
}
#[test]
fn short_buffer_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_short.ppm");
let err = write_ppm(&path, 4, 4, 0, 3, 12, &[0u8; 10]).unwrap_err();
assert!(err.to_string().contains("shorter than the frame geometry"));
}
#[test]
fn short_buffer_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_ppm_short.ppm");
let err = write_ppm(&path, 4, 4, 0, 3, 12, &[0u8; 10]).unwrap_err();
assert!(err.to_string().contains("shorter than the frame geometry"));
}
}
+111 -108
View File
@@ -31,16 +31,11 @@ use std::io::{self, Write};
use std::path::Path;
fn write_u16_le(f: &mut impl Write, v: u16) -> io::Result<()> {
f.write_all(&[v as u8, (v >> 8) as u8])
f.write_all(&[v as u8, (v >> 8) as u8])
}
fn write_u32_le(f: &mut impl Write, v: u32) -> io::Result<()> {
f.write_all(&[
v as u8,
(v >> 8) as u8,
(v >> 16) as u8,
(v >> 24) as u8,
])
f.write_all(&[v as u8, (v >> 8) as u8, (v >> 16) as u8, (v >> 24) as u8])
}
/// Write interleaved float samples as a PCM s16 WAV file.
@@ -48,120 +43,128 @@ fn write_u32_le(f: &mut impl Write, v: u32) -> io::Result<()> {
/// `data` must hold `samples * channels` values in interleaved order
/// (`[s0c0, s0c1, s1c0, s1c1, ...]`), matching what the C++ loop over
/// `oakengine_audio_data(audio, ch)[i]` emits.
pub fn write_wav(path: &Path, rate: i32, channels: i32, samples: i64, data: &[f32]) -> io::Result<()> {
let rate = u32::try_from(rate).map_err(|_| invalid_data("negative sample rate"))?;
let channels = u32::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
let samples = u64::try_from(samples).map_err(|_| invalid_data("negative sample count"))?;
if channels == 0 {
return Err(invalid_data("zero channel count"));
}
let expected = samples
.checked_mul(u64::from(channels))
.ok_or_else(|| invalid_data("sample count overflow"))?;
if data.len() as u64 != expected {
return Err(invalid_data("sample buffer length does not match rate/channels/samples"));
}
pub fn write_wav(
path: &Path,
rate: i32,
channels: i32,
samples: i64,
data: &[f32],
) -> io::Result<()> {
let rate = u32::try_from(rate).map_err(|_| invalid_data("negative sample rate"))?;
let channels = u32::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
let samples = u64::try_from(samples).map_err(|_| invalid_data("negative sample count"))?;
if channels == 0 {
return Err(invalid_data("zero channel count"));
}
let expected = samples
.checked_mul(u64::from(channels))
.ok_or_else(|| invalid_data("sample count overflow"))?;
if data.len() as u64 != expected {
return Err(invalid_data(
"sample buffer length does not match rate/channels/samples",
));
}
let data_size = expected
.checked_mul(2)
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| invalid_data("WAV data chunk exceeds 4 GiB"))?;
let byte_rate = rate
.checked_mul(channels)
.and_then(|v| v.checked_mul(2))
.ok_or_else(|| invalid_data("byte rate overflow"))?;
let block_align = channels
.checked_mul(2)
.and_then(|v| u16::try_from(v).ok())
.ok_or_else(|| invalid_data("block align overflow"))?;
let data_size = expected
.checked_mul(2)
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| invalid_data("WAV data chunk exceeds 4 GiB"))?;
let byte_rate = rate
.checked_mul(channels)
.and_then(|v| v.checked_mul(2))
.ok_or_else(|| invalid_data("byte rate overflow"))?;
let block_align = channels
.checked_mul(2)
.and_then(|v| u16::try_from(v).ok())
.ok_or_else(|| invalid_data("block align overflow"))?;
let mut f = std::fs::File::create(path)?;
f.write_all(b"RIFF")?;
write_u32_le(&mut f, 36 + data_size)?;
f.write_all(b"WAVE")?;
f.write_all(b"fmt ")?;
write_u32_le(&mut f, 16)?; // fmt chunk size
write_u16_le(&mut f, 1)?; // PCM
write_u16_le(&mut f, channels as u16)?;
write_u32_le(&mut f, rate)?;
write_u32_le(&mut f, byte_rate)?;
write_u16_le(&mut f, block_align)?;
write_u16_le(&mut f, 16)?; // bits per sample
f.write_all(b"data")?;
write_u32_le(&mut f, data_size)?;
let mut f = std::fs::File::create(path)?;
f.write_all(b"RIFF")?;
write_u32_le(&mut f, 36 + data_size)?;
f.write_all(b"WAVE")?;
f.write_all(b"fmt ")?;
write_u32_le(&mut f, 16)?; // fmt chunk size
write_u16_le(&mut f, 1)?; // PCM
write_u16_le(&mut f, channels as u16)?;
write_u32_le(&mut f, rate)?;
write_u32_le(&mut f, byte_rate)?;
write_u16_le(&mut f, block_align)?;
write_u16_le(&mut f, 16)?; // bits per sample
f.write_all(b"data")?;
write_u32_le(&mut f, data_size)?;
for &v in data {
let clamped = if v < -1.0 {
-1.0
} else if v > 1.0 {
1.0
} else {
v
};
// static_cast<int16_t>(clamped * 32767.0f): truncation toward zero.
let s = (clamped * 32767.0) as i16;
write_u16_le(&mut f, s as u16)?;
}
f.flush()
for &v in data {
let clamped = if v < -1.0 {
-1.0
} else if v > 1.0 {
1.0
} else {
v
};
// static_cast<int16_t>(clamped * 32767.0f): truncation toward zero.
let s = (clamped * 32767.0) as i16;
write_u16_le(&mut f, s as u16)?;
}
f.flush()
}
fn invalid_data(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn golden_mono_wav() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_mono.wav");
// 2 samples mono at 44100 Hz: 0.0, 0.5
write_wav(&path, 44100, 1, 2, &[0.0, 0.5]).unwrap();
#[test]
fn golden_mono_wav() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_mono.wav");
// 2 samples mono at 44100 Hz: 0.0, 0.5
write_wav(&path, 44100, 1, 2, &[0.0, 0.5]).unwrap();
let got = std::fs::read(&path).unwrap();
// 44-byte header + 2 samples * 2 bytes.
assert_eq!(got.len(), 48);
assert_eq!(&got[0..4], b"RIFF");
// chunk size = 36 + 4 = 40
assert_eq!(&got[4..8], &[40, 0, 0, 0]);
assert_eq!(&got[8..12], b"WAVE");
assert_eq!(&got[12..16], b"fmt ");
assert_eq!(&got[16..20], &[16, 0, 0, 0]);
assert_eq!(&got[20..22], &[1, 0]); // PCM
assert_eq!(&got[22..24], &[1, 0]); // mono
assert_eq!(&got[24..28], &[0x44, 0xAC, 0, 0]); // 44100
assert_eq!(&got[28..32], &[0x88, 0x58, 0x01, 0]); // byte rate 88200
assert_eq!(&got[32..34], &[2, 0]); // block align
assert_eq!(&got[34..36], &[16, 0]); // bits per sample
assert_eq!(&got[36..40], b"data");
assert_eq!(&got[40..44], &[4, 0, 0, 0]); // data size
// 0.0 -> 0; 0.5 * 32767 = 16383.5 -> truncates to 16383 (0x3FFF)
assert_eq!(&got[44..48], &[0x00, 0x00, 0xFF, 0x3F]);
let _ = std::fs::remove_file(&path);
}
let got = std::fs::read(&path).unwrap();
// 44-byte header + 2 samples * 2 bytes.
assert_eq!(got.len(), 48);
assert_eq!(&got[0..4], b"RIFF");
// chunk size = 36 + 4 = 40
assert_eq!(&got[4..8], &[40, 0, 0, 0]);
assert_eq!(&got[8..12], b"WAVE");
assert_eq!(&got[12..16], b"fmt ");
assert_eq!(&got[16..20], &[16, 0, 0, 0]);
assert_eq!(&got[20..22], &[1, 0]); // PCM
assert_eq!(&got[22..24], &[1, 0]); // mono
assert_eq!(&got[24..28], &[0x44, 0xAC, 0, 0]); // 44100
assert_eq!(&got[28..32], &[0x88, 0x58, 0x01, 0]); // byte rate 88200
assert_eq!(&got[32..34], &[2, 0]); // block align
assert_eq!(&got[34..36], &[16, 0]); // bits per sample
assert_eq!(&got[36..40], b"data");
assert_eq!(&got[40..44], &[4, 0, 0, 0]); // data size
// 0.0 -> 0; 0.5 * 32767 = 16383.5 -> truncates to 16383 (0x3FFF)
assert_eq!(&got[44..48], &[0x00, 0x00, 0xFF, 0x3F]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn stereo_interleaving_and_clamping() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_stereo.wav");
// 1 sample stereo at 48000: (-1.0, 1.0) interleaved.
write_wav(&path, 48000, 2, 1, &[-1.0, 1.0]).unwrap();
#[test]
fn stereo_interleaving_and_clamping() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_stereo.wav");
// 1 sample stereo at 48000: (-1.0, 1.0) interleaved.
write_wav(&path, 48000, 2, 1, &[-1.0, 1.0]).unwrap();
let got = std::fs::read(&path).unwrap();
assert_eq!(&got[22..24], &[2, 0]); // stereo
assert_eq!(&got[32..34], &[4, 0]); // block align
// -1.0 -> -32767 = 0x8001; 1.0 -> 32767 = 0x7FFF
assert_eq!(&got[44..48], &[0x01, 0x80, 0xFF, 0x7F]);
let _ = std::fs::remove_file(&path);
}
let got = std::fs::read(&path).unwrap();
assert_eq!(&got[22..24], &[2, 0]); // stereo
assert_eq!(&got[32..34], &[4, 0]); // block align
// -1.0 -> -32767 = 0x8001; 1.0 -> 32767 = 0x7FFF
assert_eq!(&got[44..48], &[0x01, 0x80, 0xFF, 0x7F]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn sample_count_mismatch_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_bad.wav");
let err = write_wav(&path, 48000, 2, 10, &[0.0f32; 3]).unwrap_err();
assert!(err.to_string().contains("does not match"));
}
#[test]
fn sample_count_mismatch_is_an_error() {
let dir = std::env::temp_dir();
let path = dir.join("oak_cli_test_wav_bad.wav");
let err = write_wav(&path, 48000, 2, 10, &[0.0f32; 3]).unwrap_err();
assert!(err.to_string().contains("does not match"));
}
}
+91 -58
View File
@@ -27,121 +27,154 @@
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_oak-cli")
env!("CARGO_BIN_EXE_oak-cli")
}
fn run(args: &[&str]) -> (i32, String, String) {
let out = Command::new(bin()).args(args).output().expect("spawn oak-cli");
(
out.status.code().expect("exit code"),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
let out = Command::new(bin())
.args(args)
.output()
.expect("spawn oak-cli");
(
out.status.code().expect("exit code"),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn help_prints_the_cpp_usage_text_and_exits_zero() {
let (code, stdout, stderr) = run(&["--help"]);
assert_eq!(code, 0);
assert!(stderr.is_empty());
assert!(stdout.starts_with("oak-cli - headless consumer of the liboakengine C ABI\n"));
assert!(stdout.contains("oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]"));
assert!(stdout.contains("Exit codes:"));
assert!(stdout.contains("64 usage error"));
let (code, stdout, stderr) = run(&["--help"]);
assert_eq!(code, 0);
assert!(stderr.is_empty());
assert!(stdout.starts_with("oak-cli - headless consumer of the liboakengine C ABI\n"));
assert!(stdout.contains("oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]"));
assert!(stdout.contains("Exit codes:"));
assert!(stdout.contains("64 usage error"));
}
#[test]
fn no_arguments_is_a_usage_error() {
let (code, _stdout, stderr) = run(&[]);
assert_eq!(code, 64);
assert!(stderr.contains("Usage:"));
let (code, _stdout, stderr) = run(&[]);
assert_eq!(code, 64);
assert!(stderr.contains("Usage:"));
}
#[test]
fn unknown_command_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["frobnicate"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: unknown command \"frobnicate\""));
let (code, _stdout, stderr) = run(&["frobnicate"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: unknown command \"frobnicate\""));
}
#[test]
fn info_on_a_fixture_reports_not_yet_available() {
// The fixture mirrors the ctest invocation; the deferred gate fires
// before any file access.
let (code, _stdout, stderr) = run(&["info", "tests/project_with_footage.ove"]);
assert_eq!(code, 1);
assert!(stderr.contains("error: info: not yet available"), "stderr: {stderr}");
// The crate was renamed oakfacade -> oakengine; the deferral reason
// names the current crate.
assert!(stderr.contains("oakengine"));
// The fixture mirrors the ctest invocation; the deferred gate fires
// before any file access.
let (code, _stdout, stderr) = run(&["info", "tests/project_with_footage.ove"]);
assert_eq!(code, 1);
assert!(
stderr.contains("error: info: not yet available"),
"stderr: {stderr}"
);
// The crate was renamed oakfacade -> oakengine; the deferral reason
// names the current crate.
assert!(stderr.contains("oakengine"));
}
#[test]
fn info_with_missing_argument_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["info"]);
assert_eq!(code, 64);
assert!(stderr.contains("Usage:"));
let (code, _stdout, stderr) = run(&["info"]);
assert_eq!(code, 64);
assert!(stderr.contains("Usage:"));
}
#[test]
fn probe_reports_not_yet_available() {
let (code, _stdout, stderr) = run(&["probe", "tests/demo.mp4"]);
assert_eq!(code, 1);
assert!(stderr.contains("error: probe: not yet available"), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["probe", "tests/demo.mp4"]);
assert_eq!(code, 1);
assert!(
stderr.contains("error: probe: not yet available"),
"stderr: {stderr}"
);
}
#[test]
fn render_reports_render_unavailable() {
let (code, _stdout, stderr) = run(&["render", "p.ove", "0", "1", "out"]);
assert_eq!(code, 2);
assert!(stderr.contains("error: render: not yet available"), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["render", "p.ove", "0", "1", "out"]);
assert_eq!(code, 2);
assert!(
stderr.contains("error: render: not yet available"),
"stderr: {stderr}"
);
}
#[test]
fn render_bad_seconds_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["render", "p.ove", "abc", "1", "out"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: invalid start seconds \"abc\""), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["render", "p.ove", "abc", "1", "out"]);
assert_eq!(code, 64);
assert!(
stderr.contains("error: invalid start seconds \"abc\""),
"stderr: {stderr}"
);
}
#[test]
fn render_end_not_after_start_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["render", "p.ove", "2", "1", "out"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: invalid end seconds \"1\""), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["render", "p.ove", "2", "1", "out"]);
assert_eq!(code, 64);
assert!(
stderr.contains("error: invalid end seconds \"1\""),
"stderr: {stderr}"
);
}
#[test]
fn transcode_reports_render_unavailable() {
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "960"]);
assert_eq!(code, 2);
assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "960"]);
assert_eq!(code, 2);
assert!(
stderr.contains("error: transcode: not yet available"),
"stderr: {stderr}"
);
}
#[test]
fn transcode_bad_width_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "banana"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: invalid width \"banana\""), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "banana"]);
assert_eq!(code, 64);
assert!(
stderr.contains("error: invalid width \"banana\""),
"stderr: {stderr}"
);
}
#[test]
fn transcode_nonpositive_width_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "0"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: invalid width \"0\""), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "0"]);
assert_eq!(code, 64);
assert!(
stderr.contains("error: invalid width \"0\""),
"stderr: {stderr}"
);
}
#[test]
fn transcode_unknown_format_is_a_usage_error() {
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "--format", "webm"]);
assert_eq!(code, 64);
assert!(stderr.contains("error: unknown --format \"webm\" (ppm|mp4)"), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "--format", "webm"]);
assert_eq!(code, 64);
assert!(
stderr.contains("error: unknown --format \"webm\" (ppm|mp4)"),
"stderr: {stderr}"
);
}
#[test]
fn transcode_ppm_format_is_accepted_then_reports_not_available() {
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "outdir", "960", "--format", "ppm"]);
assert_eq!(code, 2);
assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}");
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "outdir", "960", "--format", "ppm"]);
assert_eq!(code, 2);
assert!(
stderr.contains("error: transcode: not yet available"),
"stderr: {stderr}"
);
}
+137 -136
View File
@@ -69,36 +69,36 @@ pub const TYPE_ERROR: &str = "error";
#[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,
/// 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,
})
}
/// 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:
@@ -106,31 +106,31 @@ impl HandshakeMsg {
#[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,
pub time_num: i64,
pub time_den: i64,
/// Forced output size (0 = graph default).
pub width: i32,
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,
pub color_is_display: bool,
pub color_output: String,
pub color_view: String,
pub color_look: String,
/// Correlates with the eventual frame_ready.
pub ticket: i64,
/// Viewer node stable uuid in the loaded graph.
pub node: String,
pub time_num: i64,
pub time_den: i64,
/// Forced output size (0 = graph default).
pub width: i32,
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,
pub color_is_display: bool,
pub color_output: String,
pub color_view: String,
pub color_look: String,
}
/// `frame_ready` — a rendered frame is published (wire names `ticket`/
@@ -138,120 +138,121 @@ pub struct RenderFrameMsg {
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct FrameReadyMsg {
pub ticket: i64,
/// Index into the worker->main output FrameSlotPool.
pub slot: i32,
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 {
pub ticket: i64,
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 {
pub path: String,
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 }),
}
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")
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")
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[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 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_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 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 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");
}
#[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");
}
}
+32 -30
View File
@@ -53,51 +53,53 @@ pub const PROTOCOL_VERSION: i32 = 1;
/// that single option).
#[derive(Parser, Debug)]
#[command(
name = "oak-worker",
about = "Oak render worker: headless render process for the editor's worker pool",
disable_version_flag = true
name = "oak-worker",
about = "Oak render worker: headless render process for the editor's worker pool",
disable_version_flag = true
)]
struct Args {
/// Render backend to initialize: "opengl", "vulkan", "metal", "auto",
/// or "none" (no renderer; the process exits 1 like the C++ worker).
#[arg(long, default_value = "opengl")]
backend: String,
/// Render backend to initialize: "opengl", "vulkan", "metal", "auto",
/// or "none" (no renderer; the process exits 1 like the C++ worker).
#[arg(long, default_value = "opengl")]
backend: String,
}
fn main() {
let args = Args::parse();
// The facade's worker_main is the C++ oakengine_worker_main() — the
// whole worker flow. Like workermain.cpp, this main only forwards.
exit(oakengine::worker::worker_main(&args.backend.to_ascii_lowercase()));
let args = Args::parse();
// The facade's worker_main is the C++ oakengine_worker_main() — the
// whole worker flow. Like workermain.cpp, this main only forwards.
exit(oakengine::worker::worker_main(
&args.backend.to_ascii_lowercase(),
));
}
/// Log a worker-side message to stderr, mirroring worker.cpp `log_error()`
/// (the `worker: ` prefix).
pub fn log_error(message: &str) {
eprintln!("worker: {message}");
eprintln!("worker: {message}");
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn clap_parses_backend_default() {
use clap::Parser;
let args = Args::try_parse_from(["oak-worker"]).unwrap();
assert_eq!(args.backend, "opengl");
}
#[test]
fn clap_parses_backend_default() {
use clap::Parser;
let args = Args::try_parse_from(["oak-worker"]).unwrap();
assert_eq!(args.backend, "opengl");
}
#[test]
fn clap_parses_backend_flag() {
use clap::Parser;
let args = Args::try_parse_from(["oak-worker", "--backend", "none"]).unwrap();
assert_eq!(args.backend, "none");
}
#[test]
fn clap_parses_backend_flag() {
use clap::Parser;
let args = Args::try_parse_from(["oak-worker", "--backend", "none"]).unwrap();
assert_eq!(args.backend, "none");
}
#[test]
fn clap_rejects_unknown_flags() {
use clap::Parser;
assert!(Args::try_parse_from(["oak-worker", "--frobnicate"]).is_err());
}
#[test]
fn clap_rejects_unknown_flags() {
use clap::Parser;
assert!(Args::try_parse_from(["oak-worker", "--frobnicate"]).is_err());
}
}
+59 -27
View File
@@ -210,7 +210,11 @@ mod tests {
/// The "parent" side of a handshake: create an output segment holding a
/// pool, optionally an input segment, and return the handshake message
/// plus the owner regions (kept alive by the caller).
fn parent_side(slots: i32, slot_bytes: i64, input: bool) -> (Value, SharedMemoryRegion, Option<SharedMemoryRegion>) {
fn parent_side(
slots: i32,
slot_bytes: i64,
input: bool,
) -> (Value, SharedMemoryRegion, Option<SharedMemoryRegion>) {
let out_key = test_key("out");
let out_bytes = FrameSlotPool::bytes_needed(slots as u32, slot_bytes as usize);
let mut out_region = SharedMemoryRegion::new();
@@ -220,7 +224,8 @@ mod tests {
out_region.error()
);
// SAFETY: live mapping sized by bytes_needed.
let _ = unsafe { FrameSlotPool::create(out_region.data(), slots as u32, slot_bytes as usize) };
let _ =
unsafe { FrameSlotPool::create(out_region.data(), slots as u32, slot_bytes as usize) };
let (in_key, in_bytes, in_region) = if input {
let in_key = test_key("in");
@@ -228,7 +233,9 @@ mod tests {
let mut in_region = SharedMemoryRegion::new();
assert!(in_region.open(&in_key, in_bytes, ShmMode::Create));
// SAFETY: live mapping.
let _ = unsafe { FrameSlotPool::create(in_region.data(), slots as u32, slot_bytes as usize) };
let _ = unsafe {
FrameSlotPool::create(in_region.data(), slots as u32, slot_bytes as usize)
};
(Some(in_key), Some(in_bytes), Some(in_region))
} else {
(None, None, None)
@@ -332,7 +339,10 @@ mod tests {
let resp = s
.handle_line(r#"{"type":"handshake","protocol_version":1}"#)
.unwrap();
assert_eq!(resp["message"], "handshake missing output shared-memory geometry");
assert_eq!(
resp["message"],
"handshake missing output shared-memory geometry"
);
}
#[test]
@@ -342,7 +352,10 @@ mod tests {
// Ask for input slots without announcing their geometry.
hs["input_slots"] = json!(2);
let resp = s.handle_line(&hs.to_string()).unwrap();
assert_eq!(resp["message"], "handshake missing input shared-memory geometry");
assert_eq!(
resp["message"],
"handshake missing input shared-memory geometry"
);
}
#[test]
@@ -394,14 +407,16 @@ mod tests {
let mut s = WorkerSession::new();
// A key that was never created.
let resp = s
.handle_line(&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": format!("olive-rw-{}-missing", std::process::id()),
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string())
.handle_line(
&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": format!("olive-rw-{}-missing", std::process::id()),
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string(),
)
.unwrap();
assert_eq!(resp["type"], "error");
assert!(resp["message"]
@@ -421,23 +436,30 @@ mod tests {
let mut region = SharedMemoryRegion::new();
assert!(region.open(&key, bytes, ShmMode::Create));
let resp = s
.handle_line(&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": key,
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string())
.handle_line(
&json!({
"type": "handshake",
"protocol_version": 1,
"shm_key": key,
"output_slots": 4,
"slot_data_bytes": 4096,
})
.to_string(),
)
.unwrap();
assert_eq!(resp["message"], "shared memory does not contain a frame slot pool");
assert_eq!(
resp["message"],
"shared memory does not contain a frame slot pool"
);
assert!(!s.has_pools());
}
#[test]
fn handshake_bad_json_shape_is_invalid_handshake() {
let mut s = WorkerSession::new();
let resp = s.handle_line(r#"{"type":"handshake","protocol_version":"x"}"#).unwrap();
let resp = s
.handle_line(r#"{"type":"handshake","protocol_version":"x"}"#)
.unwrap();
assert_eq!(resp["message"], "invalid handshake message");
}
@@ -449,20 +471,30 @@ mod tests {
let resp = s
.handle_line(&json!({ "type": "load_graph", "path": missing }).to_string())
.unwrap();
assert_eq!(resp["message"], format!("graph file does not exist: {missing}"));
assert_eq!(
resp["message"],
format!("graph file does not exist: {missing}")
);
let empty = std::env::temp_dir().join("oak_worker_test_empty_graph.ove");
std::fs::write(&empty, b"").unwrap();
let resp = s
.handle_line(&json!({ "type": "load_graph", "path": empty.display().to_string() }).to_string())
.handle_line(
&json!({ "type": "load_graph", "path": empty.display().to_string() }).to_string(),
)
.unwrap();
assert_eq!(resp["message"], format!("graph file is empty: {}", empty.display()));
assert_eq!(
resp["message"],
format!("graph file is empty: {}", empty.display())
);
let _ = std::fs::remove_file(&empty);
let real = std::env::temp_dir().join("oak_worker_test_graph.ove");
std::fs::write(&real, b"<root/>").unwrap();
let resp = s
.handle_line(&json!({ "type": "load_graph", "path": real.display().to_string() }).to_string())
.handle_line(
&json!({ "type": "load_graph", "path": real.display().to_string() }).to_string(),
)
.unwrap();
assert!(resp["message"]
.as_str()
+2 -4
View File
@@ -90,10 +90,8 @@ pub fn attach_pools(hs: &HandshakeMsg) -> Result<AttachedPools, String> {
let mut input_region = None;
let mut input_pool = None;
if hs.input_slots > 0 {
let input_bytes = FrameSlotPool::bytes_needed(
hs.input_slots as u32,
hs.input_slot_data_bytes as usize,
);
let input_bytes =
FrameSlotPool::bytes_needed(hs.input_slots as u32, hs.input_slot_data_bytes as usize);
let mut region = SharedMemoryRegion::new();
if !region.open(&hs.input_shm_key, input_bytes, ShmMode::Attach) {
return Err(format!(
+27 -18
View File
@@ -22,34 +22,43 @@
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_oak-worker")
env!("CARGO_BIN_EXE_oak-worker")
}
#[test]
fn help_exits_zero() {
let out = Command::new(bin()).arg("--help").output().expect("spawn oak-worker");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("oak-worker"));
assert!(stdout.contains("--backend"));
let out = Command::new(bin())
.arg("--help")
.output()
.expect("spawn oak-worker");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("oak-worker"));
assert!(stdout.contains("--backend"));
}
#[test]
fn unknown_flag_is_a_clap_usage_error() {
let out = Command::new(bin()).arg("--frobnicate").output().expect("spawn oak-worker");
// clap's usage-error exit code.
assert_eq!(out.status.code(), Some(2));
let out = Command::new(bin())
.arg("--frobnicate")
.output()
.expect("spawn oak-worker");
// clap's usage-error exit code.
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn backend_none_exits_one_like_the_cpp_main() {
// Mirrors oakengine_worker_main(): without a renderer the worker cannot
// do anything and exits 1.
let out = Command::new(bin())
.args(["--backend", "none"])
.output()
.expect("spawn oak-worker");
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("no renderer initialized"), "stderr: {stderr}");
// Mirrors oakengine_worker_main(): without a renderer the worker cannot
// do anything and exits 1.
let out = Command::new(bin())
.args(["--backend", "none"])
.output()
.expect("spawn oak-worker");
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no renderer initialized"),
"stderr: {stderr}"
);
}
+9 -1
View File
@@ -121,7 +121,15 @@ pub unsafe fn oakcodec_decoder_decode_audio(
) -> 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,
decoder,
in_num,
in_den,
out_num,
out_den,
sample_rate,
channel_layout,
buf,
buf_frames,
)
}
}
+3 -1
View File
@@ -47,5 +47,7 @@ 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) }
unsafe {
oakcommon::ffi::ffmpegutils::oakcommon_ffmpegutils_get_ffmpeg_sample_format(smp_fmt, out)
}
}
+18 -57
View File
@@ -28,9 +28,7 @@ use oakcore_rs::Rational;
use crate::bridge::codec::EncodingParams;
use crate::error::{Error, OAKAUDIO_E_INVALID};
use crate::handle::{
guard, guard_handle, guard_int, guard_void, invalid_if, write_error, CHandle,
};
use crate::handle::{guard, guard_handle, guard_int, guard_void, invalid_if, write_error, CHandle};
use crate::params::{AudioParams, SampleFormat};
use crate::waveform::{AudioVisualWaveform, SamplePerChannel};
@@ -92,9 +90,7 @@ unsafe fn planar_views<'a>(
if p.is_null() {
views.push(&[]);
} else {
views.push(unsafe {
std::slice::from_raw_parts(p, frame_count as usize)
});
views.push(unsafe { std::slice::from_raw_parts(p, frame_count as usize) });
}
}
views
@@ -211,9 +207,7 @@ pub mod manager {
/// `oakaudio_manager_clear_buffered_output`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_clear_buffered_output(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
guard(|| crate::manager::clear_buffered_output(&_self))
}
@@ -225,10 +219,7 @@ pub mod manager {
/// `oakaudio_manager_seconds`: write elapsed playback seconds into `out`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_seconds(
_self: CHandle,
out: *mut c_double,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
guard(|| {
invalid_if(out.is_null())?;
let mut seconds = 0.0f64;
@@ -242,17 +233,13 @@ pub mod manager {
/// `oakaudio_manager_reset_output_clock`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_reset_output_clock(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int {
guard(|| crate::manager::reset_output_clock(&_self))
}
/// `oakaudio_manager_get_output_device`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_get_output_device(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int {
guard_int(|| crate::manager::get_output_device(&_self))
}
@@ -267,9 +254,7 @@ pub mod manager {
/// `oakaudio_manager_get_input_device`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_get_input_device(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
guard_int(|| crate::manager::get_input_device(&_self))
}
@@ -321,9 +306,7 @@ pub mod manager {
/// `oakaudio_manager_stop_recording`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_manager_stop_recording(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
guard(|| crate::manager::stop_recording(&_self))
}
@@ -782,9 +765,7 @@ pub mod waveform {
/// `oakaudio_waveform_get_channel_count`.
#[no_mangle]
pub unsafe extern "C" fn oakaudio_waveform_get_channel_count(
_self: CHandle,
) -> c_int {
pub unsafe extern "C" fn oakaudio_waveform_get_channel_count(_self: CHandle) -> c_int {
guard_int(|| Ok(crate::waveform::get(&_self)?.channel_count()))
}
@@ -835,9 +816,7 @@ pub mod waveform {
guard(|| {
// CPP-PARITY: waveform.cpp:208.
let start = rational_from_parts(start_num, start_den)?;
invalid_if(
planar.is_null() || frame_count <= 0 || sample_rate <= 0,
)?;
invalid_if(planar.is_null() || frame_count <= 0 || sample_rate <= 0)?;
let channels = crate::waveform::get(&_self)?.channel_count();
if channels <= 0 {
return Err(Error::State);
@@ -849,11 +828,7 @@ pub mod waveform {
}
}
let views = unsafe { planar_views(planar, channels, frame_count) };
crate::waveform::get_mut(&_self)?.overwrite_samples(
&views,
sample_rate,
start,
);
crate::waveform::get_mut(&_self)?.overwrite_samples(&views, sample_rate, start);
Ok(())
})
}
@@ -877,8 +852,7 @@ pub mod waveform {
let src_waveform = crate::waveform::get(&src)?;
// SAFETY: `self` and `src` are distinct handles (the FFI
// contract forbids aliasing them).
crate::waveform::get_mut(&_self)?
.overwrite_sums(src_waveform, dest, offset, length);
crate::waveform::get_mut(&_self)?.overwrite_sums(src_waveform, dest, offset, length);
Ok(())
})
}
@@ -1021,11 +995,8 @@ pub mod waveform {
std::slice::from_raw_parts(p, (start_index + length) as usize)
});
}
let sample = AudioVisualWaveform::sum_samples(
&views,
start_index as usize,
length as usize,
);
let sample =
AudioVisualWaveform::sum_samples(&views, start_index as usize, length as usize);
// CPP-PARITY: a short summary is an internal failure.
if sample.len() < channel_count as usize {
return Err(Error::Failed("sum_samples underflow".to_string()));
@@ -1053,12 +1024,7 @@ pub mod waveform {
) -> c_int {
guard_int(|| {
// CPP-PARITY: waveform.cpp:393 — both buffers are required.
invalid_if(
r#in.is_null()
|| out.is_null()
|| nb_entries <= 0
|| nb_channels <= 0,
)?;
invalid_if(r#in.is_null() || out.is_null() || nb_entries <= 0 || nb_channels <= 0)?;
// SAFETY: `in` holds `nb_entries` entries.
let entries = unsafe { std::slice::from_raw_parts(r#in, nb_entries as usize) };
let samples: Vec<SamplePerChannel> = entries
@@ -1068,11 +1034,8 @@ pub mod waveform {
max: m.max,
})
.collect();
let sample = AudioVisualWaveform::re_sum_samples(
&samples,
nb_entries as usize,
nb_channels,
);
let sample =
AudioVisualWaveform::re_sum_samples(&samples, nb_entries as usize, nb_channels);
for (i, spc) in sample.iter().enumerate() {
// SAFETY: `out` holds at least `nb_channels` entries.
unsafe {
@@ -1111,8 +1074,7 @@ pub mod waveform {
)?;
// SAFETY: the caller guarantees a NUL-terminated string.
let filename = unsafe { CStr::from_ptr(filename) };
let outcome =
crate::waveform::extract(filename, stream_index, samples_per_point)?;
let outcome = crate::waveform::extract(filename, stream_index, samples_per_point)?;
let channels = outcome.channels.max(1);
let point_count = outcome.points.len() / channels as usize;
// CPP-PARITY: the channel count is reported even for a size-only
@@ -1138,7 +1100,6 @@ pub mod waveform {
Ok(point_count as i32)
})
}
}
/// `include/audio/levelmeter.h` exports (complete inventory): stateless
+17 -7
View File
@@ -91,8 +91,7 @@ fn with_instance(h: &CHandle) -> Result<MutexGuard<'static, ManagerInner>> {
}
// 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>) };
let m: &'static Mutex<ManagerInner> = unsafe { &*(h.ctx as *const Mutex<ManagerInner>) };
Ok(m.lock().unwrap_or_else(|p| p.into_inner()))
}
@@ -290,12 +289,24 @@ pub fn start_recording(
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);
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 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 !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(),
@@ -365,4 +376,3 @@ pub fn find_device_by_name_s(name: &std::ffi::CStr, _is_output_device: bool) ->
pub fn debug_alive_count() -> i32 {
crate::handle::alive_count()
}
+1 -2
View File
@@ -92,8 +92,7 @@ impl PreviewAudioDevice {
inner.bytes_read = new_bytes_read;
data[..copy_length as usize]
.copy_from_slice(&inner.buffer[..copy_length as usize]);
data[..copy_length as usize].copy_from_slice(&inner.buffer[..copy_length as usize]);
inner.buffer.drain(..copy_length as usize);
}
}
+7 -25
View File
@@ -26,10 +26,10 @@
use std::ptr;
use std::sync::Mutex;
use ffmpeg_next as ffmpeg;
use ffmpeg::format::sample::Type as SampleType;
use ffmpeg::format::Sample;
use ffmpeg::{ChannelLayout, Error as FfmpegError};
use ffmpeg_next as ffmpeg;
use crate::error::{Error, Result};
use crate::handle::{free_handle, make_owned, CHandle};
@@ -220,12 +220,7 @@ pub fn free(self_: *mut CHandle) {
/// `// 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<()> {
pub fn open(self_: &CHandle, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> {
let p = get_processor(self_)?;
let mut inner = p.inner.lock().unwrap();
@@ -292,9 +287,7 @@ pub fn convert(
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())
if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
{
return Err(Error::Invalid);
}
@@ -315,11 +308,7 @@ pub fn convert(
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,
);
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
@@ -342,9 +331,7 @@ pub fn convert(
// 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)
};
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
}
}};
}
@@ -403,8 +390,7 @@ pub fn convert(
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;
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).
@@ -416,11 +402,7 @@ pub fn convert(
// `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,
);
ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize);
}
}
}
+1 -2
View File
@@ -67,8 +67,7 @@ pub fn place_by_source_time(
let reference_head_source = reference.source_start_time + reference.media_in;
let candidate_head_source = candidate.source_start_time + candidate.media_in;
placement.timeline_in =
reference_timeline_in + candidate_head_source - reference_head_source;
placement.timeline_in = reference_timeline_in + candidate_head_source - reference_head_source;
placement.valid = !placement.timeline_in.is_nan();
placement
}
+55 -27
View File
@@ -176,16 +176,14 @@ impl AudioVisualWaveform {
let mut i = 0usize;
while i < samples_length {
let src_start =
((i as f64 * chunk_size).round() as usize) / channels as usize;
let src_start = ((i as f64 * chunk_size).round() as usize) / channels as usize;
let src_end = (((i + channels as usize) as f64 * chunk_size).round() as usize
/ channels as usize)
.min(sample_count);
let summary = Self::sum_samples(planar, src_start, src_end - src_start);
data[i + start_index..i + start_index + summary.len()]
.copy_from_slice(&summary);
data[i + start_index..i + start_index + summary.len()].copy_from_slice(&summary);
i += channels as usize;
}
@@ -229,8 +227,7 @@ impl AudioVisualWaveform {
channels,
);
output_data[i + start_index..i + start_index + summary.len()]
.copy_from_slice(&summary);
output_data[i + start_index..i + start_index + summary.len()].copy_from_slice(&summary);
i += channels as usize;
}
@@ -276,8 +273,14 @@ impl AudioVisualWaveform {
Some((input, input_rate, input_start, input_length)) => {
let data = self.mipmapped_data.get_mut(rate).unwrap();
let (s, l) = Self::overwrite_samples_from_mipmap(
&input, input_rate, rel_start, out_rate, channels, data,
input_start, input_length,
&input,
input_rate,
rel_start,
out_rate,
channels,
data,
input_start,
input_length,
);
iter_input = Some((data.clone(), out_rate, s, l));
}
@@ -314,8 +317,11 @@ impl AudioVisualWaveform {
};
// Get our destination sample
let our_start_index =
time_to_samples((dest - self.virtual_start).to_f64(), rate_dbl, self.channels);
let our_start_index = time_to_samples(
(dest - self.virtual_start).to_f64(),
rate_dbl,
self.channels,
);
// Get our source sample, indexing with the SOURCE's channel count
let their_start_index = (offset.to_f64() * rate_dbl).floor() as usize
@@ -397,7 +403,11 @@ impl AudioVisualWaveform {
self.virtual_start = self.virtual_start + length;
let negative = length < Rational::NULL || length.to_f64() < 0.0;
let abs_length = if negative { Rational::NULL - length } else { length };
let abs_length = if negative {
Rational::NULL - length
} else {
length
};
for (rate, data) in self.mipmapped_data.iter_mut() {
let rate_dbl = rate.to_f64();
@@ -487,8 +497,11 @@ impl AudioVisualWaveform {
let rate_dbl = rate.to_f64();
let start_sample =
time_to_samples((start - self.virtual_start).to_f64(), rate_dbl, self.channels);
let start_sample = time_to_samples(
(start - self.virtual_start).to_f64(),
rate_dbl,
self.channels,
);
let mut sample_length = time_to_samples(length.to_f64(), rate_dbl, self.channels);
// Determine if the array actually has this sample. Compare in signed
@@ -507,10 +520,7 @@ impl AudioVisualWaveform {
}
// Return null samples
vec![
SamplePerChannel::default();
self.channel_count().max(0) as usize
]
vec![SamplePerChannel::default(); self.channel_count().max(0) as usize]
}
/// Reduce planar samples into min/max pairs.
@@ -554,7 +564,11 @@ impl AudioVisualWaveform {
/// point rather than {0,0} — the engine version clamped all-positive
/// (resp. all-negative) ranges to zero; fixed in oakaudio (see the
/// comment in the C++ source).
pub fn re_sum_samples(samples: &[SamplePerChannel], nb_samples: usize, nb_channels: i32) -> Sample {
pub fn re_sum_samples(
samples: &[SamplePerChannel],
nb_samples: usize,
nb_channels: i32,
) -> Sample {
let channel_count = nb_channels.max(0) as usize;
let mut summed = vec![SamplePerChannel::default(); channel_count];
@@ -681,7 +695,10 @@ fn emit_points(
let n = available.min(samples_per_point as usize);
let point = points.len() / channels as usize;
points.resize(points.len() + channels as usize, SamplePerChannel::default());
points.resize(
points.len() + channels as usize,
SamplePerChannel::default(),
);
for ch in 0..channels {
let plane = &mut pending[ch as usize];
let mut mn = plane[0];
@@ -705,7 +722,11 @@ fn emit_points(
///
/// `// CPP-PARITY: src/audio/c_api/waveform.cpp:404`
/// (`oakaudio_waveform_extract`).
pub fn extract(filename: &CStr, stream_index: i32, samples_per_point: i32) -> Result<ExtractOutcome> {
pub fn extract(
filename: &CStr,
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.
@@ -741,8 +762,11 @@ pub fn extract(filename: &CStr, stream_index: i32, samples_per_point: i32) -> Re
// rate/layout; the C++ path ran the decode through an identity
// 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, None);
let stream = CodecStream::with_block(
filename.to_string_lossy().into_owned(),
info.stream_index,
None,
);
if let Err(e) = decoder.open(&stream) {
return Err(Error::Failed(format!("failed to open decoder: {e:?}")));
}
@@ -751,9 +775,8 @@ pub fn extract(filename: &CStr, stream_index: i32, samples_per_point: i32) -> Re
let _ = decoder.close();
return Err(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 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
@@ -779,7 +802,13 @@ pub fn extract(filename: &CStr, stream_index: i32, samples_per_point: i32) -> Re
match decoder.retrieve_audio(&mut buf, &range, info.sample_rate, layout_mask) {
Ok(RetrieveAudioStatus::Success) => {
append_pending(&mut pending, &buf, channels);
emit_points(&mut pending, channels, samples_per_point, &mut points, false);
emit_points(
&mut pending,
channels,
samples_per_point,
&mut points,
false,
);
}
Ok(status) => {
result = Err(Error::Failed(format!("audio retrieve failed: {status:?}")));
@@ -802,4 +831,3 @@ pub fn extract(filename: &CStr, stream_index: i32, samples_per_point: i32) -> Re
Ok(ExtractOutcome { points, channels })
}
+7 -4
View File
@@ -58,7 +58,11 @@ pub fn extract_rms_envelope(planar: &[&[f32]], window_samples: usize) -> Vec<f64
let mut envelope = Vec::new();
let channel_count = planar.len();
let sample_count = if channel_count > 0 { planar[0].len() } else { 0 };
let sample_count = if channel_count > 0 {
planar[0].len()
} else {
0
};
if channel_count == 0 || sample_count == 0 || window_samples == 0 {
return envelope;
}
@@ -169,9 +173,8 @@ pub fn estimate_envelope_offset_valid(
return result;
}
let is_valid = |mask: &[bool], size: usize, index: usize| -> bool {
mask.len() != size || mask[index]
};
let is_valid =
|mask: &[bool], size: usize, index: usize| -> bool { mask.len() != size || mask[index] };
let mut best_score = -2.0f64;
let mut best_lag = 0i64;
+8 -5
View File
@@ -25,15 +25,15 @@ use std::mem::{align_of, size_of};
use std::sync::Mutex;
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
use oakaudio::ffi::levelmeter::{ChannelStats, MeterStats};
use oakaudio::ffi::levelmeter::oakaudio_levelmeter_analyze;
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};
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.
@@ -100,7 +100,10 @@ fn manager_singleton_and_alive_count() {
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");
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);
+27 -19
View File
@@ -20,16 +20,14 @@
mod common;
use common::write_wav_header_only;
use oakcore_rs::Rational;
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::ffi::waveform::MinMax;
use oakaudio::params::{
frames_to_rational, rational_to_samples, SampleFormat,
};
use oakaudio::params::{frames_to_rational, rational_to_samples, SampleFormat};
use oakcore_rs::Rational;
/// SampleFormat planar-first ordering matches the authoritative C++ enum:
/// f32_p == 4 == OAKAUDIO_PROCESSOR_OUTPUT_FORMAT. This guards the
@@ -105,13 +103,20 @@ fn waveform_mipmap_scale_parity() {
// 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)
};
let fine_points =
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1024, fine.as_mut_ptr(), 2) };
assert_eq!(fine_points, 1);
assert_eq!(fine[0].min, 0.0);
assert!((fine[0].max - 0.046).abs() < 1e-5, "fine max = {}", fine[0].max);
assert!((fine[1].min + 0.046).abs() < 1e-5, "fine min = {}", fine[1].min);
assert!(
(fine[0].max - 0.046).abs() < 1e-5,
"fine max = {}",
fine[0].max
);
assert!(
(fine[1].min + 0.046).abs() < 1e-5,
"fine min = {}",
fine[1].min
);
assert_eq!(fine[1].max, 0.0);
let mut coarse = [MinMax { min: 0.0, max: 0.0 }; 2];
@@ -119,8 +124,16 @@ fn waveform_mipmap_scale_parity() {
unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 64, coarse.as_mut_ptr(), 2) };
assert_eq!(coarse_points, 1);
assert_eq!(coarse[0].min, 0.0);
assert!((coarse[0].max - 0.749).abs() < 1e-5, "coarse max = {}", coarse[0].max);
assert!((coarse[1].min + 0.749).abs() < 1e-5, "coarse min = {}", coarse[1].min);
assert!(
(coarse[0].max - 0.749).abs() < 1e-5,
"coarse max = {}",
coarse[0].max
);
assert!(
(coarse[1].min + 0.749).abs() < 1e-5,
"coarse min = {}",
coarse[1].min
);
assert_eq!(coarse[1].max, 0.0);
// Coarser windows necessarily cover more source samples.
@@ -154,9 +167,7 @@ fn levelmeter_db_golden() {
/// confidence through the C ABI.
#[test]
fn waveform_sync_offset_golden() {
use oakaudio::ffi::sync::{
oakaudio_sync_estimate_envelope_offset, OffsetResult,
};
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]);
@@ -189,10 +200,7 @@ fn waveform_sync_offset_golden() {
/// overflowing the internal plane array.
#[test]
fn extract_channel_cap() {
let path = std::env::temp_dir().join(format!(
"oakaudio_cap_{}.wav",
std::process::id()
));
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;
+9 -5
View File
@@ -17,9 +17,7 @@
//! Handle plumbing contract tests (handle.rs).
use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK};
use oakaudio::handle::{
alive_count, get, guard, guard_handle, make_borrowed, make_owned, CHandle,
};
use oakaudio::handle::{alive_count, get, guard, guard_handle, make_borrowed, make_owned, CHandle};
/// make_owned starts at refcount 1; get returns a typed view; dropping the
/// handle decrements to 0.
@@ -97,9 +95,15 @@ fn null_and_guard_ok() {
/// 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::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::Failed("x".to_string()))),
-60003
);
assert_eq!(guard(|| Err(oakaudio::error::Error::NotFound)), -60004);
assert_eq!(guard(|| Err(oakaudio::error::Error::NoMem)), -60005);
+17 -12
View File
@@ -19,9 +19,7 @@
mod common;
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
use oakaudio::ffi::levelmeter::{
oakaudio_levelmeter_analyze, ChannelStats, MeterStats,
};
use oakaudio::ffi::levelmeter::{oakaudio_levelmeter_analyze, ChannelStats, MeterStats};
fn analyze(planes: &[Vec<f32>]) -> (Vec<ChannelStats>, MeterStats) {
let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
@@ -89,8 +87,12 @@ fn constant_tone_stats() {
/// near 0 dB; per-channel channels array is filled for each channel.
#[test]
fn full_scale_peak() {
let ch0: Vec<f32> = (0..64).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect();
let ch1: Vec<f32> = (0..64).map(|i| if i % 2 == 0 { -1.0 } else { 1.0 }).collect();
let ch0: Vec<f32> = (0..64)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let ch1: Vec<f32> = (0..64)
.map(|i| if i % 2 == 0 { -1.0 } else { 1.0 })
.collect();
let (channels, summary) = analyze(&[ch0, ch1]);
assert_eq!(summary.max_peak_linear, 1.0);
assert_eq!(summary.silence, 0);
@@ -129,15 +131,20 @@ fn invalid_input() {
// channel_count 0.
assert_eq!(
unsafe {
oakaudio_levelmeter_analyze(&ptr, 0, 8, std::ptr::null_mut(), 0, &mut summary)
},
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_levelmeter_analyze(
std::ptr::null(),
1,
8,
std::ptr::null_mut(),
0,
&mut summary,
)
},
OAKAUDIO_E_INVALID
);
@@ -150,9 +157,7 @@ fn invalid_input() {
);
// Negative frame count.
assert_eq!(
unsafe {
oakaudio_levelmeter_analyze(&ptr, 1, -1, std::ptr::null_mut(), 0, &mut summary)
},
unsafe { oakaudio_levelmeter_analyze(&ptr, 1, -1, std::ptr::null_mut(), 0, &mut summary) },
OAKAUDIO_E_INVALID
);
}
+56 -20
View File
@@ -24,16 +24,13 @@ use std::ffi::c_char;
use common::MANAGER_LOCK;
use oakaudio::bridge::codec::EncodingParams;
use oakaudio::error::{
OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK,
};
use oakaudio::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_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_set_output_notify_interval, oakaudio_manager_start_recording,
@@ -130,13 +127,19 @@ fn push_output_advances_clock() {
// No stream yet: seconds() reports -1.
let mut secs = 0.0f64;
assert_eq!(unsafe { oakaudio_manager_seconds(m, &mut secs) }, OAKAUDIO_OK);
assert_eq!(
unsafe { oakaudio_manager_seconds(m, &mut secs) },
OAKAUDIO_OK
);
assert_eq!(secs, -1.0);
// Without a device, push fails with a human-readable error. The
// singleton state persists across tests, so pin the no-device state
// explicitly.
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, -1) }, OAKAUDIO_OK);
assert_eq!(
unsafe { oakaudio_manager_set_output_device(m, -1) },
OAKAUDIO_OK
);
let samples = vec![0u8; 480 * 2 * 4];
let mut err = [0 as c_char; 64];
let r = unsafe {
@@ -152,10 +155,16 @@ fn push_output_advances_clock() {
)
};
assert_eq!(r, OAKAUDIO_E_FAILED);
assert!(err.iter().any(|&b| b != 0), "error_buf must carry a message");
assert!(
err.iter().any(|&b| b != 0),
"error_buf must carry a message"
);
// After selecting a device the push succeeds and the clock starts at 0.
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 0) }, OAKAUDIO_OK);
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(
@@ -185,9 +194,15 @@ fn device_selection_roundtrip() {
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK);
assert_eq!(
unsafe { oakaudio_manager_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_set_input_device(m, 7) },
OAKAUDIO_OK
);
assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
assert_eq!(unsafe { oakaudio_manager_hard_reset(m) }, OAKAUDIO_OK);
@@ -219,8 +234,14 @@ fn output_control_flags() {
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);
assert_eq!(
unsafe { oakaudio_manager_clear_buffered_output(m) },
OAKAUDIO_OK
);
assert_eq!(
unsafe { oakaudio_manager_reset_output_clock(m) },
OAKAUDIO_OK
);
// Push starts the stream, then stop_output halts it (clock -> -1).
unsafe { oakaudio_manager_set_output_device(m, 0) };
@@ -228,8 +249,14 @@ fn output_control_flags() {
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,
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
std::ptr::null_mut(),
0,
)
},
OAKAUDIO_OK
@@ -261,7 +288,8 @@ fn recording_start_stop() {
let params = encoding_params();
// With a real encoder, the attempt must at least reach the encoder
// (a failure must surface a diagnostic in error_buf, not crash).
let r = unsafe { oakaudio_manager_start_recording(m, &params, err.as_mut_ptr(), err.len() as i32) };
let r =
unsafe { oakaudio_manager_start_recording(m, &params, err.as_mut_ptr(), err.len() as i32) };
if r != 0 {
assert!(
err.iter().any(|&b| b != 0),
@@ -276,7 +304,9 @@ fn recording_start_stop() {
// 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) };
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));
@@ -309,8 +339,14 @@ fn device_name_lookup() {
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);
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.
+31 -34
View File
@@ -67,15 +67,7 @@ fn identity_convert_passthrough() {
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 n = unsafe {
oakaudio_processor_convert(
h,
in_ptrs.as_ptr(),
32,
out_ptrs.as_ptr(),
32,
)
};
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32) };
assert_eq!(n, 32);
for ch in 0..2 {
for i in 0..32 {
@@ -102,9 +94,7 @@ fn convert_capacity_truncation() {
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 n = unsafe {
oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10)
};
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10) };
assert_eq!(n, 10);
for ch in 0..2 {
for i in 0..10 {
@@ -115,9 +105,7 @@ 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 n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32) };
assert_eq!(n, 0);
unsafe { oakaudio_processor_free(&mut h) };
@@ -142,9 +130,18 @@ fn open_invalid_params() {
let empty = oakaudio::handle::CHandle::null();
assert_eq!(open_identity(empty), OAKAUDIO_E_INVALID);
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);
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) },
@@ -178,19 +175,19 @@ fn resample_and_flush() {
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
let mut total = unsafe {
oakaudio_processor_convert(h, in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
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);
// 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,
)
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
};
if n == 0 {
break;
@@ -224,18 +221,18 @@ fn tempo_stretch() {
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
let mut total = unsafe {
oakaudio_processor_convert(h, in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
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);
while total < frames as i32 {
let n = unsafe {
oakaudio_processor_convert(
h,
std::ptr::null(),
0,
out_ptrs.as_ptr(),
frames as i32,
)
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
};
if n == 0 {
break;
+4 -28
View File
@@ -46,13 +46,7 @@ fn place_by_source_time_matching() {
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,
&reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
)
};
assert_eq!(r, 0);
@@ -64,13 +58,7 @@ fn place_by_source_time_matching() {
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,
&reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
)
};
assert_eq!(r, 0);
@@ -88,13 +76,7 @@ fn place_by_source_time_delta() {
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,
&reference, &candidate, 0, 1, &mut num, &mut den, &mut valid,
)
};
assert_eq!(r, 0);
@@ -106,13 +88,7 @@ fn place_by_source_time_delta() {
// 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,
&reference, &candidate, 0, 0, &mut num, &mut den, &mut valid,
)
};
assert_eq!(r, OAKAUDIO_E_INVALID);
+34 -13
View File
@@ -43,7 +43,12 @@ fn fill_ramp(w: oakaudio::handle::CHandle) {
);
}
fn summary(w: oakaudio::handle::CHandle, start: (i64, i64), length: (i64, i64), cap: i32) -> Vec<MinMax> {
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(
@@ -70,7 +75,10 @@ fn overwrite_samples_and_length() {
fill_ramp(w);
let (mut num, mut den) = (0i64, 0i64);
assert_eq!(unsafe { oakaudio_waveform_length(w, &mut num, &mut den) }, 0);
assert_eq!(
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) },
0
);
assert_eq!(num, 1);
assert_eq!(den, 1);
@@ -95,7 +103,10 @@ fn summary_two_stage_query() {
assert_eq!(n, 1);
// Too-small capacity: same count, buffer untouched.
let mut out = [MinMax { min: -1.0, max: -1.0 }; 2];
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);
@@ -164,12 +175,20 @@ fn overwrite_silence() {
assert_eq!(pair(out[1].min, out[1].max), pair(0.0, 0.0));
let out = summary(w, (1, 2), (1, 2), 1);
assert!(out[0].max > 0.5, "second half must keep ramp data, got {:?}", out[0]);
assert!(
out[0].max > 0.5,
"second half must keep ramp data, got {:?}",
out[0]
);
assert!(out[1].min < -0.5);
let (mut num, mut den) = (0i64, 0i64);
unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!((num, den), (1, 1), "overwrite_silence must not change length");
assert_eq!(
(num, den),
(1, 1),
"overwrite_silence must not change length"
);
unsafe { oakaudio_waveform_free(&mut w) };
}
@@ -217,9 +236,7 @@ fn sum_and_resum_golden() {
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())
};
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));
@@ -255,10 +272,7 @@ fn sum_and_resum_golden() {
/// points.
#[test]
fn extract_file_and_notfound() {
let path = std::env::temp_dir().join(format!(
"oakaudio_extract_{}.wav",
std::process::id()
));
let path = std::env::temp_dir().join(format!("oakaudio_extract_{}.wav", std::process::id()));
// 8 frames of stereo ramp, 48000 Hz.
let mut samples = Vec::with_capacity(16);
for i in 0..8i16 {
@@ -306,7 +320,14 @@ fn extract_file_and_notfound() {
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)
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.
+20 -31
View File
@@ -40,20 +40,16 @@ 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;
// 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.
@@ -95,23 +91,11 @@ extern "C" {
/// `oakcommon_videoparams_equals`.
pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int;
/// `oakcommon_videoparams_set_time_base`.
pub fn oakcommon_videoparams_set_time_base(
params: OakVideoParams,
num: i64,
den: i64,
);
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,
);
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,
);
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`.
@@ -129,7 +113,10 @@ extern "C" {
/// `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);
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`.
@@ -185,11 +172,7 @@ extern "C" {
/// `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,
);
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`.
@@ -226,9 +209,17 @@ extern "C" {
/// `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, default: c_int) -> c_int;
pub fn oakcommon_config_get_int(
group: *const c_char,
key: *const c_char,
default: c_int,
) -> c_int;
/// `oakcommon_config_get_bool`.
pub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -> c_int;
pub fn oakcommon_config_get_bool(
group: *const c_char,
key: *const c_char,
default: c_int,
) -> c_int;
/// `oakcommon_config_get` (two-stage string access).
pub fn oakcommon_config_get(
group: *const c_char,
@@ -246,10 +237,8 @@ extern "C" {
/// `oakcommon_filefunctions_get_unique_file_identifier`.
pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64;
/// `oakcommon_filefunctions_get_application_path` (two-stage).
pub fn oakcommon_filefunctions_get_application_path(
buf: *mut c_char,
buf_size: c_int,
) -> c_int;
pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int)
-> c_int;
/// `oakcommon_filefunctions_free` (frees an internally cached string).
pub fn oakcommon_filefunctions_free(ptr: *mut c_void);
/// `oakcommon_colortransform_init_output`.
-4
View File
@@ -28,19 +28,15 @@ 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`
+53 -45
View File
@@ -35,9 +35,7 @@ use std::ffi::{c_char, c_int, c_void, CStr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use crate::bridge::common::{
OakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams,
};
use crate::bridge::common::{OakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams};
use crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};
use crate::handle::OAKCODEC_ABI_VERSION;
@@ -96,10 +94,7 @@ fn params_ref(ctx: *mut c_void) -> Option<&'static mut MockParams> {
fn params_get(ctx: *mut c_void) -> MockParams {
let store = params_store().lock().unwrap();
store
.get(&(ctx as usize))
.cloned()
.unwrap_or_default()
store.get(&(ctx as usize)).cloned().unwrap_or_default()
}
fn params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockParams)) {
@@ -239,11 +234,11 @@ pub extern "C" fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoPar
pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
// U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4.
match format {
0 => 1, // U8
1 => 4, // U10
2 => 2, // U16
3 => 2, // F16
4 => 4, // F32
0 => 1, // U8
1 => 4, // U10
2 => 2, // U16
3 => 2, // F16
4 => 4, // F32
_ => 0,
}
}
@@ -337,11 +332,7 @@ pub extern "C" fn oakcommon_videoparams_get_interlacing(params: OakVideoParams)
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_time_base(
params: OakVideoParams,
num: i64,
den: i64,
) {
pub extern "C" fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64) {
params_set(params.ctx, |p| {
p.time_base_num = num;
p.time_base_den = den;
@@ -350,11 +341,7 @@ pub extern "C" fn oakcommon_videoparams_set_time_base(
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_frame_rate(
params: OakVideoParams,
num: i64,
den: i64,
) {
pub extern "C" fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64) {
params_set(params.ctx, |p| {
p.frame_rate_num = num as i32;
p.frame_rate_den = den as i32;
@@ -376,7 +363,10 @@ pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio(
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int) {
pub extern "C" fn oakcommon_videoparams_set_interlacing(
params: OakVideoParams,
interlacing: c_int,
) {
params_set(params.ctx, |p| p.interlacing = interlacing);
}
@@ -394,7 +384,10 @@ pub extern "C" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, s
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int) {
pub extern "C" fn oakcommon_videoparams_set_color_range(
params: OakVideoParams,
color_range: c_int,
) {
params_set(params.ctx, |p| p.color_range = color_range);
}
@@ -412,13 +405,19 @@ pub extern "C" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int) {
pub extern "C" fn oakcommon_videoparams_set_color_primaries(
params: OakVideoParams,
primaries: c_int,
) {
params_set(params.ctx, |p| p.color_primaries = primaries);
}
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int) {
pub extern "C" fn oakcommon_videoparams_set_color_transfer(
params: OakVideoParams,
transfer: c_int,
) {
params_set(params.ctx, |p| p.color_trc = transfer);
}
@@ -462,10 +461,7 @@ fn audio_params_store() -> &'static Mutex<HashMap<usize, MockAudioParams>> {
fn audio_params_get(ctx: *const c_void) -> MockAudioParams {
let store = audio_params_store().lock().unwrap();
store
.get(&(ctx as usize))
.cloned()
.unwrap_or_default()
store.get(&(ctx as usize)).cloned().unwrap_or_default()
}
/// Per-`OakRational` backing state (an owned `(num, den)` pair).
@@ -535,10 +531,7 @@ pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioPara
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_channel_layout(
params: *mut OakAudioParams,
layout: u64,
) {
pub extern "C" fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64) {
audio_params_set(params as *mut c_void, |p| p.channel_layout = layout);
}
@@ -560,10 +553,7 @@ pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, fo
}
#[no_mangle]
pub extern "C" fn oakcore_audioparams_set_stream_index(
params: *mut OakAudioParams,
index: c_int,
) {
pub extern "C" fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int) {
audio_params_set(params as *mut c_void, |p| p.stream_index = index);
}
@@ -634,7 +624,10 @@ pub extern "C" fn oakcore_rational_free(rational: *mut c_void) {
if rational.is_null() {
return;
}
rational_store().lock().unwrap().remove(&(rational as usize));
rational_store()
.lock()
.unwrap()
.remove(&(rational as usize));
// SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a
// boxed `(i32, i32)` pair; we hold the only reference after removal.
unsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };
@@ -737,8 +730,9 @@ pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier(path: *cons
unsafe { CStr::from_ptr(path) }
.to_bytes()
.iter()
.fold(14695981039346656037u64, |acc, &b| (acc ^ b as u64).wrapping_mul(1099511628211))
as i64
.fold(14695981039346656037u64, |acc, &b| {
(acc ^ b as u64).wrapping_mul(1099511628211)
}) as i64
}
#[no_mangle]
@@ -773,7 +767,10 @@ pub extern "C" fn oakcommon_colortransform_init_output(
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams) {
pub extern "C" fn oakcommon_colortransform_get_output(
params: OakVideoParams,
out: *mut OakVideoParams,
) {
if !out.is_null() {
unsafe { *out = params.clone() };
}
@@ -821,7 +818,9 @@ pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(forma
#[no_mangle]
#[cfg(test)]
pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(
format: c_int,
) -> c_int {
format
}
@@ -894,12 +893,21 @@ pub extern "C" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {
if atom.is_null() {
return;
}
unsafe { cancel_flags().lock().unwrap().remove(&((*atom).ctx as usize)) };
unsafe {
cancel_flags()
.lock()
.unwrap()
.remove(&((*atom).ctx as usize))
};
}
#[no_mangle]
pub extern "C" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int {
(*cancel_flags().lock().unwrap().get(&(atom.ctx as usize)).unwrap_or(&false)) as c_int
(*cancel_flags()
.lock()
.unwrap()
.get(&(atom.ctx as usize))
.unwrap_or(&false)) as c_int
}
#[no_mangle]
+2 -5
View File
@@ -228,11 +228,8 @@ mod tests {
use super::*;
fn temp_subdir(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"oakcodec_conform_{}_{}",
name,
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcodec_conform_{}_{}", name, std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir.to_string_lossy().into_owned()
}
+25 -21
View File
@@ -254,10 +254,7 @@ pub trait Decoder: Send + Sync {
fn stream(&self) -> CodecStream;
/// Retrieve a video frame into CPU memory.
fn retrieve_video_frame(
&self,
p: &RetrieveVideoParams,
) -> crate::error::Result<Arc<Frame>>;
fn retrieve_video_frame(&self, p: &RetrieveVideoParams) -> crate::error::Result<Arc<Frame>>;
/// Retrieve a video frame as a render texture (owned by caller).
fn retrieve_video(&self, p: &RetrieveVideoParams) -> crate::error::Result<OakRenderTexture>;
@@ -335,29 +332,31 @@ impl Decoder for UnimplementedDecoder {
}
fn open(&self, _stream: &CodecStream) -> crate::error::Result<()> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
fn close(&self) -> crate::error::Result<()> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
fn stream(&self) -> CodecStream {
CodecStream::new()
}
fn retrieve_video_frame(
&self,
_p: &RetrieveVideoParams,
) -> crate::error::Result<Arc<Frame>> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
fn retrieve_video_frame(&self, _p: &RetrieveVideoParams) -> crate::error::Result<Arc<Frame>> {
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
fn retrieve_video(
&self,
_p: &RetrieveVideoParams,
) -> crate::error::Result<OakRenderTexture> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
fn retrieve_video(&self, _p: &RetrieveVideoParams) -> crate::error::Result<OakRenderTexture> {
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
fn retrieve_audio(
@@ -367,7 +366,9 @@ impl Decoder for UnimplementedDecoder {
_sample_rate: i32,
_channel_layout: u64,
) -> crate::error::Result<RetrieveAudioStatus> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
fn conform_audio(
@@ -378,7 +379,9 @@ impl Decoder for UnimplementedDecoder {
_sample_format: i32,
_cancelled: Option<&OakCancelAtom>,
) -> crate::error::Result<()> {
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
))
}
}
@@ -480,9 +483,10 @@ pub fn transform_image_sequence_file_name(filename: &str, number: i64) -> String
}
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => {
Path::new(parent).join(&new_filename).to_string_lossy().into_owned()
}
Some(parent) if !parent.as_os_str().is_empty() => Path::new(parent)
.join(&new_filename)
.to_string_lossy()
.into_owned(),
_ => new_filename,
}
}
+19 -12
View File
@@ -139,12 +139,12 @@ pub fn create_from_params(params: &EncodingParams) -> Option<Arc<dyn Encoder>> {
}
}
match encoder_type_from_format(params.format) {
Some(EncoderType::FFmpeg) => {
Some(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(params.clone())))
}
Some(EncoderType::OIIO) => {
Some(Arc::new(crate::oiio::OIIOEncoder { params: params.clone() }))
}
Some(EncoderType::FFmpeg) => Some(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(
params.clone(),
))),
Some(EncoderType::OIIO) => Some(Arc::new(crate::oiio::OIIOEncoder {
params: params.clone(),
})),
None => None,
}
}
@@ -230,9 +230,7 @@ pub fn filename_remove_digit_placeholder(filename: &str) -> String {
while i < bytes.len() {
// A separator is consumed only when a placeholder follows it.
let ph_start = match bytes[i] {
b'-' | b'.' | b' ' | b'_' if placeholder_range(bytes, i + 1).is_some() => {
i + 1
}
b'-' | b'.' | b' ' | b'_' if placeholder_range(bytes, i + 1).is_some() => i + 1,
_ => i,
};
match placeholder_range(bytes, ph_start) {
@@ -289,16 +287,25 @@ mod tests {
assert!(!filename_contains_digit_placeholder("out[].png"));
// digit count: number of '#' in the first placeholder.
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out_[#####].png"), 5);
assert_eq!(
image_sequence_placeholder_digit_count("/tmp/out_[#####].png"),
5
);
assert_eq!(image_sequence_placeholder_digit_count("out[#].png"), 1);
assert_eq!(image_sequence_placeholder_digit_count("a[##]b[####]c"), 2);
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out.png"), 0);
// remove: separator char before the placeholder goes with it.
assert_eq!(filename_remove_digit_placeholder("/tmp/out_[#####].png"), "/tmp/out.png");
assert_eq!(
filename_remove_digit_placeholder("/tmp/out_[#####].png"),
"/tmp/out.png"
);
assert_eq!(filename_remove_digit_placeholder("out[###].png"), "out.png");
assert_eq!(filename_remove_digit_placeholder("a_[#]b_[###]c"), "abc");
assert_eq!(filename_remove_digit_placeholder("/tmp/out.png"), "/tmp/out.png");
assert_eq!(
filename_remove_digit_placeholder("/tmp/out.png"),
"/tmp/out.png"
);
}
struct UnimplementedDummy;
+53 -91
View File
@@ -210,9 +210,7 @@ impl EncodingParams {
let source_ar = src_width as f64 / src_height as f64;
// qFuzzyCompare(export_ar, source_ar): within one part in 100000.
if (export_ar - source_ar).abs() * 100000.0
<= export_ar.abs().min(source_ar.abs())
{
if (export_ar - source_ar).abs() * 100000.0 <= export_ar.abs().min(source_ar.abs()) {
return;
}
@@ -233,16 +231,16 @@ impl EncodingParams {
/// return the empty string, matching the C++ default case.
pub fn extension(&self) -> &str {
match self.format {
0 => "mxf", // DNxHD
1 => "mkv", // Matroska
2 => "mp4", // MPEG-4 video
3 => "exr", // OpenEXR
4 => "mov", // QuickTime
5 => "png", // PNG
6 => "tiff", // TIFF
7 => "wav", // WAV
8 => "aiff", // AIFF
9 => "mp3", // MP3
0 => "mxf", // DNxHD
1 => "mkv", // Matroska
2 => "mp4", // MPEG-4 video
3 => "exr", // OpenEXR
4 => "mov", // QuickTime
5 => "png", // PNG
6 => "tiff", // TIFF
7 => "wav", // WAV
8 => "aiff", // AIFF
9 => "mp3", // MP3
10 => "flac", // FLAC
11 => "ogg", // Ogg
12 => "webm", // WebM
@@ -404,7 +402,10 @@ impl EncodingParams {
pub fn save_to_string(&self) -> String {
let mut s = String::new();
s.push_str("<export version=\"1\">");
s.push_str(&format!("<filename>{}</filename>", escape_xml(cstr(&self.filename))));
s.push_str(&format!(
"<filename>{}</filename>",
escape_xml(cstr(&self.filename))
));
s.push_str(&format!("<format>{}</format>", self.format));
s.push_str(&format!("<range>{}</range>", self.has_custom_range));
s.push_str(&format!(
@@ -416,10 +417,7 @@ impl EncodingParams {
self.custom_range_out_num, self.custom_range_out_den
));
s.push_str(&format!(
"<video enabled=\"{}\">",
self.video_enabled
));
s.push_str(&format!("<video enabled=\"{}\">", self.video_enabled));
if self.video_enabled != 0 {
s.push_str(&format!("<codec>{}</codec>", self.video_codec));
s.push_str(&format!("<width>{}</width>", self.video_width));
@@ -432,10 +430,7 @@ impl EncodingParams {
"<timebase>{}/{}</timebase>",
self.video_time_base_num, self.video_time_base_den
));
s.push_str(&format!(
"<divider>{}</divider>",
self.video_interlacing
));
s.push_str(&format!("<divider>{}</divider>", self.video_interlacing));
s.push_str(&format!(
"<pixelaspect>{}/{}</pixelaspect>",
self.video_pixel_aspect_num, self.video_pixel_aspect_den
@@ -449,12 +444,12 @@ impl EncodingParams {
"<maxbitrate>{}</maxbitrate>",
self.video_max_bit_rate
));
s.push_str(&format!(
"<bufsize>{}</bufsize>",
self.video_buffer_size
));
s.push_str(&format!("<bufsize>{}</bufsize>", self.video_buffer_size));
s.push_str(&format!("<threads>{}</threads>", self.video_threads));
s.push_str(&format!("<pixfmt>{}</pixfmt>", escape_xml(cstr(&self.video_pix_fmt))));
s.push_str(&format!(
"<pixfmt>{}</pixfmt>",
escape_xml(cstr(&self.video_pix_fmt))
));
s.push_str(&format!(
"<imgseq>{}</imgseq>",
self.video_is_image_sequence
@@ -466,10 +461,7 @@ impl EncodingParams {
}
s.push_str("</video>");
s.push_str(&format!(
"<audio enabled=\"{}\">",
self.audio_enabled
));
s.push_str(&format!("<audio enabled=\"{}\">", self.audio_enabled));
if self.audio_enabled != 0 {
s.push_str(&format!("<codec>{}</codec>", self.audio_codec));
s.push_str(&format!(
@@ -501,10 +493,7 @@ impl EncodingParams {
"<sidecarformat>{}</sidecarformat>",
self.subtitles_sidecar_format
));
s.push_str(&format!(
"<codec>{}</codec>",
self.subtitles_codec
));
s.push_str(&format!("<codec>{}</codec>", self.subtitles_codec));
}
s.push_str("</subtitles>");
@@ -623,7 +612,12 @@ impl<'a> Cursor<'a> {
self.skip_ws();
if self.starts_with("/>") {
self.i += 2;
return Ok(El { name, attrs, text: None, children: Vec::new() });
return Ok(El {
name,
attrs,
text: None,
children: Vec::new(),
});
}
if self.starts_with(">") {
self.i += 1;
@@ -656,7 +650,12 @@ impl<'a> Cursor<'a> {
}
}
self.expect_end(&name)?;
Ok(El { name, attrs, text: None, children })
Ok(El {
name,
attrs,
text: None,
children,
})
} else {
let start = self.i;
while self.i < self.s.len() && !self.starts_with("<") {
@@ -664,7 +663,12 @@ impl<'a> Cursor<'a> {
}
let text = self.s[start..self.i].to_string();
self.expect_end(&name)?;
Ok(El { name, attrs, text: Some(text), children: Vec::new() })
Ok(El {
name,
attrs,
text: Some(text),
children: Vec::new(),
})
}
}
}
@@ -684,7 +688,10 @@ fn text(el: &El) -> String {
}
fn attr<'a>(el: &'a El, name: &str) -> Option<&'a str> {
el.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
el.attrs
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
fn parse_i32(s: &str) -> i32 {
@@ -767,10 +774,7 @@ mod tests {
const EPS: f64 = 1e-9;
fn assert_close(a: f64, b: f64) {
assert!(
(a - b).abs() < EPS,
"expected {a} close to {b} (eps {EPS})"
);
assert!((a - b).abs() < EPS, "expected {a} close to {b} (eps {EPS})");
}
#[test]
@@ -865,14 +869,7 @@ mod tests {
fn generate_matrix_equal_aspect_is_identity() {
// Same 16:9 aspect: export_ar == source_ar -> fuzzy-equal -> identity.
let mut out = [0.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Fit,
1920,
1080,
1280,
720,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 1920, 1080, 1280, 720, &mut out);
assert_eq!(out, identity());
}
@@ -881,14 +878,7 @@ mod tests {
// src square (ar 1.0) -> dst 2:1 (ar 2.0). Fit: source wider/narrower
// relative to export -> the x axis is squeezed to source_ar/export_ar.
let mut out = [0.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Fit,
1000,
1000,
2000,
1000,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 1000, 1000, 2000, 1000, &mut out);
// export_ar(2.0) > source_ar(1.0); Fit => scale(source_ar/export_ar, 1).
assert_close(out[0], 0.5);
assert_close(out[5], 1.0);
@@ -899,14 +889,7 @@ mod tests {
// src square -> dst 2:1. Crop: fit inside, so we zoom the y axis by
// export_ar/source_ar and keep x unscaled.
let mut out = [0.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Crop,
1000,
1000,
2000,
1000,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Crop, 1000, 1000, 2000, 1000, &mut out);
// export_ar(2.0) > source_ar(1.0); not Fit => scale(1, export_ar/source_ar).
assert_close(out[0], 1.0);
assert_close(out[5], 2.0);
@@ -917,14 +900,7 @@ mod tests {
// src square -> dst 0.5:1 (ar 0.5). Crop: export narrower than source,
// zoom the x axis by source_ar/export_ar.
let mut out = [0.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Crop,
1000,
1000,
500,
1000,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Crop, 1000, 1000, 500, 1000, &mut out);
// export_ar(0.5) < source_ar(1.0); (ar>source) == false, (method==Fit)
// false => false == false true => scale(source_ar/export_ar, 1) = 2.0.
assert_close(out[0], 2.0);
@@ -935,25 +911,11 @@ mod tests {
fn generate_matrix_degenerate_is_identity() {
// Zero / negative source sizes must not produce inf/NaN.
let mut out = [9.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Fit,
0,
1080,
1280,
720,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Fit, 0, 1080, 1280, 720, &mut out);
assert_eq!(out, identity());
let mut out = [9.0; 16];
EncodingParams::generate_matrix(
VideoScalingMethod::Crop,
-1,
1080,
1280,
720,
&mut out,
);
EncodingParams::generate_matrix(VideoScalingMethod::Crop, -1, 1080, 1280, 720, &mut out);
assert_eq!(out, identity());
}
+19 -4
View File
@@ -310,14 +310,26 @@ mod tests {
Format::get_video_codecs(Format::MPEG4Video),
vec![Codec::H264, Codec::H264RGB, Codec::H265]
);
assert_eq!(Format::get_video_codecs(Format::OpenEXR), vec![Codec::OpenEXR]);
assert_eq!(
Format::get_video_codecs(Format::OpenEXR),
vec![Codec::OpenEXR]
);
assert_eq!(Format::get_video_codecs(Format::PNG), vec![Codec::PNG]);
assert_eq!(Format::get_video_codecs(Format::TIFF), vec![Codec::TIFF]);
assert_eq!(
Format::get_video_codecs(Format::QuickTime),
vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::ProRes, Codec::CineForm]
vec![
Codec::H264,
Codec::H264RGB,
Codec::H265,
Codec::ProRes,
Codec::CineForm
]
);
assert_eq!(
Format::get_video_codecs(Format::WebM),
vec![Codec::AV1, Codec::VP9]
);
assert_eq!(Format::get_video_codecs(Format::WebM), vec![Codec::AV1, Codec::VP9]);
// Formats without video codecs.
assert!(Format::get_video_codecs(Format::WAV).is_empty());
assert!(Format::get_video_codecs(Format::MP3).is_empty());
@@ -341,7 +353,10 @@ mod tests {
#[test]
fn subtitle_and_codec_capability_tables() {
assert_eq!(Format::get_subtitle_codecs(Format::Matroska), vec![Codec::SRT]);
assert_eq!(
Format::get_subtitle_codecs(Format::Matroska),
vec![Codec::SRT]
);
assert_eq!(Format::get_subtitle_codecs(Format::SRT), vec![Codec::SRT]);
assert!(Format::get_subtitle_codecs(Format::MPEG4Video).is_empty());
+70 -12
View File
@@ -177,7 +177,11 @@ mod tests {
}
fn temp_cache(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_conform_{}_{}", name, std::process::id()));
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()
}
@@ -185,8 +189,14 @@ mod tests {
#[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);
assert_eq!(
unsafe { oakcodec_conform_create_instance() },
crate::error::OAKCODEC_OK
);
assert_eq!(
unsafe { oakcodec_conform_destroy_instance() },
crate::error::OAKCODEC_OK
);
}
#[test]
@@ -196,14 +206,20 @@ mod tests {
// 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) };
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) };
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) };
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.
@@ -214,7 +230,9 @@ mod tests {
.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) };
let rc = unsafe {
oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
};
assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
}
@@ -225,26 +243,66 @@ mod tests {
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) };
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) };
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) };
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) };
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) };
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);
}
}
+144 -81
View File
@@ -28,9 +28,9 @@
//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports
//! read a plain [`ProbeBox`], session exports read a `Mutex<DecoderBox>`.
use std::ffi::{c_char, c_int};
#[cfg(test)]
use std::ffi::c_void;
use std::ffi::{c_char, c_int};
use std::path::Path;
use std::sync::{Arc, Mutex};
@@ -45,13 +45,13 @@ use crate::bridge::common::{
oakcommon_videoparams_get_width, oakcore_audioparams_channel_count,
oakcore_audioparams_channel_layout, oakcore_audioparams_duration,
oakcore_audioparams_sample_rate, oakcore_audioparams_stream_index,
oakcore_audioparams_time_base, oakcore_rational_denominator,
oakcore_rational_free, oakcore_rational_numerator, OakAudioParams, OakVideoParams,
oakcore_audioparams_time_base, oakcore_rational_denominator, oakcore_rational_free,
oakcore_rational_numerator, OakAudioParams, OakVideoParams,
};
use crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom};
use crate::decoder::{
CodecStream, Decoder, K_COLOR_RANGE_DEFAULT, OakCodecAudioStreamInfo,
OakCodecVideoStreamInfo, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
CodecStream, Decoder, OakCodecAudioStreamInfo, OakCodecVideoStreamInfo, RenderMode,
RetrieveAudioStatus, RetrieveVideoParams, K_COLOR_RANGE_DEFAULT,
};
use crate::footagedescription::FootageDescription;
#[cfg(test)]
@@ -230,11 +230,9 @@ pub unsafe extern "C" fn oakcodec_decoder_probe_decoder_name(
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
handle::guard_raw(|| {
match super::get_box::<ProbeBox>(&probe) {
Some(b) => super::string_out(&b.decoder_name, buf, buf_size),
None => crate::error::OAKCODEC_E_INVALID,
}
handle::guard_raw(|| match super::get_box::<ProbeBox>(&probe) {
Some(b) => super::string_out(&b.decoder_name, buf, buf_size),
None => crate::error::OAKCODEC_E_INVALID,
})
}
@@ -340,7 +338,8 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
stream_index: c_int,
) -> c_int {
handle::guard(|| {
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let b =
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let filename = match crate::ffi::c_str(filename) {
Some(f) => f,
None => return Err(crate::error::Error::Invalid),
@@ -367,7 +366,9 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
Some(x) => x,
None => {
b.last_error = format!("no decoder recognizes this file: {}", filename);
return Err(crate::error::Error::Failed("no decoder recognizes this file".to_string()));
return Err(crate::error::Error::Failed(
"no decoder recognizes this file".to_string(),
));
}
};
@@ -375,14 +376,18 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
Some(d) => d,
None => {
b.last_error = format!("failed to create decoder: {}", decoder_name);
return Err(crate::error::Error::Failed("failed to create decoder".to_string()));
return Err(crate::error::Error::Failed(
"failed to create decoder".to_string(),
));
}
};
let stream = CodecStream::with_block(filename.clone(), stream_index, None);
if decoder.open(&stream).is_err() {
b.last_error = "failed to open stream".to_string();
return Err(crate::error::Error::Failed("failed to open stream".to_string()));
return Err(crate::error::Error::Failed(
"failed to open stream".to_string(),
));
}
b.last_error.clear();
@@ -398,7 +403,8 @@ pub unsafe extern "C" fn oakcodec_decoder_open(
#[no_mangle]
pub unsafe extern "C" fn oakcodec_decoder_close(decoder: CHandle) -> c_int {
handle::guard(|| {
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let b =
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let mut b = b.lock().unwrap();
if b.open {
if let Some(d) = &b.decoder {
@@ -441,7 +447,8 @@ pub unsafe extern "C" fn oakcodec_decoder_decode_video(
denominator: c_int,
) -> CHandle {
handle::guard_handle(|| {
let b = super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let b =
super::get_box::<Mutex<DecoderBox>>(&decoder).ok_or(crate::error::Error::Invalid)?;
let d = {
let b = b.lock().unwrap();
if !b.open || b.decoder.is_none() {
@@ -498,20 +505,18 @@ pub unsafe extern "C" fn oakcodec_decoder_decode_audio(
buf: *mut f32,
buf_frames: c_int,
) -> c_int {
handle::guard_raw(|| {
unsafe {
decode_audio_inner(
decoder,
in_num,
in_den,
out_num,
out_den,
sample_rate,
channel_layout,
buf,
buf_frames,
)
}
handle::guard_raw(|| unsafe {
decode_audio_inner(
decoder,
in_num,
in_den,
out_num,
out_den,
sample_rate,
channel_layout,
buf,
buf_frames,
)
})
}
@@ -589,18 +594,16 @@ pub unsafe extern "C" fn oakcodec_decoder_conform_audio(
sample_format: c_int,
cancelled: OakCancelAtom,
) -> c_int {
handle::guard(|| {
unsafe {
conform_audio_inner(
decoder,
output_filenames,
filename_count,
sample_rate,
channel_layout,
sample_format,
cancelled,
)
}
handle::guard(|| unsafe {
conform_audio_inner(
decoder,
output_filenames,
filename_count,
sample_rate,
channel_layout,
sample_format,
cancelled,
)
})
}
@@ -678,9 +681,7 @@ pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_digit_count(
/// `oakcodec_decoder_get_image_sequence_index`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(
filename: *const c_char,
) -> i64 {
pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 {
handle::guard_i64(|| match crate::ffi::c_str(filename) {
Some(f) => crate::decoder::get_image_sequence_index(&f),
None => crate::error::OAKCODEC_E_INVALID as i64,
@@ -729,8 +730,8 @@ mod tests {
};
use crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init};
use crate::decoder::set_test_decoders;
use crate::footagedescription::StreamEntry;
use crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
use crate::footagedescription::StreamEntry;
/// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes
/// every test in this module (they share the global probe error, the
@@ -793,9 +794,8 @@ mod tests {
) -> Option<FootageDescription> {
if filename.ends_with("test_video.mp4") {
let mut desc = FootageDescription::new("fake");
let vp = unsafe {
oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000)
};
let vp =
unsafe { oakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000) };
unsafe { oakcommon_videoparams_set_stream_index(vp.clone(), 0) };
desc.push_stream(StreamEntry::Video(vp));
Some(desc)
@@ -900,11 +900,8 @@ mod tests {
}
fn media_file(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"oakcodec_ffi_dec_{}_{}",
name,
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcodec_ffi_dec_{}_{}", name, std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join(name);
let _ = std::fs::write(&path, b"media");
@@ -930,7 +927,10 @@ mod tests {
assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 1);
assert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 0);
assert_eq!(unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) }, 0);
assert_eq!(
unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) },
0
);
let mut info: OakCodecVideoStreamInfo = unsafe { std::mem::zeroed() };
let rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, &mut info) };
@@ -1001,7 +1001,10 @@ mod tests {
assert!(h.is_null());
let mut err = [0i8; 256];
unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("no filename given"));
assert_eq!(
crate::ffi::c_str(err.as_ptr()).as_deref(),
Some("no filename given")
);
// Empty filename.
let f = cstr("");
@@ -1025,12 +1028,21 @@ mod tests {
let mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };
assert!(h.is_null());
unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };
assert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains("no decoder recognizes"));
assert!(crate::ffi::c_str(err.as_ptr())
.as_deref()
.unwrap()
.contains("no decoder recognizes"));
// Empty handle on probe exports.
let empty = CHandle::null();
assert_eq!(unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) }, OAKCODEC_E_INVALID);
assert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(empty) }, 0);
assert_eq!(
unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_decoder_probe_video_stream_count(empty) },
0
);
restore();
}
@@ -1055,8 +1067,14 @@ mod tests {
let mut frame = unsafe { oakcodec_decoder_decode_video(h, 1, 30) };
assert!(!frame.is_null());
assert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_width(frame) }, 100);
assert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_height(frame) }, 50);
assert_eq!(
unsafe { crate::ffi::frame::oakcodec_frame_width(frame) },
100
);
assert_eq!(
unsafe { crate::ffi::frame::oakcodec_frame_height(frame) },
50
);
unsafe { crate::ffi::frame::oakcodec_frame_free(&mut frame) };
let rc = unsafe { oakcodec_decoder_close(h) };
@@ -1079,7 +1097,10 @@ mod tests {
assert!(frame.is_null());
let mut err = [0i8; 128];
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 128) };
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("failed to decode video frame"));
assert_eq!(
crate::ffi::c_str(err.as_ptr()).as_deref(),
Some("failed to decode video frame")
);
unsafe { oakcodec_decoder_free(&mut h) };
restore();
}
@@ -1097,9 +1118,7 @@ mod tests {
let mut buf = [0f32; 64];
let frames = unsafe {
oakcodec_decoder_decode_audio(
h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16,
)
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
};
assert_eq!(frames, 16);
// Interleaved stereo filled by the fake.
@@ -1107,20 +1126,28 @@ mod tests {
assert_eq!(buf[31], 1.0);
// Invalid args.
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16) };
let rc = unsafe {
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1) };
let rc = unsafe {
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
// Not open -> E_STATE.
let mut h2 = unsafe { oakcodec_decoder_init() };
let rc = unsafe { oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
let rc = unsafe {
oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
};
assert_eq!(rc, OAKCODEC_E_STATE);
unsafe { oakcodec_decoder_free(&mut h2) };
// Empty handle -> E_INVALID.
let empty = CHandle::null();
let rc = unsafe { oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
let rc = unsafe {
oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
};
assert_eq!(rc, OAKCODEC_E_INVALID);
unsafe { oakcodec_decoder_free(&mut h) };
@@ -1137,11 +1164,16 @@ mod tests {
let f = cstr(&v);
unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };
let mut buf = [0f32; 64];
let rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };
let rc = unsafe {
oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16)
};
assert_eq!(rc, OAKCODEC_E_STATE);
let mut err = [0i8; 256];
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };
assert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains("conform"));
assert!(crate::ffi::c_str(err.as_ptr())
.as_deref()
.unwrap()
.contains("conform"));
unsafe { oakcodec_decoder_free(&mut h) };
restore();
}
@@ -1192,9 +1224,8 @@ mod tests {
// Failure with a cancelled atom -> E_CANCELLED.
let atom = unsafe { oakrender_cancelatom_init() };
unsafe { oakrender_cancelatom_cancel(atom.clone()) };
let rc = unsafe {
oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom)
};
let rc =
unsafe { oakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom) };
assert_eq!(rc, OAKCODEC_E_CANCELLED);
unsafe { oakcodec_decoder_free(&mut h) };
@@ -1211,17 +1242,46 @@ mod tests {
unsafe { oakcodec_decoder_get_image_sequence_digit_count(f.as_ptr()) },
4
);
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) }, 1);
assert_eq!(
unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) },
1
);
let mut buf = [0i8; 128];
let rc = unsafe { oakcodec_decoder_transform_image_sequence_file_name(f.as_ptr(), 7, buf.as_mut_ptr(), 128) };
let rc = unsafe {
oakcodec_decoder_transform_image_sequence_file_name(
f.as_ptr(),
7,
buf.as_mut_ptr(),
128,
)
};
assert!(rc > 0);
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("frame_0007.png"));
assert_eq!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("frame_0007.png")
);
// NULL filename -> E_INVALID.
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) }, OAKCODEC_E_INVALID);
assert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) }, OAKCODEC_E_INVALID as i64);
assert_eq!(unsafe { oakcodec_decoder_transform_image_sequence_file_name(std::ptr::null(), 1, buf.as_mut_ptr(), 128) }, OAKCODEC_E_INVALID);
assert_eq!(
unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) },
OAKCODEC_E_INVALID
);
assert_eq!(
unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) },
OAKCODEC_E_INVALID as i64
);
assert_eq!(
unsafe {
oakcodec_decoder_transform_image_sequence_file_name(
std::ptr::null(),
1,
buf.as_mut_ptr(),
128,
)
},
OAKCODEC_E_INVALID
);
}
#[test]
@@ -1247,7 +1307,10 @@ mod tests {
assert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);
let mut err = [0i8; 256];
unsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("file not found: /missing/file.mp4"));
assert_eq!(
crate::ffi::c_str(err.as_ptr()).as_deref(),
Some("file not found: /missing/file.mp4")
);
// Empty handle -> E_INVALID and empty last_error.
let empty = CHandle::null();
+57 -20
View File
@@ -35,8 +35,8 @@ use std::sync::{Arc, Mutex};
use oakcore_rs::PixelFormat;
use crate::encodingparams::EncodingParams;
use crate::encoder::Encoder;
use crate::encodingparams::EncodingParams;
use crate::handle::{self, CHandle};
/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/
@@ -204,7 +204,8 @@ pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
value: *const c_char,
) -> c_int {
handle::guard(|| {
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
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),
@@ -233,7 +234,8 @@ pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
#[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 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);
@@ -242,16 +244,22 @@ pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
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()));
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()));
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()));
return Err(crate::error::Error::Failed(
"failed to open stream".to_string(),
));
}
b.encoder = Some(e);
b.open = true;
@@ -263,7 +271,8 @@ pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
#[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 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 = {
@@ -287,7 +296,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_audio(
frame_count: c_int,
) -> c_int {
handle::guard(|| {
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
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);
}
@@ -325,7 +335,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
out_seconds: f64,
) -> c_int {
handle::guard(|| {
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
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),
@@ -347,7 +358,8 @@ pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
#[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 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);
@@ -454,7 +466,9 @@ mod tests {
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};
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()
@@ -566,7 +580,10 @@ mod tests {
// write_video with a real frame handle.
let params = unsafe { oakcommon_videoparams_init_basic(16, 16) };
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
assert_eq!(unsafe { oakcodec_frame_allocate(fh) }, crate::error::OAKCODEC_OK);
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);
@@ -648,13 +665,24 @@ mod tests {
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"));
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);
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(""));
@@ -672,7 +700,8 @@ mod tests {
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()) };
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.
@@ -703,7 +732,10 @@ mod tests {
let p = valid_params();
let mut h = unsafe { oakcodec_encoder_init(&p) };
assert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);
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) };
@@ -732,7 +764,10 @@ mod tests {
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);
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);
@@ -760,7 +795,8 @@ mod tests {
// 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()) };
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);
@@ -769,7 +805,8 @@ mod tests {
// 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()) };
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);
+51 -25
View File
@@ -74,9 +74,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_extension(
/// `oakcodec_encoding_format_video_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(
format: c_int,
) -> c_int {
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,
@@ -104,9 +102,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
/// `oakcodec_encoding_format_audio_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(
format: c_int,
) -> c_int {
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,
@@ -134,9 +130,7 @@ pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
/// `oakcodec_encoding_format_subtitle_codec_count`.
#[no_mangle]
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(
format: c_int,
) -> c_int {
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,
@@ -201,10 +195,7 @@ pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_
/// 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 {
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),
@@ -359,7 +350,10 @@ mod tests {
// 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"));
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"));
@@ -402,7 +396,10 @@ mod tests {
);
// 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_count(13) },
1
);
assert_eq!(
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
17 // SRT
@@ -439,7 +436,10 @@ mod tests {
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);
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);
@@ -478,9 +478,18 @@ mod tests {
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);
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);
@@ -505,24 +514,38 @@ mod tests {
let mut buf = [0i8; 128];
assert_eq!(
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out_[#####].png").as_ptr()) },
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()) },
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_filename_contains_digit_placeholder(std::ptr::null()) }, 0);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr()) },
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);
assert_eq!(
unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) },
0
);
let rc = unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
@@ -532,7 +555,10 @@ mod tests {
)
};
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!(
crate::ffi::c_str(buf.as_ptr()).as_deref(),
Some("/tmp/out.png")
);
assert_eq!(
unsafe {
oakcodec_encoding_filename_remove_digit_placeholder(
+36 -12
View File
@@ -54,9 +54,7 @@ pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle {
/// (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))))
})
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::with_params(params)))))
}
/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.
@@ -144,7 +142,9 @@ pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void {
/// `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) })) {
match catch_unwind(AssertUnwindSafe(|| unsafe {
frame_const_data_inner(&frame)
})) {
Ok(p) => p,
Err(_) => std::ptr::null_mut(),
}
@@ -350,9 +350,15 @@ mod tests {
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);
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!(
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) };
@@ -365,15 +371,27 @@ mod tests {
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);
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) };
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_allocate(h) },
crate::error::OAKCODEC_E_STATE
);
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
unsafe { oakcodec_frame_free(&mut h) };
}
@@ -409,7 +427,10 @@ mod tests {
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);
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) };
@@ -423,7 +444,10 @@ mod tests {
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_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);
-1
View File
@@ -32,7 +32,6 @@
/// `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;
+60 -15
View File
@@ -292,7 +292,11 @@ mod tests {
}
fn temp_cache(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_proxy_{}_{}", name, std::process::id()));
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()
}
@@ -307,8 +311,14 @@ mod tests {
#[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);
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);
@@ -334,20 +344,38 @@ mod tests {
// 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) };
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);
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);
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];
@@ -368,7 +396,8 @@ mod tests {
// 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) };
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(),
@@ -389,29 +418,45 @@ mod tests {
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) };
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) };
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) };
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()) };
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) };
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) };
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) };
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());
+145 -87
View File
@@ -48,13 +48,13 @@ use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use ffmpeg_next as ffmpeg;
use ffmpeg::ffi as sys;
use ffmpeg::format::sample::Type as SampleType;
use ffmpeg::format::{Pixel, Sample};
use ffmpeg::media::Type as MediaType;
use ffmpeg::software::{resampling, scaling};
use ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};
use ffmpeg_next as ffmpeg;
use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};
@@ -66,8 +66,8 @@ use crate::bridge::common::{
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,
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};
@@ -94,9 +94,9 @@ const PIXEL_F32_BYTES: usize = 16;
/// Lazily initialize the FFmpeg libraries (idempotent, at most once).
fn ffmpeg_init() -> crate::error::Result<()> {
static INIT: OnceLock<Result<(), String>> = OnceLock::new();
if let Err(e) = INIT.get_or_init(|| {
ffmpeg::init().map_err(|e| format!("ffmpeg initialization failed: {e}"))
}) {
if let Err(e) = INIT
.get_or_init(|| ffmpeg::init().map_err(|e| format!("ffmpeg initialization failed: {e}")))
{
return Err(crate::error::Error::Failed(e.clone()));
}
Ok(())
@@ -258,7 +258,11 @@ impl Decoder for FFmpegDecoder {
true
}
fn probe(&self, filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<FootageDescription> {
fn probe(
&self,
filename: &str,
cancelled: Option<&OakCancelAtom>,
) -> Option<FootageDescription> {
ffmpeg_init().ok()?;
probe_file(filename, cancelled)
}
@@ -381,7 +385,13 @@ impl Decoder for FFmpegDecoder {
if !matches!(state.inner, DecoderInner::Audio(_)) {
return Err(fail("decoder is not open on an audio stream"));
}
state.conform_audio_to(output_filenames, sample_rate, channel_layout, sample_format, cancelled)
state.conform_audio_to(
output_filenames,
sample_rate,
channel_layout,
sample_format,
cancelled,
)
}
fn get_audio_start_offset(&self) -> Rational {
@@ -392,7 +402,8 @@ impl Decoder for FFmpegDecoder {
match state.as_ref() {
Some(s) if s.format_start_time != AV_NOPTS_VALUE => {
let fmt_start = Rational::new(s.format_start_time, FB_TIME_BASE);
let str_start = oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);
let str_start =
oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);
fmt_start - str_start
}
_ => Rational::new(0, 1),
@@ -490,8 +501,8 @@ impl DecoderState {
let mut dict = Dictionary::new();
dict.set("analyzeduration", "5000000");
dict.set("probesize", "20000000");
let input = ffmpeg::format::input_with_dictionary(&stream.filename(), dict)
.map_err(ffmpeg_err)?;
let input =
ffmpeg::format::input_with_dictionary(&stream.filename(), dict).map_err(ffmpeg_err)?;
let stream_index = stream.stream() as usize;
let fstream = input
@@ -512,8 +523,7 @@ impl DecoderState {
let raw = unsafe { params.as_ptr() };
input_sample_format = sample_from_raw(unsafe { (*raw).format });
input_sample_rate = unsafe { (*raw).sample_rate }.max(0) as u32;
input_channel_layout_mask =
unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
input_channel_layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();
}
let mut open_opts = Dictionary::new();
@@ -668,9 +678,8 @@ impl DecoderState {
.stream(self.stream_index)
.map(|s| s.time_base())
.unwrap_or(FfRational(1, 1));
let target = unsafe {
sys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into())
};
let target =
unsafe { sys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into()) };
let ret = unsafe {
sys::av_seek_frame(
self.input.as_mut_ptr(),
@@ -854,21 +863,31 @@ impl DecoderState {
// swscale cannot reliably output float RGBA on every build; prefer
// RGBAF32LE and fall back to RGBA64 (converted to f32 below).
let bytes = match get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h) {
Ok(ctx) => {
let mut out = ffmpeg::frame::Video::empty();
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
let stride = out.stride(0);
convert_rgba_f32_le(&out.data(0), w, h, stride)
}
Err(_) => {
let ctx = get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBA64LE, w, h)?;
let mut out = ffmpeg::frame::Video::empty();
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
let stride = out.stride(0);
convert_rgba64_to_f32(&out.data(0), w, h, stride)
}
};
let bytes =
match get_or_create_scaler(&mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h)
{
Ok(ctx) => {
let mut out = ffmpeg::frame::Video::empty();
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
let stride = out.stride(0);
convert_rgba_f32_le(&out.data(0), w, h, stride)
}
Err(_) => {
let ctx = get_or_create_scaler(
&mut video.scaler,
src_format,
w,
h,
Pixel::RGBA64LE,
w,
h,
)?;
let mut out = ffmpeg::frame::Video::empty();
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
let stride = out.stride(0);
convert_rgba64_to_f32(&out.data(0), w, h, stride)
}
};
Ok((w, h, bytes))
}
@@ -899,21 +918,17 @@ impl DecoderState {
dest.fill(0.0);
// Seek to just before the range start.
let start_ts = oak_rational(self.stream_time_base)
.time_to_timestamp(Rational::from_double(start_sec));
let start_ts =
oak_rational(self.stream_time_base).time_to_timestamp(Rational::from_double(start_sec));
self.seek(start_ts)?;
// Take the cached resampler out (or create one) so the decode loop
// below can borrow `self` freely; it is put back before returning.
let src_layout = channel_layout_from_mask(self.input_channel_layout_mask);
let mut resampler = match self
.audio
.as_mut()
.expect("audio session")
.resampler
.take()
{
Some((rate, layout, rs)) if rate == sample_rate as u32 && layout == channel_layout => rs,
let mut resampler = match self.audio.as_mut().expect("audio session").resampler.take() {
Some((rate, layout, rs)) if rate == sample_rate as u32 && layout == channel_layout => {
rs
}
_ => AudioResampler::get(
self.input_sample_format,
src_layout,
@@ -939,7 +954,9 @@ impl DecoderState {
let chunk_samples = (converted.len() / dst_channels) as i64;
let frame_start = match audio.pts() {
Some(pts) => {
let secs = oak_rational(stream_time_base).timestamp_to_time(pts).to_f64();
let secs = oak_rational(stream_time_base)
.timestamp_to_time(pts)
.to_f64();
(secs * sample_rate as f64).round() as i64
}
None => next_sample.unwrap_or(start_sample),
@@ -965,10 +982,8 @@ impl DecoderState {
}
// Put the resampler back into the cache for the next call.
self.audio
.as_mut()
.expect("audio session")
.resampler = Some((sample_rate as u32, channel_layout, resampler));
self.audio.as_mut().expect("audio session").resampler =
Some((sample_rate as u32, channel_layout, resampler));
// Flush any samples still buffered in the resampler (rate conversion
// tail), appending after the last decoded sample.
@@ -1015,7 +1030,9 @@ impl DecoderState {
cancelled: Option<&OakCancelAtom>,
) -> crate::error::Result<()> {
if self.input_channel_layout_mask == 0 {
return Err(fail("could not determine the channel layout of the audio file"));
return Err(fail(
"could not determine the channel layout of the audio file",
));
}
let target_fmt = crate::encodingparams::sample_format_from_i32(sample_format);
@@ -1080,7 +1097,6 @@ impl DecoderState {
}
Ok(())
}
}
impl AudioResampler {
@@ -1109,7 +1125,10 @@ impl AudioResampler {
/// Convert one input frame, returning one byte buffer per output plane
/// (plane 0 for packed destinations).
fn convert_to_planes(&mut self, input: &ffmpeg::frame::Audio) -> crate::error::Result<Vec<Vec<u8>>> {
fn convert_to_planes(
&mut self,
input: &ffmpeg::frame::Audio,
) -> crate::error::Result<Vec<Vec<u8>>> {
let (written, bufs) = self.swr_convert_buffers(input)?;
let _ = written;
Ok(bufs)
@@ -1117,7 +1136,10 @@ impl AudioResampler {
/// Convert one input frame into a freshly allocated output frame in the
/// destination format, layout and rate.
fn convert_to_frame(&mut self, input: &ffmpeg::frame::Audio) -> crate::error::Result<ffmpeg::frame::Audio> {
fn convert_to_frame(
&mut self,
input: &ffmpeg::frame::Audio,
) -> crate::error::Result<ffmpeg::frame::Audio> {
let (written, bufs) = self.swr_convert_buffers(input)?;
let mut out = ffmpeg::frame::Audio::new(self.dst_format, written, self.dst_layout);
out.set_rate(self.dst_rate);
@@ -1159,7 +1181,9 @@ impl AudioResampler {
if out_samples == 0 {
return Ok((0, Vec::new()));
}
let in_ptrs: Vec<*const u8> = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();
let in_ptrs: Vec<*const u8> = (0..input.planes())
.map(|i| input.data(i).as_ptr())
.collect();
let (bufs, written) = self.swr_convert(out_samples as usize, Some(&in_ptrs), in_samples)?;
Ok((written, bufs))
}
@@ -1191,7 +1215,8 @@ impl AudioResampler {
return Err(ffmpeg_err(FfmpegError::from(written)));
}
for buf in bufs.iter_mut() {
let keep = written as usize * self.dst_format.bytes()
let keep = written as usize
* self.dst_format.bytes()
* if planar { 1 } else { self.dst_channels };
buf.truncate(keep);
}
@@ -1217,7 +1242,9 @@ fn resample_to_interleaved_f32(
let channels = resampler.dst_channels;
let mut buf = vec![0f32; out_samples * channels];
let out_ptrs = [buf.as_mut_ptr() as *mut u8];
let in_ptrs: Vec<*const u8> = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();
let in_ptrs: Vec<*const u8> = (0..input.planes())
.map(|i| input.data(i).as_ptr())
.collect();
let written = unsafe {
sys::swr_convert(
resampler.ctx.as_mut_ptr(),
@@ -1369,7 +1396,8 @@ fn convert_rgba_f32_le(data: &[u8], w: u32, h: u32, stride: usize) -> Vec<u8> {
let mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];
for y in 0..h as usize {
let row = &data[y * stride..y * stride + (w as usize) * PIXEL_F32_BYTES];
let dst = &mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
let dst =
&mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
dst.copy_from_slice(row);
for px in dst.chunks_exact_mut(PIXEL_F32_BYTES) {
px[12..16].copy_from_slice(&1.0f32.to_le_bytes());
@@ -1383,8 +1411,12 @@ fn convert_rgba64_to_f32(data: &[u8], w: u32, h: u32, stride: usize) -> Vec<u8>
let mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];
for y in 0..h as usize {
let row = &data[y * stride..y * stride + (w as usize) * 8];
let dst = &mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
for (px, src_px) in dst.chunks_exact_mut(PIXEL_F32_BYTES).zip(row.chunks_exact(8)) {
let dst =
&mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];
for (px, src_px) in dst
.chunks_exact_mut(PIXEL_F32_BYTES)
.zip(row.chunks_exact(8))
{
for c in 0..3 {
let v = u16::from_le_bytes([src_px[c * 2], src_px[c * 2 + 1]]);
px[c * 4..c * 4 + 4].copy_from_slice(&(v as f32 / 65535.0).to_le_bytes());
@@ -1463,7 +1495,9 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
if cancel_atom_is_cancelled(cancelled) {
return None;
}
let Some(stream) = input.stream(i as usize) else { continue };
let Some(stream) = input.stream(i as usize) else {
continue;
};
let params = stream.parameters();
let medium = params.medium();
@@ -1475,7 +1509,9 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
.collect();
let raw = unsafe { params.as_ptr() };
source_start_time =
extract_source_start_time(&stream_meta, stream.time_base(), unsafe { (*raw).sample_rate });
extract_source_start_time(&stream_meta, stream.time_base(), unsafe {
(*raw).sample_rate
});
}
// Only proceed if a decoder exists for this stream
@@ -1500,9 +1536,16 @@ fn probe_file(filename: &str, cancelled: Option<&OakCancelAtom>) -> Option<Foota
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_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_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());
@@ -1565,7 +1608,10 @@ fn extract_source_start_time(
timebase: FfRational,
sample_rate: i32,
) -> SourceTime {
let mut out = SourceTime { valid: false, time: Rational::new(0, 1) };
let mut out = SourceTime {
valid: false,
time: Rational::new(0, 1),
};
for (key, value) in metadata {
if key == "timecode" {
let parsed = crate::timecodemetadata::SourceTime::from_timecode_string(
@@ -1578,10 +1624,8 @@ fn extract_source_start_time(
return out;
}
} else if key == "time_reference" {
let parsed = crate::timecodemetadata::SourceTime::from_bwf_time_reference(
value,
sample_rate,
);
let parsed =
crate::timecodemetadata::SourceTime::from_bwf_time_reference(value, sample_rate);
if parsed.valid {
out.valid = true;
out.time = parsed.time;
@@ -1664,7 +1708,10 @@ impl FFmpegEncoder {
/// The effective encoding parameters (configured overrides construction).
fn effective_params(&self) -> EncodingParams {
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
state.configured.clone().unwrap_or_else(|| self.params.clone())
state
.configured
.clone()
.unwrap_or_else(|| self.params.clone())
}
}
@@ -1739,7 +1786,9 @@ impl Encoder for FFmpegEncoder {
// Subtitle muxing is not exposed by the crate's encoder trait flow
// (the C++ writes through the bridge's SRT encoder); report the same
// unsupported state the stub did.
Err(fail("subtitle encoding is not supported by the ffmpeg encoder"))
Err(fail(
"subtitle encoding is not supported by the ffmpeg encoder",
))
}
fn flush(&self) -> crate::error::Result<()> {
@@ -1798,7 +1847,10 @@ impl EncoderState {
let width = params.video_width.max(1) as u32;
let height = params.video_height.max(1) as u32;
let time_base = FfRational(params.video_time_base_num, params.video_time_base_den);
let frame_rate = FfRational(params.video_time_base_den, params.video_time_base_num.max(1));
let frame_rate = FfRational(
params.video_time_base_den,
params.video_time_base_num.max(1),
);
let mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;
let stream_index = stream.index();
@@ -1944,7 +1996,10 @@ impl EncoderState {
for y in 0..(h as usize) {
let row = &data[y * linesize..y * linesize + (w as usize) * PIXEL_F32_BYTES];
let dst = &mut rgba[y * (w as usize) * 4..(y + 1) * (w as usize) * 4];
for (out_px, in_px) in dst.chunks_exact_mut(4).zip(row.chunks_exact(PIXEL_F32_BYTES)) {
for (out_px, in_px) in dst
.chunks_exact_mut(4)
.zip(row.chunks_exact(PIXEL_F32_BYTES))
{
for c in 0..4 {
let v = f32::from_le_bytes([
in_px[c * 4],
@@ -1967,7 +2022,10 @@ impl EncoderState {
// `FFmpegEncoder::write_frame` passes the frame time in seconds; the
// Rust `Frame` carries the timestamp as a rational.
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 = FfRational(
params.video_time_base_num.max(1),
params.video_time_base_den.max(1),
);
let pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;
scaled.set_pts(Some(pts));
@@ -2004,10 +2062,10 @@ impl EncoderState {
let pts = output.audio_pts;
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)
};
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);
let mut converted = audio.resampler.convert_to_frame(&input)?;
@@ -2122,20 +2180,20 @@ fn drain_audio_packets(
/// documented `ExportCodec::Codec` discriminants (see `exportcodec.rs`).
fn export_codec_to_id(codec: i32) -> Option<ffmpeg::codec::Id> {
match codec {
0 => Some(ffmpeg::codec::Id::DNXHD), // DNxHD
1 | 2 => Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB
3 => Some(ffmpeg::codec::Id::HEVC), // H265
6 => Some(ffmpeg::codec::Id::PRORES), // ProRes
7 => Some(ffmpeg::codec::Id::CFHD), // CineForm
0 => Some(ffmpeg::codec::Id::DNXHD), // DNxHD
1 | 2 => Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB
3 => Some(ffmpeg::codec::Id::HEVC), // H265
6 => Some(ffmpeg::codec::Id::PRORES), // ProRes
7 => Some(ffmpeg::codec::Id::CFHD), // CineForm
10 => Some(ffmpeg::codec::Id::MPEG2VIDEO), // MP2
11 => Some(ffmpeg::codec::Id::MP3), // MP3
12 => Some(ffmpeg::codec::Id::AAC), // AAC
13 => Some(ffmpeg::codec::Id::PCM_S16LE), // PCM
14 => Some(ffmpeg::codec::Id::OPUS), // Opus
15 => Some(ffmpeg::codec::Id::VORBIS), // Vorbis
16 => Some(ffmpeg::codec::Id::FLAC), // FLAC
17 => Some(ffmpeg::codec::Id::SUBRIP), // SRT
18 => Some(ffmpeg::codec::Id::AV1), // AV1
11 => Some(ffmpeg::codec::Id::MP3), // MP3
12 => Some(ffmpeg::codec::Id::AAC), // AAC
13 => Some(ffmpeg::codec::Id::PCM_S16LE), // PCM
14 => Some(ffmpeg::codec::Id::OPUS), // Opus
15 => Some(ffmpeg::codec::Id::VORBIS), // Vorbis
16 => Some(ffmpeg::codec::Id::FLAC), // FLAC
17 => Some(ffmpeg::codec::Id::SUBRIP), // SRT
18 => Some(ffmpeg::codec::Id::AV1), // AV1
_ => None,
}
}
+10 -10
View File
@@ -23,9 +23,9 @@
//! 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,
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 oakcore_rs::{PixelFormat, Rational};
@@ -166,9 +166,8 @@ impl Frame {
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())
});
let fmt =
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) });
Self::generate_linesize_bytes(fmt, w)
}
None => 0,
@@ -194,9 +193,7 @@ impl Frame {
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 format = pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(params) });
let linesize = Self::generate_linesize_bytes(format, width);
let size = (linesize as usize).wrapping_mul(height as usize);
@@ -399,7 +396,10 @@ mod tests {
fn linesize_is_32_byte_aligned_for_u8() {
// U8 RGBA: 4 bytes/pixel, width rounded up to a 32-byte boundary.
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 9), 4 * 32);
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 100), 4 * 128);
assert_eq!(
Frame::generate_linesize_bytes(PixelFormat::U8, 100),
4 * 128
);
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 0), 0);
}
+4 -13
View File
@@ -27,9 +27,7 @@ use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;
use crate::bridge::common::{
oakcommon_videoparams_equals, OakVideoParams,
};
use crate::bridge::common::{oakcommon_videoparams_equals, OakVideoParams};
use crate::frame::Frame;
/// `olive::FrameManager`: singleton frame pool with background GC.
@@ -76,10 +74,7 @@ impl FrameManager {
pub fn create_frame(&self, params: OakVideoParams) -> Arc<Frame> {
let frame = {
let mut pool = self.pool.lock().unwrap();
match pool
.iter()
.position(|f| frame_matches(f, &params))
{
match pool.iter().position(|f| frame_matches(f, &params)) {
Some(idx) => pool.swap_remove(idx),
None => Frame::with_params(params),
}
@@ -147,9 +142,7 @@ fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool {
let Some(frame_params) = frame.params() else {
return false;
};
let eq = unsafe {
oakcommon_videoparams_equals(frame_params.clone(), params.clone())
};
let eq = unsafe { oakcommon_videoparams_equals(frame_params.clone(), params.clone()) };
eq != 0
}
@@ -186,9 +179,7 @@ mod tests {
// 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)
});
let f2 = mgr.create_frame(unsafe { oakcommon_videoparams_init_basic(64, 64) });
assert_eq!(mgr.live_count(), 1);
assert_eq!(mgr.peak_count(), 1);
Arc::try_unwrap(f2).unwrap();
+12 -3
View File
@@ -243,7 +243,10 @@ mod tests {
#[test]
fn guard_maps_results_and_panics() {
assert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);
assert_eq!(guard(|| Err(crate::error::Error::Invalid)), OAKCODEC_E_INVALID);
assert_eq!(
guard(|| Err(crate::error::Error::Invalid)),
OAKCODEC_E_INVALID
);
assert_eq!(guard(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
let ok = guard_handle(|| Ok(make_owned(1u32)));
@@ -252,10 +255,16 @@ mod tests {
assert!(guard_handle(|| panic!("boom")).is_null());
assert_eq!(guard_raw(|| 5), 5);
assert_eq!(guard_raw(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
assert_eq!(
guard_raw(|| panic!("boom")),
crate::error::OAKCODEC_E_FAILED
);
assert_eq!(guard_i64(|| 5), 5);
assert_eq!(guard_i64(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED as i64);
assert_eq!(
guard_i64(|| panic!("boom")),
crate::error::OAKCODEC_E_FAILED as i64
);
let mut called = false;
guard_void(|| called = true);
+1 -1
View File
@@ -37,8 +37,8 @@ pub mod encodingparams;
pub mod error;
pub mod exportcodec;
pub mod exportformat;
pub mod ffmpeg;
pub mod ffi;
pub mod ffmpeg;
pub mod footagedescription;
pub mod frame;
pub mod framemanager;
+1 -2
View File
@@ -205,8 +205,7 @@ pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
if header.linesize_bytes < 0 || header.height < 0 {
return Err(crate::error::Error::Invalid);
}
let expected =
(header.linesize_bytes as u64).checked_mul(header.height as u64);
let expected = (header.linesize_bytes as u64).checked_mul(header.height as u64);
if expected != Some(header.pixel_len) {
return Err(crate::error::Error::Invalid);
}
+4 -17
View File
@@ -89,12 +89,7 @@ impl PlanarFileDevice {
/// Read `bytes_per_channel` bytes from each channel (at the current file
/// position) into `data[i][offset..]`. Returns bytes read per channel, or
/// -1 if closed or a buffer is too small.
pub fn read(
&mut self,
data: &mut [&mut [u8]],
bytes_per_channel: i64,
offset: i64,
) -> i64 {
pub fn read(&mut self, data: &mut [&mut [u8]], bytes_per_channel: i64, offset: i64) -> i64 {
if !self.is_open() {
return -1;
}
@@ -113,12 +108,7 @@ impl PlanarFileDevice {
/// Write `bytes_per_channel` bytes to each channel from `data[i][offset..]`.
/// Returns bytes written per channel, or -1.
pub fn write(
&mut self,
data: &[&[u8]],
bytes_per_channel: i64,
offset: i64,
) -> i64 {
pub fn write(&mut self, data: &[&[u8]], bytes_per_channel: i64, offset: i64) -> i64 {
if !self.is_open() {
return -1;
}
@@ -171,11 +161,8 @@ mod tests {
use super::*;
fn temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"oakcodec_planar_{}_{}",
name,
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcodec_planar_{}_{}", name, std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir
}
+36 -40
View File
@@ -256,7 +256,11 @@ impl ProxyManager {
channel_layout: 0,
sample_format: 0,
proxy_width: if params.divider <= 1 { params.width } else { 0 },
proxy_height: if params.divider <= 1 { params.height } else { 0 },
proxy_height: if params.divider <= 1 {
params.height
} else {
0
},
};
// Interim simplification: submission is synchronous.
@@ -335,11 +339,7 @@ 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,
)
crate::bridge::common::oakcommon_config_get_int(std::ptr::null(), ckey.as_ptr(), default)
}
}
@@ -348,11 +348,7 @@ 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,
)
crate::bridge::common::oakcommon_config_get_bool(std::ptr::null(), ckey.as_ptr(), default)
}
}
@@ -407,10 +403,7 @@ fn unique_file_identifier(filename: &str) -> String {
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,
)
crate::bridge::common::oakcommon_filefunctions_get_application_path(std::ptr::null_mut(), 0)
};
if size <= 1 {
return String::new();
@@ -457,11 +450,8 @@ mod tests {
use super::*;
fn temp_subdir(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"oakcodec_proxy_{}_{}",
name,
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcodec_proxy_{}_{}", name, std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir.to_string_lossy().into_owned()
}
@@ -517,10 +507,7 @@ mod tests {
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)
);
assert_eq!(f, format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id));
// Divider mode tags the divider instead of an absolute size.
let mut d = p.clone();
@@ -558,13 +545,22 @@ mod tests {
#[test]
fn proxy_state_to_string_mapping() {
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), "missing");
assert_eq!(
ProxyManager::proxy_state_to_string(ProxyState::Missing),
"missing"
);
assert_eq!(
ProxyManager::proxy_state_to_string(ProxyState::Generating),
"generating"
);
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), "ready");
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), "failed");
assert_eq!(
ProxyManager::proxy_state_to_string(ProxyState::Ready),
"ready"
);
assert_eq!(
ProxyManager::proxy_state_to_string(ProxyState::Failed),
"failed"
);
}
#[test]
@@ -581,10 +577,9 @@ mod tests {
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
let cache = temp_subdir("nostart");
let p = ProxyManager::proxy_params_default();
let (state, _f) =
ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
let (state, _f) = ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
assert_eq!(state, ProxyState::Missing);
}
@@ -595,10 +590,9 @@ mod tests {
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
std::fs::write(&f, b"x").unwrap();
let (state, filename) =
ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
let (state, filename) = ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
assert_eq!(state, ProxyState::Ready);
assert_eq!(filename, f);
}
@@ -612,10 +606,9 @@ mod tests {
);
let cache = temp_subdir("start");
let p = ProxyManager::proxy_params_default();
let (state, _f) =
ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
let (state, _f) = ProxyManager::instance()
.get_or_start(&cache, "media.mp4", 0, &p)
.unwrap();
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
assert_eq!(state, ProxyState::Generating);
}
@@ -698,6 +691,9 @@ mod tests_extra {
// Empty filename -> Missing.
assert_eq!(ProxyManager::get_proxy_state(""), ProxyState::Missing);
// A path that does not exist -> Missing.
assert_eq!(ProxyManager::get_proxy_state("/nope/nope.mp4"), ProxyState::Missing);
assert_eq!(
ProxyManager::get_proxy_state("/nope/nope.mp4"),
ProxyState::Missing
);
}
}
+19 -6
View File
@@ -32,8 +32,8 @@ use crate::bridge::common::{
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
};
use crate::decoder::{
CodecStream, Decoder, K_COLOR_RANGE_DEFAULT, RenderMode, RetrieveAudioStatus,
RetrieveVideoParams,
CodecStream, Decoder, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,
K_COLOR_RANGE_DEFAULT,
};
use crate::encoder::create_from_params;
use crate::ffmpeg::FFmpegDecoder;
@@ -118,8 +118,14 @@ 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);
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;
@@ -204,7 +210,10 @@ fn encode_h264_roundtrip_to_tmp() {
// The output exists and has a plausible size.
assert!(out.exists(), "round-trip file was not created");
assert!(out.metadata().unwrap().len() > 1000, "round-trip file is empty");
assert!(
out.metadata().unwrap().len() > 1000,
"round-trip file is empty"
);
// Probe the result: one 64x64 video stream.
let d = FFmpegDecoder::new();
@@ -246,7 +255,11 @@ fn audio_conform_writes_planar_pcm() {
let meta = std::fs::metadata(path).expect("conform output exists");
assert!(meta.len() > 0, "conform file is empty");
// 1 second at 48kHz * 4 bytes = 192 KB minimum.
assert!(meta.len() >= 192_000, "conform file too short: {}", meta.len());
assert!(
meta.len() >= 192_000,
"conform file too short: {}",
meta.len()
);
}
let _ = std::fs::remove_dir_all(&dir);
+11 -9
View File
@@ -98,10 +98,8 @@ pub struct OakCodecTaskRequest {
/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see
/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a
/// negative `OAKCODEC_E_*` code.
pub type OakCodecTaskSubmitFn = unsafe extern "C" fn(
req: *const OakCodecTaskRequest,
userdata: *mut std::ffi::c_void,
) -> i32;
pub type OakCodecTaskSubmitFn =
unsafe extern "C" fn(req: *const OakCodecTaskRequest, userdata: *mut std::ffi::c_void) -> i32;
/// One registered submit callback (extern-C from the host, or a crate
/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.
@@ -175,9 +173,7 @@ pub fn task_submit_is_registered() -> bool {
///
/// Returns `Ok(false)` when no callback is registered (nothing submitted),
/// `Ok(true)` when accepted, or `Err` when the callback rejected it.
pub fn submit_task(
req: &TaskRequest,
) -> crate::error::Result<bool> {
pub fn submit_task(req: &TaskRequest) -> crate::error::Result<bool> {
let g = TASK_SUBMIT.lock().unwrap();
match &*g {
SubmitCb::None => Ok(false),
@@ -203,7 +199,10 @@ pub fn submit_task(
if ret == OAKCODEC_OK {
Ok(true)
} else {
Err(Error::Failed(format!("task submit rejected (code {})", ret)))
Err(Error::Failed(format!(
"task submit rejected (code {})",
ret
)))
}
}
SubmitCb::Rust { cb, userdata } => {
@@ -301,7 +300,10 @@ mod tests {
#[test]
fn extern_cb_accept_returns_ok() {
let _g = REG_LOCK.lock().unwrap();
set_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());
set_task_submit_cb_extern(
Some(crate::conformmanager::test_util::accept_cb),
std::ptr::null_mut(),
);
let req = TaskRequest {
kind: TaskKind::Conform,
input_filename: "in.mp4",
+1 -2
View File
@@ -104,8 +104,7 @@ fn timecode_to_time(timecode: &str, timebase: &Rational, drop_frame: bool) -> Op
let m = real_fr_ts % frames_per10_minutes;
if m > drop_frames {
frame_count -= drop_frames
* ((m - drop_frames) / (llround(fr) * 60 - drop_frames));
frame_count -= drop_frames * ((m - drop_frames) / (llround(fr) * 60 - drop_frames));
}
frame_count -= drop_frames * 9 * d;
}
+36 -17
View File
@@ -104,12 +104,13 @@ impl CommandLineParser {
description: &str,
required: bool,
) -> Result<()> {
self.positionals.push(Box::new(CommandLinePositionalArgument {
name: name.to_string(),
description: description.to_string(),
required,
setting: None,
}));
self.positionals
.push(Box::new(CommandLinePositionalArgument {
name: name.to_string(),
description: description.to_string(),
required,
setting: None,
}));
Ok(())
}
@@ -406,11 +407,14 @@ mod tests {
let mut p = CommandLineParser::new();
p.add_option(&[cstr("h"), cstr("help")], "show help", false, "", false)
.unwrap();
p.add_option(&[cstr("o")], "output", true, "FILE", false).unwrap();
p.add_option(&[cstr("o")], "output", true, "FILE", false)
.unwrap();
p.add_option(&[cstr("secret")], "hidden opt", false, "", true)
.unwrap();
p.add_positional_argument("input", "input file", true).unwrap();
p.add_positional_argument("output", "output file", false).unwrap();
p.add_positional_argument("input", "input file", true)
.unwrap();
p.add_positional_argument("output", "output file", false)
.unwrap();
assert_eq!(p.option_count(), 3);
assert_eq!(p.positional_count(), 2);
@@ -566,12 +570,20 @@ mod tests {
fn print_help_exact() {
let mut p = CommandLineParser::new();
p.set_app_info("oak", "1.2.3");
p.add_option(&[cstr("h"), cstr("help")], "Show this help message.", false, "", false)
.unwrap();
p.add_option(
&[cstr("h"), cstr("help")],
"Show this help message.",
false,
"",
false,
)
.unwrap();
p.add_option(&[cstr("o")], "Output file.", true, "FILE", true)
.unwrap(); // hidden, omitted
p.add_option(&[cstr("t")], "Time.", true, "SEC", false).unwrap();
p.add_positional_argument("input", "Input file", true).unwrap();
p.add_option(&[cstr("t")], "Time.", true, "SEC", false)
.unwrap();
p.add_positional_argument("input", "Input file", true)
.unwrap();
let mut buf = Vec::new();
p.write_help(&mut buf, "/usr/local/bin/oak").unwrap();
@@ -789,7 +801,8 @@ Usage: oak [options] [input]
let mut p = CommandLineParser::new();
p.add_option(&[cstr("f")], "", false, "", false).unwrap();
p.add_option(&[cstr("o")], "", true, "F", false).unwrap();
p.process(&[cstr("p"), cstr("-f"), cstr("-o"), cstr("v")]).unwrap();
p.process(&[cstr("p"), cstr("-f"), cstr("-o"), cstr("v")])
.unwrap();
assert!(p.option(0).unwrap().is_set());
assert_eq!(p.option(0).unwrap().get_setting().unwrap(), "");
assert!(p.option(1).unwrap().is_set());
@@ -801,7 +814,8 @@ Usage: oak [options] [input]
#[test]
fn print_help_smoke() {
let mut p = CommandLineParser::new();
p.add_option(&[cstr("h")], "help", false, "", false).unwrap();
p.add_option(&[cstr("h")], "help", false, "", false)
.unwrap();
p.print_help("oak").unwrap();
}
@@ -834,14 +848,19 @@ Usage: oak [options] [input]
let mut buf = Vec::new();
p.write_help(&mut buf, "oak").unwrap();
let text = String::from_utf8(buf).unwrap();
assert!(text.contains("Usage: oak [options] [in] [out]\n"), "{}", text);
assert!(
text.contains("Usage: oak [options] [in] [out]\n"),
"{}",
text
);
}
/// Hidden options are omitted from help but still parse.
#[test]
fn hidden_option_parses_but_hidden_from_help() {
let mut p = CommandLineParser::new();
p.add_option(&[cstr("secret")], "shh", false, "", true).unwrap();
p.add_option(&[cstr("secret")], "shh", false, "", true)
.unwrap();
let mut buf = Vec::new();
p.write_help(&mut buf, "oak").unwrap();
let text = String::from_utf8(buf).unwrap();
+77 -32
View File
@@ -199,7 +199,10 @@ impl ConfigStore {
Some(slash) => {
let group = key[..slash].to_string();
let sub = key[slash + 1..].to_string();
sections.entry(group).or_default().insert(sub, value_to_string(value));
sections
.entry(group)
.or_default()
.insert(sub, value_to_string(value));
}
None => {
sections
@@ -234,7 +237,9 @@ impl ConfigStore {
"Failed to save application settings. The application may lack write \
permissions for this location.",
);
return Err(Error::Failed("temp config file could not be written".into()));
return Err(Error::Failed(
"temp config file could not be written".into(),
));
}
// CPP-PARITY: rename temp -> real; on POSIX this overwrites
@@ -247,7 +252,9 @@ impl ConfigStore {
"Error saving settings",
"Failed to overwrite the application settings file.",
);
return Err(Error::Failed("config.ini could not be renamed into place".into()));
return Err(Error::Failed(
"config.ini could not be renamed into place".into(),
));
}
}
@@ -444,10 +451,19 @@ impl ConfigStore {
guard.insert("DefaultSequenceHeight".into(), ConfigValue::Int(1080));
// Rational settings are stored as strings in oakcore_rational
// "num/den" form; this mirrors the old default Rational(1001, 30000).
guard.insert("DefaultSequenceFrameRate".into(), ConfigValue::String("1001/30000".into()));
guard.insert("DefaultSequencePixelAspect".into(), ConfigValue::String("1/1".into()));
guard.insert(
"DefaultSequenceFrameRate".into(),
ConfigValue::String("1001/30000".into()),
);
guard.insert(
"DefaultSequencePixelAspect".into(),
ConfigValue::String("1/1".into()),
);
guard.insert("DefaultSequenceInterlacing".into(), ConfigValue::Int(0));
guard.insert("DefaultSequenceAudioFrequency".into(), ConfigValue::Int(48000));
guard.insert(
"DefaultSequenceAudioFrequency".into(),
ConfigValue::Int(48000),
);
guard.insert("DefaultSequenceAudioLayout".into(), ConfigValue::Int(3));
guard.insert("OfflinePixelFormat".into(), ConfigValue::Int(4));
@@ -456,7 +472,10 @@ impl ConfigStore {
guard.insert("UseGLFinish".into(), ConfigValue::Bool(false));
guard.insert("ReassocLinToNonLin".into(), ConfigValue::Bool(false));
guard.insert("GraphicsBackend".into(), ConfigValue::String("opengl".into()));
guard.insert(
"GraphicsBackend".into(),
ConfigValue::String("opengl".into()),
);
guard.insert("LUTLibraryPaths".into(), ConfigValue::String(String::new()));
guard.insert("DiskCacheSaveInterval".into(), ConfigValue::Int(10000));
@@ -692,10 +711,8 @@ mod tests {
/// Point `OAK_CONFIG_DIR` at an isolated temp dir, run `f`, then clean up.
fn with_temp_config<T>(f: impl FnOnce(&Path) -> T) -> T {
let _guard = test_lock().lock().unwrap();
let dir = std::env::temp_dir().join(format!(
"oakcommon_configstore_test_{}",
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcommon_configstore_test_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::env::set_var("OAK_CONFIG_DIR", &dir);
let result = f(&dir);
@@ -763,7 +780,10 @@ mod tests {
assert_eq!(s.get_bool(None, "ProxyIncludeAudio", -1), 1);
assert_eq!(s.get(None, "GraphicsBackend").unwrap(), "opengl");
assert_eq!(s.get(None, "DefaultSequenceFrameRate").unwrap(), "1001/30000");
assert_eq!(
s.get(None, "DefaultSequenceFrameRate").unwrap(),
"1001/30000"
);
assert_eq!(s.get(None, "DefaultSequencePixelAspect").unwrap(), "1/1");
assert_eq!(s.get(None, "LUTLibraryPaths").unwrap(), "");
assert_eq!(s.get(None, "DiskCacheBehind").unwrap(), "0/1");
@@ -788,8 +808,14 @@ mod tests {
let _g = test_lock().lock().unwrap();
let s = ConfigStore::instance();
s.reset_defaults().unwrap();
assert!(matches!(s.get(None, "DefinitelyMissing"), Err(Error::NotFound)));
assert!(matches!(s.entry_type(None, "DefinitelyMissing"), Err(Error::NotFound)));
assert!(matches!(
s.get(None, "DefinitelyMissing"),
Err(Error::NotFound)
));
assert!(matches!(
s.entry_type(None, "DefinitelyMissing"),
Err(Error::NotFound)
));
assert!(matches!(s.get(None, ""), Err(Error::Invalid)));
assert!(matches!(s.entry_type(None, ""), Err(Error::Invalid)));
}
@@ -889,7 +915,10 @@ mod tests {
s.reset_defaults().unwrap();
s.set_int(Some("audio"), "sample_rate", 44100);
assert_eq!(s.get_int(Some("audio"), "sample_rate", 0), 44100);
assert_eq!(s.entry_type(Some("audio"), "sample_rate").unwrap(), EntryType::Int);
assert_eq!(
s.entry_type(Some("audio"), "sample_rate").unwrap(),
EntryType::Int
);
// Empty group and None are equivalent (flat keys).
s.set_int(Some(""), "flatkey", 7);
@@ -983,7 +1012,10 @@ UnknownTypedThing=hello
s.load().unwrap();
assert_eq!(s.get_int(None, "DefaultSequenceWidth", -1), 640);
assert_eq!(s.get_bool(None, "UseProxyMedia", -1), 0);
assert_eq!(s.get(Some("section"), "UnknownTypedThing").unwrap(), "hello");
assert_eq!(
s.get(Some("section"), "UnknownTypedThing").unwrap(),
"hello"
);
});
}
@@ -1019,8 +1051,12 @@ UnknownTypedThing=hello
_userdata: *mut c_void,
) {
unsafe {
let title = std::ffi::CStr::from_ptr(title).to_string_lossy().into_owned();
let message = std::ffi::CStr::from_ptr(message).to_string_lossy().into_owned();
let title = std::ffi::CStr::from_ptr(title)
.to_string_lossy()
.into_owned();
let message = std::ffi::CStr::from_ptr(message)
.to_string_lossy()
.into_owned();
REPORTED.lock().unwrap().push((title, message));
}
}
@@ -1042,7 +1078,9 @@ UnknownTypedThing=hello
let reported = REPORTED.lock().unwrap().clone();
assert_eq!(reported.len(), 1);
assert_eq!(reported[0].0, "Error loading settings");
assert!(reported[0].1.contains("Failed to load application settings"));
assert!(reported[0]
.1
.contains("Failed to load application settings"));
});
}
@@ -1066,12 +1104,12 @@ UnknownTypedThing=hello
#[test]
fn test_value_to_string_all_types() {
assert_eq!(
value_to_string(&ConfigValue::String("hi".into())),
"hi"
);
assert_eq!(value_to_string(&ConfigValue::String("hi".into())), "hi");
assert_eq!(value_to_string(&ConfigValue::Int(-42)), "-42");
assert_eq!(value_to_string(&ConfigValue::Int(i64::MAX)), "9223372036854775807");
assert_eq!(
value_to_string(&ConfigValue::Int(i64::MAX)),
"9223372036854775807"
);
assert_eq!(value_to_string(&ConfigValue::Double(2.5)), "2.5");
assert_eq!(value_to_string(&ConfigValue::Double(0.0)), "0");
assert_eq!(value_to_string(&ConfigValue::Bool(true)), "true");
@@ -1080,7 +1118,10 @@ UnknownTypedThing=hello
#[test]
fn test_to_entry_type() {
assert_eq!(to_entry_type(&ConfigValue::String(String::new())), EntryType::String);
assert_eq!(
to_entry_type(&ConfigValue::String(String::new())),
EntryType::String
);
assert_eq!(to_entry_type(&ConfigValue::Int(0)), EntryType::Int);
assert_eq!(to_entry_type(&ConfigValue::Double(0.0)), EntryType::Double);
assert_eq!(to_entry_type(&ConfigValue::Bool(false)), EntryType::Bool);
@@ -1352,8 +1393,11 @@ UnknownTypedThing=hello
#[test]
fn test_merge_order_defaults_file_runtime() {
with_temp_config(|dir| {
std::fs::write(dir.join("config.ini"), "DefaultSequenceWidth=800\nCustomFromFile=yes\n")
.unwrap();
std::fs::write(
dir.join("config.ini"),
"DefaultSequenceWidth=800\nCustomFromFile=yes\n",
)
.unwrap();
let s = ConfigStore::instance();
// A runtime set made BEFORE load() is wiped: load() resets to
@@ -1394,7 +1438,10 @@ FlatAfterEmptySection=ok
let s = ConfigStore::instance();
s.load().unwrap();
// Malformed lines are skipped, not errors.
assert!(matches!(s.get(None, "BareLineWithoutEquals"), Err(Error::NotFound)));
assert!(matches!(
s.get(None, "BareLineWithoutEquals"),
Err(Error::NotFound)
));
// Value keeps everything after the FIRST '='.
assert_eq!(s.get(Some("g"), "KeyWithEquals").unwrap(), "a=b");
// "[]" empties the group, so the key is flat.
@@ -1427,10 +1474,8 @@ FlatAfterEmptySection=ok
#[test]
fn test_save_failure_reports_error() {
let _g = test_lock().lock().unwrap();
let dir = std::env::temp_dir().join(format!(
"oakcommon_configstore_test_{}",
std::process::id()
));
let dir =
std::env::temp_dir().join(format!("oakcommon_configstore_test_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
// Point OAK_CONFIG_DIR at a regular FILE so writing
// "<dir>/config.ini.tmp" fails (create_dir_all on it is a silent
+14 -6
View File
@@ -262,9 +262,13 @@ mod tests {
#[test]
fn set_get_level_round_trip() {
for level in
[Level::Debug, Level::Info, Level::Warning, Level::Error, Level::Fatal]
{
for level in [
Level::Debug,
Level::Info,
Level::Warning,
Level::Error,
Level::Fatal,
] {
log_set_level(level);
assert_eq!(log_get_level(), level);
}
@@ -289,9 +293,13 @@ mod tests {
#[test]
fn filtering_drops_below_threshold() {
for threshold in
[Level::Debug, Level::Info, Level::Warning, Level::Error, Level::Fatal]
{
for threshold in [
Level::Debug,
Level::Info,
Level::Warning,
Level::Error,
Level::Fatal,
] {
log_set_level(threshold);
assert!(log(Level::Debug, "x").is_ok());
assert!(log(Level::Fatal, "y").is_ok());
+3 -1
View File
@@ -146,7 +146,9 @@ mod tests {
#[test]
fn ocio_error_converts_to_failed() {
let e = Error::from(ocio_rs::OcioError::InvalidInput("bad colorspace".to_string()));
let e = Error::from(ocio_rs::OcioError::InvalidInput(
"bad colorspace".to_string(),
));
assert!(matches!(e, Error::Failed(_)));
assert_eq!(e.code(), OAKCOMMON_E_FAILED);
assert!(format!("{e:?}").contains("bad colorspace"));
File diff suppressed because it is too large Load Diff
+102 -26
View File
@@ -157,7 +157,10 @@ fn compatible_bridge_pixel_format_list(maximum_pix_fmt: i32) -> [i32; 4] {
/// clamped to a maximum native precision (`maximum_pix_fmt == -1` for no
/// limit).
pub fn get_compatible_bridge_pixel_format(pix_fmt: i32, maximum_pix_fmt: i32) -> i32 {
find_best_pix_fmt_of_list(&compatible_bridge_pixel_format_list(maximum_pix_fmt), pix_fmt)
find_best_pix_fmt_of_list(
&compatible_bridge_pixel_format_list(maximum_pix_fmt),
pix_fmt,
)
}
/// Native pixel format usable to convert from a native frame to a bridge
@@ -327,23 +330,47 @@ mod tests {
#[test]
fn compatible_pixel_format_invalid_and_count_map_to_invalid() {
assert_eq!(get_compatible_pixel_format(PIX_FMT_INVALID), PIX_FMT_INVALID);
assert_eq!(
get_compatible_pixel_format(PIX_FMT_INVALID),
PIX_FMT_INVALID
);
assert_eq!(get_compatible_pixel_format(PIX_FMT_COUNT), PIX_FMT_INVALID);
}
#[test]
fn ffmpeg_pixel_format_rgb_channel_layout() {
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_U8, RGB_CHANNEL_COUNT), FB_PIX_FMT_RG_B24);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_U10, RGB_CHANNEL_COUNT), FB_PIX_FMT_NONE);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_U16, RGB_CHANNEL_COUNT), FB_PIX_FMT_RG_B48_LE);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_F16, RGB_CHANNEL_COUNT), FB_PIX_FMT_RGB_F16_LE);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_F32, RGB_CHANNEL_COUNT), FB_PIX_FMT_RGB_F32_LE);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U8, RGB_CHANNEL_COUNT),
FB_PIX_FMT_RG_B24
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U10, RGB_CHANNEL_COUNT),
FB_PIX_FMT_NONE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U16, RGB_CHANNEL_COUNT),
FB_PIX_FMT_RG_B48_LE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_F16, RGB_CHANNEL_COUNT),
FB_PIX_FMT_RGB_F16_LE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_F32, RGB_CHANNEL_COUNT),
FB_PIX_FMT_RGB_F32_LE
);
}
#[test]
fn ffmpeg_pixel_format_rgba_channel_layout() {
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_U8, RGBA_CHANNEL_COUNT), FB_PIX_FMT_RGBA);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_U10, RGBA_CHANNEL_COUNT), FB_PIX_FMT_NONE);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U8, RGBA_CHANNEL_COUNT),
FB_PIX_FMT_RGBA
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U10, RGBA_CHANNEL_COUNT),
FB_PIX_FMT_NONE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_U16, RGBA_CHANNEL_COUNT),
FB_PIX_FMT_RGB_A64_LE
@@ -367,9 +394,18 @@ mod tests {
#[test]
fn ffmpeg_pixel_format_invalid_and_count_return_none() {
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_INVALID, RGB_CHANNEL_COUNT), FB_PIX_FMT_NONE);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_COUNT, RGB_CHANNEL_COUNT), FB_PIX_FMT_NONE);
assert_eq!(get_ffmpeg_pixel_format(PIX_FMT_INVALID, RGBA_CHANNEL_COUNT), FB_PIX_FMT_NONE);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_INVALID, RGB_CHANNEL_COUNT),
FB_PIX_FMT_NONE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_COUNT, RGB_CHANNEL_COUNT),
FB_PIX_FMT_NONE
);
assert_eq!(
get_ffmpeg_pixel_format(PIX_FMT_INVALID, RGBA_CHANNEL_COUNT),
FB_PIX_FMT_NONE
);
}
#[test]
@@ -390,7 +426,10 @@ mod tests {
#[test]
fn native_sample_format_unknown_maps_to_invalid() {
assert_eq!(get_native_sample_format(FB_SAMPLE_FMT_NONE), SMP_FMT_INVALID);
assert_eq!(
get_native_sample_format(FB_SAMPLE_FMT_NONE),
SMP_FMT_INVALID
);
// A bogus code that does not match any bridge sample format.
assert_eq!(get_native_sample_format(12345), SMP_FMT_INVALID);
}
@@ -413,16 +452,29 @@ mod tests {
#[test]
fn ffmpeg_sample_format_invalid_and_count_return_none() {
assert_eq!(get_ffmpeg_sample_format(SMP_FMT_INVALID), FB_SAMPLE_FMT_NONE);
assert_eq!(
get_ffmpeg_sample_format(SMP_FMT_INVALID),
FB_SAMPLE_FMT_NONE
);
assert_eq!(get_ffmpeg_sample_format(SMP_FMT_COUNT), FB_SAMPLE_FMT_NONE);
}
#[test]
fn sample_format_mappings_are_inverse() {
for native in [SMP_FMT_U8, SMP_FMT_S16, SMP_FMT_S32, SMP_FMT_S64, SMP_FMT_F32, SMP_FMT_F64,
SMP_FMT_U8_P, SMP_FMT_S16_P, SMP_FMT_S32_P, SMP_FMT_S64_P, SMP_FMT_F32_P,
SMP_FMT_F64_P]
{
for native in [
SMP_FMT_U8,
SMP_FMT_S16,
SMP_FMT_S32,
SMP_FMT_S64,
SMP_FMT_F32,
SMP_FMT_F64,
SMP_FMT_U8_P,
SMP_FMT_S16_P,
SMP_FMT_S32_P,
SMP_FMT_S64_P,
SMP_FMT_F32_P,
SMP_FMT_F64_P,
] {
let bridge = get_ffmpeg_sample_format(native);
assert_eq!(get_native_sample_format(bridge), native);
}
@@ -430,17 +482,41 @@ mod tests {
#[test]
fn jpeg_space_converts_to_regular_space() {
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J420_P), FB_PIX_FMT_YU_V420_P);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J422_P), FB_PIX_FMT_YU_V422_P);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J444_P), FB_PIX_FMT_YU_V444_P);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J440_P), FB_PIX_FMT_YU_V440_P);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J411_P), FB_PIX_FMT_YU_V411_P);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J420_P),
FB_PIX_FMT_YU_V420_P
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J422_P),
FB_PIX_FMT_YU_V422_P
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J444_P),
FB_PIX_FMT_YU_V444_P
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J440_P),
FB_PIX_FMT_YU_V440_P
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YUV_J411_P),
FB_PIX_FMT_YU_V411_P
);
}
#[test]
fn jpeg_space_leaves_non_jpeg_formats_unchanged() {
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_RGBA), FB_PIX_FMT_RGBA);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_NONE), FB_PIX_FMT_NONE);
assert_eq!(convert_jpeg_space_to_regular_space(FB_PIX_FMT_YU_V420_P), FB_PIX_FMT_YU_V420_P);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_RGBA),
FB_PIX_FMT_RGBA
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_NONE),
FB_PIX_FMT_NONE
);
assert_eq!(
convert_jpeg_space_to_regular_space(FB_PIX_FMT_YU_V420_P),
FB_PIX_FMT_YU_V420_P
);
}
}
+40 -34
View File
@@ -122,9 +122,7 @@ impl FileFunctions {
// `~/.config`. The empty-root fallback is the temp directory.
#[cfg(target_os = "macos")]
let config_root = match std::env::var("HOME") {
Ok(h) if !h.is_empty() => {
PathBuf::from(h).join("Library").join("Application Support")
}
Ok(h) if !h.is_empty() => PathBuf::from(h).join("Library").join("Application Support"),
_ => PathBuf::new(),
};
#[cfg(not(target_os = "macos"))]
@@ -368,10 +366,7 @@ impl FileFunctions {
let mut temp_abs_path;
loop {
temp_abs_path = dir.join(format!(
"{}.tmp{}{}",
basename, counter, complete_suffix
));
temp_abs_path = dir.join(format!("{}.tmp{}{}", basename, counter, complete_suffix));
counter += 1;
if !temp_abs_path.exists() {
break;
@@ -463,12 +458,24 @@ mod tests {
#[test]
fn ensure_extension_appends_and_is_case_insensitive() {
let f = FileFunctions::new();
assert_eq!(f.ensure_filename_extension("foo", "ove").unwrap(), "foo.ove");
assert_eq!(
f.ensure_filename_extension("foo", "ove").unwrap(),
"foo.ove"
);
// Already present (case-insensitive): untouched.
assert_eq!(f.ensure_filename_extension("foo.OVE", "ove").unwrap(), "foo.OVE");
assert_eq!(f.ensure_filename_extension("foo.ove", "Ove").unwrap(), "foo.ove");
assert_eq!(
f.ensure_filename_extension("foo.OVE", "ove").unwrap(),
"foo.OVE"
);
assert_eq!(
f.ensure_filename_extension("foo.ove", "Ove").unwrap(),
"foo.ove"
);
// The "." in the suffix must actually be present.
assert_eq!(f.ensure_filename_extension("fooove", "ove").unwrap(), "fooove.ove");
assert_eq!(
f.ensure_filename_extension("fooove", "ove").unwrap(),
"fooove.ove"
);
// Empty inputs are no-ops.
assert_eq!(f.ensure_filename_extension("", "ove").unwrap(), "");
assert_eq!(f.ensure_filename_extension("foo", "").unwrap(), "foo");
@@ -572,10 +579,7 @@ mod tests {
std::fs::write(&from, b"new").unwrap();
std::fs::write(&to, b"old").unwrap();
assert!(f.rename_file_allow_overwrite(
&from.to_string_lossy(),
&to.to_string_lossy()
));
assert!(f.rename_file_allow_overwrite(&from.to_string_lossy(), &to.to_string_lossy()));
assert!(!from.exists());
assert_eq!(std::fs::read(&to).unwrap(), b"new");
@@ -693,7 +697,10 @@ mod tests {
fn ensure_extension_dotfile_multi_ext_and_unicode() {
let f = FileFunctions::new();
// Dotfiles and multi-extension names just get the suffix appended.
assert_eq!(f.ensure_filename_extension(".hidden", "txt").unwrap(), ".hidden.txt");
assert_eq!(
f.ensure_filename_extension(".hidden", "txt").unwrap(),
".hidden.txt"
);
assert_eq!(
f.ensure_filename_extension("archive.tar", "gz").unwrap(),
"archive.tar.gz"
@@ -764,12 +771,16 @@ mod tests {
assert_ne!(ida, idb);
// A directory also gets an identifier (C++ only checks exists()).
let idd = f.get_unique_file_identifier(&dir.to_string_lossy()).unwrap();
let idd = f
.get_unique_file_identifier(&dir.to_string_lossy())
.unwrap();
assert_eq!(idd.len(), 16);
// Empty filename -> absolute() of "" is the CWD which exists; but a
// definitely-bogus relative name yields "".
let bogus = f.get_unique_file_identifier("definitely-not-here.xyz").unwrap();
let bogus = f
.get_unique_file_identifier("definitely-not-here.xyz")
.unwrap();
assert_eq!(bogus, "");
let _ = std::fs::remove_dir_all(&dir);
@@ -878,10 +889,7 @@ mod tests {
let from = dir.join("only.txt");
let to = dir.join("fresh.txt");
std::fs::write(&from, b"data").unwrap();
assert!(f.rename_file_allow_overwrite(
&from.to_string_lossy(),
&to.to_string_lossy()
));
assert!(f.rename_file_allow_overwrite(&from.to_string_lossy(), &to.to_string_lossy()));
assert_eq!(std::fs::read(&to).unwrap(), b"data");
// Destination existing as a DIRECTORY cannot be removed by
@@ -891,10 +899,7 @@ mod tests {
std::fs::write(&from2, b"z").unwrap();
let todir = dir.join("destdir");
std::fs::create_dir(&todir).unwrap();
assert!(!f.rename_file_allow_overwrite(
&from2.to_string_lossy(),
&todir.to_string_lossy()
));
assert!(!f.rename_file_allow_overwrite(&from2.to_string_lossy(), &todir.to_string_lossy()));
let _ = std::fs::remove_dir_all(&dir);
}
@@ -904,10 +909,7 @@ mod tests {
let f = FileFunctions::new();
let temp = f.get_temp_file_path().unwrap();
// `<temp>/oak`, created on demand (`filefunctions.cpp:184-196`).
assert_eq!(
PathBuf::from(&temp),
std::env::temp_dir().join("oak")
);
assert_eq!(PathBuf::from(&temp), std::env::temp_dir().join("oak"));
assert!(Path::new(&temp).is_dir());
}
@@ -928,8 +930,12 @@ mod tests {
/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`);
/// oaknode and oakrender both call it directly now.
pub fn default_disk_cache_path() -> String {
Path::new(&FileFunctions::new().get_configuration_location().unwrap_or_default())
.join("mediacache")
.to_string_lossy()
.into_owned()
Path::new(
&FileFunctions::new()
.get_configuration_location()
.unwrap_or_default(),
)
.join("mediacache")
.to_string_lossy()
.into_owned()
}
+3 -1
View File
@@ -95,7 +95,9 @@ unsafe extern "C" fn refbox_release_owned<T: Send + Sized + 'static>(ctx: *mut s
/// release thunk (borrowed, produced by [`make_borrowed`]): at zero only
/// the box allocation is reclaimed; the value inside is forgotten — its
/// ownership remains with the borrower.
unsafe extern "C" fn refbox_release_borrowed<T: Send + Sized + 'static>(ctx: *mut std::ffi::c_void) {
unsafe extern "C" fn refbox_release_borrowed<T: Send + Sized + 'static>(
ctx: *mut std::ffi::c_void,
) {
unsafe {
let rb = ctx as *mut RefBox<T>;
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
+2 -2
View File
@@ -21,14 +21,14 @@
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod cancelatom;
pub mod colortransform;
pub mod commandlineparser;
pub mod cancelatom;
pub mod configstore;
pub mod debug;
pub mod error;
pub mod ffmpegutils;
pub mod ffi;
pub mod ffmpegutils;
pub mod filefunctions;
pub mod handle;
pub mod miscutils;
+7 -1
View File
@@ -362,7 +362,13 @@ mod tests {
for linear in [0.001, 0.01, 0.1, 0.5, 0.9, 0.99] {
let log = decibel_linear_to_logarithmic(linear).unwrap();
let back = decibel_logarithmic_to_linear(log).unwrap();
assert!((back - linear).abs() < 1e-6, "linear {} -> {} -> {}", linear, log, back);
assert!(
(back - linear).abs() < 1e-6,
"linear {} -> {} -> {}",
linear,
log,
back
);
}
// db <-> logarithmic round trip. Only non-positive db are reversible:
// a positive db pushes the logarithmic position past 0.99, which the
+57 -18
View File
@@ -177,9 +177,9 @@ impl OcioConfig {
/// Name of the role at `index` (0-based).
pub fn role_name(&self, index: i32) -> Result<String> {
self.inner.role_name(index).ok_or_else(|| {
Error::new(format!("OcioConfig::role_name: no role at index {index}"))
})
self.inner
.role_name(index)
.ok_or_else(|| Error::new(format!("OcioConfig::role_name: no role at index {index}")))
}
/// Names of all roles in config order.
@@ -208,9 +208,9 @@ impl OcioConfig {
/// The config's default display name.
pub fn default_display(&self) -> Result<String> {
self.inner.default_display().ok_or_else(|| {
Error::new("OcioConfig::default_display: config defines no displays")
})
self.inner
.default_display()
.ok_or_else(|| Error::new("OcioConfig::default_display: config defines no displays"))
}
/// The default view for `display`.
@@ -234,9 +234,9 @@ impl OcioConfig {
/// Builds a processor applying `src` through the display transform for
/// `display`/`view` in the forward direction.
pub fn display_processor(&self, src: &str, display: &str, view: &str) -> Result<OcioProcessor> {
let processor = self
.inner
.processor_display(src, display, view, TransformDirection::Forward)?;
let processor =
self.inner
.processor_display(src, display, view, TransformDirection::Forward)?;
Ok(OcioProcessor {
inner: processor.default_cpu_processor()?,
})
@@ -321,7 +321,9 @@ mod tests {
let utils = OCIOUtils::new();
// Stateless; just confirm construction works and stays usable.
assert_eq!(
utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8).unwrap(),
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8)
.unwrap(),
1
);
}
@@ -329,18 +331,53 @@ mod tests {
#[test]
fn ocio_bit_depth_maps_supported_formats() {
let utils = OCIOUtils::new();
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8).unwrap(), 1);
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::U10).unwrap(), 2);
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::U16).unwrap(), 5);
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::F16).unwrap(), 7);
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::F32).unwrap(), 8);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8)
.unwrap(),
1
);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::U10)
.unwrap(),
2
);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::U16)
.unwrap(),
5
);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::F16)
.unwrap(),
7
);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::F32)
.unwrap(),
8
);
}
#[test]
fn ocio_bit_depth_maps_invalid_and_count_to_unknown() {
let utils = OCIOUtils::new();
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::Invalid).unwrap(), 0);
assert_eq!(utils.get_ocio_bit_depth_from_pixel_format(PixelFormat::Count).unwrap(), 0);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::Invalid)
.unwrap(),
0
);
assert_eq!(
utils
.get_ocio_bit_depth_from_pixel_format(PixelFormat::Count)
.unwrap(),
0
);
}
#[test]
@@ -355,7 +392,9 @@ mod tests {
config.has_role("default").expect("has_role should work"),
"raw config should define the default role"
);
let canonical = config.canonical_name("default").expect("canonical should work");
let canonical = config
.canonical_name("default")
.expect("canonical should work");
assert_eq!(canonical, "raw");
}
}
+108 -44
View File
@@ -31,8 +31,8 @@
use crate::error::{Error, Result};
use crate::ocioutils::PixelFormat;
use oakcore_rs::Rational;
use image::{ExtendedColorType, ImageBuffer, Rgb, Rgba};
use oakcore_rs::Rational;
/// OIIO base type codes, matching `OIIO::TypeDesc::BASETYPE`.
///
@@ -211,8 +211,7 @@ impl F32Image {
/// are upscaled to float. Only the TIFF format is enabled in this crate's
/// `image` dependency; other formats fail with an error.
pub fn read_image_f32(path: &str) -> Result<F32Image> {
let img =
image::open(path).map_err(|e| Error::new(format!("image::read_image_f32: {e}")))?;
let img = image::open(path).map_err(|e| Error::new(format!("image::read_image_f32: {e}")))?;
let width = img.width() as i32;
let height = img.height() as i32;
let channels = img.color().channel_count() as i32;
@@ -274,25 +273,24 @@ pub fn write_image_f32(
}
let (w, h) = (width as u32, height as u32);
let result = if channels == 3 {
let buf = ImageBuffer::<Rgb<f32>, Vec<f32>>::from_raw(w, h, pixels.to_vec()).ok_or_else(
|| {
Error::new(format!(
let result =
if channels == 3 {
let buf = ImageBuffer::<Rgb<f32>, Vec<f32>>::from_raw(w, h, pixels.to_vec())
.ok_or_else(|| {
Error::new(format!(
"image::write_image_f32: pixel buffer does not match {width}x{height}x{channels}"
))
},
)?;
buf.save_with_format(path, image::ImageFormat::Tiff)
} else {
let buf = ImageBuffer::<Rgba<f32>, Vec<f32>>::from_raw(w, h, pixels.to_vec()).ok_or_else(
|| {
Error::new(format!(
})?;
buf.save_with_format(path, image::ImageFormat::Tiff)
} else {
let buf = ImageBuffer::<Rgba<f32>, Vec<f32>>::from_raw(w, h, pixels.to_vec())
.ok_or_else(|| {
Error::new(format!(
"image::write_image_f32: pixel buffer does not match {width}x{height}x{channels}"
))
},
)?;
buf.save_with_format(path, image::ImageFormat::Tiff)
};
})?;
buf.save_with_format(path, image::ImageFormat::Tiff)
};
result.map_err(|e| Error::new(format!("image::write_image_f32: {e}")))?;
Ok(())
}
@@ -308,30 +306,80 @@ mod tests {
#[test]
fn base_type_from_format_mapping() {
let u = utils();
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::U8).unwrap(), 2); // UINT8
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::U10).unwrap(), 0); // UNKNOWN
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::U16).unwrap(), 4); // UINT16
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::F16).unwrap(), 10); // HALF
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::F32).unwrap(), 11); // FLOAT
// Invalid / count fall through to UNKNOWN.
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::Invalid).unwrap(), 0);
assert_eq!(u.get_oiio_base_type_from_format(PixelFormat::Count).unwrap(), 0);
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::U8).unwrap(),
2
); // UINT8
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::U10).unwrap(),
0
); // UNKNOWN
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::U16).unwrap(),
4
); // UINT16
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::F16).unwrap(),
10
); // HALF
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::F32).unwrap(),
11
); // FLOAT
// Invalid / count fall through to UNKNOWN.
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::Invalid)
.unwrap(),
0
);
assert_eq!(
u.get_oiio_base_type_from_format(PixelFormat::Count)
.unwrap(),
0
);
}
#[test]
fn format_from_oiio_basetype_mapping() {
let u = utils();
assert_eq!(u.get_format_from_oiio_basetype(2).unwrap(), PixelFormat::U8);
assert_eq!(u.get_format_from_oiio_basetype(4).unwrap(), PixelFormat::U16);
assert_eq!(u.get_format_from_oiio_basetype(10).unwrap(), PixelFormat::F16);
assert_eq!(u.get_format_from_oiio_basetype(11).unwrap(), PixelFormat::F32);
assert_eq!(
u.get_format_from_oiio_basetype(4).unwrap(),
PixelFormat::U16
);
assert_eq!(
u.get_format_from_oiio_basetype(10).unwrap(),
PixelFormat::F16
);
assert_eq!(
u.get_format_from_oiio_basetype(11).unwrap(),
PixelFormat::F32
);
// Unknown / unmappable base types map to Invalid.
assert_eq!(u.get_format_from_oiio_basetype(0).unwrap(), PixelFormat::Invalid); // UNKNOWN
assert_eq!(u.get_format_from_oiio_basetype(1).unwrap(), PixelFormat::Invalid); // NONE
assert_eq!(u.get_format_from_oiio_basetype(3).unwrap(), PixelFormat::Invalid); // INT8
assert_eq!(u.get_format_from_oiio_basetype(12).unwrap(), PixelFormat::Invalid); // DOUBLE
assert_eq!(u.get_format_from_oiio_basetype(15).unwrap(), PixelFormat::Invalid); // USTRINGHASH
assert_eq!(u.get_format_from_oiio_basetype(99).unwrap(), PixelFormat::Invalid);
assert_eq!(
u.get_format_from_oiio_basetype(0).unwrap(),
PixelFormat::Invalid
); // UNKNOWN
assert_eq!(
u.get_format_from_oiio_basetype(1).unwrap(),
PixelFormat::Invalid
); // NONE
assert_eq!(
u.get_format_from_oiio_basetype(3).unwrap(),
PixelFormat::Invalid
); // INT8
assert_eq!(
u.get_format_from_oiio_basetype(12).unwrap(),
PixelFormat::Invalid
); // DOUBLE
assert_eq!(
u.get_format_from_oiio_basetype(15).unwrap(),
PixelFormat::Invalid
); // USTRINGHASH
assert_eq!(
u.get_format_from_oiio_basetype(99).unwrap(),
PixelFormat::Invalid
);
}
#[test]
@@ -393,14 +441,24 @@ mod tests {
let u = utils();
// The recovered rational should reproduce the input within the
// precision of a reduced fraction.
for input in [0.5, 1.0, 1.333_333_333_333_333_3, 1.777_777_777_777_777_7, 2.0, 2.35] {
for input in [
0.5,
1.0,
1.333_333_333_333_333_3,
1.777_777_777_777_777_7,
2.0,
2.35,
] {
let (n, d) = u.get_pixel_aspect_ratio(input).unwrap();
if d == 0 {
continue;
}
let recovered = n as f64 / d as f64;
let err = (recovered - input).abs();
assert!(err < 1e-9, "input={input} recovered={recovered} ({n}/{d}) err={err}");
assert!(
err < 1e-9,
"input={input} recovered={recovered} ({n}/{d}) err={err}"
);
}
}
@@ -414,9 +472,18 @@ mod tests {
#[test]
fn image_color_type_maps_formats() {
assert_eq!(image_color_type_for(PixelFormat::U8), Some(ExtendedColorType::L8));
assert_eq!(image_color_type_for(PixelFormat::U16), Some(ExtendedColorType::L16));
assert_eq!(image_color_type_for(PixelFormat::F32), Some(ExtendedColorType::Rgb32F));
assert_eq!(
image_color_type_for(PixelFormat::U8),
Some(ExtendedColorType::L8)
);
assert_eq!(
image_color_type_for(PixelFormat::U16),
Some(ExtendedColorType::L16)
);
assert_eq!(
image_color_type_for(PixelFormat::F32),
Some(ExtendedColorType::Rgb32F)
);
// No image representation.
assert_eq!(image_color_type_for(PixelFormat::U10), None);
assert_eq!(image_color_type_for(PixelFormat::F16), None);
@@ -461,10 +528,7 @@ mod tests {
let h = 2;
let c = 4;
let pixels: Vec<f32> = vec![
0.0, 0.25, 0.5, 1.0,
0.75, 0.5, 0.25, 1.0,
1.0, 0.0, 0.5, 0.0,
0.125, 0.625, 0.875, 1.0,
0.0, 0.25, 0.5, 1.0, 0.75, 0.5, 0.25, 1.0, 1.0, 0.0, 0.5, 0.0, 0.125, 0.625, 0.875, 1.0,
];
let (path, path_str) = temp_tiff_path("roundtrip.tif");
+16 -4
View File
@@ -104,10 +104,22 @@ pub fn get_creation_date(path: &str) -> Result<i64> {
st_gid: 0,
st_rdev: 0,
_st_pad: 0,
st_atimespec: Timespec { tv_sec: 0, tv_nsec: 0 },
st_mtimespec: Timespec { tv_sec: 0, tv_nsec: 0 },
st_ctimespec: Timespec { tv_sec: 0, tv_nsec: 0 },
st_birthtimespec: Timespec { tv_sec: 0, tv_nsec: 0 },
st_atimespec: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
st_mtimespec: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
st_ctimespec: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
st_birthtimespec: Timespec {
tv_sec: 0,
tv_nsec: 0,
},
st_size: 0,
st_blocks: 0,
st_blksize: 0,
+30 -10
View File
@@ -207,9 +207,9 @@ impl SubtitleParams {
match ev.name.as_str() {
"streamindex" => {
let text = reader.read_element_text();
let index: i32 = text.parse().map_err(|_| {
Error::Failed("invalid subtitleparams streamindex".into())
})?;
let index: i32 = text
.parse()
.map_err(|_| Error::Failed("invalid subtitleparams streamindex".into()))?;
self.set_stream_index(index);
}
"enabled" => {
@@ -534,7 +534,9 @@ fn parse_events(data: &str) -> Result<Vec<XmlEvent>> {
// b[i] == b'<'
if i + 1 >= n {
return Err(Error::Failed("unterminated '<' in subtitleparams xml".into()));
return Err(Error::Failed(
"unterminated '<' in subtitleparams xml".into(),
));
}
match b[i + 1] {
b'/' => {
@@ -582,7 +584,9 @@ fn parse_events(data: &str) -> Result<Vec<XmlEvent>> {
// <? ... ?> processing instruction (e.g. the XML decl).
match data[i + 2..].find("?>") {
Some(p) => i = i + 2 + p + 2,
None => return Err(Error::Failed("unterminated processing instruction".into())),
None => {
return Err(Error::Failed("unterminated processing instruction".into()))
}
}
}
_ => {
@@ -622,7 +626,9 @@ fn parse_events(data: &str) -> Result<Vec<XmlEvent>> {
i += 1;
}
if aname_start == i {
return Err(Error::Failed("malformed attribute in subtitleparams xml".into()));
return Err(Error::Failed(
"malformed attribute in subtitleparams xml".into(),
));
}
let aname = data[aname_start..i].to_string();
@@ -975,7 +981,10 @@ mod tests {
fn escape_helpers() {
assert_eq!(escape_text("a<b&c>d"), "a&lt;b&amp;c&gt;d");
assert_eq!(escape_attribute("a\"b<c>&d"), "a&quot;b&lt;c&gt;&amp;d");
assert_eq!(decode_entities("a&lt;b&amp;c&gt;d&quot;e&apos;f"), "a<b&c>d\"e'f");
assert_eq!(
decode_entities("a&lt;b&amp;c&gt;d&quot;e&apos;f"),
"a<b&c>d\"e'f"
);
assert_eq!(decode_entities("&#65;&#x42;"), "AB");
assert_eq!(decode_entities("keep &unknown;"), "keep &unknown;");
}
@@ -1026,7 +1035,7 @@ mod tests {
sp.load_xml("<subtitleparams><enabled>2</enabled><subtitles/></subtitleparams>")
.unwrap();
assert!(sp.enabled()); // any nonzero is true (C++ bool conversion)
// std::stoi throws on junk -> E_FAILED.
// std::stoi throws on junk -> E_FAILED.
assert!(sp
.load_xml("<subtitleparams><enabled>abc</enabled><subtitles/></subtitleparams>")
.is_err());
@@ -1136,9 +1145,20 @@ mod tests {
fn rational_reduce_matches_oakcore() {
// Cross-validate the hand-rolled reduction against the canonical
// oakcore-rs port of the C++ Rational.
for (n, d) in [(2, 4), (0, 5), (5, 0), (-3, -1), (1, -2), (7, 3), (i32::MAX, 1)] {
for (n, d) in [
(2, 4),
(0, 5),
(5, 0),
(-3, -1),
(1, -2),
(7, 3),
(i32::MAX, 1),
] {
let r = oakcore_rs::Rational::new(n as i64, d as i64);
assert_eq!(rational_reduce(n, d), (r.numerator() as i32, r.denominator() as i32));
assert_eq!(
rational_reduce(n, d),
(r.numerator() as i32, r.denominator() as i32)
);
}
for s in ["1/2", "7", "4/2", "junk", "a/b", "1/2/3", ""] {
let r = oakcore_rs::Rational::from_string(s);
+261 -61
View File
@@ -263,7 +263,6 @@ impl VideoParams {
self.depth > 1
}
/// The time base as a numerator/denominator pair.
pub fn time_base(&self) -> (i32, i32) {
self.time_base
@@ -573,7 +572,9 @@ impl VideoParams {
self.set_pixel_aspect_ratio(n, d);
}
"interlacing" => {
self.set_interlacing(interlacing_from_i32(stoi_field(&cur.read_element_text())?));
self.set_interlacing(interlacing_from_i32(stoi_field(
&cur.read_element_text(),
)?));
}
"divider" => self.set_divider(stoi_field(&cur.read_element_text())?),
"enabled" => self.set_enabled(stoi_field(&cur.read_element_text())? != 0),
@@ -594,7 +595,9 @@ impl VideoParams {
}
"colorspace" => self.set_colorspace(&cur.read_element_text()),
"colorrange" => {
self.set_color_range(color_range_from_i32(stoi_field(&cur.read_element_text())?));
self.set_color_range(color_range_from_i32(stoi_field(
&cur.read_element_text(),
)?));
}
"colorprimaries" => {
self.set_color_primaries(stoi_field(&cur.read_element_text())?);
@@ -636,7 +639,11 @@ impl VideoParams {
push_text_element(&mut out, "framerate", &rational_to_string(self.frame_rate));
push_text_element(&mut out, "starttime", &self.start_time.to_string());
push_text_element(&mut out, "duration", &self.duration.to_string());
push_text_element(&mut out, "premultipliedalpha", &bool_str(self.premultiplied_alpha));
push_text_element(
&mut out,
"premultipliedalpha",
&bool_str(self.premultiplied_alpha),
);
push_text_element(&mut out, "colorspace", &self.colorspace);
push_text_element(&mut out, "colorrange", &int_str(self.color_range as i32));
push_text_element(&mut out, "colorprimaries", &int_str(self.color_primaries));
@@ -667,7 +674,12 @@ impl VideoParams {
}
/// Total buffer size for a frame.
pub fn calculate_buffer_size(width: i32, height: i32, pixel_format: PixelFormat, channels: i32) -> i32 {
pub fn calculate_buffer_size(
width: i32,
height: i32,
pixel_format: PixelFormat,
channels: i32,
) -> i32 {
// CPP-PARITY: C++ uses int `width * height * bpp` (wraps on
// overflow); use wrapping arithmetic so debug builds don't panic.
let bpp = Self::bytes_per_pixel_for_format(pixel_format, channels);
@@ -822,7 +834,11 @@ fn int_str(v: i32) -> String {
}
fn bool_str(b: bool) -> String {
if b { "1".to_string() } else { "0".to_string() }
if b {
"1".to_string()
} else {
"0".to_string()
}
}
/// `std::to_string(float)` — fixed notation with 6 decimal places.
@@ -1236,7 +1252,10 @@ fn resolve_entity(data: &str, start: usize, semi: usize) -> Option<String> {
"quot" => Some("\"".to_string()),
"apos" => Some("'".to_string()),
_ => {
if let Some(hex) = ent.strip_prefix('#').and_then(|h| h.strip_prefix(['x', 'X'])) {
if let Some(hex) = ent
.strip_prefix('#')
.and_then(|h| h.strip_prefix(['x', 'X']))
{
let code = u32::from_str_radix(hex, 16).ok()?;
char::from_u32(code).map(|c| c.to_string())
} else if let Some(dec) = ent.strip_prefix('#') {
@@ -1275,7 +1294,9 @@ fn parse_start_tag(data: &str, start: usize) -> Option<(String, usize, bool)> {
let (name, mut i) = parse_name(data, start)?;
let mut self_closing = false;
loop {
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r' || bytes[i] == b'\n') {
while i < bytes.len()
&& (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r' || bytes[i] == b'\n')
{
i += 1;
}
if i >= bytes.len() {
@@ -1298,14 +1319,24 @@ fn parse_start_tag(data: &str, start: usize) -> Option<(String, usize, bool)> {
_ => {
// attribute name="value"
let (_, mut j) = parse_name(data, i)?;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'\r' || bytes[j] == b'\n') {
while j < bytes.len()
&& (bytes[j] == b' '
|| bytes[j] == b'\t'
|| bytes[j] == b'\r'
|| bytes[j] == b'\n')
{
j += 1;
}
if j >= bytes.len() || bytes[j] != b'=' {
return None;
}
j += 1;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'\r' || bytes[j] == b'\n') {
while j < bytes.len()
&& (bytes[j] == b' '
|| bytes[j] == b'\t'
|| bytes[j] == b'\r'
|| bytes[j] == b'\n')
{
j += 1;
}
if j >= bytes.len() || (bytes[j] != b'"' && bytes[j] != b'\'') {
@@ -1391,7 +1422,12 @@ fn parse_xml(data: &str) -> Option<Vec<XmlEvent>> {
// End element
let (name, ni) = parse_name(data, i + 2)?;
let mut j = ni;
while j < n && (bytes[j] == b' ' || bytes[j] == b'\t' || bytes[j] == b'\r' || bytes[j] == b'\n') {
while j < n
&& (bytes[j] == b' '
|| bytes[j] == b'\t'
|| bytes[j] == b'\r'
|| bytes[j] == b'\n')
{
j += 1;
}
if j >= n || bytes[j] != b'>' {
@@ -1495,7 +1531,18 @@ mod tests {
#[test]
fn new_with_time_base() {
let vp = VideoParams::new_with_time_base(1920, 1080, 1001, 30000, PixelFormat::U8, 4, 1, 1, 0, 1);
let vp = VideoParams::new_with_time_base(
1920,
1080,
1001,
30000,
PixelFormat::U8,
4,
1,
1,
0,
1,
);
assert_eq!(vp.time_base(), (1001, 30000));
// frame rate is the flipped time base.
assert_eq!(vp.frame_rate(), (30000, 1001));
@@ -1589,26 +1636,62 @@ mod tests {
#[test]
fn bytes_per_channel_for_format() {
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::U8), 1);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::U10), 0);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::U16), 2);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::F16), 2);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::F32), 4);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::Invalid), 0);
assert_eq!(VideoParams::bytes_per_channel_for_format(PixelFormat::Count), 0);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::U8),
1
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::U10),
0
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::U16),
2
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::F16),
2
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::F32),
4
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::Invalid),
0
);
assert_eq!(
VideoParams::bytes_per_channel_for_format(PixelFormat::Count),
0
);
}
#[test]
fn bytes_per_pixel_for_format() {
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::U8, 4), 4);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::U10, 4), 4);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::U10, 3), 0);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::F32, 4), 16);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U8, 4),
4
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U10, 4),
4
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U10, 3),
0
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::F32, 4),
16
);
}
#[test]
fn buffer_size() {
assert_eq!(VideoParams::calculate_buffer_size(1920, 1080, PixelFormat::U8, 4), 8294400);
assert_eq!(
VideoParams::calculate_buffer_size(1920, 1080, PixelFormat::U8, 4),
8294400
);
let vp = default_vp();
assert_eq!(vp.buffer_size(), 8294400);
assert_eq!(vp.bytes_per_channel(), 1);
@@ -1636,8 +1719,14 @@ mod tests {
#[test]
fn scaled_dimension_and_target() {
assert_eq!(VideoParams::get_scaled_dimension(1920, 2), 960);
assert_eq!(VideoParams::get_divider_for_target_resolution(3840, 2160, 1920, 1080), 2);
assert_eq!(VideoParams::get_divider_for_target_resolution(1920, 1080, 1920, 1080), 1);
assert_eq!(
VideoParams::get_divider_for_target_resolution(3840, 2160, 1920, 1080),
2
);
assert_eq!(
VideoParams::get_divider_for_target_resolution(1920, 1080, 1920, 1080),
1
);
}
#[test]
@@ -1650,22 +1739,52 @@ mod tests {
#[test]
fn format_name() {
assert_eq!(VideoParams::format_name(PixelFormat::U8).unwrap(), "8-bit");
assert_eq!(VideoParams::format_name(PixelFormat::U10).unwrap(), "10-bit Packed");
assert_eq!(VideoParams::format_name(PixelFormat::U16).unwrap(), "16-bit Integer");
assert_eq!(VideoParams::format_name(PixelFormat::F16).unwrap(), "Half-Float (16-bit)");
assert_eq!(VideoParams::format_name(PixelFormat::F32).unwrap(), "Full-Float (32-bit)");
assert_eq!(VideoParams::format_name(PixelFormat::Invalid).unwrap(), "Unknown (0xFFFFFFFF)");
assert_eq!(VideoParams::format_name(PixelFormat::Count).unwrap(), "Unknown (0x5)");
assert_eq!(
VideoParams::format_name(PixelFormat::U10).unwrap(),
"10-bit Packed"
);
assert_eq!(
VideoParams::format_name(PixelFormat::U16).unwrap(),
"16-bit Integer"
);
assert_eq!(
VideoParams::format_name(PixelFormat::F16).unwrap(),
"Half-Float (16-bit)"
);
assert_eq!(
VideoParams::format_name(PixelFormat::F32).unwrap(),
"Full-Float (32-bit)"
);
assert_eq!(
VideoParams::format_name(PixelFormat::Invalid).unwrap(),
"Unknown (0xFFFFFFFF)"
);
assert_eq!(
VideoParams::format_name(PixelFormat::Count).unwrap(),
"Unknown (0x5)"
);
}
#[test]
fn frame_rate_to_string_values() {
assert_eq!(VideoParams::frame_rate_to_string(24000, 1001).unwrap(), "23.976 FPS");
assert_eq!(
VideoParams::frame_rate_to_string(24000, 1001).unwrap(),
"23.976 FPS"
);
assert_eq!(VideoParams::frame_rate_to_string(24, 1).unwrap(), "24 FPS");
assert_eq!(VideoParams::frame_rate_to_string(25, 1).unwrap(), "25 FPS");
assert_eq!(VideoParams::frame_rate_to_string(30000, 1001).unwrap(), "29.97 FPS");
assert_eq!(VideoParams::frame_rate_to_string(60000, 1001).unwrap(), "59.9401 FPS");
assert_eq!(VideoParams::frame_rate_to_string(48000, 1001).unwrap(), "47.952 FPS");
assert_eq!(
VideoParams::frame_rate_to_string(30000, 1001).unwrap(),
"29.97 FPS"
);
assert_eq!(
VideoParams::frame_rate_to_string(60000, 1001).unwrap(),
"59.9401 FPS"
);
assert_eq!(
VideoParams::frame_rate_to_string(48000, 1001).unwrap(),
"47.952 FPS"
);
assert_eq!(VideoParams::frame_rate_to_string(1, 1).unwrap(), "1 FPS");
assert_eq!(VideoParams::frame_rate_to_string(0, 1).unwrap(), "0 FPS");
}
@@ -1700,7 +1819,18 @@ mod tests {
#[test]
fn xml_round_trip() {
let mut vp = VideoParams::new_with_time_base(1920, 1080, 1001, 30000, PixelFormat::U8, 4, 16, 15, 1, 2);
let mut vp = VideoParams::new_with_time_base(
1920,
1080,
1001,
30000,
PixelFormat::U8,
4,
16,
15,
1,
2,
);
vp.set_enabled(true);
vp.set_x(1.5);
vp.set_y(-2.5);
@@ -1764,14 +1894,20 @@ mod tests {
assert!(vp.load_xml("<width>").is_err());
assert!(vp.load_xml("<width>5</height>").is_err());
// A non-numeric value inside a properly-rooted doc must error (stoi).
assert!(vp.load_xml("<videoparams><width>notanumber</width></videoparams>").is_err());
assert!(vp
.load_xml("<videoparams><width>notanumber</width></videoparams>")
.is_err());
// Self-closing element is not malformed; the empty value is accepted
// for a text field (colorspace) but rejected for an integer field.
assert!(vp.load_xml("<videoparams><width>640</width><colorspace/><depth>2</depth></videoparams>").is_ok());
assert!(vp
.load_xml("<videoparams><width>640</width><colorspace/><depth>2</depth></videoparams>")
.is_ok());
assert_eq!(vp.width(), 640);
assert_eq!(vp.depth(), 2);
assert_eq!(vp.colorspace(), "");
assert!(vp.load_xml("<videoparams><width/><colorspace>a</colorspace></videoparams>").is_err());
assert!(vp
.load_xml("<videoparams><width/><colorspace>a</colorspace></videoparams>")
.is_err());
}
// ---- Extended coverage --------------------------------------------------
@@ -1781,12 +1917,30 @@ mod tests {
// Full format × channel matrix, mirroring get_bytes_per_pixel():
// packed u10 only supports RGBA (4); everything else is bpc*channels.
for ch in [0, 1, 3, 4] {
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::U8, ch), ch);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::U16, ch), 2 * ch);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::F16, ch), 2 * ch);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::F32, ch), 4 * ch);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::Invalid, ch), 0);
assert_eq!(VideoParams::bytes_per_pixel_for_format(PixelFormat::Count, ch), 0);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U8, ch),
ch
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U16, ch),
2 * ch
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::F16, ch),
2 * ch
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::F32, ch),
4 * ch
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::Invalid, ch),
0
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::Count, ch),
0
);
assert_eq!(
VideoParams::bytes_per_pixel_for_format(PixelFormat::U10, ch),
if ch == 4 { 4 } else { 0 }
@@ -1867,10 +2021,19 @@ mod tests {
#[test]
fn divider_for_target_resolution_edges() {
// Source already fits -> 1.
assert_eq!(VideoParams::get_divider_for_target_resolution(100, 100, 1920, 1080), 1);
assert_eq!(
VideoParams::get_divider_for_target_resolution(100, 100, 1920, 1080),
1
);
// 1921/2 = 960 fits 960, 1081/2 = 540 fits 540 -> 2.
assert_eq!(VideoParams::get_divider_for_target_resolution(1921, 1081, 960, 540), 2);
assert_eq!(VideoParams::get_divider_for_target_resolution(1921, 1081, 959, 540), 3);
assert_eq!(
VideoParams::get_divider_for_target_resolution(1921, 1081, 960, 540),
2
);
assert_eq!(
VideoParams::get_divider_for_target_resolution(1921, 1081, 959, 540),
3
);
}
#[test]
@@ -1882,9 +2045,18 @@ mod tests {
#[test]
fn frame_rate_to_string_edge_values() {
assert_eq!(VideoParams::frame_rate_to_string(-24, 1).unwrap(), "-24 FPS");
assert_eq!(VideoParams::frame_rate_to_string(1, 1000000).unwrap(), "1e-06 FPS");
assert_eq!(VideoParams::frame_rate_to_string(1000000, 1).unwrap(), "1e+06 FPS");
assert_eq!(
VideoParams::frame_rate_to_string(-24, 1).unwrap(),
"-24 FPS"
);
assert_eq!(
VideoParams::frame_rate_to_string(1, 1000000).unwrap(),
"1e-06 FPS"
);
assert_eq!(
VideoParams::frame_rate_to_string(1000000, 1).unwrap(),
"1e+06 FPS"
);
assert_eq!(VideoParams::frame_rate_to_string(0, 0).unwrap(), "nan FPS");
}
@@ -2036,7 +2208,8 @@ mod tests {
fn load_xml_partial_update_keeps_existing() {
// C++ load() only assigns fields present in the document.
let mut vp = default_vp();
vp.load_xml("<videoparams><width>320</width></videoparams>").unwrap();
vp.load_xml("<videoparams><width>320</width></videoparams>")
.unwrap();
assert_eq!(vp.width(), 320);
assert_eq!(vp.height(), 1080); // untouched
assert_eq!(vp.format(), PixelFormat::U8);
@@ -2061,8 +2234,10 @@ mod tests {
fn load_xml_numeric_prefix_parsing() {
let mut vp = VideoParams::new();
// std::stoi/std::stof consume the longest valid prefix, ignore junk.
vp.load_xml("<videoparams><width>640abc</width><x>1.5garbage</x><y> -2.5 </y></videoparams>")
.unwrap();
vp.load_xml(
"<videoparams><width>640abc</width><x>1.5garbage</x><y> -2.5 </y></videoparams>",
)
.unwrap();
assert_eq!(vp.width(), 640);
assert_eq!(vp.x(), 1.5);
assert_eq!(vp.y(), -2.5);
@@ -2090,20 +2265,35 @@ mod tests {
let mut vp = VideoParams::new();
// Unknown entity in character data is a parse error in this reader
// (stricter than the subtitle reader, which preserves it verbatim).
assert!(vp.load_xml("<videoparams><colorspace>a &bogus; b</colorspace></videoparams>").is_err());
assert!(vp
.load_xml("<videoparams><colorspace>a &bogus; b</colorspace></videoparams>")
.is_err());
// Unterminated comment.
assert!(vp.load_xml("<videoparams><!-- never ends").is_err());
// Mismatched nesting.
assert!(vp.load_xml("<videoparams><width>1</videoparams></width>").is_err());
assert!(vp
.load_xml("<videoparams><width>1</videoparams></width>")
.is_err());
}
#[test]
fn rational_helpers_match_oakcore() {
// The hand-rolled tuple rationals must agree with oakcore_rs::Rational,
// the canonical port of the C++ Rational.
for (n, d) in [(2i32, 4i32), (0, 5), (5, 0), (-3, -1), (1, -2), (7, 3), (100, 10)] {
for (n, d) in [
(2i32, 4i32),
(0, 5),
(5, 0),
(-3, -1),
(1, -2),
(7, 3),
(100, 10),
] {
let r = oakcore_rs::Rational::new(n as i64, d as i64);
assert_eq!(make_rational(n, d), (r.numerator() as i32, r.denominator() as i32));
assert_eq!(
make_rational(n, d),
(r.numerator() as i32, r.denominator() as i32)
);
}
for s in ["1/2", "7", "4/2", "junk", "a/b", "1/2/3", "-6/3"] {
let r = oakcore_rs::Rational::from_string(s);
@@ -2117,8 +2307,18 @@ mod tests {
#[test]
fn time_conversion_matches_oakcore() {
let mut vp =
VideoParams::new_with_time_base(1920, 1080, 1001, 30000, PixelFormat::U8, 4, 1, 1, 0, 1);
let mut vp = VideoParams::new_with_time_base(
1920,
1080,
1001,
30000,
PixelFormat::U8,
4,
1,
1,
0,
1,
);
vp.set_start_time(11);
let tb = oakcore_rs::Rational::new(1001, 30000);
for (n, d) in [(1i32, 1i32), (1, 2), (24000, 1001), (-3, 1), (0, 1)] {
+29 -27
View File
@@ -378,7 +378,9 @@ fn is_ws(c: char) -> bool {
/// Convert a quick-xml `QName` (raw bytes) to an owned UTF-8 string. The
/// input is always valid UTF-8 (the reader was built from a `&str`).
fn qname_string(q: impl AsRef<[u8]>) -> String {
std::str::from_utf8(q.as_ref()).map(str::to_owned).unwrap_or_default()
std::str::from_utf8(q.as_ref())
.map(str::to_owned)
.unwrap_or_default()
}
/// Append character data, merging consecutive Characters events
@@ -991,7 +993,7 @@ mod tests {
assert!(r.read_next_start_element().unwrap()); // a
assert!(r.read_next_start_element().unwrap()); // b
assert!(r.read_next_start_element().unwrap()); // c
// Next is </b>: returns false and stays put.
// Next is </b>: returns false and stays put.
assert!(!r.read_next_start_element().unwrap());
}
@@ -1022,30 +1024,30 @@ mod tests {
#[test]
fn reader_malformed_matrix() {
let cases = [
"", // no element found
" \n ", // whitespace only: no element
"<a>", // unclosed root
"<a><b></a></b>", // mismatched nesting
"<a></a></a>", // end tag without start
"<a></>", // empty end tag name
"<a>< /a>", // '<' not followed by name
"<a", // unclosed start tag
"<a b>", // attribute missing '='
"<a b=1>", // attribute value not quoted
"<a b=\"v>", // unclosed attribute value
"<a></a", // unclosed end tag
"<a><!-- c</a>", // unclosed comment
"<a><![CDATA[x</a>", // unclosed CDATA
"<?pi<a></a>", // unclosed PI
"<a>&undefined;</a>", // undefined entity
"<a>&#xZZ;</a>", // bad hex char reference
"<a>&#99999999999;</a>", // out-of-range char reference
"<a b=\"&foo;\"/>", // undefined entity in attribute
"<a/>tail", // junk after document element
"<a/><b/>", // second root element
"<a a=\"1\" a=\"2\"/>", // duplicate attribute
"<a b=\"<\"/>", // '<' in attribute value
"<a/ ><b/>", // self-close then second root
"", // no element found
" \n ", // whitespace only: no element
"<a>", // unclosed root
"<a><b></a></b>", // mismatched nesting
"<a></a></a>", // end tag without start
"<a></>", // empty end tag name
"<a>< /a>", // '<' not followed by name
"<a", // unclosed start tag
"<a b>", // attribute missing '='
"<a b=1>", // attribute value not quoted
"<a b=\"v>", // unclosed attribute value
"<a></a", // unclosed end tag
"<a><!-- c</a>", // unclosed comment
"<a><![CDATA[x</a>", // unclosed CDATA
"<?pi<a></a>", // unclosed PI
"<a>&undefined;</a>", // undefined entity
"<a>&#xZZ;</a>", // bad hex char reference
"<a>&#99999999999;</a>", // out-of-range char reference
"<a b=\"&foo;\"/>", // undefined entity in attribute
"<a/>tail", // junk after document element
"<a/><b/>", // second root element
"<a a=\"1\" a=\"2\"/>", // duplicate attribute
"<a b=\"<\"/>", // '<' in attribute value
"<a/ ><b/>", // self-close then second root
];
for doc in cases {
let r = XmlReader::new(doc).unwrap();
@@ -1071,7 +1073,7 @@ mod tests {
"<a>x</a >",
" \n<a> </a>\n ",
"<ns:a xmlns:ns=\"urn:x\" ns:b=\"v\"/>", // namespaces not processed
"<a>]]</a>", // single ']' is fine
"<a>]]</a>", // single ']' is fine
];
for doc in cases {
let r = XmlReader::new(doc).unwrap();
+2 -4
View File
@@ -26,11 +26,9 @@ use oakcommon::error::{
OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOMEM, OAKCOMMON_E_NOT_FOUND,
OAKCOMMON_E_STATE, OAKCOMMON_OK,
};
use oakcommon::ffmpegutils::{RGB_CHANNEL_COUNT, RGBA_CHANNEL_COUNT};
use oakcommon::ffmpegutils::{RGBA_CHANNEL_COUNT, RGB_CHANNEL_COUNT};
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
use oakcommon::miscutils::{
DECIBEL_MINIMUM, DropWorkflowBehavior, LoopMode,
};
use oakcommon::miscutils::{DropWorkflowBehavior, LoopMode, DECIBEL_MINIMUM};
use oakcommon::ocioutils::PixelFormat;
use oakcommon::videoparams::{ColorRange, Interlacing, VideoType};
+24 -5
View File
@@ -42,7 +42,7 @@ use std::sync::atomic::Ordering;
use oakcommon::colortransform::ColorTransform;
use oakcommon::ffi::colortransform::*;
use oakcommon::handle::{get, RefBox, CHandle, OAKCOMMON_ABI_VERSION};
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 {
@@ -93,7 +93,11 @@ fn free_ptr(p: *mut CHandle) {
/// 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) }
unsafe {
(*(h.ctx as *const RefBox<ColorTransform>))
.refs
.load(Ordering::Relaxed)
}
}
/// A successful `init_output` yields a stamped, non-null handle whose boxed
@@ -301,19 +305,34 @@ fn getters_two_stage_and_empty_handle() {
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");
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");
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!(
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),
+157 -24
View File
@@ -157,7 +157,11 @@ fn parser_free_null_handle_struct_is_noop() {
#[test]
fn set_app_info_success() {
let mut p = new_parser();
let r = oakcommon_commandlineparser_set_app_info(dup(&p), c_str("myapp").as_ptr(), c_str("1.0").as_ptr());
let r = oakcommon_commandlineparser_set_app_info(
dup(&p),
c_str("myapp").as_ptr(),
c_str("1.0").as_ptr(),
);
assert_eq!(r, OAKCOMMON_OK);
oakcommon_commandlineparser_free(&mut p);
}
@@ -220,7 +224,15 @@ fn add_option_empty_parser_is_invalid() {
let mut out = CHandle::null();
let (_names, ptrs) = c_strings(&["o"]);
let r = oakcommon_commandlineparser_add_option(
e, ptrs.as_ptr(), 1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out);
e,
ptrs.as_ptr(),
1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
}
@@ -229,7 +241,15 @@ fn add_option_null_names_is_invalid() {
let mut p = new_parser();
let mut out = CHandle::null();
let r = oakcommon_commandlineparser_add_option(
dup(&p), null_mut(), 1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out);
dup(&p),
null_mut(),
1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -240,10 +260,26 @@ fn add_option_non_positive_name_count_is_invalid() {
let mut out = CHandle::null();
let (_names, ptrs) = c_strings(&["o"]);
let r0 = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), 0, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out);
dup(&p),
ptrs.as_ptr(),
0,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(r0, OAKCOMMON_E_INVALID);
let rn = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), -1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out);
dup(&p),
ptrs.as_ptr(),
-1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(rn, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -256,7 +292,15 @@ fn add_option_null_description_is_invalid() {
let mut out = CHandle::null();
let (_names, ptrs) = c_strings(&["o"]);
let r = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), 1, null_mut(), 0, c_str("A").as_ptr(), 0, &mut out);
dup(&p),
ptrs.as_ptr(),
1,
null_mut(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -269,7 +313,15 @@ fn add_option_null_arg_placeholder_is_invalid() {
let mut out = CHandle::null();
let (_names, ptrs) = c_strings(&["o"]);
let r = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), 1, c_str("d").as_ptr(), 0, null_mut(), 0, &mut out);
dup(&p),
ptrs.as_ptr(),
1,
c_str("d").as_ptr(),
0,
null_mut(),
0,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -281,7 +333,15 @@ fn add_option_null_out_option_is_invalid() {
let mut p = new_parser();
let (_names, ptrs) = c_strings(&["o"]);
let r = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), 1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, null_mut());
dup(&p),
ptrs.as_ptr(),
1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
null_mut(),
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -295,14 +355,30 @@ fn add_option_non_utf8_name_is_invalid() {
let bad = CString::new(vec![b'x', 0xFF]).unwrap();
let ptrs = [bad.as_ptr()];
let r = oakcommon_commandlineparser_add_option(
dup(&p), ptrs.as_ptr(), 1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out);
dup(&p),
ptrs.as_ptr(),
1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
// The failed registration must not leave a partial option behind: a
// subsequent valid registration still works and lands at index 0.
let mut out2 = CHandle::null();
let (_names, ptrs2) = c_strings(&["ok"]);
let r2 = oakcommon_commandlineparser_add_option(
dup(&p), ptrs2.as_ptr(), 1, c_str("d").as_ptr(), 0, c_str("A").as_ptr(), 0, &mut out2);
dup(&p),
ptrs2.as_ptr(),
1,
c_str("d").as_ptr(),
0,
c_str("A").as_ptr(),
0,
&mut out2,
);
assert_eq!(r2, OAKCOMMON_OK);
assert!(!out2.ctx.is_null());
oakcommon_commandlineoption_free(&mut out2);
@@ -318,7 +394,12 @@ fn add_positional_argument_success() {
let mut p = new_parser();
let mut out = CHandle::null();
let r = oakcommon_commandlineparser_add_positional_argument(
dup(&p), c_str("input").as_ptr(), c_str("Input file").as_ptr(), 1, &mut out);
dup(&p),
c_str("input").as_ptr(),
c_str("Input file").as_ptr(),
1,
&mut out,
);
assert_eq!(r, OAKCOMMON_OK);
assert!(!out.ctx.is_null());
assert!(out.release.is_some());
@@ -331,7 +412,12 @@ fn add_positional_argument_empty_parser_is_invalid() {
let e = CHandle::null();
let mut out = CHandle::null();
let r = oakcommon_commandlineparser_add_positional_argument(
e, c_str("in").as_ptr(), c_str("d").as_ptr(), 1, &mut out);
e,
c_str("in").as_ptr(),
c_str("d").as_ptr(),
1,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
}
@@ -340,7 +426,12 @@ fn add_positional_argument_null_name_is_invalid() {
let mut p = new_parser();
let mut out = CHandle::null();
let r = oakcommon_commandlineparser_add_positional_argument(
dup(&p), null_mut(), c_str("d").as_ptr(), 1, &mut out);
dup(&p),
null_mut(),
c_str("d").as_ptr(),
1,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -352,7 +443,12 @@ fn add_positional_argument_null_description_is_invalid() {
let mut p = new_parser();
let mut out = CHandle::null();
let r = oakcommon_commandlineparser_add_positional_argument(
dup(&p), c_str("in").as_ptr(), null_mut(), 1, &mut out);
dup(&p),
c_str("in").as_ptr(),
null_mut(),
1,
&mut out,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -362,7 +458,12 @@ fn add_positional_argument_null_out_is_invalid() {
// CPP-PARITY: C++ allows a NULL out_argument; the Rust export requires it.
let mut p = new_parser();
let r = oakcommon_commandlineparser_add_positional_argument(
dup(&p), c_str("in").as_ptr(), c_str("d").as_ptr(), 1, null_mut());
dup(&p),
c_str("in").as_ptr(),
c_str("d").as_ptr(),
1,
null_mut(),
);
assert_eq!(r, OAKCOMMON_E_INVALID);
oakcommon_commandlineparser_free(&mut p);
}
@@ -683,7 +784,10 @@ fn option_handle_does_not_observe_process() {
let r = oakcommon_commandlineoption_get_setting(dup(&opt), null_mut(), 0);
assert_eq!(r, 1); // empty string
let mut is_set = true;
assert_eq!(oakcommon_commandlineoption_is_set(dup(&opt), &mut is_set), OAKCOMMON_OK);
assert_eq!(
oakcommon_commandlineoption_is_set(dup(&opt), &mut is_set),
OAKCOMMON_OK
);
assert!(!is_set);
oakcommon_commandlineoption_free(&mut opt);
oakcommon_commandlineparser_free(&mut p);
@@ -716,12 +820,20 @@ fn positional_get_setting_short_buffer_truncates_and_returns_required() {
OAKCOMMON_OK
);
let mut buf = [0xFFu8; 4];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, 4);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
4,
);
assert_eq!(r, 7);
assert_eq!(&buf[..3], b"abc");
assert_eq!(buf[3], 0);
let mut tiny = [0xFFu8; 1];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), tiny.as_mut_ptr() as *mut c_char, 1);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
tiny.as_mut_ptr() as *mut c_char,
1,
);
assert_eq!(r, 7);
assert_eq!(tiny[0], 0);
oakcommon_commandlinepositionalargument_free(&mut pos);
@@ -737,7 +849,11 @@ fn positional_get_setting_exact_fit_writes_full_string() {
OAKCOMMON_OK
);
let mut buf = [0xFFu8; 7];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, 7);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
7,
);
assert_eq!(r, 7);
assert_eq!(buf[6], 0);
let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
@@ -755,7 +871,11 @@ fn positional_get_setting_non_null_buf_size_zero_writes_nothing() {
OAKCOMMON_OK
);
let mut buf = [0xFFu8; 8];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, 0);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
0,
);
assert_eq!(r, 7);
assert_eq!(buf, [0xFFu8; 8]);
oakcommon_commandlinepositionalargument_free(&mut pos);
@@ -767,7 +887,11 @@ fn positional_get_setting_unset_returns_empty_string() {
let mut p = new_parser();
let mut pos = register_positional(dup(&p), "input");
let mut buf = [0xFFu8; 1];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, 1);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
1,
);
assert_eq!(r, 1);
assert_eq!(buf[0], 0);
oakcommon_commandlinepositionalargument_free(&mut pos);
@@ -786,7 +910,11 @@ fn positional_get_setting_negative_buf_size_is_invalid() {
let mut p = new_parser();
let mut pos = register_positional(dup(&p), "input");
let mut buf = [0u8; 4];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, -1);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
-1,
);
assert_eq!(r, OAKCOMMON_E_INVALID);
let r2 = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), null_mut(), -1);
assert_eq!(r2, OAKCOMMON_E_INVALID);
@@ -812,11 +940,16 @@ fn positional_get_setting_null_buf_with_size_is_invalid() {
fn positional_set_setting_success() {
let mut p = new_parser();
let mut pos = register_positional(dup(&p), "input");
let r = oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("file.mp4").as_ptr());
let r =
oakcommon_commandlinepositionalargument_set_setting(dup(&pos), c_str("file.mp4").as_ptr());
assert_eq!(r, OAKCOMMON_OK);
// Round-trip through the two-stage getter.
let mut buf = vec![0u8; 9];
let r = oakcommon_commandlinepositionalargument_get_setting(dup(&pos), buf.as_mut_ptr() as *mut c_char, 9);
let r = oakcommon_commandlinepositionalargument_get_setting(
dup(&pos),
buf.as_mut_ptr() as *mut c_char,
9,
);
assert_eq!(r, 9);
let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
assert_eq!(s.to_str().unwrap(), "file.mp4");
+45 -15
View File
@@ -89,10 +89,8 @@ impl Drop for HandlerGuard {
/// 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 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());
@@ -113,7 +111,9 @@ unsafe extern "C" fn record_handler(
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();
let s = unsafe { CStr::from_ptr(title) }
.to_string_lossy()
.into_owned();
*HANDLER_TITLE.lock().unwrap() = Some(s);
}
}
@@ -140,7 +140,10 @@ fn config_save_load_roundtrip() {
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");
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
"persisted"
);
});
}
@@ -258,7 +261,10 @@ fn config_get_exact_fit_buffer() {
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);
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
value
);
}
/// A missing key yields `OAKCOMMON_E_NOT_FOUND`.
@@ -311,7 +317,10 @@ fn config_set_get_int_roundtrip() {
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");
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
"42"
);
}
/// INT getter fallback paths: absent key, wrong type, null key.
@@ -350,7 +359,10 @@ fn config_set_get_int64_roundtrip() {
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(), missing.as_ptr(), -99),
-99
);
assert_eq!(oakcommon_config_get_int64(null(), null(), -99), -99);
}
@@ -368,8 +380,14 @@ fn config_set_get_double_roundtrip() {
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(), 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);
}
@@ -387,13 +405,19 @@ fn config_set_get_bool_roundtrip() {
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");
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");
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap(),
"false"
);
}
/// BOOL getter fallback paths: absent key, wrong type, null key.
@@ -429,7 +453,10 @@ fn config_group_prefixes_keys() {
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_get_int(group.as_ptr(), key.as_ptr(), -1),
5
);
assert_eq!(oakcommon_config_entry_type(group.as_ptr(), key.as_ptr()), 2);
}
@@ -462,7 +489,10 @@ fn config_entry_type_missing_and_invalid() {
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(), null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_config_entry_type(null(), cstr("").as_ptr()),
OAKCOMMON_E_INVALID
+97 -25
View File
@@ -47,13 +47,22 @@ mod ffi_ffmpegutils_tests {
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_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_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
@@ -102,11 +111,20 @@ mod ffi_ffmpegutils_tests {
#[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!(
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!(
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!(
oakcommon_ffmpegutils_get_compatible_pixel_format(-1, &mut out),
OAKCOMMON_OK
); // invalid
assert_eq!(out, -1);
}
@@ -115,13 +133,25 @@ mod ffi_ffmpegutils_tests {
#[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!(
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!(
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!(
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!(
oakcommon_ffmpegutils_get_ffmpeg_pixel_format(1, 3, &mut out),
OAKCOMMON_OK
); // U10 RGB
assert_eq!(out, -1); // no bridge format
}
@@ -129,13 +159,25 @@ mod ffi_ffmpegutils_tests {
#[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!(
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!(
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!(
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!(
oakcommon_ffmpegutils_get_native_sample_format(999, &mut out),
OAKCOMMON_OK
);
assert_eq!(out, -1); // unknown -> invalid
}
@@ -143,11 +185,20 @@ mod ffi_ffmpegutils_tests {
#[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!(
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!(
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!(
oakcommon_ffmpegutils_get_ffmpeg_sample_format(-1, &mut out),
OAKCOMMON_OK
); // invalid
assert_eq!(out, -1);
}
@@ -156,20 +207,41 @@ mod ffi_ffmpegutils_tests {
#[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!(
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!(
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!(
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!(
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!(
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
// 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!(
oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(-1, &mut out),
OAKCOMMON_OK
); // none
assert_eq!(out, -1);
}
}
+229 -57
View File
@@ -99,18 +99,30 @@ fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &
// 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");
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!(
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!(
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));
@@ -121,7 +133,10 @@ fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &
/// 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);
assert_eq!(
oakcommon_debug_log(0, std::ptr::null()),
OAKCOMMON_E_INVALID
);
}
/// Any non-null message returns `OK`, including out-of-range level codes
@@ -139,11 +154,26 @@ fn debug_log_returns_ok_for_any_level() {
/// 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");
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`
@@ -176,7 +206,10 @@ fn log_set_get_level_roundtrip_and_invalid() {
/// 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);
assert_eq!(
oakcommon_log_get_level(std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
// ---- misc: decibel / lerp ----
@@ -196,7 +229,10 @@ fn decibel_from_linear_known_values() {
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}");
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.
@@ -220,13 +256,25 @@ fn decibel_to_linear_known_values() {
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!(
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!(
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_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_eq!(
oakcommon_decibel_from_logarithmic(0.5, &mut out),
OAKCOMMON_OK
);
assert_close(out, -16.45, 0.05);
}
@@ -235,11 +283,20 @@ fn decibel_from_logarithmic_known_values() {
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!(
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_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_eq!(
oakcommon_decibel_to_logarithmic(-120.0, &mut out),
OAKCOMMON_OK
);
assert_close(out, 4.605e-6, 1e-9);
}
@@ -248,9 +305,15 @@ fn decibel_to_logarithmic_known_values() {
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!(
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_eq!(
oakcommon_decibel_linear_to_logarithmic(1.0, &mut out),
OAKCOMMON_OK
);
assert_close(out, 0.99, 1e-9);
}
@@ -259,11 +322,20 @@ fn decibel_linear_to_logarithmic_known_values() {
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!(
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!(
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_eq!(
oakcommon_decibel_logarithmic_to_linear(0.99, &mut out),
OAKCOMMON_OK
);
assert_close(out, 1.0, 1e-6);
}
@@ -285,13 +357,34 @@ fn lerp_known_values() {
/// 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);
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 ----
@@ -311,12 +404,30 @@ fn drop_workflow_behavior_is_valid() {
/// 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");
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
@@ -333,7 +444,10 @@ fn power_ceil_to_power_of_2() {
(9, 16),
(0x8000_0001, 0),
] {
assert_eq!(oakcommon_power_ceil_to_power_of_2(input, &mut out), OAKCOMMON_OK);
assert_eq!(
oakcommon_power_ceil_to_power_of_2(input, &mut out),
OAKCOMMON_OK
);
assert_eq!(out, expected, "ceil({input})");
}
assert_eq!(
@@ -354,7 +468,10 @@ fn power_floor_to_power_of_2() {
(9, 8),
(0x8000_0000, 0x8000_0000),
] {
assert_eq!(oakcommon_power_floor_to_power_of_2(input, &mut out), OAKCOMMON_OK);
assert_eq!(
oakcommon_power_floor_to_power_of_2(input, &mut out),
OAKCOMMON_OK
);
assert_eq!(out, expected, "floor({input})");
}
assert_eq!(
@@ -402,21 +519,48 @@ fn current_set_get_all_slots_roundtrip() {
// 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_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_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!(
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!(
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!(
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!(
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.
@@ -435,11 +579,26 @@ fn current_set_get_all_slots_roundtrip() {
);
// 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_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());
}
@@ -452,7 +611,11 @@ fn current_set_destroys_replaced_pointer() {
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_current_set_video_params(
dup(&h),
0xAAAAusize as *mut c_void,
Some(count_destroy)
),
OAKCOMMON_OK
);
assert_eq!(DESTROY_COUNT.load(Ordering::SeqCst), base);
@@ -465,7 +628,10 @@ fn current_set_destroys_replaced_pointer() {
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!(
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.
@@ -474,7 +640,10 @@ fn current_set_destroys_replaced_pointer() {
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_eq!(
oakcommon_current_get_video_params(dup(&h), &mut got),
OAKCOMMON_OK
);
assert!(got.is_null());
}
@@ -485,7 +654,10 @@ fn current_is_interactive_writes_one() {
let h = oakcommon_current_instance();
let mut out = 0i32;
assert_eq!(oakcommon_current_is_interactive(dup(&h), &mut out), OAKCOMMON_OK);
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()),
+236 -54
View File
@@ -56,8 +56,14 @@ fn make_populated() -> CHandle {
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);
assert_eq!(
oakcommon_subtitleparams_set_stream_index(dup(&h), 2),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_set_enabled(dup(&h), 0),
OAKCOMMON_OK
);
h
}
@@ -92,18 +98,30 @@ fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &
// 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");
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!(
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!(
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));
@@ -145,14 +163,26 @@ 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!(
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!(
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_set_stream_index(CHandle::null(), 1),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_stream_index(CHandle::null(), &mut si),
OAKCOMMON_E_INVALID
@@ -170,19 +200,37 @@ fn enabled_roundtrip() {
let h = make();
let mut en = -1i32;
assert_eq!(oakcommon_subtitleparams_get_enabled(dup(&h), &mut en), OAKCOMMON_OK);
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!(
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!(
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_set_enabled(CHandle::null(), 1),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_enabled(CHandle::null(), &mut en),
OAKCOMMON_E_INVALID
@@ -205,21 +253,42 @@ fn empty_set_defaults() {
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(oakcommon_subtitleparams_is_valid(dup(&h), &mut v), OAKCOMMON_OK);
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!(
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!(
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_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_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
@@ -243,17 +312,32 @@ fn add_subtitle_and_query() {
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(oakcommon_subtitleparams_is_valid(dup(&h), &mut v), OAKCOMMON_OK);
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!(
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!(
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!(
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!(
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));
@@ -262,7 +346,10 @@ fn add_subtitle_and_query() {
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!(
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));
}
@@ -273,7 +360,14 @@ fn add_subtitle_and_query() {
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_subtitleparams_add_subtitle(
CHandle::null(),
0,
1,
1,
1,
to_cstring("x").as_ptr()
),
OAKCOMMON_E_INVALID
);
assert_eq!(
@@ -287,7 +381,10 @@ fn add_subtitle_failures() {
OAKCOMMON_OK
);
let mut c = -1i32;
assert_eq!(oakcommon_subtitleparams_count(dup(&h), &mut c), OAKCOMMON_OK);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 1);
}
@@ -301,31 +398,68 @@ fn get_subtitle_out_of_range() {
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);
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(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_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_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_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_subtitleparams_get_subtitle(
dup(&h),
0,
&mut n,
&mut d,
&mut v,
std::ptr::null_mut()
),
OAKCOMMON_E_INVALID
);
}
@@ -341,17 +475,29 @@ fn clear() {
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!(
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!(
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!(
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);
assert_eq!(
oakcommon_subtitleparams_clear(CHandle::null()),
OAKCOMMON_E_INVALID
);
}
// ---- String getters ----
@@ -444,18 +590,33 @@ fn load_xml() {
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);
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!(
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!(
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!(
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_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",
@@ -463,7 +624,10 @@ fn load_xml() {
// Malformed / missing-root fragments fail with E_FAILED.
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("<subtitleparams><streamindex>").as_ptr()),
oakcommon_subtitleparams_load_xml(
dup(&h),
to_cstring("<subtitleparams><streamindex>").as_ptr()
),
OAKCOMMON_E_FAILED
);
assert_eq!(
@@ -479,7 +643,10 @@ fn load_xml() {
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);
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++
@@ -506,7 +673,10 @@ fn save_xml_two_stage() {
// 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_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,
@@ -527,13 +697,25 @@ fn save_xml_two_stage() {
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!(
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!(
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!(
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!(
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
+124 -29
View File
@@ -74,18 +74,30 @@ fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &
// 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");
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!(
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!(
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));
@@ -187,7 +199,10 @@ fn name_two_stage_getter() {
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size), "root");
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
@@ -198,7 +213,10 @@ fn name_two_stage_getter() {
#[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);
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
@@ -267,7 +285,10 @@ fn skip_current_element_skips_subtree() {
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_eq!(oakcommon_xml_reader_skip_current_element(dup(&r)), OAKCOMMON_OK);
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
@@ -382,12 +403,18 @@ 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!(
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!(
oakcommon_xml_reader_has_error(dup(&bad), &mut err),
OAKCOMMON_OK
);
assert_eq!(err, 1);
let mut found = -1i32;
assert_eq!(
@@ -398,7 +425,10 @@ fn has_error_flags_malformed_documents() {
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!(
oakcommon_xml_reader_has_error(dup(&empty), &mut err),
OAKCOMMON_OK
);
assert_eq!(err, 1);
assert_eq!(
@@ -490,11 +520,19 @@ fn writer_rejects_null_arguments() {
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_text_element(dup(&w), std::ptr::null(), to_cstring("b").as_ptr()),
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_xml_writer_write_text_element(
dup(&w),
to_cstring("a").as_ptr(),
std::ptr::null()
),
OAKCOMMON_E_INVALID
);
}
@@ -509,11 +547,19 @@ fn writer_builds_document_and_output_two_stage() {
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("a").as_ptr(), to_cstring("1").as_ptr()),
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_xml_writer_write_attribute(
dup(&w),
to_cstring("b").as_ptr(),
to_cstring("two").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
@@ -524,9 +570,18 @@ fn writer_builds_document_and_output_two_stage() {
),
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);
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.
@@ -538,11 +593,19 @@ fn writer_output_round_trips_through_reader() {
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("a").as_ptr(), to_cstring("1").as_ptr()),
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_xml_writer_write_attribute(
dup(&w),
to_cstring("b").as_ptr(),
to_cstring("two").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
@@ -553,7 +616,10 @@ fn writer_output_round_trips_through_reader() {
),
OAKCOMMON_OK
);
assert_eq!(oakcommon_xml_writer_write_end_element(dup(&w)), 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);
@@ -569,7 +635,10 @@ fn writer_output_round_trips_through_reader() {
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size), "root");
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
@@ -593,16 +662,29 @@ fn writer_output_round_trips_through_reader() {
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_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_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");
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"/>`.
@@ -614,11 +696,21 @@ fn writer_self_closing_empty_element() {
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("k").as_ptr(), to_cstring("v").as_ptr()),
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"/>"#);
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
@@ -646,7 +738,10 @@ fn writer_escapes_text_and_attributes() {
),
OAKCOMMON_OK
);
assert_eq!(oakcommon_xml_writer_write_end_element(dup(&w)), 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 &amp; bye &lt;there&gt;</e><a q="x&quot;y&amp;z"/>"#,
+26 -10
View File
@@ -68,7 +68,10 @@ fn ocio_roles_and_canonical_names() {
assert!(!roles.is_empty(), "config should define roles");
eprintln!("roles: {:?}", roles);
assert!(config.has_role("scene_linear").unwrap(), "scene_linear role should exist");
assert!(
config.has_role("scene_linear").unwrap(),
"scene_linear role should exist"
);
assert!(config.has_role("default").unwrap());
assert!(!config.has_role("no_such_role").unwrap());
@@ -101,8 +104,16 @@ fn ocio_processor_apply_rgba() {
let processor = config.processor("Linear", "sRGB OETF").unwrap();
let mut px = [0.18f32, 0.18f32, 0.18f32, 1.0f32];
processor.apply_rgba(&mut px).unwrap();
assert!(px[0] > 0.18f32, "sRGB OETF should lift 0.18 linear, got {}", px[0]);
assert!(px[0] < 1.0f32 + 1e-6, "sRGB OETF output should be <= 1.0, got {}", px[0]);
assert!(
px[0] > 0.18f32,
"sRGB OETF should lift 0.18 linear, got {}",
px[0]
);
assert!(
px[0] < 1.0f32 + 1e-6,
"sRGB OETF output should be <= 1.0, got {}",
px[0]
);
assert!(px.iter().all(|v| v.is_finite()));
// Display-referred path: scene_linear -> default sRGB view.
@@ -112,7 +123,11 @@ fn ocio_processor_apply_rgba() {
let mut px = [0.18f32, 0.18f32, 0.18f32, 1.0f32];
processor.apply_rgba(&mut px).unwrap();
assert!(px.iter().all(|v| v.is_finite()));
assert!(px[0] > 0.18f32, "display processor should also lift 0.18, got {}", px[0]);
assert!(
px[0] > 0.18f32,
"display processor should also lift 0.18, got {}",
px[0]
);
}
#[test]
@@ -128,12 +143,16 @@ fn ocio_error_paths() {
let config = OcioConfig::from_file(&path).unwrap();
// Unknown destination color space.
let err = config.processor("Linear", "No Such Color Space").unwrap_err();
let err = config
.processor("Linear", "No Such Color Space")
.unwrap_err();
eprintln!("unknown colorspace error: {err:?}");
assert!(matches!(err, Error::Failed(_)));
// Unknown display/view.
let err = config.display_processor("Linear", "No Such Display", "No View").unwrap_err();
let err = config
.display_processor("Linear", "No Such Display", "No View")
.unwrap_err();
eprintln!("unknown display error: {err:?}");
assert!(matches!(err, Error::Failed(_)));
}
@@ -150,10 +169,7 @@ fn image_f32_round_trip() {
let h = 2;
let c = 4;
let pixels: Vec<f32> = vec![
0.0, 0.25, 0.5, 1.0,
0.75, 0.5, 0.25, 1.0,
1.0, 0.0, 0.5, 0.0,
0.125, 0.625, 0.875, 1.0,
0.0, 0.25, 0.5, 1.0, 0.75, 0.5, 0.25, 1.0, 1.0, 0.0, 0.5, 0.0, 0.125, 0.625, 0.875, 1.0,
];
write_image_f32(&path_str, w, h, c, &pixels).expect("write should succeed");
+2 -1
View File
@@ -58,7 +58,8 @@ impl CHandle {
release: None,
abi_version: 0,
}
} /// Whether this is the empty (zero) handle.
}
/// Whether this is the empty (zero) handle.
pub fn is_null(&self) -> bool {
self.ctx.is_null()
}
+322 -316
View File
@@ -25,10 +25,16 @@ const REDUCE_MAX: i128 = i32::MAX as i128;
/// because the reduce cap is `INT_MAX`, `INT_MIN` reduces to `-2147483647/1`
/// (not `-2147483648/1`). Arithmetic treats this value (and its positive
/// counterpart) as a sentinel that propagates NaN.
const RATIONAL_MIN: Rational = Rational { num: -2147483647, den: 1 };
const RATIONAL_MIN: Rational = Rational {
num: -2147483647,
den: 1,
};
/// `RATIONAL_MAX` (`Rational(INT_MAX)`, i.e. `2147483647/1`).
const RATIONAL_MAX: Rational = Rational { num: 2147483647, den: 1 };
const RATIONAL_MAX: Rational = Rational {
num: 2147483647,
den: 1,
};
/// A rational number, always kept reduced with a non-negative
/// denominator (mirrors `olive::core::Rational`).
@@ -44,28 +50,28 @@ const RATIONAL_MAX: Rational = Rational { num: 2147483647, den: 1 };
/// project XML ("0/0", RATIONAL_MIN/MAX sentinels).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct Rational {
num: i64,
den: i64,
num: i64,
den: i64,
}
/// Signed Euclidean GCD on absolute values (mirrors the C++
/// `i64_gcd`). Computed in `i128` so that `i64::MIN`-class inputs
/// cannot overflow when negated.
fn i64_gcd(mut a: i128, mut b: i128) -> i128 {
if a < 0 {
a = -a;
}
if b < 0 {
b = -b;
}
if a < 0 {
a = -a;
}
if b < 0 {
b = -b;
}
while b != 0 {
let t = a % b;
a = b;
b = t;
}
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
a
}
/// Reduce `num`/`den` in place so that `|num| <= max` and `den <= max`,
@@ -73,409 +79,409 @@ fn i64_gcd(mut a: i128, mut b: i128) -> i128 {
/// ported from FFmpeg's `av_reduce`). Implemented in `i128` so the
/// intermediate products never overflow for any `i64` input.
fn reduce_fraction(num: &mut i128, den: &mut i128, max: i128) {
if *den == 0 {
*num = 0;
return;
}
if *den == 0 {
*num = 0;
return;
}
let sign = (*num < 0) != (*den < 0);
let sign = (*num < 0) != (*den < 0);
let gcd = i64_gcd(*num, *den);
if gcd != 0 {
*num = if *num < 0 { -*num } else { *num } / gcd;
*den = if *den < 0 { -*den } else { *den } / gcd;
}
let gcd = i64_gcd(*num, *den);
if gcd != 0 {
*num = if *num < 0 { -*num } else { *num } / gcd;
*den = if *den < 0 { -*den } else { *den } / gcd;
}
if *num <= max && *den <= max {
*num = if sign { -*num } else { *num };
return;
}
if *num <= max && *den <= max {
*num = if sign { -*num } else { *num };
return;
}
// Continued fraction approximation (FFmpeg's av_reduce).
let mut a0n: i128 = 0;
let mut a0d: i128 = 1;
let mut a1n: i128 = 1;
let mut a1d: i128 = 0;
// Continued fraction approximation (FFmpeg's av_reduce).
let mut a0n: i128 = 0;
let mut a0d: i128 = 1;
let mut a1n: i128 = 1;
let mut a1d: i128 = 0;
let mut n = *num;
let mut d = *den;
let mut n = *num;
let mut d = *den;
while d != 0 {
let x = n / d;
let next_den = n - d * x;
let a2n = x * a1n + a0n;
let a2d = x * a1d + a0d;
while d != 0 {
let x = n / d;
let next_den = n - d * x;
let a2n = x * a1n + a0n;
let a2d = x * a1d + a0d;
if a2n > max || a2d > max {
let mut x = x;
if a1n != 0 {
x = (max - a0n) / a1n;
}
if a1d != 0 && (max - a0d) / a1d < x {
x = (max - a0d) / a1d;
}
if a2n > max || a2d > max {
let mut x = x;
if a1n != 0 {
x = (max - a0n) / a1n;
}
if a1d != 0 && (max - a0d) / a1d < x {
x = (max - a0d) / a1d;
}
if d * (2 * x * a1d + a0d) > n * a1d {
a1n = x * a1n + a0n;
a1d = x * a1d + a0d;
}
break;
}
if d * (2 * x * a1d + a0d) > n * a1d {
a1n = x * a1n + a0n;
a1d = x * a1d + a0d;
}
break;
}
a0n = a1n;
a0d = a1d;
a1n = a2n;
a1d = a2d;
n = d;
d = next_den;
}
a0n = a1n;
a0d = a1d;
a1n = a2n;
a1d = a2d;
n = d;
d = next_den;
}
*num = if sign { -a1n } else { a1n };
*den = a1d;
*num = if sign { -a1n } else { a1n };
*den = a1d;
}
/// C `frexp`: split into mantissa in [0.5, 1) and base-2 exponent.
/// Only used by `from_double`; NaN/inf/zero pass through with exp 0.
fn frexp(x: f64, exp: &mut i32) -> f64 {
if x == 0.0 || x.is_nan() || x.is_infinite() {
*exp = 0;
return x;
}
let bits = x.to_bits();
let raw = ((bits >> 52) & 0x7ff) as i32;
if raw == 0 {
// Subnormal: scale up into the normal range first.
let scaled = x * 9007199254740992.0; // 2^53
let mut e = 0;
let m = frexp(scaled, &mut e);
*exp = e - 53;
return m;
}
*exp = raw - 1022;
f64::from_bits((bits & !(0x7ffu64 << 52)) | (1022u64 << 52))
if x == 0.0 || x.is_nan() || x.is_infinite() {
*exp = 0;
return x;
}
let bits = x.to_bits();
let raw = ((bits >> 52) & 0x7ff) as i32;
if raw == 0 {
// Subnormal: scale up into the normal range first.
let scaled = x * 9007199254740992.0; // 2^53
let mut e = 0;
let m = frexp(scaled, &mut e);
*exp = e - 53;
return m;
}
*exp = raw - 1022;
f64::from_bits((bits & !(0x7ffu64 << 52)) | (1022u64 << 52))
}
/// Apply C++ `fix_signs`: negative denominators are normalized by
/// flipping both signs; `0/0` stays as the NaN sentinel; a zero
/// numerator becomes `0/1`.
fn fix_signs(num: &mut i64, den: &mut i64) {
if *den < 0 {
*den = -*den;
*num = -*num;
} else if *den == 0 {
*num = 0;
} else if *num == 0 {
*den = 1;
}
if *den < 0 {
*den = -*den;
*num = -*num;
} else if *den == 0 {
*num = 0;
} else if *num == 0 {
*den = 1;
}
}
/// Build a rational from already-reduced `i128` values, applying
/// `fix_signs` and narrowing to `i64` (safe: `reduce_fraction` caps at
/// `i32::MAX`).
fn from_reduced(num: i128, den: i128) -> Rational {
let mut num = num as i64;
let mut den = den as i64;
fix_signs(&mut num, &mut den);
Rational { num, den }
let mut num = num as i64;
let mut den = den as i64;
fix_signs(&mut num, &mut den);
Rational { num, den }
}
/// Compare two fractions exactly (C++ `compare_fractions`). Non-NaN
/// inputs yield `-1`/`0`/`1`; the `0/0` cases return `i32::MIN`
/// (meaningless, never used for total ordering).
fn compare_fractions(an: i64, ad: i64, bn: i64, bd: i64) -> i32 {
let tmp = an as i128 * bd as i128 - bn as i128 * ad as i128;
let tmp = an as i128 * bd as i128 - bn as i128 * ad as i128;
if tmp != 0 {
// C++: `((tmp ^ ad ^ bd) >> 63) | 1` == sign of tmp (dens are >= 0).
if tmp > 0 {
1
} else {
-1
}
} else if bd != 0 && ad != 0 {
0
} else if an != 0 && bn != 0 {
((an >> 31) - (bn >> 31)) as i32
} else {
i32::MIN
}
if tmp != 0 {
// C++: `((tmp ^ ad ^ bd) >> 63) | 1` == sign of tmp (dens are >= 0).
if tmp > 0 {
1
} else {
-1
}
} else if bd != 0 && ad != 0 {
0
} else if an != 0 && bn != 0 {
((an >> 31) - (bn >> 31)) as i32
} else {
i32::MIN
}
}
/// Parse a single C++ `strtol`-style integer (base 10); garbage or
/// empty input yields 0.
fn to_int(s: &str) -> i64 {
s.trim().parse::<i64>().unwrap_or(0)
s.trim().parse::<i64>().unwrap_or(0)
}
/// Rounding modes for the C++ `Timecode` conversion helpers.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Rounding {
Round,
Floor,
Round,
Floor,
}
/// C++ `Rational::flipped` as a free function: swap numerator and
/// denominator, then `fix_signs`. A null rational (0/0 or 0/n) is left
/// unchanged.
fn flipped(r: Rational) -> Rational {
if r.num == 0 {
return r;
}
let mut num = r.den;
let mut den = r.num;
fix_signs(&mut num, &mut den);
Rational { num, den }
if r.num == 0 {
return r;
}
let mut num = r.den;
let mut den = r.num;
fix_signs(&mut num, &mut den);
Rational { num, den }
}
/// C++ `Timecode::timestamp_to_time`: `timebase.num * ts / timebase.den`,
/// reduced against `INT_MAX`.
fn timestamp_to_time(ts: i64, timebase: Rational) -> Rational {
let mut num = timebase.num as i128 * ts as i128;
let mut den = timebase.den as i128;
reduce_fraction(&mut num, &mut den, REDUCE_MAX);
from_reduced(num, den)
let mut num = timebase.num as i128 * ts as i128;
let mut den = timebase.den as i128;
reduce_fraction(&mut num, &mut den, REDUCE_MAX);
from_reduced(num, den)
}
/// C++ `Timecode::time_to_timestamp` (any rounding mode), given an
/// explicit timebase.
pub(crate) fn time_to_timestamp_rnd(time: Rational, timebase: Rational, rnd: Rounding) -> i64 {
let d = time.to_f64() * flipped(timebase).to_f64();
let d = time.to_f64() * flipped(timebase).to_f64();
if d.is_nan() {
return 0;
}
if d.is_nan() {
return 0;
}
let eps = 0.000000000001;
let eps = 0.000000000001;
match rnd {
Rounding::Round => d.round() as i64,
Rounding::Floor => {
if d > d.ceil() - eps {
d.ceil() as i64
} else {
d.floor() as i64
}
}
}
match rnd {
Rounding::Round => d.round() as i64,
Rounding::Floor => {
if d > d.ceil() - eps {
d.ceil() as i64
} else {
d.floor() as i64
}
}
}
}
/// C++ `Timecode::snap_time_to_timebase` with the `k_floor` rounding
/// used by `TimeRangeListFrameIterator`.
pub(crate) fn snap_time_to_timebase(time: Rational, timebase: Rational) -> Rational {
let ts = time_to_timestamp_rnd(time, timebase, Rounding::Floor);
timestamp_to_time(ts, timebase)
let ts = time_to_timestamp_rnd(time, timebase, Rounding::Floor);
timestamp_to_time(ts, timebase)
}
impl Rational {
/// The invalid sentinel (C++ `Rational()`, i.e. 0/0).
pub const NULL: Rational = Rational { num: 0, den: 0 };
/// The invalid sentinel (C++ `Rational()`, i.e. 0/0).
pub const NULL: Rational = Rational { num: 0, den: 0 };
/// Construct reduced; `new(0, 0)` yields [`Rational::NULL`].
pub fn new(num: i64, den: i64) -> Rational {
let mut num = num;
let mut den = den;
fix_signs(&mut num, &mut den);
let mut num = num as i128;
let mut den = den as i128;
reduce_fraction(&mut num, &mut den, REDUCE_MAX);
Rational {
num: num as i64,
den: den as i64,
}
}
/// Construct reduced; `new(0, 0)` yields [`Rational::NULL`].
pub fn new(num: i64, den: i64) -> Rational {
let mut num = num;
let mut den = den;
fix_signs(&mut num, &mut den);
let mut num = num as i128;
let mut den = den as i128;
reduce_fraction(&mut num, &mut den, REDUCE_MAX);
Rational {
num: num as i64,
den: den as i64,
}
}
/// Numerator of the reduced form.
pub fn numerator(self) -> i64 {
self.num
}
/// Numerator of the reduced form.
pub fn numerator(self) -> i64 {
self.num
}
/// Denominator of the reduced form (0 for the null sentinel).
pub fn denominator(self) -> i64 {
self.den
}
/// Denominator of the reduced form (0 for the null sentinel).
pub fn denominator(self) -> i64 {
self.den
}
/// True for the null sentinel (`num == 0`, so 0/0 and 0/1).
pub fn is_null(self) -> bool {
self.num == 0
}
/// True for the null sentinel (`num == 0`, so 0/0 and 0/1).
pub fn is_null(self) -> bool {
self.num == 0
}
/// True for the NaN sentinel (`den == 0`, only 0/0 after
/// normalization). C++ `isNaN()`.
pub fn is_nan(self) -> bool {
self.den == 0
}
/// True for the NaN sentinel (`den == 0`, only 0/0 after
/// normalization). C++ `isNaN()`.
pub fn is_nan(self) -> bool {
self.den == 0
}
/// True when this value equals `RATIONAL_MIN` or `RATIONAL_MAX`
/// (the sentinels that propagate NaN through arithmetic in C++).
fn is_minmax(self) -> bool {
self == RATIONAL_MIN || self == RATIONAL_MAX
}
/// True when this value equals `RATIONAL_MIN` or `RATIONAL_MAX`
/// (the sentinels that propagate NaN through arithmetic in C++).
fn is_minmax(self) -> bool {
self == RATIONAL_MIN || self == RATIONAL_MAX
}
/// Parse the C++ text format; invalid input yields the null
/// sentinel (C++ `fromString` behavior).
pub fn from_string(s: &str) -> Rational {
let elements: Vec<&str> = s.split('/').collect();
match elements.len() {
1 => Rational::new(to_int(elements[0]), 1),
2 => Rational::new(to_int(elements[0]), to_int(elements[1])),
_ => Rational::NULL,
}
}
/// Parse the C++ text format; invalid input yields the null
/// sentinel (C++ `fromString` behavior).
pub fn from_string(s: &str) -> Rational {
let elements: Vec<&str> = s.split('/').collect();
match elements.len() {
1 => Rational::new(to_int(elements[0]), 1),
2 => Rational::new(to_int(elements[0]), to_int(elements[1])),
_ => Rational::NULL,
}
}
/// Format identical to C++ `toString()`.
pub fn to_display_string(self) -> String {
format!("{}/{}", self.num, self.den)
}
/// Format identical to C++ `toString()`.
pub fn to_display_string(self) -> String {
format!("{}/{}", self.num, self.den)
}
/// Truncating conversion to f64 (C++ `toDouble`).
pub fn to_f64(self) -> f64 {
if self.den != 0 {
self.num as f64 / self.den as f64
} else {
f64::NAN
}
}
/// Truncating conversion to f64 (C++ `toDouble`).
pub fn to_f64(self) -> f64 {
if self.den != 0 {
self.num as f64 / self.den as f64
} else {
f64::NAN
}
}
/// f64 → Rational (C++ `Rational::from_double`, continued-fraction
/// port of FFmpeg's `av_d2q`; NaN and |v| > INT_MAX+3 yield the
/// 0/0 NaN sentinel).
/// `// CPP-PARITY: core/src/util/rational.cpp:39`
pub fn from_double(value: f64) -> Rational {
if value.is_nan() || value.abs() > i32::MAX as f64 + 3.0 {
return Rational::NULL;
}
/// f64 → Rational (C++ `Rational::from_double`, continued-fraction
/// port of FFmpeg's `av_d2q`; NaN and |v| > INT_MAX+3 yield the
/// 0/0 NaN sentinel).
/// `// CPP-PARITY: core/src/util/rational.cpp:39`
pub fn from_double(value: f64) -> Rational {
if value.is_nan() || value.abs() > i32::MAX as f64 + 3.0 {
return Rational::NULL;
}
let mut exponent = 0;
let _ = frexp(value, &mut exponent);
exponent = (exponent - 1).max(0);
let den: i64 = 1i64 << (62 - exponent);
let num: i64 = (value * den as f64 + 0.5).floor() as i64;
let mut exponent = 0;
let _ = frexp(value, &mut exponent);
exponent = (exponent - 1).max(0);
let den: i64 = 1i64 << (62 - exponent);
let num: i64 = (value * den as f64 + 0.5).floor() as i64;
let mut rnum = num as i128;
let mut rden = den as i128;
reduce_fraction(&mut rnum, &mut rden, i32::MAX as i128);
let mut rnum = num as i128;
let mut rden = den as i128;
reduce_fraction(&mut rnum, &mut rden, i32::MAX as i128);
if (rnum == 0 || rden == 0) && value != 0.0 {
// Too small to represent above; retry at maximum precision.
rnum = (value * i64::MAX as f64) as i64 as i128;
rden = i64::MAX as i128;
reduce_fraction(&mut rnum, &mut rden, i32::MAX as i128);
}
if (rnum == 0 || rden == 0) && value != 0.0 {
// Too small to represent above; retry at maximum precision.
rnum = (value * i64::MAX as f64) as i64 as i128;
rden = i64::MAX as i128;
reduce_fraction(&mut rnum, &mut rden, i32::MAX as i128);
}
from_reduced(rnum, rden)
}
from_reduced(rnum, rden)
}
/// Frame-number conversion using this value as a timebase
/// (C++ `Timecode::time_to_timestamp` semantics, rounding mode
/// included).
pub fn time_to_timestamp(self, time: Rational) -> i64 {
time_to_timestamp_rnd(time, self, Rounding::Round)
}
/// Frame-number conversion using this value as a timebase
/// (C++ `Timecode::time_to_timestamp` semantics, rounding mode
/// included).
pub fn time_to_timestamp(self, time: Rational) -> i64 {
time_to_timestamp_rnd(time, self, Rounding::Round)
}
/// Inverse of [`Rational::time_to_timestamp`]
/// (C++ `Timecode::timestamp_to_time`).
pub fn timestamp_to_time(self, ts: i64) -> Rational {
timestamp_to_time(ts, self)
}
/// Inverse of [`Rational::time_to_timestamp`]
/// (C++ `Timecode::timestamp_to_time`).
pub fn timestamp_to_time(self, ts: i64) -> Rational {
timestamp_to_time(ts, self)
}
}
impl std::ops::Add for Rational {
type Output = Rational;
fn add(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128 + rhs.num as i128 * self.den as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
type Output = Rational;
fn add(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128 + rhs.num as i128 * self.den as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
}
impl std::ops::Sub for Rational {
type Output = Rational;
fn sub(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128 - rhs.num as i128 * self.den as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
type Output = Rational;
fn sub(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128 - rhs.num as i128 * self.den as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
}
impl std::ops::Mul for Rational {
type Output = Rational;
fn mul(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.num as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
type Output = Rational;
fn mul(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.num as i128;
let mut d = self.den as i128 * rhs.den as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
}
impl std::ops::Div for Rational {
type Output = Rational;
fn div(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128;
let mut d = self.den as i128 * rhs.num as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
type Output = Rational;
fn div(self, rhs: Rational) -> Rational {
if self.is_minmax() || rhs.is_minmax() {
return Rational::NULL;
}
if self.is_nan() {
return self;
}
if rhs.is_nan() {
return Rational::NULL;
}
let mut n = self.num as i128 * rhs.den as i128;
let mut d = self.den as i128 * rhs.num as i128;
reduce_fraction(&mut n, &mut d, REDUCE_MAX);
from_reduced(n, d)
}
}
impl PartialOrd for Rational {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Rational {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
// NaN (0/0) orders before everything and equals itself, keeping
// `Ord` consistent with the derived structural `Eq` (C++ makes
// 0/0 == 0/0 false, but this crate deliberately keeps Eq).
match (self.den == 0, other.den == 0) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
(false, false) => match compare_fractions(self.num, self.den, other.num, other.den) {
0 => Ordering::Equal,
1 => Ordering::Greater,
_ => Ordering::Less,
},
}
}
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
// NaN (0/0) orders before everything and equals itself, keeping
// `Ord` consistent with the derived structural `Eq` (C++ makes
// 0/0 == 0/0 false, but this crate deliberately keeps Eq).
match (self.den == 0, other.den == 0) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
(false, false) => match compare_fractions(self.num, self.den, other.num, other.den) {
0 => Ordering::Equal,
1 => Ordering::Greater,
_ => Ordering::Less,
},
}
}
}
+98 -98
View File
@@ -24,37 +24,37 @@
#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PixelFormat {
/// Invalid/unspecified.
Invalid = -1,
/// 8-bit unsigned per channel.
U8 = 0,
/// 10-bit unsigned per channel (packed).
U10 = 1,
/// 16-bit unsigned per channel.
U16 = 2,
/// 16-bit half float.
F16 = 3,
/// 32-bit float (primary pipeline format).
F32 = 4,
/// Invalid/unspecified.
Invalid = -1,
/// 8-bit unsigned per channel.
U8 = 0,
/// 10-bit unsigned per channel (packed).
U10 = 1,
/// 16-bit unsigned per channel.
U16 = 2,
/// 16-bit half float.
F16 = 3,
/// 32-bit float (primary pipeline format).
F32 = 4,
}
impl PixelFormat {
/// Bytes per channel (C++ `byte_count`; Invalid -> 0, U10 -> 4
/// since it is packed RGBA10A2 stored as 4 bytes per pixel).
pub fn bytes_per_channel(self) -> usize {
match self {
PixelFormat::Invalid => 0,
PixelFormat::U8 => 1,
PixelFormat::U10 => 4,
PixelFormat::U16 | PixelFormat::F16 => 2,
PixelFormat::F32 => 4,
}
}
/// Bytes per channel (C++ `byte_count`; Invalid -> 0, U10 -> 4
/// since it is packed RGBA10A2 stored as 4 bytes per pixel).
pub fn bytes_per_channel(self) -> usize {
match self {
PixelFormat::Invalid => 0,
PixelFormat::U8 => 1,
PixelFormat::U10 => 4,
PixelFormat::U16 | PixelFormat::F16 => 2,
PixelFormat::F32 => 4,
}
}
/// Bytes per pixel for `channels` (C++ `bytes_per_pixel`).
pub fn bytes_per_pixel(self, channels: usize) -> usize {
self.bytes_per_channel() * channels
}
/// Bytes per pixel for `channels` (C++ `bytes_per_pixel`).
pub fn bytes_per_pixel(self, channels: usize) -> usize {
self.bytes_per_channel() * channels
}
}
/// Audio sample format (values identical to
@@ -65,81 +65,81 @@ impl PixelFormat {
#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SampleFormat {
/// Invalid/unspecified.
Invalid = -1,
/// Unsigned 8-bit planar.
U8Planar = 0,
/// Signed 16-bit planar.
S16Planar = 1,
/// Signed 32-bit planar.
S32Planar = 2,
/// Signed 64-bit planar.
S64Planar = 3,
/// 32-bit float planar.
F32Planar = 4,
/// 64-bit float planar.
F64Planar = 5,
/// Unsigned 8-bit packed.
U8 = 6,
/// Signed 16-bit packed.
S16 = 7,
/// Signed 32-bit packed.
S32 = 8,
/// Signed 64-bit packed.
S64 = 9,
/// 32-bit float packed.
F32 = 10,
/// 64-bit float packed.
F64 = 11,
/// Invalid/unspecified.
Invalid = -1,
/// Unsigned 8-bit planar.
U8Planar = 0,
/// Signed 16-bit planar.
S16Planar = 1,
/// Signed 32-bit planar.
S32Planar = 2,
/// Signed 64-bit planar.
S64Planar = 3,
/// 32-bit float planar.
F32Planar = 4,
/// 64-bit float planar.
F64Planar = 5,
/// Unsigned 8-bit packed.
U8 = 6,
/// Signed 16-bit packed.
S16 = 7,
/// Signed 32-bit packed.
S32 = 8,
/// Signed 64-bit packed.
S64 = 9,
/// 32-bit float packed.
F32 = 10,
/// 64-bit float packed.
F64 = 11,
}
impl SampleFormat {
/// Bytes per sample (C++ `byte_count`; Invalid -> 0).
pub fn bytes_per_sample(self) -> usize {
match self {
SampleFormat::Invalid => 0,
SampleFormat::U8Planar | SampleFormat::U8 => 1,
SampleFormat::S16Planar | SampleFormat::S16 => 2,
SampleFormat::S32Planar
| SampleFormat::S32
| SampleFormat::F32Planar
| SampleFormat::F32 => 4,
SampleFormat::S64Planar
| SampleFormat::S64
| SampleFormat::F64Planar
| SampleFormat::F64 => 8,
}
}
/// Bytes per sample (C++ `byte_count`; Invalid -> 0).
pub fn bytes_per_sample(self) -> usize {
match self {
SampleFormat::Invalid => 0,
SampleFormat::U8Planar | SampleFormat::U8 => 1,
SampleFormat::S16Planar | SampleFormat::S16 => 2,
SampleFormat::S32Planar
| SampleFormat::S32
| SampleFormat::F32Planar
| SampleFormat::F32 => 4,
SampleFormat::S64Planar
| SampleFormat::S64
| SampleFormat::F64Planar
| SampleFormat::F64 => 8,
}
}
/// True for planar layouts (C++ `is_planar`).
pub fn is_planar(self) -> bool {
(self as i32) >= 0 && (self as i32) < 6
}
/// True for planar layouts (C++ `is_planar`).
pub fn is_planar(self) -> bool {
(self as i32) >= 0 && (self as i32) < 6
}
/// Packed counterpart of a planar format (and vice versa;
/// C++ `to_packed`/`to_planar`).
pub fn to_packed(self) -> SampleFormat {
match self {
SampleFormat::U8Planar => SampleFormat::U8,
SampleFormat::S16Planar => SampleFormat::S16,
SampleFormat::S32Planar => SampleFormat::S32,
SampleFormat::S64Planar => SampleFormat::S64,
SampleFormat::F32Planar => SampleFormat::F32,
SampleFormat::F64Planar => SampleFormat::F64,
other => other,
}
}
/// Packed counterpart of a planar format (and vice versa;
/// C++ `to_packed`/`to_planar`).
pub fn to_packed(self) -> SampleFormat {
match self {
SampleFormat::U8Planar => SampleFormat::U8,
SampleFormat::S16Planar => SampleFormat::S16,
SampleFormat::S32Planar => SampleFormat::S32,
SampleFormat::S64Planar => SampleFormat::S64,
SampleFormat::F32Planar => SampleFormat::F32,
SampleFormat::F64Planar => SampleFormat::F64,
other => other,
}
}
/// See [`SampleFormat::to_packed`].
pub fn to_planar(self) -> SampleFormat {
match self {
SampleFormat::U8 => SampleFormat::U8Planar,
SampleFormat::S16 => SampleFormat::S16Planar,
SampleFormat::S32 => SampleFormat::S32Planar,
SampleFormat::S64 => SampleFormat::S64Planar,
SampleFormat::F32 => SampleFormat::F32Planar,
SampleFormat::F64 => SampleFormat::F64Planar,
other => other,
}
}
/// See [`SampleFormat::to_packed`].
pub fn to_planar(self) -> SampleFormat {
match self {
SampleFormat::U8 => SampleFormat::U8Planar,
SampleFormat::S16 => SampleFormat::S16Planar,
SampleFormat::S32 => SampleFormat::S32Planar,
SampleFormat::S64 => SampleFormat::S64Planar,
SampleFormat::F32 => SampleFormat::F32Planar,
SampleFormat::F64 => SampleFormat::F64Planar,
other => other,
}
}
}
+187 -192
View File
@@ -22,117 +22,112 @@ use crate::rational::{self, Rational};
/// Half-open time range [in, out) — mirrors `olive::core::TimeRange`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct TimeRange {
in_: Rational,
out: Rational,
in_: Rational,
out: Rational,
}
impl TimeRange {
/// Construct and normalize (C++ ctor calls `normalize()`: if `out <
/// in` the two are swapped). The doc comment on the skeleton claimed
/// normalization is not performed; matching C++ takes precedence.
pub fn new(in_: Rational, out: Rational) -> TimeRange {
let mut r = TimeRange { in_, out };
r.normalize();
r
}
/// Construct and normalize (C++ ctor calls `normalize()`: if `out <
/// in` the two are swapped). The doc comment on the skeleton claimed
/// normalization is not performed; matching C++ takes precedence.
pub fn new(in_: Rational, out: Rational) -> TimeRange {
let mut r = TimeRange { in_, out };
r.normalize();
r
}
/// Inclusive start.
pub fn in_(&self) -> Rational {
self.in_
}
/// Inclusive start.
pub fn in_(&self) -> Rational {
self.in_
}
/// Exclusive end.
pub fn out(&self) -> Rational {
self.out
}
/// Exclusive end.
pub fn out(&self) -> Rational {
self.out
}
/// `out - in`. When either endpoint is a `RATIONAL_MIN/MAX`
/// sentinel the subtraction propagates NaN, matching C++ which
/// stores the same sentinel value for `length_`.
pub fn length(&self) -> Rational {
self.out - self.in_
}
/// `out - in`. When either endpoint is a `RATIONAL_MIN/MAX`
/// sentinel the subtraction propagates NaN, matching C++ which
/// stores the same sentinel value for `length_`.
pub fn length(&self) -> Rational {
self.out - self.in_
}
/// True when `t` lies in [in, out).
pub fn contains(&self, t: Rational) -> bool {
t >= self.in_ && t < self.out
}
/// True when `t` lies in [in, out).
pub fn contains(&self, t: Rational) -> bool {
t >= self.in_ && t < self.out
}
/// True when `self` contains `compare`, honoring inclusivity of the
/// in/out edges (C++ `TimeRange::contains(TimeRange)`).
fn contains_range(
&self,
compare: &TimeRange,
in_inclusive: bool,
out_inclusive: bool,
) -> bool {
let contains_in = if in_inclusive {
compare.in_ >= self.in_
} else {
compare.in_ > self.in_
};
let contains_out = if out_inclusive {
compare.out <= self.out
} else {
compare.out < self.out
};
contains_in && contains_out
}
/// True when `self` contains `compare`, honoring inclusivity of the
/// in/out edges (C++ `TimeRange::contains(TimeRange)`).
fn contains_range(&self, compare: &TimeRange, in_inclusive: bool, out_inclusive: bool) -> bool {
let contains_in = if in_inclusive {
compare.in_ >= self.in_
} else {
compare.in_ > self.in_
};
let contains_out = if out_inclusive {
compare.out <= self.out
} else {
compare.out < self.out
};
contains_in && contains_out
}
/// True when `self` and `a` overlap, honoring edge inclusivity
/// (C++ `TimeRange::overlaps_with`).
fn overlaps_with(&self, a: &TimeRange, in_inclusive: bool, out_inclusive: bool) -> bool {
let does_not_overlap_in = if in_inclusive {
a.out < self.in_
} else {
a.out <= self.in_
};
let does_not_overlap_out = if out_inclusive {
a.in_ > self.out
} else {
a.in_ >= self.out
};
!does_not_overlap_in && !does_not_overlap_out
}
/// True when `self` and `a` overlap, honoring edge inclusivity
/// (C++ `TimeRange::overlaps_with`).
fn overlaps_with(&self, a: &TimeRange, in_inclusive: bool, out_inclusive: bool) -> bool {
let does_not_overlap_in = if in_inclusive {
a.out < self.in_
} else {
a.out <= self.in_
};
let does_not_overlap_out = if out_inclusive {
a.in_ > self.out
} else {
a.in_ >= self.out
};
!does_not_overlap_in && !does_not_overlap_out
}
/// Intersection; empty when disjoint (C++ `intersected`).
///
/// Note: C++ normalizes the result, so disjoint inputs produce a
/// swapped (in > out) range rather than an "empty" marker; we match
/// that bit-for-bit.
pub fn intersected(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::max(self.in_, other.in_),
std::cmp::min(self.out, other.out),
)
}
/// Intersection; empty when disjoint (C++ `intersected`).
///
/// Note: C++ normalizes the result, so disjoint inputs produce a
/// swapped (in > out) range rather than an "empty" marker; we match
/// that bit-for-bit.
pub fn intersected(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::max(self.in_, other.in_),
std::cmp::min(self.out, other.out),
)
}
/// Union that also merges touching ranges (C++ `combined`).
pub fn combined(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::min(self.in_, other.in_),
std::cmp::max(self.out, other.out),
)
}
/// Union that also merges touching ranges (C++ `combined`).
pub fn combined(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::min(self.in_, other.in_),
std::cmp::max(self.out, other.out),
)
}
/// C++ `set_in` + `normalize`.
fn set_in(&mut self, in_: Rational) {
self.in_ = in_;
self.normalize();
}
/// C++ `set_in` + `normalize`.
fn set_in(&mut self, in_: Rational) {
self.in_ = in_;
self.normalize();
}
/// C++ `set_out` + `normalize`.
fn set_out(&mut self, out: Rational) {
self.out = out;
self.normalize();
}
/// C++ `set_out` + `normalize`.
fn set_out(&mut self, out: Rational) {
self.out = out;
self.normalize();
}
/// C++ `normalize`: swap if `out < in`.
fn normalize(&mut self) {
if self.out < self.in_ {
std::mem::swap(&mut self.out, &mut self.in_);
}
}
/// C++ `normalize`: swap if `out < in`.
fn normalize(&mut self) {
if self.out < self.in_ {
std::mem::swap(&mut self.out, &mut self.in_);
}
}
}
/// Normalized (sorted, non-overlapping) list of ranges — mirrors
@@ -140,115 +135,115 @@ impl TimeRange {
/// subtraction semantics.
#[derive(Clone, Debug, Default)]
pub struct TimeRangeList {
ranges: Vec<TimeRange>,
ranges: Vec<TimeRange>,
}
impl TimeRangeList {
/// Empty list.
pub fn new() -> Self {
TimeRangeList { ranges: Vec::new() }
}
/// Empty list.
pub fn new() -> Self {
TimeRangeList { ranges: Vec::new() }
}
/// True when any element fully contains `range` (C++
/// `TimeRangeList::contains`, inclusive edges).
fn contains_range(&self, range: &TimeRange) -> bool {
self.ranges
.iter()
.any(|r| r.contains_range(range, true, true))
}
/// True when any element fully contains `range` (C++
/// `TimeRangeList::contains`, inclusive edges).
fn contains_range(&self, range: &TimeRange) -> bool {
self.ranges
.iter()
.any(|r| r.contains_range(range, true, true))
}
/// Insert a range, merging overlaps and touching neighbors
/// (C++ `insert(TimeRange)`).
pub fn insert(&mut self, range: TimeRange) {
// If the list already fully contains this range, nothing to do.
if self.contains_range(&range) {
return;
}
/// Insert a range, merging overlaps and touching neighbors
/// (C++ `insert(TimeRange)`).
pub fn insert(&mut self, range: TimeRange) {
// If the list already fully contains this range, nothing to do.
if self.contains_range(&range) {
return;
}
let mut range = range;
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
if compare.overlaps_with(&range, true, true) {
range = compare.combined(&range);
self.ranges.remove(i);
} else {
i += 1;
}
}
let mut range = range;
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
if compare.overlaps_with(&range, true, true) {
range = compare.combined(&range);
self.ranges.remove(i);
} else {
i += 1;
}
}
self.ranges.push(range);
}
self.ranges.push(range);
}
/// Subtract a range (C++ `remove`, via `util_remove`).
pub fn remove(&mut self, range: TimeRange) {
let mut additions: Vec<TimeRange> = Vec::new();
/// Subtract a range (C++ `remove`, via `util_remove`).
pub fn remove(&mut self, range: TimeRange) {
let mut additions: Vec<TimeRange> = Vec::new();
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
if range.contains_range(&compare, true, true) {
// The removal range entirely encompasses this element.
self.ranges.remove(i);
} else if compare.contains_range(&range, false, false) {
// The removal range is strictly inside this element:
// split it into two.
let mut new_range = compare;
new_range.set_in(range.out);
let mut trimmed = compare;
trimmed.set_out(range.in_);
self.ranges[i] = trimmed;
additions.push(new_range);
break;
} else {
if compare.in_ < range.in_ && compare.out > range.in_ {
// This element's out overlaps the range's in: trim it.
self.ranges[i].set_out(range.in_);
} else if compare.in_ < range.out && compare.out > range.out {
// This element's in overlaps the range's out: trim it.
self.ranges[i].set_in(range.out);
}
i += 1;
}
}
if range.contains_range(&compare, true, true) {
// The removal range entirely encompasses this element.
self.ranges.remove(i);
} else if compare.contains_range(&range, false, false) {
// The removal range is strictly inside this element:
// split it into two.
let mut new_range = compare;
new_range.set_in(range.out);
let mut trimmed = compare;
trimmed.set_out(range.in_);
self.ranges[i] = trimmed;
additions.push(new_range);
break;
} else {
if compare.in_ < range.in_ && compare.out > range.in_ {
// This element's out overlaps the range's in: trim it.
self.ranges[i].set_out(range.in_);
} else if compare.in_ < range.out && compare.out > range.out {
// This element's in overlaps the range's out: trim it.
self.ranges[i].set_in(range.out);
}
i += 1;
}
}
self.ranges.extend(additions);
}
self.ranges.extend(additions);
}
/// Sorted ranges view.
pub fn ranges(&self) -> &[TimeRange] {
&self.ranges
}
/// Sorted ranges view.
pub fn ranges(&self) -> &[TimeRange] {
&self.ranges
}
/// True when the list has no ranges.
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
/// True when the list has no ranges.
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
/// Total covered duration (sum of each range's length).
pub fn total_length(&self) -> Rational {
let mut total = Rational::new(0, 1);
for r in &self.ranges {
total = total + r.length();
}
total
}
/// Total covered duration (sum of each range's length).
pub fn total_length(&self) -> Rational {
let mut total = Rational::new(0, 1);
for r in &self.ranges {
total = total + r.length();
}
total
}
/// First time covered by any range (C++ `in()` on the first range);
/// null rational when empty.
pub fn first(&self) -> Rational {
match self.ranges.first() {
Some(r) => r.in_(),
None => Rational::NULL,
}
}
/// First time covered by any range (C++ `in()` on the first range);
/// null rational when empty.
pub fn first(&self) -> Rational {
match self.ranges.first() {
Some(r) => r.in_(),
None => Rational::NULL,
}
}
/// Frame-accurate iteration helper: snap a time to the containing
/// frame grid of `timebase` (C++ TimeRangeListFrameIterator snap,
/// `k_floor` rounding).
pub fn snap(&self, time: Rational, timebase: Rational) -> Rational {
let _ = self; // self carries no state relevant to a single snap
rational::snap_time_to_timebase(time, timebase)
}
/// Frame-accurate iteration helper: snap a time to the containing
/// frame grid of `timebase` (C++ TimeRangeListFrameIterator snap,
/// `k_floor` rounding).
pub fn snap(&self, time: Rational, timebase: Rational) -> Rational {
let _ = self; // self carries no state relevant to a single snap
rational::snap_time_to_timebase(time, timebase)
}
}
+274 -255
View File
@@ -16,33 +16,32 @@
//! oakcore-rs contract tests. The oracle is the C++ oakcore behavior:
/// every case below names the C++ semantic it pins down.
use oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange, TimeRangeList};
/// Construction reduces (2/4 -> 1/2), normalizes sign (1/-2 -> -1/2),
/// and 0/0 is the null sentinel. C++: Rational ctor + reduced().
#[test]
fn rational_reduction_and_sentinel() {
assert_eq!(Rational::new(2, 4), Rational::new(1, 2));
assert_eq!(Rational::new(2, 4).numerator(), 1);
assert_eq!(Rational::new(2, 4).denominator(), 2);
assert_eq!(Rational::new(1, -2), Rational::new(-1, 2));
assert_eq!(Rational::new(1, -2).numerator(), -1);
assert_eq!(Rational::new(1, -2).denominator(), 2);
assert_eq!(Rational::new(2, 4), Rational::new(1, 2));
assert_eq!(Rational::new(2, 4).numerator(), 1);
assert_eq!(Rational::new(2, 4).denominator(), 2);
assert_eq!(Rational::new(1, -2), Rational::new(-1, 2));
assert_eq!(Rational::new(1, -2).numerator(), -1);
assert_eq!(Rational::new(1, -2).denominator(), 2);
// 0/0 is the null/invalid sentinel.
let n = Rational::new(0, 0);
assert!(n.is_null());
assert!(n.is_nan());
assert_eq!(n, Rational::NULL);
// 0/0 is the null/invalid sentinel.
let n = Rational::new(0, 0);
assert!(n.is_null());
assert!(n.is_nan());
assert_eq!(n, Rational::NULL);
// A zero numerator normalizes the denominator to 1 (0/5 -> 0/1).
let z = Rational::new(0, 5);
assert!(z.is_null());
assert!(!z.is_nan());
assert_eq!(z, Rational::new(0, 1));
// A zero numerator normalizes the denominator to 1 (0/5 -> 0/1).
let z = Rational::new(0, 5);
assert!(z.is_null());
assert!(!z.is_nan());
assert_eq!(z, Rational::new(0, 1));
assert_eq!(Rational::NULL, Rational::new(0, 0));
assert_eq!(Rational::NULL, Rational::new(0, 0));
}
/// Arithmetic matches C++ exactly, including 30000/1001-style video
@@ -50,271 +49,291 @@ fn rational_reduction_and_sentinel() {
/// division by zero yields the C++ result (null propagation).
#[test]
fn rational_arithmetic_video_rates() {
assert_eq!(
Rational::new(1001, 30000) * Rational::new(30000, 1001),
Rational::new(1, 1)
);
assert_eq!(Rational::new(1, 3) + Rational::new(1, 6), Rational::new(1, 2));
assert_eq!(Rational::new(1, 2) - Rational::new(1, 3), Rational::new(1, 6));
assert_eq!(Rational::new(2, 3) * Rational::new(3, 4), Rational::new(1, 2));
assert_eq!(
Rational::new(1001, 30000) * Rational::new(30000, 1001),
Rational::new(1, 1)
);
assert_eq!(
Rational::new(1, 3) + Rational::new(1, 6),
Rational::new(1, 2)
);
assert_eq!(
Rational::new(1, 2) - Rational::new(1, 3),
Rational::new(1, 6)
);
assert_eq!(
Rational::new(2, 3) * Rational::new(3, 4),
Rational::new(1, 2)
);
// Division by a zero-value rational yields 0/0 (NaN): the denominator
// becomes 0 and reduce_fraction forces the numerator to 0.
let d = Rational::new(1, 1) / Rational::new(0, 1);
assert!(d.is_nan());
assert!(d.is_null());
assert_eq!(d, Rational::NULL);
// Division by a zero-value rational yields 0/0 (NaN): the denominator
// becomes 0 and reduce_fraction forces the numerator to 0.
let d = Rational::new(1, 1) / Rational::new(0, 1);
assert!(d.is_nan());
assert!(d.is_null());
assert_eq!(d, Rational::NULL);
// 0/0 on the left propagates the (unchanged) NaN self.
assert_eq!(Rational::NULL + Rational::new(1, 2), Rational::NULL);
// 0/0 on the right yields NULL for every operator.
assert_eq!(Rational::new(1, 2) + Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) - Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) * Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) / Rational::NULL, Rational::NULL);
// 0/0 on the left propagates the (unchanged) NaN self.
assert_eq!(Rational::NULL + Rational::new(1, 2), Rational::NULL);
// 0/0 on the right yields NULL for every operator.
assert_eq!(Rational::new(1, 2) + Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) - Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) * Rational::NULL, Rational::NULL);
assert_eq!(Rational::new(1, 2) / Rational::NULL, Rational::NULL);
}
/// from_string/to_string round-trip incl. sentinel spellings used by
/// project XML ("0/0", RATIONAL_MIN/MAX); garbage input -> null.
#[test]
fn rational_string_roundtrip() {
assert_eq!(Rational::from_string("0/0"), Rational::NULL);
assert_eq!(
Rational::from_string("2147483647"),
Rational::new(2147483647, 1)
);
assert_eq!(
Rational::from_string("-2147483647/1"),
Rational::new(-2147483647, 1)
);
assert_eq!(Rational::from_string("0/0"), Rational::NULL);
assert_eq!(
Rational::from_string("2147483647"),
Rational::new(2147483647, 1)
);
assert_eq!(
Rational::from_string("-2147483647/1"),
Rational::new(-2147483647, 1)
);
// Garbage single token parses as 0 -> 0/1 (null but not NaN).
let g = Rational::from_string("abc");
assert!(g.is_null());
assert!(!g.is_nan());
assert_eq!(g, Rational::new(0, 1));
assert_eq!(g.to_display_string(), "0/1");
// Garbage single token parses as 0 -> 0/1 (null but not NaN).
let g = Rational::from_string("abc");
assert!(g.is_null());
assert!(!g.is_nan());
assert_eq!(g, Rational::new(0, 1));
assert_eq!(g.to_display_string(), "0/1");
assert_eq!(Rational::new(30000, 1001).to_display_string(), "30000/1001");
assert_eq!(Rational::new(30000, 1001).to_display_string(), "30000/1001");
// More than two '/' elements -> null sentinel.
assert_eq!(Rational::from_string("a/b/c"), Rational::NULL);
// More than two '/' elements -> null sentinel.
assert_eq!(Rational::from_string("a/b/c"), Rational::NULL);
}
/// Ordering across denominators (1/3 vs 1001/3000) and equality of
/// differently-reduced equal values.
#[test]
fn rational_ordering() {
assert!(Rational::new(1, 3) < Rational::new(1001, 3000));
assert!(Rational::new(1, 3) > Rational::new(1, 4));
assert_eq!(Rational::new(1, 3), Rational::new(2, 6));
assert!(Rational::new(1, 2) > Rational::new(1, 3));
assert!(Rational::new(1, 3) < Rational::new(1001, 3000));
assert!(Rational::new(1, 3) > Rational::new(1, 4));
assert_eq!(Rational::new(1, 3), Rational::new(2, 6));
assert!(Rational::new(1, 2) > Rational::new(1, 3));
// NaN orders before everything and equals itself.
assert!(Rational::NULL < Rational::new(1, 1));
assert!(Rational::new(1, 1) > Rational::NULL);
assert_eq!(Rational::NULL, Rational::NULL);
// NaN orders before everything and equals itself.
assert!(Rational::NULL < Rational::new(1, 1));
assert!(Rational::new(1, 1) > Rational::NULL);
assert_eq!(Rational::NULL, Rational::NULL);
// Total order sorts a mixed list.
let mut v = vec![Rational::new(1, 2), Rational::new(1, 4), Rational::new(1, 3)];
v.sort();
assert_eq!(
v,
vec![Rational::new(1, 4), Rational::new(1, 3), Rational::new(1, 2)]
);
// Total order sorts a mixed list.
let mut v = vec![
Rational::new(1, 2),
Rational::new(1, 4),
Rational::new(1, 3),
];
v.sort();
assert_eq!(
v,
vec![
Rational::new(1, 4),
Rational::new(1, 3),
Rational::new(1, 2)
]
);
}
/// time_to_timestamp/timestamp_to_time match C++ Timecode rounding
/// (half-away-from-zero at frame boundaries), incl. negative times.
#[test]
fn timecode_rounding() {
// 29.97 fps: the timebase is seconds-per-frame = 1001/30000.
let tb = Rational::new(1001, 30000);
// 29.97 fps: the timebase is seconds-per-frame = 1001/30000.
let tb = Rational::new(1001, 30000);
// 0 seconds -> 0 frames.
assert_eq!(tb.time_to_timestamp(Rational::new(0, 1)), 0);
// 0 seconds -> 0 frames.
assert_eq!(tb.time_to_timestamp(Rational::new(0, 1)), 0);
// 1 full frame.
assert_eq!(tb.time_to_timestamp(Rational::new(1001, 30000)), 1);
// 1 full frame.
assert_eq!(tb.time_to_timestamp(Rational::new(1001, 30000)), 1);
// Half a frame (0.5 * 1001/30000 s) rounds half-away-from-zero -> 1.
let half = Rational::new(1001, 60000);
assert_eq!(tb.time_to_timestamp(half), 1);
// Half a frame (0.5 * 1001/30000 s) rounds half-away-from-zero -> 1.
let half = Rational::new(1001, 60000);
assert_eq!(tb.time_to_timestamp(half), 1);
// Negative half frame rounds to -1 (llround half away from zero).
assert_eq!(tb.time_to_timestamp(Rational::new(-1001, 60000)), -1);
// Negative half frame rounds to -1 (llround half away from zero).
assert_eq!(tb.time_to_timestamp(Rational::new(-1001, 60000)), -1);
// 30 frames round-trip to exactly 1001/1000 s.
assert_eq!(tb.timestamp_to_time(30), Rational::new(1001, 1000));
// 30 frames round-trip to exactly 1001/1000 s.
assert_eq!(tb.timestamp_to_time(30), Rational::new(1001, 1000));
// timestamp -> time -> timestamp round trip.
assert_eq!(tb.time_to_timestamp(tb.timestamp_to_time(29)), 29);
assert_eq!(tb.time_to_timestamp(tb.timestamp_to_time(300)), 300);
// timestamp -> time -> timestamp round trip.
assert_eq!(tb.time_to_timestamp(tb.timestamp_to_time(29)), 29);
assert_eq!(tb.time_to_timestamp(tb.timestamp_to_time(300)), 300);
// Video-rate time: 30 frames at 1001/1000 s -> 30.
assert_eq!(tb.time_to_timestamp(Rational::new(1001, 1000)), 30);
// Video-rate time: 30 frames at 1001/1000 s -> 30.
assert_eq!(tb.time_to_timestamp(Rational::new(1001, 1000)), 30);
}
/// TimeRange: contains/intersected/combined, touching ranges,
/// zero-length ranges. C++: TimeRange methods.
#[test]
fn timerange_ops() {
let r = TimeRange::new(Rational::new(0, 1), Rational::new(10, 1));
assert!(r.contains(Rational::new(5, 1)));
assert!(r.contains(Rational::new(0, 1)));
assert!(!r.contains(Rational::new(10, 1)));
assert!(!r.contains(Rational::new(-1, 1)));
assert_eq!(r.length(), Rational::new(10, 1));
let r = TimeRange::new(Rational::new(0, 1), Rational::new(10, 1));
assert!(r.contains(Rational::new(5, 1)));
assert!(r.contains(Rational::new(0, 1)));
assert!(!r.contains(Rational::new(10, 1)));
assert!(!r.contains(Rational::new(-1, 1)));
assert_eq!(r.length(), Rational::new(10, 1));
let a = TimeRange::new(Rational::new(0, 1), Rational::new(10, 1));
let b = TimeRange::new(Rational::new(5, 1), Rational::new(15, 1));
assert_eq!(
a.intersected(&b),
TimeRange::new(Rational::new(5, 1), Rational::new(10, 1))
);
assert_eq!(
a.combined(&b),
TimeRange::new(Rational::new(0, 1), Rational::new(15, 1))
);
let a = TimeRange::new(Rational::new(0, 1), Rational::new(10, 1));
let b = TimeRange::new(Rational::new(5, 1), Rational::new(15, 1));
assert_eq!(
a.intersected(&b),
TimeRange::new(Rational::new(5, 1), Rational::new(10, 1))
);
assert_eq!(
a.combined(&b),
TimeRange::new(Rational::new(0, 1), Rational::new(15, 1))
);
// Touching ranges combined into one.
let c = TimeRange::new(Rational::new(10, 1), Rational::new(20, 1));
assert_eq!(
a.combined(&c),
TimeRange::new(Rational::new(0, 1), Rational::new(20, 1))
);
// Touching ranges combined into one.
let c = TimeRange::new(Rational::new(10, 1), Rational::new(20, 1));
assert_eq!(
a.combined(&c),
TimeRange::new(Rational::new(0, 1), Rational::new(20, 1))
);
// Zero-length range.
let z = TimeRange::new(Rational::new(5, 1), Rational::new(5, 1));
assert_eq!(z.length(), Rational::new(0, 1));
assert!(!z.contains(Rational::new(5, 1)));
// Zero-length range.
let z = TimeRange::new(Rational::new(5, 1), Rational::new(5, 1));
assert_eq!(z.length(), Rational::new(0, 1));
assert!(!z.contains(Rational::new(5, 1)));
// out < in is normalized by swapping.
let swapped = TimeRange::new(Rational::new(10, 1), Rational::new(5, 1));
assert_eq!(
swapped,
TimeRange::new(Rational::new(5, 1), Rational::new(10, 1))
);
// out < in is normalized by swapping.
let swapped = TimeRange::new(Rational::new(10, 1), Rational::new(5, 1));
assert_eq!(
swapped,
TimeRange::new(Rational::new(5, 1), Rational::new(10, 1))
);
}
/// TimeRangeList insert merges overlapping AND touching ranges;
/// remove splits. C++: TimeRangeList.
#[test]
fn timerangelist_normalization() {
let mut list = TimeRangeList::new();
assert!(list.is_empty());
assert_eq!(list.first(), Rational::NULL);
let mut list = TimeRangeList::new();
assert!(list.is_empty());
assert_eq!(list.first(), Rational::NULL);
list.insert(TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
list.insert(TimeRange::new(Rational::new(20, 1), Rational::new(30, 1)));
// Overlaps (0,10): merges into (0,15), leaving (0,15) + (20,30).
list.insert(TimeRange::new(Rational::new(5, 1), Rational::new(15, 1)));
list.insert(TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
list.insert(TimeRange::new(Rational::new(20, 1), Rational::new(30, 1)));
// Overlaps (0,10): merges into (0,15), leaving (0,15) + (20,30).
list.insert(TimeRange::new(Rational::new(5, 1), Rational::new(15, 1)));
assert!(!list.is_empty());
assert_eq!(list.total_length(), Rational::new(25, 1));
assert_eq!(list.ranges().len(), 2);
let mut ins: Vec<_> = list.ranges().iter().map(|r| r.in_()).collect();
ins.sort();
assert_eq!(ins, vec![Rational::new(0, 1), Rational::new(20, 1)]);
assert!(!list.is_empty());
assert_eq!(list.total_length(), Rational::new(25, 1));
assert_eq!(list.ranges().len(), 2);
let mut ins: Vec<_> = list.ranges().iter().map(|r| r.in_()).collect();
ins.sort();
assert_eq!(ins, vec![Rational::new(0, 1), Rational::new(20, 1)]);
// Touching inserts merge into a single range.
let mut t = TimeRangeList::new();
t.insert(TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
t.insert(TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)));
assert_eq!(t.ranges().len(), 1);
assert_eq!(
t.ranges()[0],
TimeRange::new(Rational::new(0, 1), Rational::new(20, 1))
);
// Touching inserts merge into a single range.
let mut t = TimeRangeList::new();
t.insert(TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
t.insert(TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)));
assert_eq!(t.ranges().len(), 1);
assert_eq!(
t.ranges()[0],
TimeRange::new(Rational::new(0, 1), Rational::new(20, 1))
);
// remove splits (0,20) into (0,5) + (15,20) around the removed (5,15).
let mut s = TimeRangeList::new();
s.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
s.remove(TimeRange::new(Rational::new(5, 1), Rational::new(15, 1)));
assert_eq!(s.ranges().len(), 2);
let mut outs: Vec<_> = s.ranges().iter().map(|r| r.out()).collect();
outs.sort();
assert_eq!(outs, vec![Rational::new(5, 1), Rational::new(20, 1)]);
// remove splits (0,20) into (0,5) + (15,20) around the removed (5,15).
let mut s = TimeRangeList::new();
s.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
s.remove(TimeRange::new(Rational::new(5, 1), Rational::new(15, 1)));
assert_eq!(s.ranges().len(), 2);
let mut outs: Vec<_> = s.ranges().iter().map(|r| r.out()).collect();
outs.sort();
assert_eq!(outs, vec![Rational::new(5, 1), Rational::new(20, 1)]);
// first() returns the first range's in().
let mut f = TimeRangeList::new();
f.insert(TimeRange::new(Rational::new(3, 1), Rational::new(7, 1)));
assert_eq!(f.first(), Rational::new(3, 1));
// first() returns the first range's in().
let mut f = TimeRangeList::new();
f.insert(TimeRange::new(Rational::new(3, 1), Rational::new(7, 1)));
assert_eq!(f.first(), Rational::new(3, 1));
}
/// Frame-grid snap semantics (C++ TimeRangeListFrameIterator::Snap).
#[test]
fn timerangelist_snap() {
let list = TimeRangeList::new();
let tb = Rational::new(1001, 30000); // 29.97 fps, seconds per frame.
let list = TimeRangeList::new();
let tb = Rational::new(1001, 30000); // 29.97 fps, seconds per frame.
// 0.5 frames floors down to frame 0.
let half = Rational::new(1001, 60000);
assert_eq!(list.snap(half, tb), Rational::new(0, 1));
// 0.5 frames floors down to frame 0.
let half = Rational::new(1001, 60000);
assert_eq!(list.snap(half, tb), Rational::new(0, 1));
// Exactly 1 frame stays put.
let one = Rational::new(1001, 30000);
assert_eq!(list.snap(one, tb), Rational::new(1001, 30000));
// Exactly 1 frame stays put.
let one = Rational::new(1001, 30000);
assert_eq!(list.snap(one, tb), Rational::new(1001, 30000));
// 1.5 frames (1001/20000 s) floors down to 1 frame.
let one_and_half = Rational::new(1001, 20000);
assert_eq!(list.snap(one_and_half, tb), Rational::new(1001, 30000));
// 1.5 frames (1001/20000 s) floors down to 1 frame.
let one_and_half = Rational::new(1001, 20000);
assert_eq!(list.snap(one_and_half, tb), Rational::new(1001, 30000));
// 30 frames round-trip exactly.
assert_eq!(list.snap(Rational::new(1001, 1000), tb), Rational::new(1001, 1000));
// 30 frames round-trip exactly.
assert_eq!(
list.snap(Rational::new(1001, 1000), tb),
Rational::new(1001, 1000)
);
}
/// Enum discriminants identical to the C++ enums (C ABI contract).
#[test]
fn format_enum_values_match_cpp() {
assert_eq!(PixelFormat::Invalid as i32, -1);
assert_eq!(PixelFormat::U8 as i32, 0);
assert_eq!(PixelFormat::U10 as i32, 1);
assert_eq!(PixelFormat::U16 as i32, 2);
assert_eq!(PixelFormat::F16 as i32, 3);
assert_eq!(PixelFormat::F32 as i32, 4);
assert_eq!(PixelFormat::Invalid as i32, -1);
assert_eq!(PixelFormat::U8 as i32, 0);
assert_eq!(PixelFormat::U10 as i32, 1);
assert_eq!(PixelFormat::U16 as i32, 2);
assert_eq!(PixelFormat::F16 as i32, 3);
assert_eq!(PixelFormat::F32 as i32, 4);
assert_eq!(PixelFormat::Invalid.bytes_per_channel(), 0);
assert_eq!(PixelFormat::U8.bytes_per_channel(), 1);
assert_eq!(PixelFormat::U10.bytes_per_channel(), 4); // packed RGBA10A2
assert_eq!(PixelFormat::U16.bytes_per_channel(), 2);
assert_eq!(PixelFormat::F16.bytes_per_channel(), 2);
assert_eq!(PixelFormat::F32.bytes_per_channel(), 4);
assert_eq!(PixelFormat::F32.bytes_per_pixel(4), 16);
assert_eq!(PixelFormat::Invalid.bytes_per_channel(), 0);
assert_eq!(PixelFormat::U8.bytes_per_channel(), 1);
assert_eq!(PixelFormat::U10.bytes_per_channel(), 4); // packed RGBA10A2
assert_eq!(PixelFormat::U16.bytes_per_channel(), 2);
assert_eq!(PixelFormat::F16.bytes_per_channel(), 2);
assert_eq!(PixelFormat::F32.bytes_per_channel(), 4);
assert_eq!(PixelFormat::F32.bytes_per_pixel(4), 16);
assert_eq!(SampleFormat::Invalid as i32, -1);
assert_eq!(SampleFormat::U8Planar as i32, 0);
assert_eq!(SampleFormat::S16Planar as i32, 1);
assert_eq!(SampleFormat::S32Planar as i32, 2);
assert_eq!(SampleFormat::S64Planar as i32, 3);
assert_eq!(SampleFormat::F32Planar as i32, 4);
assert_eq!(SampleFormat::F64Planar as i32, 5);
assert_eq!(SampleFormat::U8 as i32, 6);
assert_eq!(SampleFormat::S16 as i32, 7);
assert_eq!(SampleFormat::S32 as i32, 8);
assert_eq!(SampleFormat::S64 as i32, 9);
assert_eq!(SampleFormat::F32 as i32, 10);
assert_eq!(SampleFormat::F64 as i32, 11);
assert_eq!(SampleFormat::Invalid as i32, -1);
assert_eq!(SampleFormat::U8Planar as i32, 0);
assert_eq!(SampleFormat::S16Planar as i32, 1);
assert_eq!(SampleFormat::S32Planar as i32, 2);
assert_eq!(SampleFormat::S64Planar as i32, 3);
assert_eq!(SampleFormat::F32Planar as i32, 4);
assert_eq!(SampleFormat::F64Planar as i32, 5);
assert_eq!(SampleFormat::U8 as i32, 6);
assert_eq!(SampleFormat::S16 as i32, 7);
assert_eq!(SampleFormat::S32 as i32, 8);
assert_eq!(SampleFormat::S64 as i32, 9);
assert_eq!(SampleFormat::F32 as i32, 10);
assert_eq!(SampleFormat::F64 as i32, 11);
assert_eq!(SampleFormat::Invalid.bytes_per_sample(), 0);
assert_eq!(SampleFormat::U8.bytes_per_sample(), 1);
assert_eq!(SampleFormat::S16.bytes_per_sample(), 2);
assert_eq!(SampleFormat::S32.bytes_per_sample(), 4);
assert_eq!(SampleFormat::F32.bytes_per_sample(), 4);
assert_eq!(SampleFormat::F64.bytes_per_sample(), 8);
assert_eq!(SampleFormat::F64Planar.bytes_per_sample(), 8);
assert_eq!(SampleFormat::Invalid.bytes_per_sample(), 0);
assert_eq!(SampleFormat::U8.bytes_per_sample(), 1);
assert_eq!(SampleFormat::S16.bytes_per_sample(), 2);
assert_eq!(SampleFormat::S32.bytes_per_sample(), 4);
assert_eq!(SampleFormat::F32.bytes_per_sample(), 4);
assert_eq!(SampleFormat::F64.bytes_per_sample(), 8);
assert_eq!(SampleFormat::F64Planar.bytes_per_sample(), 8);
assert!(SampleFormat::U8Planar.is_planar());
assert!(SampleFormat::F64Planar.is_planar());
assert!(!SampleFormat::U8.is_planar());
assert!(!SampleFormat::Invalid.is_planar());
assert!(SampleFormat::U8Planar.is_planar());
assert!(SampleFormat::F64Planar.is_planar());
assert!(!SampleFormat::U8.is_planar());
assert!(!SampleFormat::Invalid.is_planar());
assert_eq!(SampleFormat::U8Planar.to_packed(), SampleFormat::U8);
assert_eq!(SampleFormat::F64Planar.to_packed(), SampleFormat::F64);
assert_eq!(SampleFormat::U8.to_packed(), SampleFormat::U8);
assert_eq!(SampleFormat::U8.to_planar(), SampleFormat::U8Planar);
assert_eq!(SampleFormat::S16.to_planar(), SampleFormat::S16Planar);
assert_eq!(SampleFormat::F64.to_planar(), SampleFormat::F64Planar);
assert_eq!(SampleFormat::U8Planar.to_packed(), SampleFormat::U8);
assert_eq!(SampleFormat::F64Planar.to_packed(), SampleFormat::F64);
assert_eq!(SampleFormat::U8.to_packed(), SampleFormat::U8);
assert_eq!(SampleFormat::U8.to_planar(), SampleFormat::U8Planar);
assert_eq!(SampleFormat::S16.to_planar(), SampleFormat::S16Planar);
assert_eq!(SampleFormat::F64.to_planar(), SampleFormat::F64Planar);
}
/// Extreme i64 inputs (which C++ `int` could never receive) must reduce
@@ -322,23 +341,23 @@ fn format_enum_values_match_cpp() {
/// RATIONAL_MIN/MAX sentinels propagate NaN through arithmetic.
#[test]
fn rational_large_inputs_and_minmax() {
let mn = Rational::new(i64::MIN, 1);
assert_eq!(mn.numerator(), -2147483647);
assert_eq!(mn.denominator(), 1);
let mn = Rational::new(i64::MIN, 1);
assert_eq!(mn.numerator(), -2147483647);
assert_eq!(mn.denominator(), 1);
let mx = Rational::new(i64::MAX, 1);
assert_eq!(mx.numerator(), 2147483647);
assert_eq!(mx.denominator(), 1);
let mx = Rational::new(i64::MAX, 1);
assert_eq!(mx.numerator(), 2147483647);
assert_eq!(mx.denominator(), 1);
let mx = Rational::new(2147483647, 1);
assert!(!mx.is_null()); // 2147483647/1 is a valid value, not null
let r = mx + Rational::new(1, 1);
assert!(r.is_nan());
assert_eq!(r, Rational::NULL);
let mx = Rational::new(2147483647, 1);
assert!(!mx.is_null()); // 2147483647/1 is a valid value, not null
let r = mx + Rational::new(1, 1);
assert!(r.is_nan());
assert_eq!(r, Rational::NULL);
let mn = Rational::new(-2147483647, 1);
let r = Rational::new(1, 1) * mn;
assert!(r.is_nan());
let mn = Rational::new(-2147483647, 1);
let r = Rational::new(1, 1) * mn;
assert!(r.is_nan());
}
/// Additional edge cases: full-encompassing remove erases, partial
@@ -346,40 +365,40 @@ fn rational_large_inputs_and_minmax() {
/// invalid format conversions are identities.
#[test]
fn additional_edge_cases() {
// remove fully encompassing an element erases it.
let mut l = TimeRangeList::new();
l.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l.remove(TimeRange::new(Rational::new(-5, 1), Rational::new(25, 1)));
assert!(l.is_empty());
// remove fully encompassing an element erases it.
let mut l = TimeRangeList::new();
l.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l.remove(TimeRange::new(Rational::new(-5, 1), Rational::new(25, 1)));
assert!(l.is_empty());
// Trim the element's out down to the removal's in.
let mut l2 = TimeRangeList::new();
l2.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l2.remove(TimeRange::new(Rational::new(5, 1), Rational::new(25, 1)));
assert_eq!(l2.ranges().len(), 1);
assert_eq!(
l2.ranges()[0],
TimeRange::new(Rational::new(0, 1), Rational::new(5, 1))
);
// Trim the element's out down to the removal's in.
let mut l2 = TimeRangeList::new();
l2.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l2.remove(TimeRange::new(Rational::new(5, 1), Rational::new(25, 1)));
assert_eq!(l2.ranges().len(), 1);
assert_eq!(
l2.ranges()[0],
TimeRange::new(Rational::new(0, 1), Rational::new(5, 1))
);
// Trim the element's in up to the removal's out.
let mut l3 = TimeRangeList::new();
l3.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l3.remove(TimeRange::new(Rational::new(-5, 1), Rational::new(5, 1)));
assert_eq!(l3.ranges().len(), 1);
assert_eq!(
l3.ranges()[0],
TimeRange::new(Rational::new(5, 1), Rational::new(20, 1))
);
// Trim the element's in up to the removal's out.
let mut l3 = TimeRangeList::new();
l3.insert(TimeRange::new(Rational::new(0, 1), Rational::new(20, 1)));
l3.remove(TimeRange::new(Rational::new(-5, 1), Rational::new(5, 1)));
assert_eq!(l3.ranges().len(), 1);
assert_eq!(
l3.ranges()[0],
TimeRange::new(Rational::new(5, 1), Rational::new(20, 1))
);
// A NaN timebase yields 0 frames.
assert_eq!(Rational::NULL.time_to_timestamp(Rational::new(1, 1)), 0);
// to_f64 of the null sentinel is NaN.
assert!(Rational::NULL.to_f64().is_nan());
// A NaN timebase yields 0 frames.
assert_eq!(Rational::NULL.time_to_timestamp(Rational::new(1, 1)), 0);
// to_f64 of the null sentinel is NaN.
assert!(Rational::NULL.to_f64().is_nan());
// Invalid format conversions are identities.
assert_eq!(SampleFormat::Invalid.to_packed(), SampleFormat::Invalid);
assert_eq!(SampleFormat::Invalid.to_planar(), SampleFormat::Invalid);
// Invalid format conversions are identities.
assert_eq!(SampleFormat::Invalid.to_packed(), SampleFormat::Invalid);
assert_eq!(SampleFormat::Invalid.to_planar(), SampleFormat::Invalid);
}
/// from_double: C++ Rational::from_double parity — NaN and huge values
+12
View File
@@ -61,6 +61,18 @@ OakUndoCommand oaktimeline_place_block_command(OakNodeTrackList list,
OakUndoCommand oaktimeline_replace_block_with_gap_command(
OakNodeTrack track, OakNodeBlock block);
/**
* @brief The capi's move-clip assembly: gap the block's old spot and place
* it at `in` on `track_index` of `list` as ONE undoable entry
* (olive::TrackReplaceBlockWithGapCommand + olive::TrackPlaceBlockCommand
* inside a MultiUndoCommand).
*/
OakUndoCommand oaktimeline_move_block_command(OakNodeTrackList list,
int track_index,
OakNodeBlock block,
int64_t in_num,
int64_t in_den);
/**
* @brief olive::BlockTrimCommand. `mode` is an OakTimelineMovementMode
* value (k_trim_in / k_trim_out).
+10 -11
View File
@@ -81,7 +81,9 @@ pub extern "C" fn oakengine_audio_get_output_device() -> i64 {
if m.is_null() {
return Ok(PA_NO_DEVICE);
}
Ok(i64::from(unsafe { a::oakaudio_manager_get_output_device(m) }))
Ok(i64::from(unsafe {
a::oakaudio_manager_get_output_device(m)
}))
})
}
@@ -105,7 +107,9 @@ pub extern "C" fn oakengine_audio_get_input_device() -> i64 {
if m.is_null() {
return Ok(PA_NO_DEVICE);
}
Ok(i64::from(unsafe { a::oakaudio_manager_get_input_device(m) }))
Ok(i64::from(unsafe {
a::oakaudio_manager_get_input_device(m)
}))
})
}
@@ -460,14 +464,7 @@ pub unsafe extern "C" fn oakengine_audio_processor_open(
let out_layout = a::oakcore_audioparams_channel_layout(to);
let out_format = a::oakcore_audioparams_format(to);
Error::from_module(a::oakaudio_processor_open(
handle,
in_rate,
in_layout,
in_format,
out_rate,
out_layout,
out_format,
tempo,
handle, in_rate, in_layout, in_format, out_rate, out_layout, out_format, tempo,
))
})
}
@@ -486,7 +483,9 @@ pub unsafe extern "C" fn oakengine_audio_processor_close(p: *mut OakEngineAudioP
/// `oakengine_audio_processor_is_open` — 1 when open, 0 when NULL.
#[no_mangle]
pub unsafe extern "C" fn oakengine_audio_processor_is_open(p: *mut OakEngineAudioProcessor) -> c_int {
pub unsafe extern "C" fn oakengine_audio_processor_is_open(
p: *mut OakEngineAudioProcessor,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if p.is_null() {
return Ok(0);
+138 -70
View File
@@ -54,24 +54,24 @@ use crate::handle::CHandle;
/// 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_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;
pub fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64;
@@ -111,16 +111,27 @@ pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, bytes: i64) -
/// 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) }
_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
@@ -183,7 +194,14 @@ pub fn oakaudio_manager_start_recording(
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) }
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
@@ -213,16 +231,20 @@ pub fn oakaudio_processor_free(_self: *mut CHandle) {
/// 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) }
_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
@@ -240,63 +262,109 @@ pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
/// 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) }
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) }
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) }
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) }
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,
)
}
}
+37 -28
View File
@@ -59,7 +59,6 @@ use crate::handle::CHandle;
/// `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 {
@@ -67,7 +66,6 @@ pub fn zeroed_encoding_params() -> EncodingParamsPOD {
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 {
@@ -83,10 +81,10 @@ pub fn oakcodec_encoding_format_name(format: c_int, buf: *mut c_char, 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 {
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) }
}
@@ -153,13 +151,15 @@ pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
/// 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) }
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
@@ -183,7 +183,9 @@ pub fn oakcodec_encoding_sample_format_at(format: c_int, codec: c_int, index: c_
/// 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) }
unsafe {
oakcodec::ffi::format::oakcodec_encoding_filename_contains_digit_placeholder(filename)
}
}
/// Direct call into the `oakcodec` crate (single-lib unification; the
@@ -195,24 +197,32 @@ pub fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) ->
/// 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) }
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) }
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
@@ -230,4 +240,3 @@ pub fn oakcodec_encoder_init(params: *const EncodingParamsPOD) -> CHandle {
pub fn oakcodec_encoder_free(encoder: *mut CHandle) {
unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
}
+294 -144
View File
@@ -51,8 +51,9 @@ 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)>;
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).
@@ -81,27 +82,27 @@ pub fn oakcommon_config_set(group: *const c_char, key: *const c_char, value: *co
/// 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 {
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 {
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 {
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) }
}
@@ -113,7 +114,11 @@ pub fn oakcommon_config_get_double(group: *const c_char, key: *const c_char, fal
/// 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 {
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) }
}
@@ -149,7 +154,10 @@ pub fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) ->
/// 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 {
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) }
}
@@ -173,8 +181,14 @@ pub fn oakcommon_videoparams_init_basic(
) -> CHandle {
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_init_basic(
width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den,
interlacing, divider,
width,
height,
pixel_format,
nb_channels,
pixel_aspect_num,
pixel_aspect_den,
interlacing,
divider,
)
}
}
@@ -195,8 +209,16 @@ pub fn oakcommon_videoparams_init_with_time_base(
) -> 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,
width,
height,
time_base_num,
time_base_den,
pixel_format,
nb_channels,
pixel_aspect_num,
pixel_aspect_den,
interlacing,
divider,
)
}
}
@@ -234,61 +256,97 @@ pub fn oakcommon_videoparams_set_height(params: CHandle, height: c_int) -> c_int
/// 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) }
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) }
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) }
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) }
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) }
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) }
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
@@ -306,13 +364,17 @@ pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int
/// 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) }
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) }
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_interlacing(params, interlacing)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
@@ -342,28 +404,45 @@ pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_
/// 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) }
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) }
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) }
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) }
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_set_color_range(params, color_range)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
@@ -381,7 +460,9 @@ pub fn oakcommon_videoparams_get_effective_width(params: CHandle, width: *mut c_
/// 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) }
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_effective_height(params, height)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
@@ -405,61 +486,93 @@ pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int {
/// 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) }
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) }
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) }
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) }
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) }
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) }
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) }
unsafe {
oakcommon::ffi::videoparams::oakcommon_videoparams_get_bytes_per_channel_for_format(
pixel_format,
)
}
}
/// Direct call into the `oakcommon` crate (single-lib unification; the
@@ -468,16 +581,26 @@ 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) }
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) }
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
@@ -494,7 +617,11 @@ pub fn oakcommon_videoparams_get_time_in_timebase_units(
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) }
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
@@ -506,11 +633,13 @@ pub fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle {
/// 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) }
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
@@ -528,41 +657,53 @@ pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int {
/// 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) }
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) }
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) }
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) }
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
@@ -592,11 +733,13 @@ pub fn oakcommon_xml_reader_name(reader: CHandle, buf: *mut c_char, buf_size: c_
/// 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) }
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
@@ -614,23 +757,27 @@ pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int)
/// 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) }
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) }
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
@@ -660,10 +807,10 @@ pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_
/// 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 {
writer: CHandle,
name: *const c_char,
value: *const c_char,
) -> c_int {
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, name, value) }
}
@@ -676,10 +823,10 @@ pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_cha
/// 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 {
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) }
}
@@ -728,12 +875,15 @@ pub fn oakcommon_decibel_to_logarithmic(db: f64, out_logarithmic: *mut f64) -> c
/// 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) }
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) }
unsafe {
oakcommon::ffi::misc::oakcommon_decibel_logarithmic_to_linear(logarithmic, out_linear)
}
}
+3 -3
View File
@@ -35,9 +35,9 @@
pub mod audio;
pub mod codec;
pub mod common;
pub mod undo;
pub mod node;
pub mod plugin;
pub mod render;
pub mod node;
pub mod timeline;
pub mod task;
pub mod timeline;
pub mod undo;
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More