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:
@@ -113,3 +113,4 @@ otio-install/
|
||||
# Rust
|
||||
**/target/
|
||||
tarpaulin-out/
|
||||
.env
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, ¶ms, err.as_mut_ptr(), err.len() as i32) };
|
||||
let r =
|
||||
unsafe { oakaudio_manager_start_recording(m, ¶ms, err.as_mut_ptr(), err.len() as i32) };
|
||||
if r != 0 {
|
||||
assert!(
|
||||
err.iter().any(|&b| b != 0),
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ¶ms))
|
||||
{
|
||||
match pool.iter().position(|f| frame_matches(f, ¶ms)) {
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"));
|
||||
|
||||
+290
-157
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<b&c>d");
|
||||
assert_eq!(escape_attribute("a\"b<c>&d"), "a"b<c>&d");
|
||||
assert_eq!(decode_entities("a<b&c>d"e'f"), "a<b&c>d\"e'f");
|
||||
assert_eq!(
|
||||
decode_entities("a<b&c>d"e'f"),
|
||||
"a<b&c>d\"e'f"
|
||||
);
|
||||
assert_eq!(decode_entities("AB"), "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);
|
||||
|
||||
@@ -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)] {
|
||||
|
||||
@@ -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>�</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>�</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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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 & bye <there></e><a q="x"y&z"/>"#,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1180
-401
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
Reference in New Issue
Block a user