diff --git a/.gitignore b/.gitignore index 600535e91..47e47db15 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,4 @@ otio-install/ # Rust **/target/ tarpaulin-out/ +.env \ No newline at end of file diff --git a/build.rs b/build.rs index f83fce754..4370ec662 100644 --- a/build.rs +++ b/build.rs @@ -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 diff --git a/crates/oak-cli/src/cmd/info.rs b/crates/oak-cli/src/cmd/info.rs index 0afc3ab17..7fa7fefb6 100644 --- a/crates/oak-cli/src/cmd/info.rs +++ b/crates/oak-cli/src/cmd/info.rs @@ -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) } diff --git a/crates/oak-cli/src/cmd/mod.rs b/crates/oak-cli/src/cmd/mod.rs index 01689efcc..c0bed6163 100644 --- a/crates/oak-cli/src/cmd/mod.rs +++ b/crates/oak-cli/src/cmd/mod.rs @@ -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 } diff --git a/crates/oak-cli/src/cmd/probe.rs b/crates/oak-cli/src/cmd/probe.rs index 6ea68943a..499aa36bf 100644 --- a/crates/oak-cli/src/cmd/probe.rs +++ b/crates/oak-cli/src/cmd/probe.rs @@ -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) } diff --git a/crates/oak-cli/src/cmd/render.rs b/crates/oak-cli/src/cmd/render.rs index f58d302f8..bfa9b30ad 100644 --- a/crates/oak-cli/src/cmd/render.rs +++ b/crates/oak-cli/src/cmd/render.rs @@ -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) } diff --git a/crates/oak-cli/src/cmd/transcode.rs b/crates/oak-cli/src/cmd/transcode.rs index 8b180c2b7..1ea46b76f 100644 --- a/crates/oak-cli/src/cmd/transcode.rs +++ b/crates/oak-cli/src/cmd/transcode.rs @@ -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, - format: Option, -) -> i32 { - if let Some(w) = &width { - match w.parse::() { - 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, format: Option) -> i32 { + if let Some(w) = &width { + match w.parse::() { + 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) } diff --git a/crates/oak-cli/src/deferred.rs b/crates/oak-cli/src/deferred.rs index 4f1b565bd..5fcd0adad 100644 --- a/crates/oak-cli/src/deferred.rs +++ b/crates/oak-cli/src/deferred.rs @@ -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()); + } + } } diff --git a/crates/oak-cli/src/ffi.rs b/crates/oak-cli/src/ffi.rs index 98353b2b7..2ca1e0551 100644 --- a/crates/oak-cli/src/ffi.rs +++ b/crates/oak-cli/src/ffi.rs @@ -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; + Option; // --------------------------------------------------------------------------- // 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() + } } diff --git a/crates/oak-cli/src/fmt.rs b/crates/oak-cli/src/fmt.rs index 8f89afd04..8ad5a5c91 100644 --- a/crates/oak-cli/src/fmt.rs +++ b/crates/oak-cli/src/fmt.rs @@ -31,27 +31,27 @@ /// `Project: ` (`cmd_info`). pub fn project_line(name: &str) -> String { - format!("Project: {name}") + format!("Project: {name}") } /// `File: ` (`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: ` (`cmd_info`). pub fn sequences_line(count: i64) -> String { - format!("Sequences: {count}") + format!("Sequences: {count}") } /// `Footage: ` (`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: ` (`cmd_probe`). pub fn decoder_line(decoder: &str) -> String { - format!("Decoder: {decoder}") + format!("Decoder: {decoder}") } /// `Duration: s` (`cmd_probe`). pub fn duration_line(seconds: f64) -> String { - format!("Duration: {seconds:.6} s") + format!("Duration: {seconds:.6} s") } /// `Video streams: ` (`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: ` (`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: ` (`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" + ); + } } diff --git a/crates/oak-cli/src/main.rs b/crates/oak-cli/src/main.rs index 400f9d94f..7af048adc 100644 --- a/crates/oak-cli/src/main.rs +++ b/crates/oak-cli/src/main.rs @@ -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 . - 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, - /// Output format: "mp4" (default) or "ppm". - #[arg(long = "format")] - format: Option, - }, + /// 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 . + 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, + /// Output format: "mp4" (default) or "ppm". + #[arg(long = "format")] + format: Option, + }, } fn main() { - let args: Vec = std::env::args().skip(1).collect(); + let args: Vec = 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}"); } diff --git a/crates/oak-cli/src/ppm.rs b/crates/oak-cli/src/ppm.rs index 34794aa8b..5276061e7 100644 --- a/crates/oak-cli/src/ppm.rs +++ b/crates/oak-cli/src/ppm.rs @@ -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(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(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 { - 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 { + 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")); + } } diff --git a/crates/oak-cli/src/wav.rs b/crates/oak-cli/src/wav.rs index 90ba0edc3..54ba3eaa0 100644 --- a/crates/oak-cli/src/wav.rs +++ b/crates/oak-cli/src/wav.rs @@ -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(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(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")); + } } diff --git a/crates/oak-cli/tests/cli.rs b/crates/oak-cli/tests/cli.rs index 677b725fc..5c5228c98 100644 --- a/crates/oak-cli/tests/cli.rs +++ b/crates/oak-cli/tests/cli.rs @@ -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 [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 [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}" + ); } diff --git a/crates/oak-worker/src/ipc.rs b/crates/oak-worker/src/ipc.rs index 399c515f0..ad6df018e 100644 --- a/crates/oak-worker/src/ipc.rs +++ b/crates/oak-worker/src/ipc.rs @@ -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, - /// 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, + /// 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) -> 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"); + } } diff --git a/crates/oak-worker/src/main.rs b/crates/oak-worker/src/main.rs index dc4235e8c..f84598541 100644 --- a/crates/oak-worker/src/main.rs +++ b/crates/oak-worker/src/main.rs @@ -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()); + } } diff --git a/crates/oak-worker/src/session.rs b/crates/oak-worker/src/session.rs index 40119f52f..474a18ada 100644 --- a/crates/oak-worker/src/session.rs +++ b/crates/oak-worker/src/session.rs @@ -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) { + fn parent_side( + slots: i32, + slot_bytes: i64, + input: bool, + ) -> (Value, SharedMemoryRegion, Option) { 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"").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() diff --git a/crates/oak-worker/src/transport.rs b/crates/oak-worker/src/transport.rs index 91759e5b1..128c5a83a 100644 --- a/crates/oak-worker/src/transport.rs +++ b/crates/oak-worker/src/transport.rs @@ -90,10 +90,8 @@ pub fn attach_pools(hs: &HandshakeMsg) -> Result { 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!( diff --git a/crates/oak-worker/tests/worker.rs b/crates/oak-worker/tests/worker.rs index 654e492b1..f07602e59 100644 --- a/crates/oak-worker/tests/worker.rs +++ b/crates/oak-worker/tests/worker.rs @@ -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}" + ); } diff --git a/crates/oakaudio/src/bridge/codec.rs b/crates/oakaudio/src/bridge/codec.rs index 22061b368..3e0d596fc 100644 --- a/crates/oakaudio/src/bridge/codec.rs +++ b/crates/oakaudio/src/bridge/codec.rs @@ -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, ) } } diff --git a/crates/oakaudio/src/bridge/common.rs b/crates/oakaudio/src/bridge/common.rs index 9bd1172d6..14e18bee0 100644 --- a/crates/oakaudio/src/bridge/common.rs +++ b/crates/oakaudio/src/bridge/common.rs @@ -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) + } } diff --git a/crates/oakaudio/src/ffi.rs b/crates/oakaudio/src/ffi.rs index e947036e3..7ee9ed45e 100644 --- a/crates/oakaudio/src/ffi.rs +++ b/crates/oakaudio/src/ffi.rs @@ -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 = 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 diff --git a/crates/oakaudio/src/manager.rs b/crates/oakaudio/src/manager.rs index 91ea37f34..7fec54ae6 100644 --- a/crates/oakaudio/src/manager.rs +++ b/crates/oakaudio/src/manager.rs @@ -91,8 +91,7 @@ fn with_instance(h: &CHandle) -> Result> { } // 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 = - unsafe { &*(h.ctx as *const Mutex) }; + let m: &'static Mutex = unsafe { &*(h.ctx as *const Mutex) }; 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::()); - 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::() + ); + 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() } - diff --git a/crates/oakaudio/src/previewdevice.rs b/crates/oakaudio/src/previewdevice.rs index 66bc724f9..fa6de1ef5 100644 --- a/crates/oakaudio/src/previewdevice.rs +++ b/crates/oakaudio/src/previewdevice.rs @@ -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); } } diff --git a/crates/oakaudio/src/processor.rs b/crates/oakaudio/src/processor.rs index abf7ec41e..f7a199972 100644 --- a/crates/oakaudio/src/processor.rs +++ b/crates/oakaudio/src/processor.rs @@ -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::` 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::(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); } } } diff --git a/crates/oakaudio/src/synchronizer.rs b/crates/oakaudio/src/synchronizer.rs index 18f1eaa55..64438da44 100644 --- a/crates/oakaudio/src/synchronizer.rs +++ b/crates/oakaudio/src/synchronizer.rs @@ -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 } diff --git a/crates/oakaudio/src/waveform.rs b/crates/oakaudio/src/waveform.rs index 764962139..9c3a1454e 100644 --- a/crates/oakaudio/src/waveform.rs +++ b/crates/oakaudio/src/waveform.rs @@ -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 { +pub fn extract( + filename: &CStr, + stream_index: i32, + samples_per_point: i32, +) -> Result { // 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 }) } - diff --git a/crates/oakaudio/src/waveformsync.rs b/crates/oakaudio/src/waveformsync.rs index 264e8dd28..11dfc49be 100644 --- a/crates/oakaudio/src/waveformsync.rs +++ b/crates/oakaudio/src/waveformsync.rs @@ -58,7 +58,11 @@ pub fn extract_rms_envelope(planar: &[&[f32]], window_samples: usize) -> Vec 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; diff --git a/crates/oakaudio/tests/ffi_test.rs b/crates/oakaudio/tests/ffi_test.rs index 037f12b57..f831f98b1 100644 --- a/crates/oakaudio/tests/ffi_test.rs +++ b/crates/oakaudio/tests/ffi_test.rs @@ -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); diff --git a/crates/oakaudio/tests/golden_test.rs b/crates/oakaudio/tests/golden_test.rs index 1950dfbbe..9c70056a2 100644 --- a/crates/oakaudio/tests/golden_test.rs +++ b/crates/oakaudio/tests/golden_test.rs @@ -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 = (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; diff --git a/crates/oakaudio/tests/handle_test.rs b/crates/oakaudio/tests/handle_test.rs index 4bdeda0af..1960c891e 100644 --- a/crates/oakaudio/tests/handle_test.rs +++ b/crates/oakaudio/tests/handle_test.rs @@ -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); diff --git a/crates/oakaudio/tests/levelmeter_test.rs b/crates/oakaudio/tests/levelmeter_test.rs index 19b9158a4..d66d4076c 100644 --- a/crates/oakaudio/tests/levelmeter_test.rs +++ b/crates/oakaudio/tests/levelmeter_test.rs @@ -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]) -> (Vec, 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 = (0..64).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect(); - let ch1: Vec = (0..64).map(|i| if i % 2 == 0 { -1.0 } else { 1.0 }).collect(); + let ch0: Vec = (0..64) + .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }) + .collect(); + let ch1: Vec = (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 ); } diff --git a/crates/oakaudio/tests/manager_test.rs b/crates/oakaudio/tests/manager_test.rs index 5cb5d99a5..def9a1b53 100644 --- a/crates/oakaudio/tests/manager_test.rs +++ b/crates/oakaudio/tests/manager_test.rs @@ -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. diff --git a/crates/oakaudio/tests/processor_test.rs b/crates/oakaudio/tests/processor_test.rs index 3163e260d..bf6a8801c 100644 --- a/crates/oakaudio/tests/processor_test.rs +++ b/crates/oakaudio/tests/processor_test.rs @@ -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; diff --git a/crates/oakaudio/tests/sync_test.rs b/crates/oakaudio/tests/sync_test.rs index f7650e398..a1b0a0020 100644 --- a/crates/oakaudio/tests/sync_test.rs +++ b/crates/oakaudio/tests/sync_test.rs @@ -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); diff --git a/crates/oakaudio/tests/waveform_test.rs b/crates/oakaudio/tests/waveform_test.rs index 2b3d3b960..930c9a75c 100644 --- a/crates/oakaudio/tests/waveform_test.rs +++ b/crates/oakaudio/tests/waveform_test.rs @@ -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 { +fn summary( + w: oakaudio::handle::CHandle, + start: (i64, i64), + length: (i64, i64), + cap: i32, +) -> Vec { 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. diff --git a/crates/oakcodec/src/bridge/common.rs b/crates/oakcodec/src/bridge/common.rs index 27cab20e0..8642355f8 100644 --- a/crates/oakcodec/src/bridge/common.rs +++ b/crates/oakcodec/src/bridge/common.rs @@ -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`. diff --git a/crates/oakcodec/src/bridge/render.rs b/crates/oakcodec/src/bridge/render.rs index 4d1923b62..11cc28fd5 100644 --- a/crates/oakcodec/src/bridge/render.rs +++ b/crates/oakcodec/src/bridge/render.rs @@ -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` diff --git a/crates/oakcodec/src/bridge/test_stubs.rs b/crates/oakcodec/src/bridge/test_stubs.rs index d724ad75e..0ac44f6ae 100644 --- a/crates/oakcodec/src/bridge/test_stubs.rs +++ b/crates/oakcodec/src/bridge/test_stubs.rs @@ -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> { 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] diff --git a/crates/oakcodec/src/conformmanager.rs b/crates/oakcodec/src/conformmanager.rs index 50a6cd3b4..361205f8f 100644 --- a/crates/oakcodec/src/conformmanager.rs +++ b/crates/oakcodec/src/conformmanager.rs @@ -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() } diff --git a/crates/oakcodec/src/decoder.rs b/crates/oakcodec/src/decoder.rs index 84f6bbd3e..9327e7704 100644 --- a/crates/oakcodec/src/decoder.rs +++ b/crates/oakcodec/src/decoder.rs @@ -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>; + fn retrieve_video_frame(&self, p: &RetrieveVideoParams) -> crate::error::Result>; /// Retrieve a video frame as a render texture (owned by caller). fn retrieve_video(&self, p: &RetrieveVideoParams) -> crate::error::Result; @@ -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> { - Err(crate::error::Error::Failed("decoder not yet implemented".to_string())) + fn retrieve_video_frame(&self, _p: &RetrieveVideoParams) -> crate::error::Result> { + Err(crate::error::Error::Failed( + "decoder not yet implemented".to_string(), + )) } - fn retrieve_video( - &self, - _p: &RetrieveVideoParams, - ) -> crate::error::Result { - Err(crate::error::Error::Failed("decoder not yet implemented".to_string())) + fn retrieve_video(&self, _p: &RetrieveVideoParams) -> crate::error::Result { + 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 { - 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, } } diff --git a/crates/oakcodec/src/encoder.rs b/crates/oakcodec/src/encoder.rs index 1ccb08990..dd2e36008 100644 --- a/crates/oakcodec/src/encoder.rs +++ b/crates/oakcodec/src/encoder.rs @@ -139,12 +139,12 @@ pub fn create_from_params(params: &EncodingParams) -> Option> { } } 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; diff --git a/crates/oakcodec/src/encodingparams.rs b/crates/oakcodec/src/encodingparams.rs index 985a8df29..1c9c328f5 100644 --- a/crates/oakcodec/src/encodingparams.rs +++ b/crates/oakcodec/src/encodingparams.rs @@ -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(""); - s.push_str(&format!("{}", escape_xml(cstr(&self.filename)))); + s.push_str(&format!( + "{}", + escape_xml(cstr(&self.filename)) + )); s.push_str(&format!("{}", self.format)); s.push_str(&format!("{}", 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!( - "