diff --git a/Cargo.lock b/Cargo.lock
index 07b787cfa..049ab21b4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,6 +182,28 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+[[package]]
+name = "alsa"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
+dependencies = [
+ "alsa-sys",
+ "bitflags 2.13.1",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "android_system_properties"
version = "0.1.6"
@@ -191,6 +213,16 @@ dependencies = [
"libc",
]
+[[package]]
+name = "annotate-snippets"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
+dependencies = [
+ "anstyle",
+ "unicode-width",
+]
+
[[package]]
name = "anstream"
version = "1.0.0"
@@ -330,7 +362,7 @@ dependencies = [
"chrono",
"half",
"hashbrown 0.17.1",
- "num-complex 0.4.6",
+ "num-complex",
"num-integer",
"num-traits",
]
@@ -465,7 +497,7 @@ version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
- "libloading",
+ "libloading 0.8.9",
]
[[package]]
@@ -486,6 +518,19 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "asio-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0dd5af36bd2d2aa75674a9b75846bbfc7e208b6f8c6ed6944442f80bf3f4d2"
+dependencies = [
+ "bindgen 0.72.1",
+ "cc",
+ "num-derive",
+ "num-traits",
+ "walkdir",
+]
+
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -761,7 +806,7 @@ dependencies = [
"arg_enum_proc_macro",
"arrayvec",
"log",
- "num-rational 0.4.2",
+ "num-rational",
"num-traits",
"pastey",
"rayon",
@@ -780,7 +825,7 @@ dependencies = [
"arrayvec",
"log",
"nom 8.0.0",
- "num-rational 0.4.2",
+ "num-rational",
"v_frame",
]
@@ -854,10 +899,13 @@ version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
+ "annotate-snippets",
"bitflags 2.13.1",
"cexpr",
"clang-sys",
"itertools 0.13.0",
+ "log",
+ "prettyplease",
"proc-macro2",
"quote",
"regex",
@@ -1181,6 +1229,16 @@ dependencies = [
"nom 7.1.3",
]
+[[package]]
+name = "cfg-expr"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -1246,7 +1304,7 @@ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
- "libloading",
+ "libloading 0.8.9",
]
[[package]]
@@ -1398,6 +1456,16 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "command-fds"
version = "0.3.3"
@@ -1488,6 +1556,12 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "cookie-factory"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1623,6 +1697,20 @@ dependencies = [
"libm",
]
+[[package]]
+name = "coreaudio-rs"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
+dependencies = [
+ "bitflags 2.13.1",
+ "libc",
+ "objc2-audio-toolbox",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+]
+
[[package]]
name = "cosmic-text"
version = "0.19.0"
@@ -1647,6 +1735,41 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "cpal"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028"
+dependencies = [
+ "alsa",
+ "asio-sys",
+ "block2 0.6.2",
+ "coreaudio-rs",
+ "dasp_sample",
+ "futures",
+ "jack",
+ "jni",
+ "js-sys",
+ "libc",
+ "mach2 0.6.0",
+ "ndk",
+ "ndk-context",
+ "num-derive",
+ "num-traits",
+ "objc2 0.6.4",
+ "objc2-audio-toolbox",
+ "objc2-avf-audio",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "pipewire",
+ "pulseaudio",
+ "web-sys",
+ "windows 0.62.2",
+ "windows-core 0.62.2",
+]
+
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1801,6 +1924,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "dasp_sample"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
+
[[package]]
name = "data-url"
version = "0.3.2"
@@ -1956,7 +2085,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
- "libloading",
+ "libloading 0.8.9",
]
[[package]]
@@ -2042,6 +2171,17 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+[[package]]
+name = "enum-primitive-derive"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c"
+dependencies = [
+ "num-traits",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -2169,7 +2309,7 @@ dependencies = [
"half",
"lebe",
"miniz_oxide",
- "num-complex 0.4.6",
+ "num-complex",
"pulp",
"rayon-core",
"smallvec",
@@ -2823,7 +2963,7 @@ dependencies = [
"itertools 0.14.0",
"log",
"lyon",
- "mach2",
+ "mach2 0.5.0",
"metal 0.33.0",
"num_cpus",
"objc",
@@ -2975,7 +3115,7 @@ dependencies = [
"itertools 0.14.0",
"libc",
"log",
- "mach2",
+ "mach2 0.5.0",
"metal 0.33.0",
"objc",
"objc2-app-kit 0.3.2",
@@ -3169,7 +3309,7 @@ dependencies = [
"itertools 0.14.0",
"libc",
"log",
- "mach2",
+ "mach2 0.5.0",
"nix 0.29.0",
"percent-encoding",
"regex",
@@ -3674,6 +3814,63 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+[[package]]
+name = "jack"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7811b07bcac5dafabf814ab52c4b0ca9b7948aa1e279f572f03aa6544d47d27"
+dependencies = [
+ "bitflags 2.13.1",
+ "jack-sys",
+ "lazy_static",
+ "libc",
+ "log",
+]
+
+[[package]]
+name = "jack-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab"
+dependencies = [
+ "bitflags 1.3.2",
+ "lazy_static",
+ "libc",
+ "libloading 0.7.4",
+ "log",
+ "pkg-config",
+]
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.20",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -3730,7 +3927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
dependencies = [
"libc",
- "libloading",
+ "libloading 0.8.9",
"pkg-config",
]
@@ -3851,6 +4048,16 @@ dependencies = [
"cc",
]
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
[[package]]
name = "libloading"
version = "0.8.9"
@@ -3876,6 +4083,33 @@ dependencies = [
"libc",
]
+[[package]]
+name = "libspa"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e"
+dependencies = [
+ "bitflags 2.13.1",
+ "cc",
+ "cookie-factory",
+ "libc",
+ "libspa-sys",
+ "nom 8.0.0",
+ "rustix 1.1.4",
+ "system-deps",
+]
+
+[[package]]
+name = "libspa-sys"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ad52764fca54818486f3cf75afec844d1f1a1568c24dcee25d41b1ab007dda"
+dependencies = [
+ "bindgen 0.72.1",
+ "cc",
+ "system-deps",
+]
+
[[package]]
name = "libsqlite3-sys"
version = "0.37.0"
@@ -4035,6 +4269,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "mach2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -4167,6 +4407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
+ "log",
"wasi",
"windows-sys 0.61.2",
]
@@ -4241,6 +4482,26 @@ dependencies = [
"getrandom 0.2.17",
]
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.1",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys 0.6.0+11769913",
+ "num_enum",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
[[package]]
name = "ndk-sys"
version = "0.5.0+25.2.9519653"
@@ -4333,19 +4594,6 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "num"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
-dependencies = [
- "num-complex 0.2.4",
- "num-integer",
- "num-iter",
- "num-rational 0.2.4",
- "num-traits",
-]
-
[[package]]
name = "num"
version = "0.4.3"
@@ -4353,10 +4601,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
- "num-complex 0.4.6",
+ "num-complex",
"num-integer",
"num-iter",
- "num-rational 0.4.2",
+ "num-rational",
"num-traits",
]
@@ -4387,16 +4635,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "num-complex"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
-dependencies = [
- "autocfg",
- "num-traits",
-]
-
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -4443,17 +4681,6 @@ dependencies = [
"num-traits",
]
-[[package]]
-name = "num-rational"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
[[package]]
name = "num-rational"
version = "0.4.2"
@@ -4485,6 +4712,28 @@ dependencies = [
"libc",
]
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "oak"
version = "0.5.0"
@@ -4503,16 +4752,12 @@ name = "oak-cli"
version = "0.1.0"
dependencies = [
"clap",
- "oakengine",
]
[[package]]
name = "oak-worker"
version = "0.1.0"
dependencies = [
- "clap",
- "oakengine",
- "oakrender",
"serde",
"serde_json",
]
@@ -4521,12 +4766,12 @@ dependencies = [
name = "oakaudio"
version = "0.1.0"
dependencies = [
+ "cpal",
"ffmpeg-next",
"oakcodec",
"oakcommon",
"oakcore-rs",
"oakffmpeg-link",
- "portaudio",
"thiserror 2.0.20",
]
@@ -4535,6 +4780,7 @@ name = "oakcodec"
version = "0.1.0"
dependencies = [
"ffmpeg-next",
+ "oakcommon",
"oakcore-rs",
"oakffmpeg-link",
"thiserror 2.0.20",
@@ -4631,7 +4877,6 @@ dependencies = [
name = "oakstorage"
version = "0.1.0"
dependencies = [
- "oakcodec",
"oakcore-rs",
"oaknode",
"oakotio",
@@ -4652,7 +4897,6 @@ dependencies = [
"oaknode",
"oakotio",
"oakrender",
- "oaktimeline",
"oakundo",
"thiserror 2.0.20",
]
@@ -4748,6 +4992,55 @@ dependencies = [
"objc2-foundation 0.3.2",
]
+[[package]]
+name = "objc2-audio-toolbox"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
+dependencies = [
+ "bitflags 2.13.1",
+ "libc",
+ "objc2 0.6.4",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-avf-audio"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-audio"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
+dependencies = [
+ "dispatch2",
+ "objc2 0.6.4",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
+[[package]]
+name = "objc2-core-audio-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+]
+
[[package]]
name = "objc2-core-data"
version = "0.2.2"
@@ -4767,7 +5060,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.13.1",
+ "block2 0.6.2",
"dispatch2",
+ "libc",
"objc2 0.6.4",
]
@@ -4808,6 +5103,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.1",
+ "block2 0.6.2",
+ "libc",
"objc2 0.6.4",
"objc2-core-foundation",
]
@@ -4952,7 +5249,7 @@ dependencies = [
"hkdf 0.12.4",
"hmac 0.12.1",
"md-5 0.10.6",
- "num 0.4.3",
+ "num",
"num-bigint-dig",
"pbkdf2",
"serde",
@@ -5204,6 +5501,31 @@ dependencies = [
"futures-io",
]
+[[package]]
+name = "pipewire"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38"
+dependencies = [
+ "bitflags 2.13.1",
+ "libc",
+ "libspa",
+ "libspa-sys",
+ "pipewire-sys",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "pipewire-sys"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2089f245b548723e60325773c27f586b7a2372c79ea941b246cd0d654706adc"
+dependencies = [
+ "bindgen 0.72.1",
+ "libspa-sys",
+ "system-deps",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -5287,27 +5609,6 @@ dependencies = [
"portable-atomic",
]
-[[package]]
-name = "portaudio"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d64242ed9c0236ba1470729856c0e69363ecd2339940230161383f1238804f6"
-dependencies = [
- "bitflags 2.13.1",
- "libc",
- "num 0.2.1",
- "portaudio-sys2",
-]
-
-[[package]]
-name = "portaudio-sys2"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86a36ee35d719327fee8827321bd41f3ee7a2cc2335aa28f9168ffed3e82c6b8"
-dependencies = [
- "pkg-config",
-]
-
[[package]]
name = "postage"
version = "0.5.0"
@@ -5486,7 +5787,7 @@ dependencies = [
"bytemuck",
"cfg-if",
"libm",
- "num-complex 0.4.6",
+ "num-complex",
"paste",
"pulp-wasm-simd-flag",
"raw-cpuid",
@@ -5500,6 +5801,22 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740"
+[[package]]
+name = "pulseaudio"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d70623bd7967a9ca4c2ae0e807fc380b291f98480fc037042305ec643a4d3373"
+dependencies = [
+ "bitflags 2.13.1",
+ "byteorder",
+ "enum-primitive-derive",
+ "futures",
+ "log",
+ "mio",
+ "num-traits",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "pxfm"
version = "0.1.30"
@@ -6513,6 +6830,16 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
[[package]]
name = "simd_helpers"
version = "0.1.0"
@@ -7145,6 +7472,19 @@ dependencies = [
"windows 0.57.0",
]
+[[package]]
+name = "system-deps"
+version = "7.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 1.1.4+spec-1.1.0",
+ "version-compare",
+]
+
[[package]]
name = "taffy"
version = "0.10.1"
@@ -7181,6 +7521,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -7761,6 +8107,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -8208,7 +8560,7 @@ dependencies = [
"js-sys",
"khronos-egl",
"libc",
- "libloading",
+ "libloading 0.8.9",
"log",
"metal 0.31.0",
"naga 25.0.1",
@@ -8253,7 +8605,7 @@ dependencies = [
"js-sys",
"khronos-egl",
"libc",
- "libloading",
+ "libloading 0.8.9",
"log",
"naga 29.0.4",
"ndk-sys 0.6.0+11769913",
diff --git a/crates/oakaudio/Cargo.toml b/crates/oakaudio/Cargo.toml
index 719b7ede4..2ba338a47 100644
--- a/crates/oakaudio/Cargo.toml
+++ b/crates/oakaudio/Cargo.toml
@@ -18,8 +18,7 @@ oakcodec = { path = "../oakcodec" }
# crate calls ffmpeg-next directly (same as oakcodec). The oakffmpeg-link
# dependency above emits the transitive link flags of the static FFmpeg.
ffmpeg-next = "9"
-# Real audio output (M12 P1): the preview playback stream. The PortAudio
-# C library must be installed (macOS: brew install portaudio).
-portaudio = "0.8"
+
+cpal = {version = "0.18", features = ["pulseaudio", "pipewire", "jack", "asio"]}
# `std::error::Error` impls for the crate-internal error enum.
thiserror = "2"
diff --git a/crates/oakaudio/src/bridge/codec.rs b/crates/oakaudio/src/bridge/codec.rs
deleted file mode 100644
index 3e0d596fc..000000000
--- a/crates/oakaudio/src/bridge/codec.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! oakcodec C ABI calls (audio encoding + decoding for the manager and
-//! waveform extraction) — now direct Rust calls into the oakcodec crate
-//! (single-lib unification, see `docs/zh/plans/riir/single-lib.md`).
-//!
-//! The previous `*mut c_void` handle convention drifted from the real
-//! codec C ABI (which uses the shared [`CHandle`]); the wrappers below
-//! match the oakcodec ffi signatures exactly.
-
-use std::ffi::{c_char, c_int};
-
-use crate::handle::CHandle;
-
-/// `oakcodec_encoding_params` — single-lib unification: aliases the
-/// oakcodec crate's POD (identical layout; only the audio fields are
-/// consumed by oakaudio).
-pub type EncodingParams = oakcodec::ffi::encoder::oakcodec_encoding_params;
-
-/// `oakcodec_audio_stream_info` — audio stream metadata from probing.
-/// Single-lib unification: aliases the oakcodec crate's POD.
-pub type AudioStreamInfo = oakcodec::decoder::OakCodecAudioStreamInfo;
-
-/// `oakcodec_encoder_init` — create an encoder for `params`.
-pub unsafe fn oakcodec_encoder_init(params: *const EncodingParams) -> CHandle {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params) }
-}
-
-/// `oakcodec_encoder_free`.
-pub unsafe fn oakcodec_encoder_free(encoder: *mut CHandle) {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(encoder) }
-}
-
-/// `oakcodec_encoder_open`.
-pub unsafe fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_open(encoder) }
-}
-
-/// `oakcodec_encoder_write_audio` — feed interleaved `f32` audio.
-pub unsafe fn oakcodec_encoder_write_audio(
- encoder: CHandle,
- samples: *const f32,
- frame_count: c_int,
-) -> c_int {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_write_audio(encoder, samples, frame_count) }
-}
-
-/// `oakcodec_encoder_flush`.
-pub unsafe fn oakcodec_encoder_flush(encoder: CHandle) -> c_int {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_flush(encoder) }
-}
-
-/// `oakcodec_encoder_last_error` — copy the last error string into `buf`.
-pub unsafe fn oakcodec_encoder_last_error(
- encoder: CHandle,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_last_error(encoder, buf, buf_size) }
-}
-
-/// `oakcodec_decoder_probe` — create a probe handle for a file.
-pub unsafe fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle {
- unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe(filename) }
-}
-
-/// `oakcodec_decoder_free`.
-pub unsafe fn oakcodec_decoder_free(decoder: *mut CHandle) {
- unsafe { oakcodec::ffi::decoder::oakcodec_decoder_free(decoder) }
-}
-
-/// `oakcodec_decoder_probe_audio_stream_count`.
-pub unsafe fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -> c_int {
- unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_audio_stream_count(probe) }
-}
-
-/// `oakcodec_decoder_probe_get_audio_stream` — copy audio stream info.
-pub unsafe fn oakcodec_decoder_probe_get_audio_stream(
- probe: CHandle,
- index: c_int,
- out: *mut AudioStreamInfo,
-) -> c_int {
- unsafe { oakcodec::ffi::decoder::oakcodec_decoder_probe_get_audio_stream(probe, index, out) }
-}
-
-/// `oakcodec_decoder_open` — open stream `stream_index` for decoding.
-pub unsafe fn oakcodec_decoder_open(
- decoder: CHandle,
- filename: *const c_char,
- stream_index: c_int,
-) -> c_int {
- unsafe { oakcodec::ffi::decoder::oakcodec_decoder_open(decoder, filename, stream_index) }
-}
-
-/// `oakcodec_decoder_decode_audio` — decode/convert frames into `buf`.
-#[allow(clippy::too_many_arguments)]
-pub unsafe fn oakcodec_decoder_decode_audio(
- decoder: CHandle,
- in_num: c_int,
- in_den: c_int,
- out_num: c_int,
- out_den: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- buf: *mut f32,
- buf_frames: c_int,
-) -> c_int {
- unsafe {
- oakcodec::ffi::decoder::oakcodec_decoder_decode_audio(
- decoder,
- in_num,
- in_den,
- out_num,
- out_den,
- sample_rate,
- channel_layout,
- buf,
- buf_frames,
- )
- }
-}
diff --git a/crates/oakaudio/src/bridge/common.rs b/crates/oakaudio/src/bridge/common.rs
deleted file mode 100644
index 14e18bee0..000000000
--- a/crates/oakaudio/src/bridge/common.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! oakcommon C ABI calls (config access + ffmpeg format conversion) —
-//! now direct Rust calls into the oakcommon crate (single-lib
-//! unification, see `docs/zh/plans/riir/single-lib.md`).
-
-use std::ffi::{c_char, c_int};
-
-/// `oakcommon_config_get` — copy a config string value into `buf`
-/// (two-stage; returns the required size including NUL).
-pub unsafe fn oakcommon_config_get(
- group: *const c_char,
- key: *const c_char,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- unsafe { oakcommon::ffi::config::oakcommon_config_get(group, key, buf, buf_size) }
-}
-
-/// `oakcommon_config_get_int` — read an integer config value with a
-/// default.
-pub unsafe fn oakcommon_config_get_int(
- group: *const c_char,
- key: *const c_char,
- default: c_int,
-) -> c_int {
- unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) }
-}
-
-/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format` — map an ffmpeg
-/// sample format enum to the oak core format, or the reverse.
-pub unsafe fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(
- smp_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- unsafe {
- oakcommon::ffi::ffmpegutils::oakcommon_ffmpegutils_get_ffmpeg_sample_format(smp_fmt, out)
- }
-}
diff --git a/crates/oakaudio/src/bridge/mod.rs b/crates/oakaudio/src/bridge/mod.rs
deleted file mode 100644
index de5389c9d..000000000
--- a/crates/oakaudio/src/bridge/mod.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! C ABI imports from other oak modules (signatures mirror the public
-//! headers verbatim; resolved at link time).
-
-pub mod codec;
-pub mod common;
diff --git a/crates/oakaudio/src/config.rs b/crates/oakaudio/src/config.rs
index 4be8313b8..d2f9d7799 100644
--- a/crates/oakaudio/src/config.rs
+++ b/crates/oakaudio/src/config.rs
@@ -17,20 +17,16 @@
//! The `audio_config` namespace from `src/audio/src/configbridge.*`:
//! audio-specific configuration read through the oakcommon C ABI.
+use std::error::Error;
use std::ffi::CString;
-
+use std::str::FromStr;
+use oakcommon::configstore::*;
/// PortAudio output buffer size in frames; 0 = let PortAudio choose.
///
/// `// CPP-PARITY: src/audio/src/configbridge.cpp:30`
/// (`audio_config::output_buffer_size`).
pub fn output_buffer_size() -> i32 {
- unsafe {
- crate::bridge::common::oakcommon_config_get_int(
- std::ptr::null(),
- c"AudioOutputBufferSize".as_ptr(),
- 0,
- )
- }
+ ConfigStore::instance().get_int(None, "AudioOutputBufferSize", 0)
}
/// Name of the configured audio device for `is_output_device`
@@ -39,35 +35,19 @@ pub fn output_buffer_size() -> i32 {
/// `// CPP-PARITY: src/audio/src/configbridge.cpp:36`
/// (`audio_config::device_name`): two-stage size query; `size <= 1`
/// (absent or empty string) yields the empty string.
-pub fn device_name(is_output_device: bool) -> CString {
+pub fn device_name(is_output_device: bool) -> Result> {
let key = if is_output_device {
- c"AudioOutput"
+ "AudioOutput"
} else {
- c"AudioInput"
+ "AudioInput"
};
- unsafe {
- let size = crate::bridge::common::oakcommon_config_get(
- std::ptr::null(),
- key.as_ptr(),
- std::ptr::null_mut(),
- 0,
- );
- if size <= 1 {
- // Absent (OAKCOMMON_E_NOT_FOUND) or empty
- return CString::default();
- }
- let mut buf = vec![0u8; size as usize];
- if crate::bridge::common::oakcommon_config_get(
- std::ptr::null(),
- key.as_ptr(),
- buf.as_mut_ptr() as *mut std::ffi::c_char,
- size,
- ) < 0
- {
- return CString::default();
- }
- // The buffer is NUL-terminated by the callee.
- CString::from_vec_with_nul(buf)
- .unwrap_or_else(|e| CString::new(e.into_bytes()).unwrap_or_default())
+ let store = ConfigStore::instance();
+ let size = i32::from_str(store.get(None, key)?.as_str())?;
+ if size <= 1 {
+ // Absent (OAKCOMMON_E_NOT_FOUND) or empty
+ return Err(Box::new(crate::error::Error::NotFound));
}
+ let mut buf = vec![0u8; size as usize];
+ let name = store.get(None, key)?;
+ Ok(name)
}
diff --git a/crates/oakaudio/src/error.rs b/crates/oakaudio/src/error.rs
index fccc727c3..8df48f254 100644
--- a/crates/oakaudio/src/error.rs
+++ b/crates/oakaudio/src/error.rs
@@ -33,7 +33,7 @@ pub const OAKAUDIO_E_NOT_FOUND: i32 = -60004;
pub const OAKAUDIO_E_NOMEM: i32 = -60005;
/// Crate-internal result type; the FFI layer maps it to the codes.
-pub type Result = std::result::Result;
+pub type Result = std::result::Result>;
/// Crate-internal error.
#[derive(Debug, thiserror::Error)]
diff --git a/crates/oakaudio/src/ffi.rs b/crates/oakaudio/src/ffi.rs
deleted file mode 100644
index 992fb6f3e..000000000
--- a/crates/oakaudio/src/ffi.rs
+++ /dev/null
@@ -1,1248 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! C ABI export layer: implements `include/audio/*.h` verbatim.
-//!
-//! Organization: one submodule per public header. The authoritative
-//! function list is the header itself; each submodule below carries a
-//! complete inventory comment plus the export stubs. Bodies only
-//! unwrap handles, call safe Rust, and map results through
-//! [`crate::handle::guard*`].
-
-use std::ffi::{c_char, c_double, c_float, c_int, CStr};
-
-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::params::{AudioParams, SampleFormat};
-
-/// One decoded extraction, cached between the two-stage contract's size
-/// query and its data query (see `oakaudio_waveform_extract`).
-thread_local! {
- static WAVEFORM_EXTRACT_CACHE: std::cell::RefCell<
- Option<((String, i32, i32), crate::waveform::ExtractOutcome)>,
- > = const { std::cell::RefCell::new(None) };
-}
-use crate::waveform::{AudioVisualWaveform, SamplePerChannel};
-
-/// Map a C sample-format int to the native [`SampleFormat`]; out-of-range
-/// values map to `Invalid` (which callers reject with `OAKAUDIO_E_INVALID`).
-fn sample_format_from_c(format: c_int) -> SampleFormat {
- match format {
- 0 => SampleFormat::U8Planar,
- 1 => SampleFormat::S16Planar,
- 2 => SampleFormat::S32Planar,
- 3 => SampleFormat::S64Planar,
- 4 => SampleFormat::F32Planar,
- 5 => SampleFormat::F64Planar,
- 6 => SampleFormat::U8,
- 7 => SampleFormat::S16,
- 8 => SampleFormat::S32,
- 9 => SampleFormat::S64,
- 10 => SampleFormat::F32,
- 11 => SampleFormat::F64,
- _ => SampleFormat::Invalid,
- }
-}
-
-/// Convert a C bool mask into a `Vec`; NULL means "all windows valid"
-/// (an empty vec, which the correlation treats as all-valid).
-///
-/// `// CPP-PARITY: src/audio/c_api/sync.cpp:40` (`to_mask`).
-fn to_bool_mask(mask: *const u8, len: usize) -> Vec {
- if mask.is_null() {
- return Vec::new();
- }
- // SAFETY: the caller guarantees `len` valid bytes at `mask` when
- // non-NULL (the header requires masks to match the envelope length).
- unsafe { std::slice::from_raw_parts(mask, len) }
- .iter()
- .map(|&b| b != 0)
- .collect()
-}
-
-/// Build a `Vec<&[f32]>` of planar views from a C plane array. NULL planes
-/// become empty slices; the caller guarantees `planar` points to at least
-/// `channel_count` entries and each plane to `frame_count` floats for the
-/// duration of the call.
-///
-/// # Safety
-/// `planar` must be NULL (yields an empty vec) or valid for
-/// `channel_count` plane pointers, each valid for `frame_count` floats.
-unsafe fn planar_views<'a>(
- planar: *const *const f32,
- channel_count: i32,
- frame_count: i32,
-) -> Vec<&'a [f32]> {
- if planar.is_null() {
- return Vec::new();
- }
- let mut views: Vec<&'a [f32]> = Vec::with_capacity(channel_count.max(0) as usize);
- for ch in 0..channel_count {
- let p = unsafe { *planar.add(ch as usize) };
- if p.is_null() {
- views.push(&[]);
- } else {
- views.push(unsafe { std::slice::from_raw_parts(p, frame_count as usize) });
- }
- }
- views
-}
-
-/// View a C double array; NULL or a non-positive length yields an empty
-/// slice.
-///
-/// # Safety
-/// `ptr` must be valid for `len` doubles when both are non-zero.
-unsafe fn double_slice<'a>(ptr: *const c_double, len: c_int) -> &'a [f64] {
- if ptr.is_null() || len <= 0 {
- &[]
- } else {
- unsafe { std::slice::from_raw_parts(ptr, len as usize) }
- }
-}
-
-/// Convert a C (num, den) rational pair; `Error::Invalid` when `den` is
-/// zero (waveform.h: "den must be non-zero"; mirrors C++ `make_rational`).
-///
-/// `// CPP-PARITY: src/audio/c_api/waveform.cpp:56` (`make_rational`).
-fn rational_from_parts(num: i64, den: i64) -> crate::error::Result {
- invalid_if(den == 0)?;
- Ok(Rational::new(num, den))
-}
-
-/// `include/audio/manager.h` exports (complete inventory): the singleton
-/// `OakAudioManager` is a borrowed handle (addref/release no-ops);
-/// `oakaudio_manager_create_instance` /
-/// `oakaudio_manager_destroy_instance` /
-/// `oakaudio_manager_instance` manage the singleton;
-/// `oakaudio_manager_free` / `set_output_notify_interval` /
-/// `push_to_output` / `clear_buffered_output` / `stop_output` /
-/// `seconds` / `reset_output_clock` / `get_output_device` /
-/// `set_output_device` / `get_input_device` / `set_input_device` /
-/// `hard_reset` / `start_recording` / `stop_recording` /
-/// `find_config_device_by_name_s` / `find_device_by_name_s` /
-/// `oakaudio_debug_alive_count`.
-pub mod manager {
- use super::*;
-
- /// `oakaudio_manager_create_instance`: OAKAUDIO_OK, or OAKAUDIO_E_*.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_create_instance() -> c_int {
- guard(|| crate::manager::create_instance())
- }
-
- /// `oakaudio_manager_destroy_instance`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_destroy_instance() {
- guard_void(|| crate::manager::destroy_instance());
- }
-
- /// `oakaudio_manager_instance`: borrowed handle (never owned).
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_instance() -> CHandle {
- guard_handle(|| Ok(crate::manager::instance()))
- }
-
- /// `oakaudio_manager_free`: NULL/empty no-op.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_free(_self: *mut CHandle) {
- crate::manager::free(_self);
- }
-
- /// `oakaudio_manager_set_output_notify_interval`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_set_output_notify_interval(
- _self: CHandle,
- bytes: i64,
- ) -> c_int {
- guard(|| crate::manager::set_output_notify_interval(&_self, bytes))
- }
-
- /// `oakaudio_manager_push_to_output`: queue raw interleaved sample bytes.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_push_to_output(
- _self: CHandle,
- rate: c_int,
- layout: u64,
- format: c_int,
- samples: *const c_char,
- samples_size: i64,
- error_buf: *mut c_char,
- error_buf_size: c_int,
- ) -> c_int {
- guard(|| {
- // CPP-PARITY: `rate <= 0 || !samples || samples_size < 0`; an
- // unrepresentable sample format is additionally rejected (the
- // C++ casts the raw int into the enum without validating).
- invalid_if(rate <= 0 || samples.is_null() || samples_size < 0)?;
- let params = AudioParams {
- sample_rate: rate,
- channel_layout: layout,
- format: sample_format_from_c(format),
- };
- invalid_if(params.format == SampleFormat::Invalid)?;
- let samples: &[u8] = if samples_size <= 0 {
- &[]
- } else {
- // SAFETY: the caller guarantees `samples_size` bytes.
- unsafe { std::slice::from_raw_parts(samples as *const u8, samples_size as usize) }
- };
- let r = crate::manager::push_to_output(&_self, params, samples, &mut []);
- // CPP-PARITY: the manager's error string is surfaced through
- // error_buf on the failure path only.
- if let Err(Error::Failed(msg)) = &r {
- write_error(msg, error_buf, error_buf_size);
- }
- r
- })
- }
-
- /// `oakaudio_manager_clear_buffered_output`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
- guard(|| crate::manager::clear_buffered_output(&_self))
- }
-
- /// `oakaudio_manager_stop_output`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_stop_output(_self: CHandle) -> c_int {
- guard(|| crate::manager::stop_output(&_self))
- }
-
- /// `oakaudio_manager_output_levels` — per-channel linear peaks of the
- /// buffered output into `peaks` (up to `capacity` entries). Returns
- /// the channel count (0 when nothing is buffered or no output is
- /// configured). OAKAUDIO_E_INVALID for NULL/zero-capacity out args.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_output_levels(
- _self: CHandle,
- peaks: *mut c_float,
- capacity: c_int,
- ) -> c_int {
- guard_int(|| {
- invalid_if(peaks.is_null() || capacity <= 0)?;
- let slice = unsafe { std::slice::from_raw_parts_mut(peaks, capacity as usize) };
- crate::manager::output_levels(&_self, slice)
- })
- }
-
- /// `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 {
- guard(|| {
- invalid_if(out.is_null())?;
- let mut seconds = 0.0f64;
- crate::manager::seconds(&_self, &mut seconds)?;
- unsafe {
- *out = seconds;
- }
- Ok(())
- })
- }
-
- /// `oakaudio_manager_reset_output_clock`.
- #[no_mangle]
- 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 {
- guard_int(|| crate::manager::get_output_device(&_self))
- }
-
- /// `oakaudio_manager_set_output_device`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_set_output_device(
- _self: CHandle,
- device: c_int,
- ) -> c_int {
- guard(|| crate::manager::set_output_device(&_self, device))
- }
-
- /// `oakaudio_manager_get_input_device`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
- guard_int(|| crate::manager::get_input_device(&_self))
- }
-
- /// `oakaudio_manager_set_input_device`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_set_input_device(
- _self: CHandle,
- device: c_int,
- ) -> c_int {
- guard(|| crate::manager::set_input_device(&_self, device))
- }
-
- /// `oakaudio_manager_hard_reset`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int {
- guard(|| crate::manager::hard_reset(&_self))
- }
-
- /// `oakaudio_manager_start_recording`: record input to `params` via the
- /// oakcodec encoder.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_start_recording(
- _self: CHandle,
- params: *const EncodingParams,
- error_buf: *mut c_char,
- error_buf_size: c_int,
- ) -> c_int {
- guard(|| {
- // SAFETY: the caller guarantees `params` is a valid pointer when
- // non-NULL.
- let Some(params) = (unsafe { params.as_ref() }) else {
- // CPP-PARITY: `!params || !params->audio_enabled`; the error
- // string is written on the invalid path too so callers can
- // always diagnose a failed start.
- write_error("invalid recording parameters", error_buf, error_buf_size);
- return Err(Error::Invalid);
- };
- if params.audio_enabled == 0 {
- write_error("invalid recording parameters", error_buf, error_buf_size);
- return Err(Error::Invalid);
- }
- let r = crate::manager::start_recording(&_self, params, &mut []);
- if let Err(Error::Failed(msg)) = &r {
- write_error(msg, error_buf, error_buf_size);
- }
- r
- })
- }
-
- /// `oakaudio_manager_stop_recording`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
- guard(|| crate::manager::stop_recording(&_self))
- }
-
- /// `oakaudio_manager_find_config_device_by_name_s`: static, no handle.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_find_config_device_by_name_s(
- is_output_device: c_int,
- ) -> c_int {
- crate::manager::find_config_device_by_name_s(is_output_device != 0)
- }
-
- /// `oakaudio_manager_find_device_by_name_s`: static, no handle.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_manager_find_device_by_name_s(
- name: *const c_char,
- is_output_device: c_int,
- ) -> c_int {
- if name.is_null() {
- return OAKAUDIO_E_INVALID;
- }
- // SAFETY: the caller guarantees a NUL-terminated string.
- let name = unsafe { CStr::from_ptr(name) };
- crate::manager::find_device_by_name_s(name, is_output_device != 0)
- }
-
- /// `oakaudio_debug_alive_count`: surviving object count.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_debug_alive_count() -> c_int {
- crate::manager::debug_alive_count()
- }
-}
-
-/// `include/audio/processor.h` exports (complete inventory): the neutral
-/// by-value `OakAudioProcessor` handle (refcounted object, no singleton
-/// semantics); `oakaudio_processor_init` / `oakaudio_processor_free` /
-/// `oakaudio_processor_open` / `oakaudio_processor_close` /
-/// `oakaudio_processor_is_open` / `oakaudio_processor_convert` /
-/// `oakaudio_processor_flush`. `OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4`
-/// (`SampleFormat::f32_p`) pins the planar-first sample format ordering.
-pub mod processor {
- use super::*;
-
- /// `oakaudio_processor_init`: new processor, refcount 1.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_init() -> CHandle {
- guard_handle(crate::processor::init)
- }
-
- /// `oakaudio_processor_free`: NULL/empty no-op.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_free(_self: *mut CHandle) {
- crate::processor::free(_self);
- }
-
- /// `oakaudio_processor_open`: configure a resampling/conversion graph.
- ///
- /// Validation order and codes follow the C++ exactly (empty handle,
- /// already-open state, rates/speed, forced planar-float output);
- /// `in_format` is passed through unvalidated, matching `SampleFormat(
- /// SampleFormat::Format(in_format))` in the C++.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_open(
- _self: CHandle,
- in_rate: c_int,
- in_layout: u64,
- in_format: c_int,
- out_rate: c_int,
- out_layout: u64,
- out_format: c_int,
- speed: c_double,
- ) -> c_int {
- guard(|| {
- let from = AudioParams {
- sample_rate: in_rate,
- channel_layout: in_layout,
- format: sample_format_from_c(in_format),
- };
- let to = AudioParams {
- sample_rate: out_rate,
- channel_layout: out_layout,
- format: sample_format_from_c(out_format),
- };
- crate::processor::open(&_self, from, to, speed)
- })
- }
-
- /// `oakaudio_processor_close`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_close(_self: CHandle) -> c_int {
- guard(|| crate::processor::close(&_self))
- }
-
- /// `oakaudio_processor_is_open`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
- guard_int(|| Ok(crate::processor::is_open(&_self)? as i32))
- }
-
- /// `oakaudio_processor_convert`: resample/convert planar float in-place.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_convert(
- _self: CHandle,
- in_planar: *const *const f32,
- in_frame_count: c_int,
- out_planar: *const *mut f32,
- out_capacity_frames: c_int,
- ) -> c_int {
- guard_int(|| {
- crate::processor::convert(
- &_self,
- in_planar,
- in_frame_count,
- out_planar,
- out_capacity_frames,
- )
- })
- }
-
- /// `oakaudio_processor_flush`: drain buffered frames.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_processor_flush(_self: CHandle) -> c_int {
- guard(|| crate::processor::flush(&_self))
- }
-}
-
-/// `include/audio/sync.h` exports (complete inventory): stateless placement
-/// and envelope helpers (no handle); `oakaudio_offset_result`,
-/// `oakaudio_stretch_offset_result`, `oakaudio_source_clip` value structs;
-/// `oakaudio_sync_extract_rms_envelope` /
-/// `oakaudio_sync_estimate_envelope_offset` /
-/// `oakaudio_sync_estimate_stretch_and_offset` /
-/// `oakaudio_sync_place_by_source_time` /
-/// `oakaudio_sync_place_by_waveform_offset`.
-pub mod sync {
- use super::*;
-
- /// `oakaudio_offset_result` — candidate offset + correlation confidence.
- #[repr(C)]
- pub struct OffsetResult {
- /// Offset of the candidate relative to the reference, in samples.
- pub offset_samples: i64,
- /// Normalized correlation confidence in `[0, 1]`.
- pub confidence: c_double,
- /// Whether an offset could be determined.
- pub valid: c_int,
- }
-
- /// `oakaudio_stretch_offset_result` — rate + offset + confidence.
- #[repr(C)]
- pub struct StretchOffsetResult {
- /// Playback rate aligning the candidate (`> 1` = speed up).
- pub rate: c_double,
- /// Offset in samples.
- pub offset_samples: i64,
- /// Normalized correlation confidence in `[0, 1]`.
- pub confidence: c_double,
- /// Whether a rate+offset could be determined.
- pub valid: c_int,
- }
-
- /// `oakaudio_source_clip` — one clip's source-time metadata.
- #[repr(C)]
- pub struct SourceClip {
- /// Source start time numerator (seconds).
- pub source_start_time_num: i64,
- /// Source start time denominator (seconds).
- pub source_start_time_den: i64,
- /// Media in point numerator (seconds).
- pub media_in_num: i64,
- /// Media in point denominator (seconds).
- pub media_in_den: i64,
- /// Whether `source_start_time` is set.
- pub has_source_start_time: c_int,
- }
-
- /// `oakaudio_sync_extract_rms_envelope`: windowed RMS envelope.
- ///
- /// Two-stage contract: `out == NULL` or `capacity < windows` returns the
- /// required window count without writing.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_sync_extract_rms_envelope(
- planar: *const *const f32,
- channel_count: c_int,
- frame_count: c_int,
- window_samples: u64,
- out: *mut c_double,
- capacity: c_int,
- ) -> c_int {
- guard_int(|| {
- // CPP-PARITY: sync.cpp:58 — every plane must be non-NULL.
- invalid_if(
- planar.is_null()
- || channel_count <= 0
- || frame_count < 0
- || window_samples == 0
- || capacity < 0,
- )?;
- for ch in 0..channel_count {
- // SAFETY: `planar` is non-NULL with `channel_count` entries.
- if unsafe { *planar.add(ch as usize) }.is_null() {
- return Err(Error::Invalid);
- }
- }
- let views = unsafe { planar_views(planar, channel_count, frame_count) };
- let envelope =
- crate::waveformsync::extract_rms_envelope(&views, window_samples as usize);
- let windows = envelope.len();
- if out.is_null() || (capacity as usize) < windows {
- return Ok(windows as i32);
- }
- unsafe {
- std::ptr::copy_nonoverlapping(envelope.as_ptr(), out, windows);
- }
- Ok(windows as i32)
- })
- }
-
- /// `oakaudio_sync_estimate_envelope_offset`: masked envelope correlation.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_sync_estimate_envelope_offset(
- reference: *const c_double,
- reference_len: c_int,
- candidate: *const c_double,
- candidate_len: c_int,
- reference_valid: *const u8,
- candidate_valid: *const u8,
- window_samples: u64,
- max_offset_windows: i64,
- out: *mut OffsetResult,
- ) -> c_int {
- guard(|| {
- // CPP-PARITY: sync.cpp:93 — NULL arrays and non-positive lengths
- // are invalid; masks may be NULL (all windows valid).
- invalid_if(
- out.is_null()
- || reference.is_null()
- || candidate.is_null()
- || reference_len <= 0
- || candidate_len <= 0
- || window_samples == 0
- || max_offset_windows < 0,
- )?;
- let reference = unsafe { double_slice(reference, reference_len) };
- let candidate = unsafe { double_slice(candidate, candidate_len) };
- let ref_valid = to_bool_mask(reference_valid, reference_len as usize);
- let cand_valid = to_bool_mask(candidate_valid, candidate_len as usize);
- let result = crate::waveformsync::estimate_envelope_offset_valid(
- reference,
- candidate,
- &ref_valid,
- &cand_valid,
- window_samples as usize,
- max_offset_windows,
- );
- // SAFETY: `out` is non-NULL (checked above).
- unsafe {
- *out = OffsetResult {
- offset_samples: result.offset_samples,
- confidence: result.confidence,
- valid: result.valid as c_int,
- };
- }
- Ok(())
- })
- }
-
- /// `oakaudio_sync_estimate_stretch_and_offset`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_sync_estimate_stretch_and_offset(
- reference: *const c_double,
- reference_len: c_int,
- candidate: *const c_double,
- candidate_len: c_int,
- reference_valid: *const u8,
- candidate_valid: *const u8,
- window_samples: u64,
- max_offset_windows: i64,
- min_rate: c_double,
- max_rate: c_double,
- rate_step: c_double,
- out: *mut StretchOffsetResult,
- ) -> c_int {
- guard(|| {
- // CPP-PARITY: sync.cpp:123.
- invalid_if(
- out.is_null()
- || reference.is_null()
- || candidate.is_null()
- || reference_len <= 0
- || candidate_len <= 0
- || window_samples == 0
- || max_offset_windows < 0
- || min_rate <= 0.0
- || max_rate < min_rate
- || rate_step <= 0.0,
- )?;
- let reference = unsafe { double_slice(reference, reference_len) };
- let candidate = unsafe { double_slice(candidate, candidate_len) };
- let ref_valid = to_bool_mask(reference_valid, reference_len as usize);
- let cand_valid = to_bool_mask(candidate_valid, candidate_len as usize);
- let result = crate::waveformsync::estimate_stretch_and_offset(
- reference,
- candidate,
- &ref_valid,
- &cand_valid,
- window_samples as usize,
- max_offset_windows,
- min_rate,
- max_rate,
- rate_step,
- );
- // SAFETY: `out` is non-NULL (checked above).
- unsafe {
- *out = StretchOffsetResult {
- rate: result.rate,
- offset_samples: result.offset_samples,
- confidence: result.confidence,
- valid: result.valid as c_int,
- };
- }
- Ok(())
- })
- }
-
- /// `oakaudio_sync_place_by_source_time`: timeline placement by source time.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_sync_place_by_source_time(
- reference: *const SourceClip,
- candidate: *const SourceClip,
- reference_timeline_in_num: i64,
- reference_timeline_in_den: i64,
- out_num: *mut i64,
- out_den: *mut i64,
- out_valid: *mut c_int,
- ) -> c_int {
- guard(|| {
- // CPP-PARITY: sync.cpp:153 — every denominator must be non-zero.
- // SAFETY: non-NULL pointers are valid for one struct each.
- let Some(reference) = (unsafe { reference.as_ref() }) else {
- return Err(Error::Invalid);
- };
- let Some(candidate) = (unsafe { candidate.as_ref() }) else {
- return Err(Error::Invalid);
- };
- invalid_if(
- out_num.is_null()
- || out_den.is_null()
- || out_valid.is_null()
- || reference.source_start_time_den == 0
- || reference.media_in_den == 0
- || candidate.source_start_time_den == 0
- || candidate.media_in_den == 0
- || reference_timeline_in_den == 0,
- )?;
- let reference_clip = crate::synchronizer::SourceClip {
- source_start_time: Rational::new(
- reference.source_start_time_num,
- reference.source_start_time_den,
- ),
- media_in: Rational::new(reference.media_in_num, reference.media_in_den),
- has_source_start_time: reference.has_source_start_time != 0,
- };
- let candidate_clip = crate::synchronizer::SourceClip {
- source_start_time: Rational::new(
- candidate.source_start_time_num,
- candidate.source_start_time_den,
- ),
- media_in: Rational::new(candidate.media_in_num, candidate.media_in_den),
- has_source_start_time: candidate.has_source_start_time != 0,
- };
- let placement = crate::synchronizer::place_by_source_time(
- &reference_clip,
- &candidate_clip,
- Rational::new(reference_timeline_in_num, reference_timeline_in_den),
- );
- // SAFETY: out pointers are non-NULL (checked above).
- unsafe {
- *out_num = placement.timeline_in.numerator();
- *out_den = placement.timeline_in.denominator();
- *out_valid = placement.valid as c_int;
- }
- Ok(())
- })
- }
-
- /// `oakaudio_sync_place_by_waveform_offset`: timeline placement from an
- /// offset in samples.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_sync_place_by_waveform_offset(
- reference_timeline_in_num: i64,
- reference_timeline_in_den: i64,
- candidate_offset_samples: i64,
- sample_rate: c_int,
- out_num: *mut i64,
- out_den: *mut i64,
- out_valid: *mut c_int,
- ) -> c_int {
- guard(|| {
- // CPP-PARITY: sync.cpp:191.
- invalid_if(
- out_num.is_null()
- || out_den.is_null()
- || out_valid.is_null()
- || reference_timeline_in_den == 0,
- )?;
- let placement = crate::synchronizer::place_by_waveform_offset(
- Rational::new(reference_timeline_in_num, reference_timeline_in_den),
- candidate_offset_samples,
- sample_rate,
- );
- // SAFETY: out pointers are non-NULL (checked above).
- unsafe {
- *out_num = placement.timeline_in.numerator();
- *out_den = placement.timeline_in.denominator();
- *out_valid = placement.valid as c_int;
- }
- Ok(())
- })
- }
-}
-
-/// `include/audio/waveform.h` exports (complete inventory): neutral by-value
-/// `OakAudioWaveform` handle (refcounted); `oakaudio_min_max` value struct;
-/// `oakaudio_waveform_init` / `oakaudio_waveform_free` /
-/// `oakaudio_waveform_get_channel_count` / `oakaudio_waveform_set_channel_count`
-/// / `oakaudio_waveform_length` / `oakaudio_waveform_overwrite_samples` /
-/// `oakaudio_waveform_overwrite_sums` / `oakaudio_waveform_overwrite_silence`
-/// / `oakaudio_waveform_trim_in` / `oakaudio_waveform_resize` /
-/// `oakaudio_waveform_trim_range` / `oakaudio_waveform_get_summary` /
-/// `oakaudio_waveform_sum_samples_s` / `oakaudio_waveform_re_sum_s` /
-/// `oakaudio_waveform_extract`.
-pub mod waveform {
- use super::*;
-
- /// `oakaudio_min_max` — one min/max pair.
- #[repr(C)]
- #[derive(Clone, Copy, Debug, PartialEq)]
- pub struct MinMax {
- /// Minimum amplitude in the window.
- pub min: f32,
- /// Maximum amplitude in the window.
- pub max: f32,
- }
-
- /// `oakaudio_waveform_init`: new waveform, refcount 1.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_init() -> CHandle {
- guard_handle(crate::waveform::init)
- }
-
- /// `oakaudio_waveform_free`: NULL/empty no-op.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_free(_self: *mut CHandle) {
- crate::waveform::free(_self);
- }
-
- /// `oakaudio_waveform_get_channel_count`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_get_channel_count(_self: CHandle) -> c_int {
- guard_int(|| Ok(crate::waveform::get(&_self)?.channel_count()))
- }
-
- /// `oakaudio_waveform_set_channel_count`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_set_channel_count(
- _self: CHandle,
- channels: c_int,
- ) -> c_int {
- guard(|| {
- invalid_if(channels < 0)?;
- crate::waveform::get_mut(&_self)?.set_channel_count(channels);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_length`: length as a rational pair.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_length(
- _self: CHandle,
- num: *mut i64,
- den: *mut i64,
- ) -> c_int {
- guard(|| {
- invalid_if(num.is_null() || den.is_null())?;
- let length = crate::waveform::get(&_self)?.length();
- unsafe {
- *num = length.numerator();
- *den = length.denominator();
- }
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_overwrite_samples`.
- ///
- /// Validation order follows the C++: invalid args (incl. `frame_count
- /// <= 0`) first, then the channel-count state check (`E_STATE`).
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_overwrite_samples(
- _self: CHandle,
- planar: *const *const f32,
- frame_count: c_int,
- sample_rate: c_int,
- start_num: i64,
- start_den: i64,
- ) -> c_int {
- 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)?;
- let channels = crate::waveform::get(&_self)?.channel_count();
- if channels <= 0 {
- return Err(Error::State);
- }
- for ch in 0..channels {
- // SAFETY: `planar` is non-NULL with `channels` entries.
- if unsafe { *planar.add(ch as usize) }.is_null() {
- return Err(Error::Invalid);
- }
- }
- let views = unsafe { planar_views(planar, channels, frame_count) };
- crate::waveform::get_mut(&_self)?.overwrite_samples(&views, sample_rate, start);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_overwrite_sums`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_overwrite_sums(
- _self: CHandle,
- src: CHandle,
- dest_num: i64,
- dest_den: i64,
- offset_num: i64,
- offset_den: i64,
- length_num: i64,
- length_den: i64,
- ) -> c_int {
- guard(|| {
- let dest = rational_from_parts(dest_num, dest_den)?;
- let offset = rational_from_parts(offset_num, offset_den)?;
- let length = rational_from_parts(length_num, length_den)?;
- 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);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_overwrite_silence`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_overwrite_silence(
- _self: CHandle,
- start_num: i64,
- start_den: i64,
- length_num: i64,
- length_den: i64,
- ) -> c_int {
- guard(|| {
- let start = rational_from_parts(start_num, start_den)?;
- let length = rational_from_parts(length_num, length_den)?;
- crate::waveform::get_mut(&_self)?.overwrite_silence(start, length);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_trim_in`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_trim_in(
- _self: CHandle,
- length_num: i64,
- length_den: i64,
- ) -> c_int {
- guard(|| {
- let length = rational_from_parts(length_num, length_den)?;
- crate::waveform::get_mut(&_self)?.trim_in(length);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_resize`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_resize(
- _self: CHandle,
- length_num: i64,
- length_den: i64,
- ) -> c_int {
- guard(|| {
- let length = rational_from_parts(length_num, length_den)?;
- invalid_if(length < Rational::new(0, 1))?;
- crate::waveform::get_mut(&_self)?.resize(length);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_trim_range`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_trim_range(
- _self: CHandle,
- in_num: i64,
- in_den: i64,
- length_num: i64,
- length_den: i64,
- ) -> c_int {
- guard(|| {
- let r#in = rational_from_parts(in_num, in_den)?;
- let length = rational_from_parts(length_num, length_den)?;
- crate::waveform::get_mut(&_self)?.trim_range(r#in, length);
- Ok(())
- })
- }
-
- /// `oakaudio_waveform_get_summary`: channel-interleaved min/max pairs.
- ///
- /// Two-stage contract: `out_pairs == NULL` or `capacity_points` smaller
- /// than the required count returns the count without writing.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_get_summary(
- _self: CHandle,
- start_num: i64,
- start_den: i64,
- length_num: i64,
- length_den: i64,
- out_pairs: *mut MinMax,
- capacity_points: c_int,
- ) -> c_int {
- guard_int(|| {
- // CPP-PARITY: waveform.cpp:337 — `length <= 0` is invalid.
- let start = rational_from_parts(start_num, start_den)?;
- let length = rational_from_parts(length_num, length_den)?;
- invalid_if(length <= Rational::new(0, 1) || capacity_points < 0)?;
- let sample = crate::waveform::get(&_self)?.get_summary_from_time(start, length);
- let channels = crate::waveform::get(&_self)?.channel_count().max(1) as usize;
- let points = sample.len() / channels;
- if out_pairs.is_null() || (capacity_points as usize) < points {
- return Ok(points as i32);
- }
- // SAFETY: `out_pairs` holds at least `points * channels`
- // entries (capacity is in points, checked above); `Sample` is
- // channel-interleaved `SamplePerChannel`.
- for (i, spc) in sample.iter().take(points * channels).enumerate() {
- unsafe {
- *out_pairs.add(i) = MinMax {
- min: spc.min,
- max: spc.max,
- };
- }
- }
- Ok(points as i32)
- })
- }
-
- /// `oakaudio_waveform_sum_samples_s`: static, no handle.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_sum_samples_s(
- planar: *const *const f32,
- channel_count: c_int,
- start_index: c_int,
- length: c_int,
- out: *mut MinMax,
- ) -> c_int {
- guard_int(|| {
- // CPP-PARITY: waveform.cpp:364 — `out` is required (no
- // two-stage query) and `length <= 0` is invalid.
- invalid_if(
- planar.is_null()
- || out.is_null()
- || channel_count <= 0
- || start_index < 0
- || length <= 0,
- )?;
- for ch in 0..channel_count {
- // SAFETY: `planar` is non-NULL with `channel_count` entries.
- if unsafe { *planar.add(ch as usize) }.is_null() {
- return Err(Error::Invalid);
- }
- }
- // Each plane must hold at least `start_index + length` floats;
- // the C++ SampleBuffer has exactly that span.
- let mut views: Vec<&[f32]> = Vec::with_capacity(channel_count as usize);
- for ch in 0..channel_count {
- // SAFETY: non-NULL planes of `start_index + length` floats.
- let p = unsafe { *planar.add(ch as usize) };
- views.push(unsafe {
- std::slice::from_raw_parts(p, (start_index + 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()));
- }
- for (i, spc) in sample.iter().enumerate() {
- // SAFETY: `out` holds at least `channel_count` entries.
- unsafe {
- *out.add(i) = MinMax {
- min: spc.min,
- max: spc.max,
- };
- }
- }
- Ok(crate::error::OAKAUDIO_OK)
- })
- }
-
- /// `oakaudio_waveform_re_sum_s`: static, no handle.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_re_sum_s(
- r#in: *const MinMax,
- nb_entries: c_int,
- nb_channels: c_int,
- out: *mut MinMax,
- ) -> 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)?;
- // SAFETY: `in` holds `nb_entries` entries.
- let entries = unsafe { std::slice::from_raw_parts(r#in, nb_entries as usize) };
- let samples: Vec = entries
- .iter()
- .map(|m| SamplePerChannel {
- min: m.min,
- max: m.max,
- })
- .collect();
- 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 {
- *out.add(i) = MinMax {
- min: spc.min,
- max: spc.max,
- };
- }
- }
- Ok(crate::error::OAKAUDIO_OK)
- })
- }
-
- /// `oakaudio_waveform_extract`: whole-file waveform via oakcodec decoder.
- ///
- /// Two-stage contract: `out_pairs == NULL` or `capacity_points` smaller
- /// than the required point count returns the count without writing; the
- /// channel count is reported whenever `out_channel_count` is non-NULL.
- /// Decoding/probing lives in [`crate::waveform::extract`].
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_waveform_extract(
- filename: *const c_char,
- stream_index: c_int,
- samples_per_point: c_int,
- out_pairs: *mut MinMax,
- capacity_points: c_int,
- out_channel_count: *mut c_int,
- ) -> c_int {
- guard_int(|| {
- // CPP-PARITY: waveform.cpp:409.
- invalid_if(
- filename.is_null()
- || stream_index < 0
- || samples_per_point <= 0
- || capacity_points < 0,
- )?;
- // SAFETY: the caller guarantees a NUL-terminated string.
- let filename = unsafe { CStr::from_ptr(filename) };
- // The two-stage contract runs the query twice; decode the
- // stream ONCE and reuse the outcome for the data pass (the
- // FFmpeg decoder is not safe against back-to-back sessions
- // in the same process).
- let key = (
- filename.to_string_lossy().into_owned(),
- stream_index,
- samples_per_point,
- );
- let outcome = {
- // The cache slot is borrowed inside the closure only (the
- // guard cannot escape the `with` scope).
- let cached = WAVEFORM_EXTRACT_CACHE.with(|c| {
- c.borrow()
- .as_ref()
- .filter(|(k, _)| k == &key)
- .map(|(_, o)| o.clone())
- });
- match cached {
- Some(o) => o,
- None => {
- let outcome =
- crate::waveform::extract(filename, stream_index, samples_per_point)?;
- WAVEFORM_EXTRACT_CACHE.with(|c| *c.borrow_mut() = Some((key, outcome.clone())));
- outcome
- }
- }
- };
- 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
- // query.
- if !out_channel_count.is_null() {
- unsafe {
- *out_channel_count = outcome.channels;
- }
- }
- if out_pairs.is_null() || (capacity_points as usize) < point_count {
- return Ok(point_count as i32);
- }
- for (i, spc) in outcome.points.iter().enumerate() {
- // SAFETY: `out_pairs` holds `point_count * channels` entries
- // (capacity is in points, checked above).
- unsafe {
- *out_pairs.add(i) = MinMax {
- min: spc.min,
- max: spc.max,
- };
- }
- }
- Ok(point_count as i32)
- })
- }
-}
-
-/// `include/audio/levelmeter.h` exports (complete inventory): stateless
-/// peak/RMS/VU/LUFS analysis of planar float audio (no handle);
-/// `oakaudio_channel_stats`, `oakaudio_meter_stats` value structs;
-/// `oakaudio_levelmeter_analyze`.
-pub mod levelmeter {
- use super::*;
-
- /// `oakaudio_channel_stats` — per-channel analysis results (dB floor -200).
- #[repr(C)]
- pub struct ChannelStats {
- /// Peak amplitude, linear scale.
- pub peak_linear: c_double,
- /// Peak amplitude, decibel scale.
- pub peak_db: c_double,
- /// Root-mean-square level, linear scale.
- pub rms_linear: c_double,
- /// Root-mean-square level, decibel scale.
- pub rms_db: c_double,
- /// VU-meter ballistics reading, decibel scale.
- pub vu_db: c_double,
- }
-
- /// `oakaudio_meter_stats` — buffer-wide summary.
- #[repr(C)]
- pub struct MeterStats {
- /// Maximum peak across all channels, linear scale.
- pub max_peak_linear: c_double,
- /// Integrated loudness (EBU R128 LUFS).
- pub integrated_lufs: c_double,
- /// Whether every channel was silent below the noise gate.
- pub silence: c_int,
- }
-
- /// `oakaudio_levelmeter_analyze`.
- ///
- /// Validation follows the C++: null planar / non-positive channel count
- /// / negative frame count / undersized `channels` buffer, and the
- /// no-output double-null case, are all `OAKAUDIO_E_INVALID`.
- #[no_mangle]
- pub unsafe extern "C" fn oakaudio_levelmeter_analyze(
- planar: *const *const f32,
- channel_count: c_int,
- frame_count: c_int,
- channels: *mut ChannelStats,
- channels_capacity: c_int,
- summary: *mut MeterStats,
- ) -> c_int {
- guard_int(|| {
- // CPP-PARITY: levelmeter.cpp:39.
- invalid_if(
- planar.is_null()
- || channel_count <= 0
- || frame_count < 0
- || (!channels.is_null() && channels_capacity < channel_count),
- )?;
- invalid_if(channels.is_null() && summary.is_null())?;
- for ch in 0..channel_count {
- // SAFETY: `planar` is non-NULL with `channel_count` entries.
- if unsafe { *planar.add(ch as usize) }.is_null() {
- return Err(Error::Invalid);
- }
- }
- let views = unsafe { planar_views(planar, channel_count, frame_count) };
- let stats = crate::levelmeter::analyze_sample_buffer(&views);
-
- if !channels.is_null() {
- for (i, ch) in stats.channels.iter().enumerate() {
- // SAFETY: capacity >= channel_count (checked above).
- unsafe {
- *channels.add(i) = ChannelStats {
- peak_linear: ch.peak_linear,
- peak_db: ch.peak_db,
- rms_linear: ch.rms_linear,
- rms_db: ch.rms_db,
- vu_db: ch.vu_db,
- };
- }
- }
- }
- if !summary.is_null() {
- unsafe {
- *summary = MeterStats {
- max_peak_linear: stats.max_peak_linear,
- integrated_lufs: stats.integrated_lufs,
- silence: stats.silence as c_int,
- };
- }
- }
- Ok(crate::error::OAKAUDIO_OK)
- })
- }
-}
diff --git a/crates/oakaudio/src/handle.rs b/crates/oakaudio/src/handle.rs
deleted file mode 100644
index 8ebe47b52..000000000
--- a/crates/oakaudio/src/handle.rs
+++ /dev/null
@@ -1,226 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! Refcounted-handle scaffolding. Same pattern as the oaknode crate
-//! (`src/node/rust/src/handle.rs`); intentionally duplicated rather
-//! than shared — each module DLL must run its own addref/release code
-//! (the function pointers in a handle always point into the DLL that
-//! created the object).
-//!
-//! The `AudioManager` is the one exception: it uses the same `CHandle`
-//! layout but with singleton semantics (addref/release no-ops). See
-//! `manager.rs` and `include/audio/manager.h`.
-//!
-//! `// CPP-PARITY: src/audio/c_api/refcounted.h` (RefCounted box,
-//! make_handle_in_place, free_handle) and `src/audio/c_api/alive.cpp`
-//! (alive ledger behind `oakaudio_debug_alive_count`).
-
-use std::panic::{catch_unwind, AssertUnwindSafe};
-use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
-
-use crate::error::{Error, OAKAUDIO_E_FAILED, OAKAUDIO_OK};
-
-/// ABI version stamped into every handle.
-pub const OAKAUDIO_ABI_VERSION: u32 = 1;
-
-/// Live-object ledger behind `oakaudio_debug_alive_count`.
-///
-/// `// CPP-PARITY: src/audio/c_api/alive.cpp` (`g_alive`).
-static ALIVE: AtomicI32 = AtomicI32::new(0);
-
-/// Current number of live refcounted oakaudio objects.
-pub fn alive_count() -> i32 {
- ALIVE.load(Ordering::Relaxed)
-}
-
-/// Heap box behind a handle's `ctx`.
-pub struct RefBox {
- /// Atomic reference count.
- pub refs: AtomicU32,
- /// Boxed value.
- pub value: T,
-}
-
-/// The shared ABI value-handle type (single-lib unification, see
-/// `docs/zh/plans/riir/single-lib.md`): one canonical
-/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
-/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
-/// source-compatible. It is `Clone + Copy` (handles cross the C ABI by
-/// value) and `Send + Sync` (refcounted, shared across threads).
-pub use oakcore_rs::handle::CHandle;
-
-/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_addref`).
-unsafe extern "C" fn owned_addref(ctx: *mut std::ffi::c_void) {
- // SAFETY: `ctx` is either NULL or points to a `RefBox` created by
- // `make_owned`; we only touch it through the reference while it is live.
- if let Some(b) = unsafe { (ctx as *const RefBox).as_ref() } {
- b.refs.fetch_add(1, Ordering::Relaxed);
- }
-}
-
-/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`ref_counted_release`):
-/// destroys the box at zero and decrements the alive ledger.
-unsafe extern "C" fn owned_release(ctx: *mut std::ffi::c_void) {
- // SAFETY: `ctx` is either NULL or points to a live `RefBox` created
- // by `make_owned`; the refcount guards against double-free, and the box
- // is only reclaimed once the count reaches zero.
- if let Some(b) = unsafe { (ctx as *const RefBox).as_ref() } {
- if b.refs.fetch_sub(1, Ordering::AcqRel) == 1 {
- drop(unsafe { Box::from_raw(ctx as *mut RefBox) });
- ALIVE.fetch_sub(1, Ordering::Relaxed);
- }
- }
-}
-
-/// Owned handle with count 1; empty on allocation failure.
-pub fn make_owned(value: T) -> CHandle {
- let b = Box::new(RefBox {
- refs: AtomicU32::new(1),
- value,
- });
- ALIVE.fetch_add(1, Ordering::Relaxed);
- CHandle {
- ctx: Box::into_raw(b) as *mut std::ffi::c_void,
- addref: Some(owned_addref::),
- release: Some(owned_release::),
- abi_version: OAKAUDIO_ABI_VERSION,
- }
-}
-
-/// No-op addref/release for singleton (borrowed) handles.
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`singleton_addref` /
-/// `singleton_release`).
-unsafe extern "C" fn noop_ref(_ctx: *mut std::ffi::c_void) {}
-
-/// Borrowed handle for an object owned elsewhere (addref/release are
-/// no-ops; nothing is ever freed through the handle).
-///
-/// # Safety
-/// Caller guarantees `ptr` outlives every derived handle.
-pub unsafe fn make_borrowed(ptr: *mut T) -> CHandle {
- CHandle {
- ctx: ptr as *mut std::ffi::c_void,
- addref: Some(noop_ref),
- release: Some(noop_ref),
- abi_version: OAKAUDIO_ABI_VERSION,
- }
-}
-
-/// Typed view into an owned handle; `None` for empty handles.
-///
-/// # Safety
-/// `T` must be the boxed type.
-pub unsafe fn get(h: &CHandle) -> Option<&T> {
- if h.ctx.is_null() {
- return None;
- }
- // SAFETY: caller guarantees `h` is a valid owned handle whose ctx points
- // to a `RefBox`; the handle stays alive through the returned borrow.
- let v = unsafe { &(*(h.ctx as *const RefBox)).value };
- Some(v)
-}
-
-/// Typed mutable view into an owned handle; `None` for empty handles.
-///
-/// # Safety
-/// `T` must be the boxed type.
-pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> {
- if h.ctx.is_null() {
- return None;
- }
- // SAFETY: caller guarantees `h` is a valid owned handle whose ctx points
- // to a `RefBox`; the handle stays alive through the returned borrow,
- // and the caller must not alias it with other live borrows.
- let v = unsafe { &mut (*(h.ctx as *mut RefBox)).value };
- Some(v)
-}
-
-/// Shared free() body: release the ctx, no-op on NULL/empty handle.
-///
-/// `// CPP-PARITY: src/audio/c_api/refcounted.h` (`free_handle`).
-///
-/// # Safety
-/// `self_` must be a valid handle pointer or NULL.
-pub unsafe fn free_handle(h: *mut CHandle) {
- // SAFETY: caller guarantees `h` is a valid handle pointer or NULL.
- if let Some(r) = unsafe { h.as_mut() } {
- if !r.ctx.is_null() {
- if let Some(release) = r.release {
- // SAFETY: the release fn belongs to the same DLL and
- // accepts the ctx it originally created.
- unsafe { release(r.ctx) };
- }
- r.ctx = std::ptr::null_mut();
- }
- }
-}
-
-/// Panic-catching FFI wrapper for i32-returning exports.
-pub fn guard crate::error::Result<()>>(f: F) -> i32 {
- match catch_unwind(AssertUnwindSafe(f)) {
- Ok(Ok(())) => OAKAUDIO_OK,
- Ok(Err(e)) => e.code(),
- Err(_) => OAKAUDIO_E_FAILED,
- }
-}
-
-/// Panic-catching FFI wrapper for handle-returning exports.
-pub fn guard_handle crate::error::Result>(f: F) -> CHandle {
- match catch_unwind(AssertUnwindSafe(f)) {
- Ok(Ok(h)) => h,
- Ok(Err(_)) | Err(_) => CHandle::null(),
- }
-}
-
-/// Panic-catching FFI wrapper for i32 value-returning exports (errors map
-/// to the negative error code).
-pub fn guard_int crate::error::Result>(f: F) -> i32 {
- match catch_unwind(AssertUnwindSafe(f)) {
- Ok(Ok(v)) => v,
- Ok(Err(e)) => e.code(),
- Err(_) => OAKAUDIO_E_FAILED,
- }
-}
-
-/// Panic-catching FFI wrapper for void exports.
-pub fn guard_void(f: F) {
- let _ = catch_unwind(AssertUnwindSafe(f));
-}
-
-/// Copy a human-readable error string into a C buffer (NUL-terminated,
-/// truncated to fit). Returns the required size including the NUL.
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp` (`write_error`).
-pub fn write_error(s: &str, buf: *mut std::ffi::c_char, buf_size: i32) {
- if !buf.is_null() && buf_size > 0 {
- let bytes = s.as_bytes();
- let n = bytes.len().min((buf_size - 1) as usize);
- unsafe {
- std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
- *(buf as *mut u8).add(n) = 0;
- }
- }
-}
-
-/// Convenience: map a condition to an Invalid error.
-pub fn invalid_if(cond: bool) -> crate::error::Result<()> {
- if cond {
- Err(Error::Invalid)
- } else {
- Ok(())
- }
-}
diff --git a/crates/oakaudio/src/lib.rs b/crates/oakaudio/src/lib.rs
index 43d7aa264..a7d781126 100644
--- a/crates/oakaudio/src/lib.rs
+++ b/crates/oakaudio/src/lib.rs
@@ -30,11 +30,8 @@
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
-pub mod bridge;
pub mod config;
pub mod error;
-pub mod ffi;
-pub mod handle;
pub mod levelmeter;
pub mod manager;
pub mod params;
diff --git a/crates/oakaudio/src/manager.rs b/crates/oakaudio/src/manager.rs
index d7512ed88..8c7088fa5 100644
--- a/crates/oakaudio/src/manager.rs
+++ b/crates/oakaudio/src/manager.rs
@@ -26,13 +26,15 @@
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Mutex, MutexGuard, OnceLock};
-
-use crate::bridge::codec::EncodingParams;
+use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
+use cpal::{Device, DeviceId};
+use cpal::traits::{DeviceTrait, HostTrait};
+use oakcodec::encoder::Encoder;
+use oakcodec::encodingparams::EncodingParams;
use crate::error::{Error, Result};
-use crate::handle::{make_borrowed, CHandle};
use crate::params::AudioParams;
use crate::previewdevice::PreviewAudioDevice;
+use crate::error::Error::NotFound;
/// `paNoDevice` (PortAudio "no device" sentinel; also the default when no
/// device is configured).
@@ -46,7 +48,11 @@ static DESTROYED: AtomicBool = AtomicBool::new(false);
/// Manager state (all device/stream fields; PortAudio itself is not bridged,
/// see [`ManagerInner::default`] for the degradations).
-struct ManagerInner {
+///
+/// Public since the single-lib unification (the deleted `ffi`/`bridge`
+/// C ABI is gone): the oakengine facade calls the singleton's methods
+/// directly through [`instance`] instead of crossing the old C ABI.
+pub struct ManagerInner {
/// Current output device index.
output_device: i32,
/// Current input device index.
@@ -62,8 +68,8 @@ struct ManagerInner {
/// The real PortAudio output stream (M12 P1; opened lazily on the
/// first pushed samples).
output_device_stream: crate::outputdevice::PortAudioOutput,
- /// Active oakcodec recording encoder (NULL when idle).
- recording: Option,
+ /// Active oakcodec recording encoder (None when idle).
+ recording: Option>,
}
// SAFETY: the raw encoder pointer is only touched while the manager mutex is
@@ -89,290 +95,308 @@ impl Default for ManagerInner {
}
}
-/// Lock the manager behind a handle; `OAKAUDIO_E_STATE` for empty handles.
-fn with_instance(h: &CHandle) -> Result> {
- if h.is_null() {
- return Err(Error::State);
+impl ManagerInner {
+ /// Create the process-wide AudioManager (no-op when it exists). Returns
+ /// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`.
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new`
+ /// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly
+ /// panics, so the error code is never produced).
+ pub fn create_instance() -> Result<()> {
+ DESTROYED.store(false, Ordering::SeqCst);
+ let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
+ Ok(())
}
- // SAFETY: `instance()` only creates borrowed handles whose ctx points at
- // the MANAGER Mutex, which lives in a static for the whole process.
- let m: &'static Mutex = unsafe { &*(h.ctx as *const Mutex) };
- Ok(m.lock().unwrap_or_else(|p| p.into_inner()))
-}
-/// Create the process-wide AudioManager (no-op when it exists). Returns
-/// `OAKAUDIO_OK` or `OAKAUDIO_E_NOMEM`.
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp:73` (C++ allocates with `new`
-/// and reports `OAKAUDIO_E_NOMEM` on exception; Rust allocation infallibly
-/// panics, so the error code is never produced).
-pub fn create_instance() -> Result<()> {
- DESTROYED.store(false, Ordering::SeqCst);
- let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
- Ok(())
-}
-
-/// Destroy the process-wide AudioManager (no-op when absent).
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is
-/// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED`
-/// flag makes [`instance`] return an empty handle (and a later
-/// [`create_instance`] resurrects the existing box).
-pub fn destroy_instance() {
- DESTROYED.store(true, Ordering::SeqCst);
- // The C++ singleton is deleted on destroy; the OnceLock cannot be
- // reset, so the resurrection must at least come back with a fresh
- // playback state (the previous session's output params/buffer would
- // otherwise leak into the next session's meters and clock).
- if let Some(m) = MANAGER.get() {
- let mut inner = m.lock().unwrap_or_else(|e| e.into_inner());
- inner.output_params = None;
- inner.output_buffer.clear();
- inner.output_started = false;
- }
-}
-
-/// Return a handle to the process-wide AudioManager (borrowed; empty when
-/// no instance exists).
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a
-/// borrowed singleton whose addref/release are no-ops).
-pub fn instance() -> CHandle {
- if DESTROYED.load(Ordering::SeqCst) {
- return CHandle::null();
- }
- match MANAGER.get() {
- Some(m) => {
- // SAFETY: `m` is the process-wide singleton; borrowed handles do
- // not free it, so it outlives every handle.
- unsafe { make_borrowed(m as *const _ as *mut Mutex) }
- }
- None => CHandle::null(),
- }
-}
-
-/// Release a manager handle. No-op (singleton), safe on NULL/empty.
-///
-/// `// CPP-PARITY: src/audio/c_api/manager.cpp:95` — releasing never
-/// destroys; just clear the caller's copy.
-pub fn free(self_: *mut CHandle) {
- unsafe {
- if let Some(h) = self_.as_mut() {
- h.ctx = std::ptr::null_mut();
+ /// Destroy the process-wide AudioManager (no-op when absent).
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/manager.cpp:85` — the C++ singleton is
+ /// deleted and re-creatable; `OnceLock` cannot be reset, so a `DESTROYED`
+ /// flag makes [`instance`] return an empty handle (and a later
+ /// [`create_instance`] resurrects the existing box).
+ pub fn destroy_instance() {
+ DESTROYED.store(true, Ordering::SeqCst);
+ // The C++ singleton is deleted on destroy; the OnceLock cannot be
+ // reset, so the resurrection must at least come back with a fresh
+ // playback state (the previous session's output params/buffer would
+ // otherwise leak into the next session's meters and clock).
+ if let Some(m) = MANAGER.get() {
+ let mut inner = m.lock().unwrap_or_else(|e| e.into_inner());
+ inner.output_params = None;
+ inner.output_buffer.clear();
+ inner.output_started = false;
}
}
-}
-/// Bytes between output-notify pulses (0 disables).
-pub fn set_output_notify_interval(self_: &CHandle, bytes: i64) -> Result<()> {
- if bytes < 0 {
- return Err(Error::Invalid);
+ /// Return a handle to the process-wide AudioManager (borrowed; empty when
+ /// no instance exists).
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/manager.cpp:90` (`wrap`; the handle is a
+ /// borrowed singleton whose addref/release are no-ops).
+ pub fn instance(&self) -> Option<&Self> {
+ if DESTROYED.load(Ordering::SeqCst) {
+ return None;
+ }
+ match MANAGER.get() {
+ Some(m) => {
+ // SAFETY: `m` is the process-wide singleton; borrowed handles do
+ // not free it, so it outlives every handle.
+ Some(self)
+ }
+ None => None,
+ }
}
- let mut m = with_instance(self_)?;
- m.output_buffer.set_notify_interval(bytes);
- Ok(())
-}
-/// Push a block of samples to the output device, opening/restarting the
-/// stream when the params changed.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio
-/// open/start path is not bridged; the buffer is configured and written
-/// directly and the "stream" is marked running. `error_buf` is written by
-/// the FFI layer from the returned error.
-pub fn push_to_output(
- self_: &CHandle,
- params: AudioParams,
- samples: &[u8],
- _error_buf: &mut [u8],
-) -> Result<()> {
- let mut m = with_instance(self_)?;
- if m.output_params.as_ref() != Some(¶ms) {
- m.output_params = Some(params);
- m.output_buffer.set_params(params);
- // M12 P1: open (or re-open) the real output stream on a format
- // change; device < 0 selects the system default. A stream
- // failure keeps the samples buffered (silent playback) instead
- // of failing the push.
- let device = m.output_device;
- let rate = params.sample_rate;
+ /// Bytes between output-notify pulses (0 disables).
+ pub fn set_output_notify_interval(&self, bytes: i64) -> Result<()> {
+ if bytes < 0 {
+ return Err(Box::new(Error::Invalid));
+ }
+ self.output_buffer.set_notify_interval(bytes);
+ Ok(())
+ }
+
+ /// Push a block of samples to the output device, opening/restarting the
+ /// stream when the params changed.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:111` — the PortAudio
+ /// open/start path is not bridged; the buffer is configured and written
+ /// directly and the "stream" is marked running. `error_buf` is written by
+ /// the FFI layer from the returned error.
+ pub fn push_to_output(
+ &mut self,
+ params: AudioParams,
+ samples: &[u8],
+ _error_buf: &mut [u8],
+ ) -> Result<()> {
+ if self.output_params.as_ref() != Some(¶ms) {
+ self.output_params = Some(params);
+ self.output_buffer.set_params(params);
+ // M12 P1: open (or re-open) the real output stream on a format
+ // change; device < 0 selects the system default. A stream
+ // failure keeps the samples buffered (silent playback) instead
+ // of failing the push.
+ let device = self.output_device;
+ let rate = params.sample_rate;
+ let channels = params.channel_count();
+ let sink = self.output_buffer.clone();
+ let _ = self.output_device_stream.ensure_open(device, rate, channels, sink);
+ }
+ self.output_buffer.write(samples);
+ self.output_started = true;
+ Ok(())
+ }
+
+ /// Discard buffered output.
+ pub fn clear_buffered_output(&self) -> Result<()> {
+ self.output_buffer.clear();
+ Ok(())
+ }
+
+ /// Stop the output stream.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts
+ /// the stream and clears the buffer).
+ pub fn stop_output(&mut self) -> Result<()> {
+ self.output_started = false;
+ self.output_buffer.clear();
+ Ok(())
+ }
+
+ /// Seconds of audio consumed by the output device since the last reset,
+ /// compensated for output latency; negative when no stream is running.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's
+ /// `outputLatency` is not representable without a live stream, so the buffer
+ /// clock is used directly (the `max(0, ...)` clamp is kept).
+ pub fn seconds(&self, out: &mut f64) -> Result<()> {
+ if !self.output_started {
+ *out = -1.0;
+ return Ok(());
+ }
+ let rate = self.output_params.map(|p| p.sample_rate).unwrap_or(0);
+ if rate <= 0 {
+ *out = -1.0;
+ return Ok(());
+ }
+ let secs = self.output_buffer.output_frames_consumed() as f64 / f64::from(rate);
+ *out = secs.max(0.0);
+ Ok(())
+ }
+
+ /// Restart the output clock at zero.
+ pub fn reset_output_clock(&self) -> Result<()> {
+ self.output_buffer.reset_output_frames();
+ Ok(())
+ }
+
+ /// Current output device index (`paNoDevice` = -1) or a negative error code.
+ pub fn get_output_device(&self) -> Result {
+ Ok(self.output_device)
+ }
+
+ /// Set the output device index.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is
+ /// recorded and the stream closed; PortAudio's index validation and name
+ /// logging are not bridged).
+ pub fn set_output_device(&mut self, device: i32) -> Result<()> {
+ self.output_device = device;
+ self.output_started = false;
+ self.output_buffer.clear();
+ // The stream reopens with the new device on the next push.
+ self.output_device_stream.close();
+ Ok(())
+ }
+
+ /// Current input device index or a negative error code.
+ pub fn get_input_device(&self) -> Result {
+ Ok(self.input_device)
+ }
+
+ /// Set the input device index.
+ pub fn set_input_device(&mut self, device: i32) -> Result<()> {
+ self.input_device = device;
+ Ok(())
+ }
+
+ /// Close the output stream and re-initialize PortAudio.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/
+ /// initialize is not bridged; the output side is reset).
+ pub fn hard_reset(&mut self) -> Result<()> {
+ self.output_started = false;
+ self.output_buffer.clear();
+ self.output_device_stream.close();
+ Ok(())
+ }
+
+ /// Start recording the input device to a file via the oakcodec encoder
+ /// (direct Rust calls; the encoder is a `dyn Encoder` value). The input
+ /// stream is always captured as interleaved f32.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open;
+ /// the PortAudio input stream is not bridged). On failure the encoder's
+ /// last-error string is surfaced when available.
+ pub fn start_recording(
+ &mut self,
+ params: &EncodingParams,
+ _error_buf: &mut [u8],
+ ) -> Result<()> {
+ if self.input_device == PA_NO_DEVICE {
+ return Err(Box::new(Error::Failed("no input device".to_string())));
+ }
+ let encoder = oakcodec::encoder::create_from_params(params)
+ .ok_or_else(|| Error::Failed("failed to create encoder for recording".to_string()))?;
+ encoder
+ .configure(params)
+ .map_err(|e| Error::Failed(format!("encoder configure failed: {e:?}")))?;
+ if let Err(e) = encoder.open() {
+ let detail = encoder.get_error();
+ let msg = if detail.is_empty() {
+ format!("failed to open encoder for recording: {e:?}")
+ } else {
+ format!("failed to open encoder for recording: {detail}")
+ };
+ return Err(Box::new(Error::Failed(msg)));
+ }
+ self.recording = Some(encoder);
+ Ok(())
+ }
+
+ /// Stop recording.
+ ///
+ /// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input
+ /// stream is not bridged; the encoder is flushed and closed).
+ pub fn stop_recording(&mut self) -> Result<()> {
+ if let Some(encoder) = self.recording.take() {
+ let _ = encoder.flush();
+ let _ = encoder.close();
+ }
+ Ok(())
+ }
+
+
+
+ /// Peak level (linear, 0..1 and above) of each channel of the buffered,
+ /// not-yet-consumed output, written to `peaks` in channel order.
+ /// Returns the channel count (0 when no output is configured or the
+ /// layout is unknown). Only packed/planar F32 buffers are analyzed —
+ /// other formats report zeroed peaks. The analysis window is the most
+ /// recent 8192 frames of the queue.
+ ///
+ /// There is no C++ counterpart (the Qt side metered inside the audio
+ /// output callback); with the output callback unbridged this is how the
+ /// UI reads levels.
+ pub fn output_levels(&self, peaks: &mut [f32]) -> Result {
+ let Some(params) = self.output_params else {
+ return Ok(0);
+ };
let channels = params.channel_count();
- let sink = m.output_buffer.clone();
- let _ = m.output_device_stream.ensure_open(device, rate, channels, sink);
- }
- m.output_buffer.write(samples);
- m.output_started = true;
- Ok(())
-}
-
-/// Discard buffered output.
-pub fn clear_buffered_output(self_: &CHandle) -> Result<()> {
- let mut m = with_instance(self_)?;
- m.output_buffer.clear();
- Ok(())
-}
-
-/// Stop the output stream.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:229` (`stop_output` aborts
-/// the stream and clears the buffer).
-pub fn stop_output(self_: &CHandle) -> Result<()> {
- let mut m = with_instance(self_)?;
- m.output_started = false;
- m.output_buffer.clear();
- Ok(())
-}
-
-/// Seconds of audio consumed by the output device since the last reset,
-/// compensated for output latency; negative when no stream is running.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:169` — PortAudio's
-/// `outputLatency` is not representable without a live stream, so the buffer
-/// clock is used directly (the `max(0, ...)` clamp is kept).
-pub fn seconds(self_: &CHandle, out: &mut f64) -> Result<()> {
- let m = with_instance(self_)?;
- if !m.output_started {
- *out = -1.0;
- return Ok(());
- }
- let rate = m.output_params.map(|p| p.sample_rate).unwrap_or(0);
- if rate <= 0 {
- *out = -1.0;
- return Ok(());
- }
- let secs = m.output_buffer.output_frames_consumed() as f64 / f64::from(rate);
- *out = secs.max(0.0);
- Ok(())
-}
-
-/// Restart the output clock at zero.
-pub fn reset_output_clock(self_: &CHandle) -> Result<()> {
- let m = with_instance(self_)?;
- m.output_buffer.reset_output_frames();
- Ok(())
-}
-
-/// Current output device index (`paNoDevice` = -1) or a negative error code.
-pub fn get_output_device(self_: &CHandle) -> Result {
- let m = with_instance(self_)?;
- Ok(m.output_device)
-}
-
-/// Set the output device index.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:238` (the device is
-/// recorded and the stream closed; PortAudio's index validation and name
-/// logging are not bridged).
-pub fn set_output_device(self_: &CHandle, device: i32) -> Result<()> {
- let mut m = with_instance(self_)?;
- m.output_device = device;
- m.output_started = false;
- m.output_buffer.clear();
- // The stream reopens with the new device on the next push.
- m.output_device_stream.close();
- Ok(())
-}
-
-/// Current input device index or a negative error code.
-pub fn get_input_device(self_: &CHandle) -> Result {
- let m = with_instance(self_)?;
- Ok(m.input_device)
-}
-
-/// Set the input device index.
-pub fn set_input_device(self_: &CHandle, device: i32) -> Result<()> {
- let mut m = with_instance(self_)?;
- m.input_device = device;
- Ok(())
-}
-
-/// Close the output stream and re-initialize PortAudio.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:271` (PortAudio terminate/
-/// initialize is not bridged; the output side is reset).
-pub fn hard_reset(self_: &CHandle) -> Result<()> {
- let mut m = with_instance(self_)?;
- m.output_started = false;
- m.output_buffer.clear();
- m.output_device_stream.close();
- Ok(())
-}
-
-/// Start recording the input device to a file via the oakcodec encoder C
-/// ABI. The input stream is always captured as interleaved f32.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:278` (encoder init/open;
-/// the PortAudio input stream is not bridged). On failure the encoder's
-/// last-error string is surfaced when available.
-pub fn start_recording(
- self_: &CHandle,
- params: &EncodingParams,
- _error_buf: &mut [u8],
-) -> Result<()> {
- let mut m = with_instance(self_)?;
- if m.input_device == PA_NO_DEVICE {
- return Err(Error::Failed("no input device".to_string()));
- }
- eprintln!(
- "MANAGER before encoder_init: audio_enabled={} codec={}",
- params.audio_enabled, params.audio_codec
- );
- let mut enc = unsafe { crate::bridge::codec::oakcodec_encoder_init(params) };
- eprintln!(
- "MANAGER encoder_init null? {} ptr={:p} size={}",
- enc.is_null(),
- params as *const EncodingParams,
- std::mem::size_of::()
- );
- let direct =
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_init(params as *const EncodingParams) };
- eprintln!("MANAGER direct init null? {}", direct.is_null());
- if !direct.is_null() {
- let mut d = direct;
- unsafe { oakcodec::ffi::encoder::oakcodec_encoder_free(&mut d) };
- }
- if enc.is_null() {
- return Err(Error::Failed(
- "failed to open encoder for recording".to_string(),
- ));
- }
- let open_r = unsafe { crate::bridge::codec::oakcodec_encoder_open(enc) };
- if open_r != 0 {
- let mut buf = [0i8; 512];
- let n = unsafe {
- crate::bridge::codec::oakcodec_encoder_last_error(
- enc,
- buf.as_mut_ptr(),
- buf.len() as i32,
- )
- };
- let msg = if n > 0 {
- let s = buf.split(|c| *c == 0).next().unwrap_or(&[]);
- let bytes: Vec = s.iter().map(|&b| b as u8).collect();
- String::from_utf8_lossy(&bytes).into_owned()
- } else {
- "failed to open encoder for recording".to_string()
- };
- unsafe { crate::bridge::codec::oakcodec_encoder_free(&mut enc) };
- return Err(Error::Failed(msg));
- }
- m.recording = Some(enc);
- Ok(())
-}
-
-/// Stop recording.
-///
-/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:328` (the PortAudio input
-/// stream is not bridged; the encoder is flushed and freed).
-pub fn stop_recording(self_: &CHandle) -> Result<()> {
- let mut m = with_instance(self_)?;
- if let Some(mut enc) = m.recording.take() {
- unsafe {
- crate::bridge::codec::oakcodec_encoder_flush(enc);
- crate::bridge::codec::oakcodec_encoder_free(&mut enc);
+ if channels <= 0 {
+ return Ok(0);
}
+ let n = (channels as usize).min(peaks.len());
+ for p in &mut peaks[..n] {
+ *p = 0.0;
+ }
+ use crate::params::SampleFormat;
+ let packed = params.format == SampleFormat::F32;
+ let planar = params.format == SampleFormat::F32Planar;
+ if !packed && !planar {
+ return Ok(channels);
+ }
+ let frames_max = 8192i64;
+ let bpf = channels as i64 * 4;
+ let mut buf = vec![0u8; (bpf * frames_max) as usize];
+ let got = self.output_buffer.peek_tail(&mut buf);
+ // Whole frames only; the tail is what we hold, so leading partial
+ // bytes (when the queue is not frame-aligned) are dropped.
+ let frames = got / bpf;
+ if frames == 0 {
+ return Ok(channels);
+ }
+ let bytes = (frames * bpf) as usize;
+ let buf = &buf[..bytes];
+ let frame_count = frames as usize;
+ let channel_count = channels as usize;
+ let mut planes: Vec> = vec![Vec::with_capacity(frame_count); channel_count];
+ if packed {
+ for frame in buf.chunks_exact(bpf as usize) {
+ for ch in 0..channel_count {
+ let b = &frame[ch * 4..ch * 4 + 4];
+ planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
+ }
+ }
+ } else {
+ for (ch, plane) in planes.iter_mut().enumerate() {
+ let start = ch * frame_count * 4;
+ for b in buf[start..start + frame_count * 4].chunks_exact(4) {
+ plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
+ }
+ }
+ }
+ let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
+ let stats = crate::levelmeter::analyze_sample_buffer(&views);
+ for (i, p) in peaks[..n].iter_mut().enumerate() {
+ *p = stats.channels[i].peak_linear as f32;
+ }
+ Ok(channels)
}
- Ok(())
+
+}
+
+/// Lock the process-wide manager singleton (the direct-Rust replacement
+/// for the deleted C ABI's `oakaudio_manager_instance`): `None` when no
+/// instance exists (never created, or destroyed since the last
+/// [`ManagerInner::create_instance`]).
+pub fn instance() -> Option> {
+ if DESTROYED.load(Ordering::SeqCst) {
+ return None;
+ }
+ MANAGER
+ .get()
+ .map(|m| m.lock().unwrap_or_else(|e| e.into_inner()))
}
/// Device index named by the configuration ("AudioOutput"/"AudioInput"), or
@@ -380,9 +404,9 @@ pub fn stop_recording(self_: &CHandle) -> Result<()> {
/// not initialized.
///
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:404`.
-pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 {
- let name = crate::config::device_name(is_output_device);
- find_device_by_name_s(&name, is_output_device)
+pub fn find_config_device_by_name_s(is_output_device: bool) -> Result {
+ let name = crate::config::device_name(is_output_device)?;
+ find_device_by_name_s_or_default(&name, is_output_device)
}
/// Device index whose name matches `name` exactly (empty matches nothing,
@@ -391,82 +415,31 @@ pub fn find_config_device_by_name_s(is_output_device: bool) -> i32 {
/// `// CPP-PARITY: src/audio/src/audiomanager.cpp:410` — PortAudio device
/// enumeration cannot be bridged from this crate, so the result is always
/// `paNoDevice` and the caller falls back to the default device.
-pub fn find_device_by_name_s(name: &std::ffi::CStr, _is_output_device: bool) -> i32 {
- let _ = name;
- PA_NO_DEVICE
-}
-
-/// Peak level (linear, 0..1 and above) of each channel of the buffered,
-/// not-yet-consumed output, written to `peaks` in channel order.
-/// Returns the channel count (0 when no output is configured or the
-/// layout is unknown). Only packed/planar F32 buffers are analyzed —
-/// other formats report zeroed peaks. The analysis window is the most
-/// recent 8192 frames of the queue.
-///
-/// There is no C++ counterpart (the Qt side metered inside the audio
-/// output callback); with the output callback unbridged this is how the
-/// UI reads levels.
-pub fn output_levels(self_: &CHandle, peaks: &mut [f32]) -> Result {
- let m = with_instance(self_)?;
- let Some(params) = m.output_params else {
- return Ok(0);
- };
- let channels = params.channel_count();
- if channels <= 0 {
- return Ok(0);
- }
- let n = (channels as usize).min(peaks.len());
- for p in &mut peaks[..n] {
- *p = 0.0;
- }
- use crate::params::SampleFormat;
- let packed = params.format == SampleFormat::F32;
- let planar = params.format == SampleFormat::F32Planar;
- if !packed && !planar {
- return Ok(channels);
- }
- let frames_max = 8192i64;
- let bpf = channels as i64 * 4;
- let mut buf = vec![0u8; (bpf * frames_max) as usize];
- let got = m.output_buffer.peek_tail(&mut buf);
- // Whole frames only; the tail is what we hold, so leading partial
- // bytes (when the queue is not frame-aligned) are dropped.
- let frames = got / bpf;
- if frames == 0 {
- return Ok(channels);
- }
- let bytes = (frames * bpf) as usize;
- let buf = &buf[..bytes];
- let frame_count = frames as usize;
- let channel_count = channels as usize;
- let mut planes: Vec> = vec![Vec::with_capacity(frame_count); channel_count];
- if packed {
- for frame in buf.chunks_exact(bpf as usize) {
- for ch in 0..channel_count {
- let b = &frame[ch * 4..ch * 4 + 4];
- planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
- }
+pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) -> Result {
+ let host = cpal::default_host();
+ let device = host.devices()?.find(|d| {
+ if d.id().is_err(){
+ return false;
}
- } else {
- for (ch, plane) in planes.iter_mut().enumerate() {
- let start = ch * frame_count * 4;
- for b in buf[start..start + frame_count * 4].chunks_exact(4) {
- plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
- }
+ if d.supports_output() && d.id().unwrap().id() == name {
+ return true;
+ }
+ false
+ });
+ if let Some(device) = device {
+ Ok(device)
+ }
+ else{
+ if let Some(device) = host.default_output_device() {
+ Ok(device)
+ }
+ else{
+ Err(Box::new(Error::NotFound))
}
}
- let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
- let stats = crate::levelmeter::analyze_sample_buffer(&views);
- for (i, p) in peaks[..n].iter_mut().enumerate() {
- *p = stats.channels[i].peak_linear as f32;
- }
- Ok(channels)
+
}
-/// Number of live oakaudio reference-counted objects (leak check).
-pub fn debug_alive_count() -> i32 {
- crate::handle::alive_count()
-}
#[cfg(test)]
mod tests {
@@ -485,40 +458,9 @@ mod tests {
// is_active=true while delivering zero callbacks.
use std::sync::atomic::AtomicI64 as A;
static PROBE: A = A::new(0);
- let can_play = (|| {
- let pa = portaudio::PortAudio::new().ok()?;
- let dev = pa.default_output_device().ok()?;
- let info = pa.device_info(dev).ok()?;
- let params = portaudio::StreamParameters::::new(
- dev,
- 2,
- true,
- info.default_low_output_latency,
- );
- let settings = portaudio::OutputStreamSettings::new(params, 48000.0, 512);
- let cb = move |args: portaudio::OutputStreamCallbackArgs| {
- PROBE.fetch_add(args.frames as i64, Ordering::Relaxed);
- portaudio::Continue
- };
- let mut stream = pa.open_non_blocking_stream(settings, cb).ok()?;
- stream.start().ok()?;
- let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
- while std::time::Instant::now() < deadline && PROBE.load(Ordering::Relaxed) == 0 {
- std::thread::sleep(std::time::Duration::from_millis(50));
- }
- let got = PROBE.load(Ordering::Relaxed) > 0;
- let _ = stream.stop();
- Some(got)
- })();
- if can_play != Some(true) {
- eprintln!("audio session cannot deliver callbacks; skipping");
- return;
- }
DESTROYED.store(false, Ordering::SeqCst);
- let _ = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
- let h = instance();
- assert!(!h.is_null());
+ let manager = MANAGER.get_or_init(|| Mutex::new(ManagerInner::default()));
// A 440 Hz sine, 0.2 s at 48 kHz stereo, packed F32.
let params = AudioParams {
@@ -537,8 +479,7 @@ mod tests {
.iter()
.flat_map(|s| s.to_le_bytes())
.collect();
- push_to_output(
- &h,
+ manager.lock().unwrap().push_to_output(
params,
&bytes,
&mut vec![0u8; 256],
@@ -548,29 +489,24 @@ mod tests {
// Give the audio thread time to consume. PortAudio/CoreAudio
// stream startup can take SECONDS in some environments (audio HAL
// device probing), so poll with a generous deadline instead of a
- // fixed sleep.
+ // fixed sleep. The callback advances the clock for EVERY invocation
+ // (underrun counts too), so the pushed frames drain within a couple
+ // of hundred milliseconds of real audio time once the stream runs.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
+ let target = frames as i64 - 1024;
let mut consumed = 0i64;
while std::time::Instant::now() < deadline {
- {
- let m = with_instance(&h).unwrap();
- consumed = m.output_buffer.output_frames_consumed();
- }
- if consumed > 0 {
+ consumed = manager.lock().unwrap().output_buffer.output_frames_consumed();
+ if consumed >= target {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert!(
- consumed > 0,
- "the output callback must consume pushed frames"
- );
- assert!(
- consumed >= frames as i64 - 1024,
- "most pushed frames are consumed: {consumed}"
+ consumed >= target,
+ "the output callback must consume the pushed frames (consumed {consumed} of {frames})"
);
- let mut m = with_instance(&h).unwrap();
- m.output_device_stream.close();
+ manager.lock().unwrap().output_device_stream.close();
}
}
diff --git a/crates/oakaudio/src/outputdevice.rs b/crates/oakaudio/src/outputdevice.rs
index a91b82f1b..94d4fe549 100644
--- a/crates/oakaudio/src/outputdevice.rs
+++ b/crates/oakaudio/src/outputdevice.rs
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! M12 P1: the real audio output device (PortAudio, direct crate call).
+//! M12 P1: the real audio output device (cpal, direct crate call).
//!
//! The playback stream's callback pulls interleaved bytes from the
//! shared [`PreviewAudioDevice`] and advances the output clock; underrun
@@ -25,32 +25,38 @@
//! device's mutex (a short critical section; the buffer is drained in
//! whole frames).
+use std::cell::RefCell;
use std::sync::Arc;
-use portaudio::{
- DeviceIndex, OutputStreamCallbackArgs, OutputStreamSettings, StreamParameters, Stream,
+use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
+use cpal::{
+ BufferSize, Device, Host, SampleFormat, SampleRate, Stream, StreamConfig,
+ SupportedBufferSize,
};
-
use crate::previewdevice::PreviewAudioDevice;
-/// The default PortAudio frames-per-buffer for the preview stream.
+/// The default frames-per-buffer requested for the preview stream
+/// (clamped to the device's supported range).
const FRAMES_PER_BUFFER: u32 = 512;
-/// An open (or openable) PortAudio output stream.
+/// An open (or openable) cpal output stream.
pub struct PortAudioOutput {
- /// PortAudio session (created lazily; `None` when unavailable).
- pa: Option,
+ /// Audio host/session (created lazily; `None` when unavailable).
+ host: Option,
/// The live stream (None when closed).
- stream: Option>>,
+ stream: Option,
/// The (device, rate, channels) the stream was opened with.
opened: Option<(i32, i32, i32)>,
}
+/// Backwards-compatible name for [`PortAudioOutput`].
+pub type AudioOutput = PortAudioOutput;
+
impl PortAudioOutput {
- /// Create the device (PortAudio initialized lazily on first use).
+ /// Create the device (the host is created lazily on first use).
pub fn new() -> PortAudioOutput {
PortAudioOutput {
- pa: None,
+ host: Some(cpal::default_host()),
stream: None,
opened: None,
}
@@ -61,16 +67,17 @@ impl PortAudioOutput {
self.stream.is_some()
}
- /// Close the stream (stop + drop).
+ /// Close the stream (pause + drop).
pub fn close(&mut self) {
- if let Some(mut s) = self.stream.take() {
- let _ = s.stop();
+ if let Some(stream) = self.stream.take() {
+ let _ = stream.pause();
}
self.opened = None;
}
/// Ensure an output stream is open for `(device, rate, channels)` and
- /// pulling from `sink`. `device` < 0 selects the system default.
+ /// pulling from `sink`. `device` < 0 selects the system default output;
+ /// any other value is an index into the host's output device list.
/// Failures leave the output silent (samples still buffer).
pub fn ensure_open(
&mut self,
@@ -87,70 +94,117 @@ impl PortAudioOutput {
}
self.close();
- let pa = match &self.pa {
- Some(pa) => pa.clone(),
- None => {
- let pa = portaudio::PortAudio::new()
- .map_err(|e| format!("PortAudio init failed: {e:?}"))?;
- self.pa = Some(pa);
- match self.pa.as_ref() {
- Some(pa) => pa.clone(),
- None => unreachable!("just assigned"),
- }
- }
- };
+ if self.host.is_none(){
+ self.host = Some(cpal::default_host())
+ }
- let dev = if device >= 0 {
- DeviceIndex(device as u32)
- } else {
- pa.default_output_device()
- .map_err(|e| format!("no default output device: {e:?}"))?
- };
- let info = pa
- .device_info(dev)
- .map_err(|e| format!("device info failed: {e:?}"))?;
- let latency = info.default_low_output_latency;
- let params = StreamParameters::::new(dev, channels, true, latency);
- let settings = OutputStreamSettings::::new(params, rate as f64, FRAMES_PER_BUFFER);
+ let host = self.host.as_ref().unwrap().clone();
+
+ let output_device = resolve_device(&host, device)
+ .ok_or_else(|| "no output device available".to_string())?;
+ let config = pick_config(&output_device, rate, channels)?;
// The callback pulls whole frames from the shared device and
// advances the output clock (underrun → silence). `read` locks
// the device internally; no other locks are taken on the audio
// thread. The scratch buffer is allocated once and reused —
// allocating per callback would violate the real-time rule.
+ let channels_usize = channels.max(1) as usize;
let sink_cb = sink.clone();
- let scratch = std::cell::RefCell::new(Vec::::new());
- let callback = move |args: OutputStreamCallbackArgs| {
- let out = args.buffer;
- let frames = args.frames;
- let total = frames * channels.max(1) as usize;
+ let scratch = RefCell::new(Vec::::new());
+ let callback = move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| {
+ let total = out.len();
let mut scratch = scratch.borrow_mut();
scratch.resize(total * 4, 0);
- let byte_buf = &mut *scratch;
- let got = sink_cb.read(byte_buf);
- let frames_got = (got as usize) / (channels.max(1) as usize * 4);
- // Convert the interleaved f32 bytes in place to a sample
- // slice (PortAudio writes f32s directly).
- let n_samples = frames_got * channels.max(1) as usize;
- for i in 0..n_samples {
- let b = &byte_buf[i * 4..i * 4 + 4];
- out[i] = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
+ let got = sink_cb.read(scratch.as_mut_slice());
+ let frames_got = (got as usize) / (channels_usize * 4);
+ // Convert the interleaved f32 bytes in place to the sample
+ // slice (cpal hands us f32s directly); zero-fill the underrun
+ // remainder with silence.
+ let n_samples = frames_got * channels_usize;
+ for (i, s) in out.iter_mut().enumerate() {
+ if i < n_samples {
+ let b = &scratch[i * 4..i * 4 + 4];
+ *s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
+ } else {
+ *s = 0.0;
+ }
}
- for s in out.iter_mut().skip(n_samples) {
- *s = 0.0; // underrun: silence
- }
- sink_cb.add_output_frames(frames as i64);
- portaudio::Continue
+ sink_cb.add_output_frames((total / channels_usize) as i64);
+ };
+ let err_callback = |err: cpal::Error| {
+ eprintln!("output stream error: {err}");
};
- let mut stream = pa
- .open_non_blocking_stream(settings, callback)
- .map_err(|e| format!("output stream open failed: {e:?}"))?;
+ let stream = output_device
+ .build_output_stream(config, callback, err_callback, None)
+ .map_err(|e| format!("output stream open failed: {e}"))?;
stream
- .start()
- .map_err(|e| format!("output stream start failed: {e:?}"))?;
+ .play()
+ .map_err(|e| format!("output stream start failed: {e}"))?;
self.stream = Some(stream);
self.opened = Some((device, rate, channels));
Ok(())
}
}
+
+impl Default for PortAudioOutput {
+ fn default() -> Self {
+ PortAudioOutput::new()
+ }
+}
+
+/// Resolve a PortAudio-style device index to a cpal device: an index >= 0
+/// picks the Nth output device of the host; anything else (notably -1,
+/// `paNoDevice`) falls back to the host's default output device.
+fn resolve_device(host: &Host, index: i32) -> Option {
+ if index >= 0 {
+ if let Ok(mut devices) = host.output_devices() {
+ if let Some(device) = devices.nth(index as usize) {
+ return Some(device);
+ }
+ }
+ }
+ host.default_output_device()
+}
+
+/// Pick an F32 stream config for `(rate, channels)`. The callback always
+/// interprets the buffer as interleaved f32 with the requested channel
+/// count, so a config with another format or channel count is never used;
+/// the sample rate is clamped into the device's supported range. The
+/// requested buffer size is honored when it lies inside the supported
+/// range.
+fn pick_config(device: &Device, rate: i32, channels: i32) -> Result {
+ let want_rate = rate as u32;
+ let want_channels = channels as u16;
+
+ if let Ok(mut configs) = device.supported_output_configs() {
+ while let Some(c) = configs.next() {
+ if c.sample_format() != SampleFormat::F32 || c.channels() != want_channels {
+ continue;
+ }
+ let rate = want_rate.clamp(c.min_sample_rate(), c.max_sample_rate());
+ let buffer_size = match c.buffer_size() {
+ SupportedBufferSize::Range { min, max } => {
+ BufferSize::Fixed(FRAMES_PER_BUFFER.clamp(*min, *max))
+ }
+ SupportedBufferSize::Unknown => BufferSize::Default,
+ };
+ return Ok(StreamConfig {
+ channels: want_channels,
+ sample_rate: rate,
+ buffer_size,
+ });
+ }
+ }
+
+ let fallback = device
+ .default_output_config()
+ .map_err(|e| format!("output device config unavailable: {e}"))?;
+ if fallback.sample_format() != SampleFormat::F32 || fallback.channels() != want_channels {
+ return Err(format!(
+ "output device supports no F32/{want_channels}-channel stream"
+ ));
+ }
+ Ok(fallback.config())
+}
diff --git a/crates/oakaudio/src/processor.rs b/crates/oakaudio/src/processor.rs
index f7a199972..4ef99d6a8 100644
--- a/crates/oakaudio/src/processor.rs
+++ b/crates/oakaudio/src/processor.rs
@@ -32,11 +32,10 @@ use ffmpeg::{ChannelLayout, Error as FfmpegError};
use ffmpeg_next as ffmpeg;
use crate::error::{Error, Result};
-use crate::handle::{free_handle, make_owned, CHandle};
use crate::params::{AudioParams, SampleFormat};
-/// A closed audio processor (reference count 1; `ctx == NULL` on allocation
-/// failure).
+/// An audio processor; created closed, configured with
+/// [`open`](Processor::open).
pub struct Processor {
inner: Mutex,
}
@@ -54,8 +53,8 @@ struct ProcessorInner {
}
// SAFETY: the filter graph's raw pointers are only dereferenced through the
-// FFmpeg API while the mutex is held, so all access is serialized; the
-// handle's refcount keeps the box alive.
+// FFmpeg API while the processor's mutex is held, so all access is
+// serialized.
unsafe impl Send for ProcessorInner {}
impl Default for ProcessorInner {
@@ -136,7 +135,7 @@ fn fix_channel_layout(params: AudioParams) -> AudioParams {
fn build_graph(from: &AudioParams, to: &AudioParams, speed: f64) -> Result {
let in_format = to_ffmpeg_sample_format(from.format);
if in_format == Sample::None {
- return Err(Error::Failed("invalid input sample format".to_string()));
+ return Err(Box::new(Error::Failed("invalid input sample format".to_string())));
}
let abuffer = ffmpeg::filter::find("abuffer")
@@ -196,236 +195,223 @@ fn is_drain(e: &FfmpegError) -> bool {
|| matches!(e, FfmpegError::Other { errno } if *errno == ffmpeg::error::EAGAIN)
}
-/// Borrow the processor state behind a handle.
-fn get_processor(self_: &CHandle) -> Result<&Processor> {
- // SAFETY: every non-empty handle returned by `init` boxes a `Processor`.
- unsafe { crate::handle::get::(self_) }.ok_or(Error::Invalid)
-}
-
-/// Create a closed processor.
-pub fn init() -> Result {
- Ok(make_owned(Processor {
- inner: Mutex::new(ProcessorInner::default()),
- }))
-}
-
-/// Release one reference to a processor (NULL/empty no-op).
-pub fn free(self_: *mut CHandle) {
- unsafe { free_handle(self_) };
-}
-
-/// Open the resampling/format-conversion graph. `out_format` is accepted for
-/// interface completeness but the conversion output is always planar f32.
-///
-/// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order:
-/// empty handle, already-open state, invalid rates/speed, forced output
-/// format) and `src/audio/src/audioprocessor.cpp:82` (graph creation).
-pub fn open(self_: &CHandle, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> {
- let p = get_processor(self_)?;
- let mut inner = p.inner.lock().unwrap();
-
- if inner.graph.is_some() {
- // C++: "tried to open a processor that was already open"
- return Err(Error::State);
- }
- if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 {
- return Err(Error::Invalid);
- }
- // The C ABI delivers planar float output only; force the output format
- // stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4).
- if to.format != SampleFormat::F32Planar {
- return Err(Error::Invalid);
- }
-
- let from_fixed = fix_channel_layout(from);
- let to_fixed = fix_channel_layout(to);
-
- // C++: "failed to create audio filter graph"
- let graph = build_graph(&from_fixed, &to_fixed, speed)?;
-
- inner.graph = Some(graph);
- inner.out_frame = ffmpeg::frame::Audio::empty();
- inner.from = from_fixed;
- inner.to = to_fixed;
- Ok(())
-}
-
-/// Close the graph (safe when closed; handle must be non-empty).
-pub fn close(self_: &CHandle) -> Result<()> {
- let p = get_processor(self_)?;
- let mut inner = p.inner.lock().unwrap();
-
- inner.graph = None;
- inner.out_frame = ffmpeg::frame::Audio::empty();
- Ok(())
-}
-
-/// 1 when open, 0 when closed; error for an empty handle.
-pub fn is_open(self_: &CHandle) -> Result {
- let p = get_processor(self_)?;
- let inner = p.inner.lock().unwrap();
- Ok(inner.graph.is_some())
-}
-
-/// Push planar float input and pull converted output. Returns the number of
-/// output frames written.
-///
-/// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state
-/// check, null `out_planar` short-circuit) and
-/// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting).
-pub fn convert(
- self_: &CHandle,
- in_planar: *const *const f32,
- in_frame_count: i32,
- out_planar: *const *mut f32,
- out_capacity_frames: i32,
-) -> Result {
- let p = get_processor(self_)?;
- let mut guard = p.inner.lock().unwrap();
- let inner = &mut *guard;
-
- if inner.graph.is_none() {
- return Err(Error::State);
- }
- if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
- {
- return Err(Error::Invalid);
- }
-
- let channels = inner.to.channel_count();
- if channels <= 0 {
- return Err(Error::State);
- }
-
- let from = inner.from;
- let graph = inner.graph.as_mut().unwrap();
- let out_frame = &mut inner.out_frame;
-
- if in_frame_count > 0 {
- // The FFI layer has no way to know the input plane count, so the
- // plane pointer array is walked using the input spec recorded at
- // `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`).
- let nb = in_frame_count as usize;
- let in_channels = from.channel_count().max(0) as usize;
- let layout = channel_layout_from_mask(from.channel_layout);
- let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
- frame.set_rate(from.sample_rate as u32);
- let planar = from.format.is_planar();
- // `plane_mut::` requires the exact sample type of the frame
- // format, so the copy dispatches on the recorded input format.
- macro_rules! fill {
- ($t:ty) => {{
- if planar {
- for ch in 0..in_channels {
- // SAFETY: `in_planar` is non-null here and the FFI
- // contract guarantees at least `from.channel_count()`
- // entries, each pointing at `nb` samples of the
- // recorded input format.
- let src = unsafe { *in_planar.add(ch) } as *const $t;
- let dst = frame.plane_mut::<$t>(ch);
- unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) };
- }
- } else {
- // Packed input: a single plane at `in_planar[0]`.
- // SAFETY: see above; the plane holds `nb * channels`
- // samples.
- let src = unsafe { *in_planar } as *const $t;
- let dst = frame.plane_mut::<$t>(0);
- unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
- }
- }};
+impl Processor {
+ /// Create a closed processor.
+ pub fn init() -> Processor {
+ Processor {
+ inner: Mutex::new(ProcessorInner::default()),
}
- match from.format {
- SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8),
- SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16),
- SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32),
- SampleFormat::S64Planar | SampleFormat::S64 => {
- // ffmpeg-next's typed plane API has no `i64` impl; copy the
- // 8-byte samples through the raw plane pointers.
- if planar {
- for ch in 0..in_channels {
+ }
+
+ /// Open the resampling/format-conversion graph. `out_format` is accepted
+ /// for interface completeness but the conversion output is always planar
+ /// f32.
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/processor.cpp:43` (validation order:
+ /// already-open state, invalid rates/speed, forced output format) and
+ /// `src/audio/src/audioprocessor.cpp:82` (graph creation).
+ pub fn open(&self, from: AudioParams, to: AudioParams, speed: f64) -> Result<()> {
+ let mut inner = self.inner.lock().unwrap();
+
+ if inner.graph.is_some() {
+ // C++: "tried to open a processor that was already open"
+ return Err(Box::from(Error::State));
+ }
+ if from.sample_rate <= 0 || to.sample_rate <= 0 || speed <= 0.0 {
+ return Err(Box::from(Error::Invalid));
+ }
+ // The C ABI delivers planar float output only; force the output format
+ // stage to f32p (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT == 4).
+ if to.format != SampleFormat::F32Planar {
+ return Err(Box::from(Error::Invalid));
+ }
+
+ let from_fixed = fix_channel_layout(from);
+ let to_fixed = fix_channel_layout(to);
+
+ // C++: "failed to create audio filter graph"
+ let graph = build_graph(&from_fixed, &to_fixed, speed)?;
+
+ inner.graph = Some(graph);
+ inner.out_frame = ffmpeg::frame::Audio::empty();
+ inner.from = from_fixed;
+ inner.to = to_fixed;
+ Ok(())
+ }
+
+ /// Close the graph (safe when closed).
+ pub fn close(&self) -> Result<()> {
+ let mut inner = self.inner.lock().unwrap();
+
+ inner.graph = None;
+ inner.out_frame = ffmpeg::frame::Audio::empty();
+ Ok(())
+ }
+
+ /// 1 when open, 0 when closed.
+ pub fn is_open(&self) -> Result {
+ let inner = self.inner.lock().unwrap();
+ Ok(inner.graph.is_some())
+ }
+
+ /// Push planar float input and pull converted output. Returns the number
+ /// of output frames written.
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state
+ /// check, null `out_planar` short-circuit) and
+ /// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting).
+ pub fn convert(
+ &self,
+ in_planar: *const *const f32,
+ in_frame_count: i32,
+ out_planar: *const *mut f32,
+ out_capacity_frames: i32,
+ ) -> Result {
+ let mut guard = self.inner.lock().unwrap();
+ let inner = &mut *guard;
+
+ if inner.graph.is_none() {
+ return Err(Box::from(Error::State));
+ }
+ if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
+ {
+ return Err(Box::from(Error::Invalid));
+ }
+
+ let channels = inner.to.channel_count();
+ if channels <= 0 {
+ return Err(Box::from(Error::State));
+ }
+
+ let from = inner.from;
+ let graph = inner.graph.as_mut().unwrap();
+ let out_frame = &mut inner.out_frame;
+
+ if in_frame_count > 0 {
+ // The FFI layer has no way to know the input plane count, so the
+ // plane pointer array is walked using the input spec recorded at
+ // `open` (`// CPP-PARITY: src/audio/src/audioprocessor.cpp:141`).
+ let nb = in_frame_count as usize;
+ let in_channels = from.channel_count().max(0) as usize;
+ let layout = channel_layout_from_mask(from.channel_layout);
+ let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
+ frame.set_rate(from.sample_rate as u32);
+ let planar = from.format.is_planar();
+ // `plane_mut::` requires the exact sample type of the frame
+ // format, so the copy dispatches on the recorded input format.
+ macro_rules! fill {
+ ($t:ty) => {{
+ if planar {
+ for ch in 0..in_channels {
+ // SAFETY: `in_planar` is non-null here and the FFI
+ // contract guarantees at least `from.channel_count()`
+ // entries, each pointing at `nb` samples of the
+ // recorded input format.
+ let src = unsafe { *in_planar.add(ch) } as *const $t;
+ let dst = frame.plane_mut::<$t>(ch);
+ unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb) };
+ }
+ } else {
+ // Packed input: a single plane at `in_planar[0]`.
+ // SAFETY: see above; the plane holds `nb * channels`
+ // samples.
+ let src = unsafe { *in_planar } as *const $t;
+ let dst = frame.plane_mut::<$t>(0);
+ unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
+ }
+ }};
+ }
+ match from.format {
+ SampleFormat::U8Planar | SampleFormat::U8 => fill!(u8),
+ SampleFormat::S16Planar | SampleFormat::S16 => fill!(i16),
+ SampleFormat::S32Planar | SampleFormat::S32 => fill!(i32),
+ SampleFormat::S64Planar | SampleFormat::S64 => {
+ // ffmpeg-next's typed plane API has no `i64` impl; copy the
+ // 8-byte samples through the raw plane pointers.
+ if planar {
+ for ch in 0..in_channels {
+ // SAFETY: same contract as above; the plane is
+ // `nb * 8` bytes.
+ let src = unsafe { *in_planar.add(ch) } as *const u8;
+ let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) };
+ unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) };
+ }
+ } else {
// SAFETY: same contract as above; the plane is
- // `nb * 8` bytes.
- let src = unsafe { *in_planar.add(ch) } as *const u8;
- let dst = unsafe { *(*frame.as_mut_ptr()).extended_data.add(ch) };
- unsafe { ptr::copy_nonoverlapping(src, dst, nb * 8) };
+ // `nb * channels * 8` bytes.
+ let src = unsafe { *in_planar } as *const u8;
+ let dst = unsafe { *(*frame.as_mut_ptr()).extended_data };
+ unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) };
}
- } else {
- // SAFETY: same contract as above; the plane is
- // `nb * channels * 8` bytes.
- let src = unsafe { *in_planar } as *const u8;
- let dst = unsafe { *(*frame.as_mut_ptr()).extended_data };
- unsafe { ptr::copy_nonoverlapping(src, dst, nb * in_channels * 8) };
}
+ SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32),
+ SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64),
+ SampleFormat::Invalid => return Err(Box::new(Error::State)),
}
- SampleFormat::F32Planar | SampleFormat::F32 => fill!(f32),
- SampleFormat::F64Planar | SampleFormat::F64 => fill!(f64),
- SampleFormat::Invalid => return Err(Error::State),
- }
- if let Err(e) = graph.get("in").unwrap().source().add(&frame) {
- return Err(Error::Failed(format!(
- "failed to add frame to buffersrc: {e}"
- )));
- }
- }
-
- // C++: `out_planar ? &buf : nullptr` — with no destination, the input is
- // pushed but nothing is pulled.
- if out_planar.is_null() {
- return Ok(0);
- }
-
- let mut total: i64 = 0;
- loop {
- let pulled = graph.get("out").unwrap().sink().frame(out_frame);
- match pulled {
- Ok(()) => {}
- Err(e) if is_drain(&e) => break,
- Err(e) => {
- return Err(Error::Failed(format!(
- "failed to pull from buffersink: {e}"
- )))
+ if let Err(e) = graph.get("in").unwrap().source().add(&frame) {
+ return Err(Box::new(Error::Failed(format!(
+ "failed to add frame to buffersrc: {e}"
+ ))));
}
}
- let nb = out_frame.samples() as i32;
- if nb > 0 && total < i64::from(out_capacity_frames) {
- let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32;
- for ch in 0..channels {
- // SAFETY: the FFI contract guarantees at least `channels`
- // entries in `out_planar` (NULL entries are skipped).
- let dst = unsafe { *out_planar.add(ch as usize) };
- if dst.is_null() {
- continue;
- }
- // Output is planar f32 (enforced by open()); each plane is
- // `to_copy` float samples.
- let src = out_frame.plane::(ch as usize);
- unsafe {
- ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize);
+ // C++: `out_planar ? &buf : nullptr` — with no destination, the input is
+ // pushed but nothing is pulled.
+ if out_planar.is_null() {
+ return Ok(0);
+ }
+
+ let mut total: i64 = 0;
+ loop {
+ let pulled = graph.get("out").unwrap().sink().frame(out_frame);
+ match pulled {
+ Ok(()) => {}
+ Err(e) if is_drain(&e) => break,
+ Err(e) => {
+ return Err(Box::from(Error::Failed(format!(
+ "failed to pull from buffersink: {e}"
+ ))))
}
}
+
+ let nb = out_frame.samples() as i32;
+ if nb > 0 && total < i64::from(out_capacity_frames) {
+ let to_copy = (i64::from(out_capacity_frames) - total).min(i64::from(nb)) as i32;
+ for ch in 0..channels {
+ // SAFETY: the FFI contract guarantees at least `channels`
+ // entries in `out_planar` (NULL entries are skipped).
+ let dst = unsafe { *out_planar.add(ch as usize) };
+ if dst.is_null() {
+ continue;
+ }
+ // Output is planar f32 (enforced by open()); each plane is
+ // `to_copy` float samples.
+ let src = out_frame.plane::(ch as usize);
+ unsafe {
+ ptr::copy_nonoverlapping(src.as_ptr(), dst, to_copy as usize);
+ }
+ }
+ }
+ total += i64::from(nb);
}
- total += i64::from(nb);
+
+ Ok(total.min(i64::from(out_capacity_frames)) as i32)
}
- Ok(total.min(i64::from(out_capacity_frames)) as i32)
-}
+ /// Signal end-of-input to the graph (flushes internal delay).
+ ///
+ /// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (state check)
+ /// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a
+ /// negative push return is logged only).
+ pub fn flush(&self) -> Result<()> {
+ let mut inner = self.inner.lock().unwrap();
-/// Signal end-of-input to the graph (flushes internal delay).
-///
-/// `// CPP-PARITY: src/audio/c_api/processor.cpp:137` (empty handle, state)
-/// and `src/audio/src/audioprocessor.cpp:210` (flush has no failure path; a
-/// negative push return is logged only).
-pub fn flush(self_: &CHandle) -> Result<()> {
- let p = get_processor(self_)?;
- let mut inner = p.inner.lock().unwrap();
-
- let Some(graph) = inner.graph.as_mut() else {
- return Err(Error::State);
- };
- let _ = graph.get("in").unwrap().source().flush();
- Ok(())
+ let Some(graph) = inner.graph.as_mut() else {
+ return Err(Box::from(Error::State));
+ };
+ let _ = graph.get("in").unwrap().source().flush();
+ Ok(())
+ }
}
/// Format of the conversion output (always planar f32).
diff --git a/crates/oakaudio/src/waveform.rs b/crates/oakaudio/src/waveform.rs
index 7ec8ce313..ec29a467d 100644
--- a/crates/oakaudio/src/waveform.rs
+++ b/crates/oakaudio/src/waveform.rs
@@ -23,13 +23,12 @@
use std::collections::BTreeMap;
use std::ffi::CStr;
-use oakcodec::decoder::{CodecStream, Decoder as _, RetrieveAudioStatus};
+use oakcodec::decoder::{receive_list_of_all_decoders, CodecStream, Decoder as _, RetrieveAudioStatus};
use oakcodec::ffmpeg::FFmpegDecoder;
+use oakcodec::footagedescription::FootageDescription;
use oakcore_rs::{Rational, TimeRange};
-use crate::bridge::codec::AudioStreamInfo;
use crate::error::{Error, Result};
-use crate::handle::{free_handle, make_owned, CHandle};
/// Maximum channel count accepted by [`extract`]. The C++ plane array is a
/// fixed `OAKAUDIO_EXTRACT_MAX_CHANNELS` (64) stack buffer; the Rust
@@ -625,32 +624,6 @@ impl RecipOrZero for f64 {
}
}
-// ---- Handle plumbing (mirrors processor.rs) --------------------------------
-
-/// Create an empty waveform behind a refcounted handle (count 1).
-pub fn init() -> Result {
- Ok(make_owned(AudioVisualWaveform::new()))
-}
-
-/// Release one reference to a waveform (NULL/empty no-op).
-pub fn free(self_: *mut CHandle) {
- unsafe { free_handle(self_) };
-}
-
-/// Borrow the waveform behind a handle; `OAKAUDIO_E_INVALID` for empty.
-pub fn get(self_: &CHandle) -> Result<&AudioVisualWaveform> {
- // SAFETY: every non-empty handle returned by `init` boxes an
- // `AudioVisualWaveform`.
- unsafe { crate::handle::get::(self_) }.ok_or(Error::Invalid)
-}
-
-/// Mutable variant of [`get`].
-pub fn get_mut(self_: &CHandle) -> Result<&mut AudioVisualWaveform> {
- // SAFETY: every non-empty handle returned by `init` boxes an
- // `AudioVisualWaveform`.
- unsafe { crate::handle::get_mut::(self_) }.ok_or(Error::Invalid)
-}
-
// ---- Whole-file extraction --------------------------------------------------
/// Result of [`extract`]: channel-interleaved min/max pairs.
@@ -714,10 +687,34 @@ fn emit_points(
}
}
+/// Probe a file with every registered decoder and return the first valid
+/// description (non-empty decoder id and at least one stream).
+///
+/// Replaces the former `oakcodec_decoder_probe` C ABI call (mirrors
+/// `probe_with_any_decoder` in oakcodec's ffi layer).
+fn probe_description(filename: &str) -> Option {
+ for decoder in receive_list_of_all_decoders() {
+ // `continue`, not `?`: an unimplemented/unsupported decoder must
+ // fall through to the next one (the FFmpeg entry is the
+ // format-agnostic fallback last in the registry).
+ let Some(desc) = decoder.probe(filename, None) else {
+ continue;
+ };
+ if !desc.decoder().is_empty()
+ && (desc.video_stream_count() > 0
+ || desc.audio_stream_count() > 0
+ || desc.subtitle_stream_count() > 0)
+ {
+ return Some(desc);
+ }
+ }
+ None
+}
+
/// Decode a whole audio stream to a channel-interleaved min/max summary.
///
-/// The stream is probed through the oakcodec decoder C ABI and decoded with
-/// oakcodec's in-process FFmpeg decoder (interleaved f32 at the native
+/// The stream is probed through oakcodec's in-process decoder registry and
+/// decoded with oakcodec's FFmpeg decoder (interleaved f32 at the native
/// rate/layout), then reduced to one point per `samples_per_point` source
/// samples.
///
@@ -728,34 +725,33 @@ pub fn extract(
stream_index: i32,
samples_per_point: i32,
) -> Result {
- // Probe for the stream's native rate/layout (stateless).
- // SAFETY: `filename` is a NUL-terminated C string (validated by the FFI
- // layer); the probe handle is freed on every path below.
- let mut probe = unsafe { crate::bridge::codec::oakcodec_decoder_probe(filename.as_ptr()) };
- if probe.is_null() {
- return Err(Error::NotFound);
+ // Probe for the stream's native rate/layout with the in-process decoder
+ // registry (the Rust equivalent of the former oakcodec decoder probe
+ // C ABI call).
+ let filename_str = filename.to_string_lossy();
+ let desc = probe_description(&filename_str).ok_or(Box::new(Error::NotFound))?;
+ let ap = desc
+ .get_audio_stream(stream_index as usize)
+ .ok_or(Box::new(Error::NotFound))?;
+
+ // The probed stream metadata is a plain value type (oakcodec's
+ // `AudioParams`); the fields map one-to-one onto the former
+ // `oakcodec_audio_stream_info` POD.
+ let sample_rate = ap.sample_rate;
+ let channel_count = ap.channel_count();
+ let channel_layout = ap.channel_layout;
+ let container_stream_index = ap.stream_index;
+ let duration_ts = ap.duration;
+ let (time_base_num, time_base_den) = ap.time_base;
+
+ if sample_rate <= 0 || channel_count <= 0 {
+ return Err(Box::new(Error::Failed("invalid audio stream".to_string())));
}
- let mut info = unsafe { std::mem::zeroed::() };
- let r = unsafe {
- crate::bridge::codec::oakcodec_decoder_probe_get_audio_stream(
- probe,
- stream_index,
- &mut info,
- )
- };
- // SAFETY: `probe` was created above and is no longer used.
- unsafe { crate::bridge::codec::oakcodec_decoder_free(&mut probe) };
- if r != 0 {
- return Err(Error::NotFound);
- }
- if info.sample_rate <= 0 || info.channel_count <= 0 {
- return Err(Error::Failed("invalid audio stream".to_string()));
- }
- let channels = info.channel_count;
+ let channels = channel_count;
if channels > EXTRACT_MAX_CHANNELS {
- return Err(Error::Failed(format!(
+ return Err(Box::new(Error::Failed(format!(
"stream has {channels} channels (max {EXTRACT_MAX_CHANNELS})"
- )));
+ ))));
}
// Decode the whole stream through oakcodec's FFmpeg decoder
@@ -764,23 +760,23 @@ pub fn extract(
// fb_audio_graph to obtain planar f32).
let decoder = FFmpegDecoder::new();
let stream = CodecStream::with_block(
- filename.to_string_lossy().into_owned(),
- info.stream_index,
+ filename_str.into_owned(),
+ container_stream_index,
None,
);
if let Err(e) = decoder.open(&stream) {
- return Err(Error::Failed(format!("failed to open decoder: {e:?}")));
+ return Err(Box::new(Error::Failed(format!("failed to open decoder: {e:?}"))));
}
- if info.time_base_num <= 0 || info.time_base_den <= 0 || info.duration_ts <= 0 {
+ if time_base_num <= 0 || time_base_den <= 0 || duration_ts <= 0 {
let _ = decoder.close();
- return Err(Error::Failed("invalid audio stream duration".to_string()));
+ return Err(Box::new(Error::Failed("invalid audio stream duration".to_string())));
}
let duration_sec =
- info.duration_ts as f64 * f64::from(info.time_base_num) / f64::from(info.time_base_den);
- let total_frames = (duration_sec * f64::from(info.sample_rate)).round() as i64;
- let layout_mask = if info.channel_layout != 0 {
- info.channel_layout
+ duration_ts as f64 * f64::from(time_base_num) / f64::from(time_base_den);
+ let total_frames = (duration_sec * f64::from(sample_rate)).round() as i64;
+ let layout_mask = if channel_layout != 0 {
+ channel_layout
} else {
ffmpeg_next::ChannelLayout::default(channels).bits()
};
@@ -796,11 +792,11 @@ pub fn extract(
while offset < total_frames {
let frames = (total_frames - offset).min(CHUNK_FRAMES);
let range = TimeRange::new(
- Rational::new(offset, i64::from(info.sample_rate)),
- Rational::new(offset + frames, i64::from(info.sample_rate)),
+ Rational::new(offset, i64::from(sample_rate)),
+ Rational::new(offset + frames, i64::from(sample_rate)),
);
let mut buf = vec![0f32; frames as usize * channels as usize];
- match decoder.retrieve_audio(&mut buf, &range, info.sample_rate, layout_mask) {
+ match decoder.retrieve_audio(&mut buf, &range, sample_rate, layout_mask) {
Ok(RetrieveAudioStatus::Success) => {
append_pending(&mut pending, &buf, channels);
emit_points(
@@ -812,11 +808,11 @@ pub fn extract(
);
}
Ok(status) => {
- result = Err(Error::Failed(format!("audio retrieve failed: {status:?}")));
+ result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {status:?}"))));
break;
}
Err(e) => {
- result = Err(Error::Failed(format!("audio retrieve failed: {e:?}")));
+ result = Err(Box::new(Error::Failed(format!("audio retrieve failed: {e:?}"))));
break;
}
}
diff --git a/crates/oakaudio/tests/common/mod.rs b/crates/oakaudio/tests/common/mod.rs
index b4cd50841..ee4c5e088 100644
--- a/crates/oakaudio/tests/common/mod.rs
+++ b/crates/oakaudio/tests/common/mod.rs
@@ -14,68 +14,43 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! Shared test helpers and bridge stubs for the oakaudio contract suite.
+//! Shared helpers for the oakaudio contract suite.
//!
-//! The library imports the other oak modules (`oakcommon`, `oakcodec`)
-//! through the `bridge/` wrappers. A standalone `cargo test` run has no
-//! host dylibs to link, so [`mod stubs`] provides minimal definitions — a
-//! no-op config/encoder and a WAV-header probe (real decoding in
-//! `waveform::extract` goes through oakcodec's in-process FFmpeg decoder;
-//! the processor drives a real FFmpeg filter graph via ffmpeg-next). The
-//! exhaustive behavior matrix is pinned by the unchanged C++ gtest suite
-//! (`src/audio/tests`).
+//! The former bridge stubs are gone with the deleted C ABI: every test
+//! now calls the crate's public Rust API directly (manager singleton,
+//! processor, levelmeter, waveform, synchronizer). Decoding in
+//! `waveform::extract` goes through oakcodec's in-process FFmpeg
+//! decoder; the processor drives a real FFmpeg filter graph.
use std::path::Path;
use std::sync::Mutex;
-/// Serializes tests that mutate process-wide state (the manager singleton,
-/// the alive ledger) within one test binary.
+/// Serializes tests that mutate process-wide state (the manager singleton)
+/// within one test binary.
pub static MANAGER_LOCK: Mutex<()> = Mutex::new(());
+/// Lock the manager singleton for a test. The manager state persists across
+/// tests (the `OnceLock` cannot be reset), so a panicked test must not
+/// poison the lock for the rest of the binary.
+pub fn lock_manager() -> std::sync::MutexGuard<'static, ()> {
+ MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner())
+}
+
/// Build a planar f32 buffer with `channel_count` channels of `frame_count`
/// frames from a single-channel `source` (replicated per channel).
pub fn planar_from(source: &[f32], channel_count: usize) -> Vec> {
(0..channel_count).map(|_| source.to_vec()).collect()
}
-/// A deterministic pseudo-random planar buffer (fixed seed) for stable
-/// golden vectors. Samples land in `[-1, 1)`.
-pub fn noisy_planar(channel_count: usize, frame_count: usize, seed: u64) -> Vec> {
- let mut state = seed | 1;
- let mut next = move || {
- state = state
- .wrapping_mul(6364136223846793005)
- .wrapping_add(1442695040888963407);
- ((state >> 33) as u64 & 0xFFFF) as f32 / 65535.0 * 2.0 - 1.0
- };
- let mut data = Vec::with_capacity(channel_count);
- for _ in 0..channel_count {
- data.push((0..frame_count).map(|_| next()).collect());
- }
- data
-}
-
/// A silence buffer: every sample is exactly `0.0`.
pub fn silence_planar(channel_count: usize, frame_count: usize) -> Vec> {
vec![vec![0.0; frame_count]; channel_count]
}
-/// Total number of `SamplePerChannel` entries in a channel-interleaved
-/// waveform sample for `points` points across `channels` channels.
-pub fn interleaved_len(points: usize, channels: usize) -> usize {
- points * channels
-}
-
-/// Convert an `oakaudio`-style `min_max` pair layout into a plain tuple for
-/// comparison in tests.
-pub fn pair(min: f32, max: f32) -> (f32, f32) {
- (min, max)
-}
-
// ---- Minimal WAV fixture helpers -------------------------------------------
/// Write a 16-bit PCM WAV file (`fmt` chunk + `data` chunk, standard 44-byte
-/// header). The test stubs decode exactly this layout.
+/// header). Decoded by oakcodec's FFmpeg decoder in `waveform::extract`.
pub fn write_wav(path: &Path, channels: u16, rate: u32, samples: &[i16]) -> std::io::Result<()> {
let block_align = channels * 2;
let byte_rate = rate * u32::from(block_align);
@@ -124,286 +99,3 @@ pub fn write_wav_header_only(path: &Path, channels: u16, rate: u32) -> std::io::
out.extend_from_slice(&0u32.to_le_bytes());
std::fs::write(path, out)
}
-
-// ---- Bridge stubs -----------------------------------------------------------
-
-#[allow(dead_code)]
-pub mod stubs {
- use std::ffi::{c_char, c_int, c_void, CStr};
- use std::path::Path;
-
- use oakaudio::bridge::codec::AudioStreamInfo;
- use oakaudio::handle::CHandle;
-
- // ------------------------- oakcommon ---------------------------------
-
- /// Not-found for every key: `config::device_name`'s two-stage query
- /// treats `size <= 1` as absent and returns the empty string, so the
- /// config-driven device lookup degrades to `paNoDevice` (the documented
- /// bridge degradation).
- #[no_mangle]
- pub extern "C" fn oakcommon_config_get(
- _group: *const c_char,
- _key: *const c_char,
- _buf: *mut c_char,
- _buf_size: c_int,
- ) -> c_int {
- -1
- }
-
- /// Every integer config reads its default.
- #[no_mangle]
- pub extern "C" fn oakcommon_config_get_int(
- _group: *const c_char,
- _key: *const c_char,
- default: c_int,
- ) -> c_int {
- default
- }
-
- /// Core `SampleFormat` (planar-first) → `FBSampleFormat` (AVSampleFormat
- /// order), mirroring `src/common/src/ffmpegutils.cpp:83`.
- ///
- /// `// CPP-PARITY: src/common/src/ffmpegutils.cpp:83`
- /// (`FFmpegUtils::get_ffmpeg_sample_format`).
- #[no_mangle]
- pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(
- smp_fmt: c_int,
- out: *mut c_int,
- ) -> c_int {
- let mapped = match smp_fmt {
- 0 => 5, // u8_p -> fb_sample_fmt_u8_p
- 1 => 6, // s16_p -> fb_sample_fmt_s16_p
- 2 => 7, // s32_p -> fb_sample_fmt_s32_p
- 3 => 11, // s64_p -> fb_sample_fmt_s64_p
- 4 => 8, // f32_p -> fb_sample_fmt_fltp
- 5 => 9, // f64_p -> fb_sample_fmt_dblp
- 6 => 0, // u8 -> fb_sample_fmt_u8
- 7 => 1, // s16 -> fb_sample_fmt_s16
- 8 => 2, // s32 -> fb_sample_fmt_s32
- 9 => 10, // s64 -> fb_sample_fmt_s64
- 10 => 3, // f32 -> fb_sample_fmt_flt
- 11 => 4, // f64 -> fb_sample_fmt_dbl
- _ => -1, // invalid/count -> fb_sample_fmt_none
- };
- if out.is_null() {
- return -1;
- }
- // SAFETY: the caller guarantees a writable int.
- unsafe { *out = mapped };
- 0
- }
-
- // ------------------------- oakcodec ----------------------------------
-
- /// ffmpeg-style default channel layout mask for `nb_channels` (only the
- /// popcount is load-bearing for oakaudio). Masks above 63 channels
- /// cannot be represented in a u64 and yield 0 (the extract cap check
- /// rejects such streams before any layout use).
- fn layout_for(channels: i32) -> u64 {
- match channels {
- 1 => 0x4,
- 2 => 0x3,
- n if n > 0 && n < 64 => (1u64 << n) - 1,
- _ => 0,
- }
- }
-
- /// WAV header facts parsed by `parse_wav`.
- struct WavInfo {
- channels: i32,
- rate: i32,
- block_align: usize,
- frames: i64,
- }
-
- /// Parse the standard 44-byte PCM WAV header the fixture writer emits.
- fn parse_wav(path: &Path) -> Option {
- let bytes = std::fs::read(path).ok()?;
- if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
- return None;
- }
- if &bytes[12..16] != b"fmt " || &bytes[36..40] != b"data" {
- return None;
- }
- let fmt_size = u32::from_le_bytes(bytes[16..20].try_into().ok()?);
- if fmt_size < 16 {
- return None;
- }
- let audio_format = u16::from_le_bytes(bytes[20..22].try_into().ok()?);
- let channels = u16::from_le_bytes(bytes[22..24].try_into().ok()?);
- let rate = u32::from_le_bytes(bytes[24..28].try_into().ok()?);
- let block_align = u16::from_le_bytes(bytes[32..34].try_into().ok()?);
- let bits = u16::from_le_bytes(bytes[34..36].try_into().ok()?);
- let data_size = u32::from_le_bytes(bytes[40..44].try_into().ok()?);
- if audio_format != 1 || bits != 16 || channels == 0 || rate == 0 || block_align == 0 {
- return None;
- }
- let frames = (data_size as usize / block_align as usize) as i64;
- Some(WavInfo {
- channels: i32::from(channels),
- rate: rate as i32,
- block_align: block_align as usize,
- frames,
- })
- }
-
- // All handles below use the shared `CHandle` ABI (single-lib
- // unification): oakaudio's `bridge::codec` wrappers call the oakcodec
- // crate's `#[no_mangle]` ffi functions directly, so these stubs must
- // match the real oakcodec ffi signatures exactly.
-
- struct StubEncoder;
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_init(params: *const c_void) -> CHandle {
- if params.is_null() {
- return CHandle::null();
- }
- CHandle {
- ctx: Box::into_raw(Box::new(StubEncoder)) as *mut c_void,
- addref: None,
- release: None,
- abi_version: 0,
- }
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_open(_encoder: CHandle) -> c_int {
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_write_audio(
- _encoder: CHandle,
- _samples: *const f32,
- _frame_count: c_int,
- ) -> c_int {
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_flush(_encoder: CHandle) -> c_int {
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_last_error(
- _encoder: CHandle,
- _buf: *mut c_char,
- _buf_size: c_int,
- ) -> c_int {
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) {
- if !encoder.is_null() {
- // SAFETY: the pointer was created by `oakcodec_encoder_init`;
- // the caller owns `encoder` and expects it cleared.
- let e = unsafe { &mut *encoder };
- if !e.ctx.is_null() {
- drop(unsafe { Box::from_raw(e.ctx as *mut StubEncoder) });
- e.ctx = std::ptr::null_mut();
- }
- }
- }
-
- /// Probe result for a decodable WAV file.
- struct StubProbe {
- channels: i32,
- sample_rate: i32,
- frames: i64,
- layout: u64,
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle {
- if filename.is_null() {
- return CHandle::null();
- }
- // SAFETY: the C string is NUL-terminated (caller contract).
- let cname = unsafe { CStr::from_ptr(filename) };
- let path = Path::new(cname.to_str().unwrap_or(""));
- match parse_wav(path) {
- Some(info) => CHandle {
- ctx: Box::into_raw(Box::new(StubProbe {
- channels: info.channels,
- sample_rate: info.rate,
- frames: info.frames,
- layout: layout_for(info.channels),
- })) as *mut c_void,
- addref: None,
- release: None,
- abi_version: 0,
- },
- None => CHandle::null(),
- }
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_free(probe: *mut CHandle) {
- if !probe.is_null() {
- // SAFETY: the pointer was created by `oakcodec_decoder_probe`;
- // the caller owns `probe` and expects it cleared.
- let p = unsafe { &mut *probe };
- if !p.ctx.is_null() {
- drop(unsafe { Box::from_raw(p.ctx as *mut StubProbe) });
- p.ctx = std::ptr::null_mut();
- }
- }
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_probe_audio_stream_count(_probe: CHandle) -> c_int {
- 1
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_probe_get_audio_stream(
- probe: CHandle,
- index: c_int,
- out: *mut AudioStreamInfo,
- ) -> c_int {
- if probe.ctx.is_null() || out.is_null() || index != 0 {
- return -1;
- }
- // SAFETY: the probe ctx is a live `StubProbe`; `out` is a
- // caller-owned info struct.
- let p = unsafe { &*(probe.ctx as *const StubProbe) };
- unsafe {
- (*out).stream_index = 0;
- (*out).sample_rate = p.sample_rate;
- (*out).channel_layout = p.layout;
- (*out).channel_count = p.channels;
- (*out).duration_ts = p.frames;
- (*out).time_base_num = 1;
- (*out).time_base_den = p.sample_rate;
- }
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_open(
- _decoder: CHandle,
- _filename: *const c_char,
- _stream_index: c_int,
- ) -> c_int {
- 0
- }
-
- #[no_mangle]
- pub extern "C" fn oakcodec_decoder_decode_audio(
- _decoder: CHandle,
- _in_num: c_int,
- _in_den: c_int,
- _out_num: c_int,
- _out_den: c_int,
- _sample_rate: c_int,
- _channel_layout: u64,
- _buf: *mut f32,
- _buf_frames: c_int,
- ) -> c_int {
- 0
- }
-}
diff --git a/crates/oakaudio/tests/ffi_test.rs b/crates/oakaudio/tests/ffi_test.rs
deleted file mode 100644
index f831f98b1..000000000
--- a/crates/oakaudio/tests/ffi_test.rs
+++ /dev/null
@@ -1,200 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! FFI-layer contract tests (ffi.rs). The exhaustive matrix runs against
-//! the unchanged C++ gtest suite (`src/audio/tests`); these tests pin
-//! Rust-side specifics (handle contracts, the singleton ledger, struct
-//! layout).
-
-mod common;
-
-use std::mem::{align_of, size_of};
-use std::sync::Mutex;
-
-use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
-use oakaudio::ffi::levelmeter::oakaudio_levelmeter_analyze;
-use oakaudio::ffi::levelmeter::{ChannelStats, MeterStats};
-use oakaudio::ffi::manager::{
- oakaudio_debug_alive_count, oakaudio_manager_create_instance,
- oakaudio_manager_destroy_instance, oakaudio_manager_free, oakaudio_manager_instance,
-};
-use oakaudio::ffi::processor::{oakaudio_processor_free, oakaudio_processor_init};
-use oakaudio::ffi::sync::{OffsetResult, SourceClip};
-use oakaudio::ffi::waveform::{oakaudio_waveform_free, oakaudio_waveform_init};
-
-/// Serializes tests that touch the process-wide singleton and the alive
-/// ledger.
-static LOCK: Mutex<()> = Mutex::new(());
-
-/// Every exported handle-returning function (processor_init, waveform_init)
-/// returns ctx==NULL on failure and a valid refcounted handle on success,
-/// with abi_version == OAKAUDIO_ABI_VERSION stamped.
-#[test]
-fn handle_contract_all_exports() {
- let _guard = LOCK.lock().unwrap();
-
- let mut p = unsafe { oakaudio_processor_init() };
- assert!(!p.ctx.is_null());
- assert_eq!(p.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION);
-
- let mut w = unsafe { oakaudio_waveform_init() };
- assert!(!w.ctx.is_null());
- assert_eq!(w.abi_version, oakaudio::handle::OAKAUDIO_ABI_VERSION);
-
- unsafe { oakaudio_processor_free(&mut p) };
- unsafe { oakaudio_waveform_free(&mut w) };
-}
-
-/// free(NULL)/free(empty) are no-ops across every free export.
-#[test]
-fn free_null_noop_all_exports() {
- let _guard = LOCK.lock().unwrap();
-
- let mut p = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_processor_free(&mut p) };
- assert!(p.ctx.is_null());
-
- let mut w = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_waveform_free(&mut w) };
- assert!(w.ctx.is_null());
-
- let mut m = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_manager_free(&mut m) };
- assert!(m.ctx.is_null());
-
- // NULL pointer itself is a no-op.
- unsafe { oakaudio_processor_free(std::ptr::null_mut()) };
- unsafe { oakaudio_waveform_free(std::ptr::null_mut()) };
- unsafe { oakaudio_manager_free(std::ptr::null_mut()) };
-}
-
-/// The manager singleton: instance() is the same borrowed handle across
-/// calls; create/destroy flip validity; oakaudio_debug_alive_count moves
-/// predictably and returns to baseline.
-#[test]
-fn manager_singleton_and_alive_count() {
- let _guard = LOCK.lock().unwrap();
-
- let before = unsafe { oakaudio_debug_alive_count() };
-
- unsafe { oakaudio_manager_destroy_instance() };
- let none = unsafe { oakaudio_manager_instance() };
- assert!(none.ctx.is_null());
- // The empty instance handle is the shared `null()` (no ABI version).
- assert_eq!(none.abi_version, 0);
-
- unsafe { oakaudio_manager_create_instance() };
- let m1 = unsafe { oakaudio_manager_instance() };
- let m2 = unsafe { oakaudio_manager_instance() };
- assert!(!m1.ctx.is_null());
- assert_eq!(
- m1.ctx, m2.ctx,
- "instance() must be the same borrowed handle"
- );
-
- // A processor bumps the ledger; freeing it returns to baseline.
- assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
- let mut p = unsafe { oakaudio_processor_init() };
- assert_eq!(unsafe { oakaudio_debug_alive_count() }, before + 1);
- unsafe { oakaudio_processor_free(&mut p) };
- assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
-
- // Destroy flips the singleton back to empty; create resurrects it.
- unsafe { oakaudio_manager_destroy_instance() };
- assert!(unsafe { oakaudio_manager_instance() }.ctx.is_null());
- unsafe { oakaudio_manager_create_instance() };
- assert!(!unsafe { oakaudio_manager_instance() }.ctx.is_null());
-}
-
-/// oakaudio_levelmeter_analyze with NULL summary still computes per-channel
-/// stats, and a NULL channels array with capacity 0 is accepted when only
-/// the summary is wanted.
-#[test]
-fn levelmeter_partial_outputs() {
- let data = [0.5f32; 64];
- let planes = [data.as_ptr()];
-
- // channels only (summary NULL)
- let mut channels = [ChannelStats {
- peak_linear: 0.0,
- peak_db: 0.0,
- rms_linear: 0.0,
- rms_db: 0.0,
- vu_db: 0.0,
- }];
- assert_eq!(
- unsafe {
- oakaudio_levelmeter_analyze(
- planes.as_ptr(),
- 1,
- 64,
- channels.as_mut_ptr(),
- 1,
- std::ptr::null_mut(),
- )
- },
- OAKAUDIO_OK
- );
- assert!((channels[0].peak_linear - 0.5).abs() < 1e-9);
-
- // summary only (channels NULL, capacity 0)
- let mut summary = MeterStats {
- max_peak_linear: 0.0,
- integrated_lufs: 0.0,
- silence: 0,
- };
- assert_eq!(
- unsafe {
- oakaudio_levelmeter_analyze(
- planes.as_ptr(),
- 1,
- 64,
- std::ptr::null_mut(),
- 0,
- &mut summary,
- )
- },
- OAKAUDIO_OK
- );
- assert!((summary.max_peak_linear - 0.5).abs() < 1e-9);
-
- // Both NULL is invalid.
- assert_eq!(
- unsafe {
- oakaudio_levelmeter_analyze(
- planes.as_ptr(),
- 1,
- 64,
- std::ptr::null_mut(),
- 0,
- std::ptr::null_mut(),
- )
- },
- OAKAUDIO_E_INVALID
- );
-}
-
-/// sync value structs (offset_result/source_clip) are 24/40 bytes and
-/// repr(C)-aligned as the C headers dictate, so layout never drifts.
-#[test]
-fn sync_struct_layout() {
- assert_eq!(size_of::(), 24);
- assert_eq!(align_of::(), 8);
- assert_eq!(size_of::(), 40);
- assert_eq!(align_of::(), 8);
- // The stretch result (f64, i64, f64, i32) pads to 32 bytes.
- assert_eq!(size_of::(), 32);
-}
diff --git a/crates/oakaudio/tests/golden_test.rs b/crates/oakaudio/tests/golden_test.rs
index 9c70056a2..46d333403 100644
--- a/crates/oakaudio/tests/golden_test.rs
+++ b/crates/oakaudio/tests/golden_test.rs
@@ -19,15 +19,12 @@
mod common;
+use std::ffi::CString;
+
use common::write_wav_header_only;
-use oakaudio::ffi::waveform::MinMax;
-use oakaudio::ffi::waveform::{
- oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_summary,
- oakaudio_waveform_init, oakaudio_waveform_overwrite_samples,
- oakaudio_waveform_set_channel_count,
-};
-use oakaudio::params::{frames_to_rational, rational_to_samples, SampleFormat};
use oakcore_rs::Rational;
+use oakaudio::params::{frames_to_rational, rational_to_samples, AudioParams, SampleFormat};
+use oakaudio::waveform::{extract, AudioVisualWaveform};
/// SampleFormat planar-first ordering matches the authoritative C++ enum:
/// f32_p == 4 == OAKAUDIO_PROCESSOR_OUTPUT_FORMAT. This guards the
@@ -60,7 +57,7 @@ fn sample_time_conversion_roundtrip() {
/// rational conversion edge cases (NaN / out-of-range / tiny -> null).
#[test]
fn params_value_types() {
- use oakaudio::params::{rational_from_double, AudioParams};
+ use oakaudio::params::rational_from_double;
let p = AudioParams {
sample_rate: 48000,
@@ -89,23 +86,17 @@ fn waveform_mipmap_scale_parity() {
// 1024 ramp samples @ 48000 Hz, two channels.
let ch0: Vec = (0..1024).map(|i| i as f32 * 0.001).collect();
let ch1: Vec = (0..1024).map(|i| -(i as f32) * 0.001).collect();
- let planes = [ch0.as_ptr(), ch1.as_ptr()];
+ let planes = [ch0.as_slice(), ch1.as_slice()];
- let mut w = unsafe { oakaudio_waveform_init() };
- assert!(!w.ctx.is_null());
- assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0);
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 1024, 48000, 0, 1) },
- 0
- );
+ let mut w = AudioVisualWaveform::new();
+ w.set_channel_count(2);
+ w.overwrite_samples(&planes, 48000, Rational::new(0, 1));
// One summary point is produced for any queried window; a 1/1024 s
// window (one 1024-rate mipmap point ~ 46.875 source samples) must
// bracket a narrower range than a 1/64 s window (~750 samples).
- let mut fine = [MinMax { min: 0.0, max: 0.0 }; 2];
- let fine_points =
- unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1024, fine.as_mut_ptr(), 2) };
- assert_eq!(fine_points, 1);
+ let fine = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 1024));
+ assert_eq!(fine.len(), 2);
assert_eq!(fine[0].min, 0.0);
assert!(
(fine[0].max - 0.046).abs() < 1e-5,
@@ -119,10 +110,8 @@ fn waveform_mipmap_scale_parity() {
);
assert_eq!(fine[1].max, 0.0);
- let mut coarse = [MinMax { min: 0.0, max: 0.0 }; 2];
- let coarse_points =
- unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 64, coarse.as_mut_ptr(), 2) };
- assert_eq!(coarse_points, 1);
+ let coarse = w.get_summary_from_time(Rational::new(0, 1), Rational::new(1, 64));
+ assert_eq!(coarse.len(), 2);
assert_eq!(coarse[0].min, 0.0);
assert!(
(coarse[0].max - 0.749).abs() < 1e-5,
@@ -139,8 +128,6 @@ fn waveform_mipmap_scale_parity() {
// Coarser windows necessarily cover more source samples.
assert!(coarse[0].max > fine[0].max);
assert!(coarse[1].min < fine[1].min);
-
- unsafe { oakaudio_waveform_free(&mut w) };
}
/// levelmeter dB conversion: peak_db == 20*log10(peak_linear) and the
@@ -164,57 +151,30 @@ fn levelmeter_db_golden() {
/// waveformsync envelope offset golden: a reference ramp delayed by two
/// windows in the candidate is recovered as +2 windows with full
-/// confidence through the C ABI.
+/// confidence.
#[test]
fn waveform_sync_offset_golden() {
- use oakaudio::ffi::sync::{oakaudio_sync_estimate_envelope_offset, OffsetResult};
let reference: Vec = (0..10).map(|i| i as f64 * 0.1 + 0.1).collect();
let mut candidate = vec![0.0f64; 10];
candidate[2..].copy_from_slice(&reference[..8]);
- let mut out = OffsetResult {
- offset_samples: 0,
- confidence: 0.0,
- valid: 0,
- };
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- reference.as_ptr(),
- reference.len() as i32,
- candidate.as_ptr(),
- candidate.len() as i32,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 1);
+ let out = oakaudio::waveformsync::estimate_envelope_offset(&reference, &candidate, 100, 10);
+ assert!(out.valid);
assert_eq!(out.offset_samples, 200);
assert!((out.confidence - 1.0).abs() < 1e-9);
}
-/// oakaudio_waveform_extract channel cap: a stream claiming more than
-/// OAKAUDIO_EXTRACT_MAX_CHANNELS (64) channels is rejected rather than
-/// overflowing the internal plane array.
+/// waveform::extract channel cap: a stream claiming more than
+/// EXTRACT_MAX_CHANNELS (64) channels is rejected rather than overflowing
+/// the internal plane array.
#[test]
fn extract_channel_cap() {
let path = std::env::temp_dir().join(format!("oakaudio_cap_{}.wav", std::process::id()));
write_wav_header_only(&path, 65, 48000).unwrap();
- let mut out_channels = 0i32;
- let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
- let r = unsafe {
- oakaudio_waveform_extract(
- cpath.as_ptr(),
- 0,
- 4,
- std::ptr::null_mut(),
- 0,
- &mut out_channels,
- )
- };
- assert!(r < 0, "oversized stream must be rejected, got {r}");
+ let cpath = CString::new(path.to_str().unwrap()).unwrap();
+ assert!(
+ extract(&cpath, 0, 4).is_err(),
+ "oversized stream must be rejected"
+ );
std::fs::remove_file(&path).ok();
}
diff --git a/crates/oakaudio/tests/handle_test.rs b/crates/oakaudio/tests/handle_test.rs
deleted file mode 100644
index 537f59e94..000000000
--- a/crates/oakaudio/tests/handle_test.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! Handle plumbing contract tests (handle.rs).
-
-use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK};
-use std::sync::{Mutex, MutexGuard};
-
-use oakaudio::handle::{alive_count, get, guard, guard_handle, make_borrowed, make_owned, CHandle};
-
-/// Serializes the tests: the crate's live-object ledger is process-global,
-/// so parallel tests in this file would race the count assertions.
-fn ledger_lock() -> MutexGuard<'static, ()> {
- static LOCK: Mutex<()> = Mutex::new(());
- LOCK.lock().unwrap_or_else(|e| e.into_inner())
-}
-
-/// make_owned starts at refcount 1; get returns a typed view; dropping the
-/// handle decrements to 0.
-#[test]
-fn owned_lifecycle() {
- let _l = ledger_lock();
- let before = alive_count();
- let mut h = make_owned(42u32);
- assert!(!h.is_null());
- assert_eq!(alive_count(), before + 1);
-
- // SAFETY: `h` boxes a u32 created above.
- let v = unsafe { get::(&h) }.unwrap();
- assert_eq!(*v, 42);
-
- // addref/release round-trip through the function pointers.
- let addref = h.addref.unwrap();
- let release = h.release.unwrap();
- // SAFETY: the ctx was created by make_owned.
- unsafe { addref(h.ctx) };
- assert_eq!(alive_count(), before + 1); // count unchanged, still 1 box
- unsafe { release(h.ctx) };
-
- // Dropping the box (release to zero) decrements the ledger.
- let release = h.release.unwrap();
- // SAFETY: h.ctx is the box created above; refcount is 1.
- unsafe { release(h.ctx) };
- h.ctx = std::ptr::null_mut();
- assert_eq!(alive_count(), before);
-}
-
-/// make_borrowed creates a borrow-only handle whose release frees only the
-/// box, never the underlying object.
-#[test]
-fn borrowed_release() {
- let mut value = Box::new(7i32);
- // SAFETY: `value` outlives the handle; the ctx points directly at the
- // box (not a RefBox), so it is read through the raw pointer.
- let mut h = unsafe { make_borrowed(&mut *value) };
- assert!(!h.is_null());
-
- // SAFETY: `h.ctx` points at the box created above.
- let v = unsafe { &*(h.ctx as *const i32) };
- assert_eq!(*v, 7);
-
- // release is a no-op: the box still lives.
- let release = h.release.unwrap();
- // SAFETY: noop_ref for borrowed handles.
- unsafe { release(h.ctx) };
- assert_eq!(*value, 7);
-
- h.ctx = std::ptr::null_mut();
-}
-
-/// CHandle::null() yields an empty handle; guard over an Ok(()) returns
-/// OAKAUDIO_OK (0) and guard_handle over Ok returns a valid handle.
-#[test]
-fn null_and_guard_ok() {
- let null = CHandle::null();
- assert!(null.is_null());
- // The shared `null()` stamps no ABI version (single-lib unification).
- assert_eq!(null.abi_version, 0);
-
- assert_eq!(guard(|| Ok(())), OAKAUDIO_OK);
-
- let h = guard_handle(|| Ok(make_owned(1u32)));
- assert!(!h.is_null());
- // Release it so the alive ledger returns to baseline (tests share the
- // process-wide ledger and run in parallel).
- // SAFETY: `h.ctx` is the box created above; refcount is 1.
- unsafe { (h.release.unwrap())(h.ctx) };
-}
-
-/// guard maps an Err to the negative error code without panicking; a
-/// panicking body is caught and returns a failure code rather than
-/// unwinding across the FFI boundary.
-#[test]
-fn guard_error_and_panic() {
- assert_eq!(
- guard(|| Err(oakaudio::error::Error::Invalid)),
- OAKAUDIO_E_INVALID
- );
- assert_eq!(guard(|| Err(oakaudio::error::Error::State)), -60002);
- assert_eq!(
- guard(|| Err(oakaudio::error::Error::Failed("x".to_string()))),
- -60003
- );
- assert_eq!(guard(|| Err(oakaudio::error::Error::NotFound)), -60004);
- assert_eq!(guard(|| Err(oakaudio::error::Error::NoMem)), -60005);
-
- assert_eq!(guard(|| panic!("boom")), OAKAUDIO_E_FAILED);
-
- let h = guard_handle(|| Err(oakaudio::error::Error::Invalid));
- assert!(h.is_null());
- let h2 = guard_handle(|| panic!("boom"));
- assert!(h2.is_null());
-}
diff --git a/crates/oakaudio/tests/levelmeter_test.rs b/crates/oakaudio/tests/levelmeter_test.rs
index d66d4076c..c5eaf14f5 100644
--- a/crates/oakaudio/tests/levelmeter_test.rs
+++ b/crates/oakaudio/tests/levelmeter_test.rs
@@ -14,53 +14,28 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! AudioLevelMeter contract tests (levelmeter.rs), through the C ABI.
+//! AudioLevelMeter contract tests (levelmeter.rs), calling the public
+//! `analyze_sample_buffer` API.
mod common;
-use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_OK};
-use oakaudio::ffi::levelmeter::{oakaudio_levelmeter_analyze, ChannelStats, MeterStats};
+use oakaudio::levelmeter::{analyze_sample_buffer, Stats};
-fn analyze(planes: &[Vec]) -> (Vec, MeterStats) {
- let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
- let mut channels: Vec = (0..planes.len())
- .map(|_| ChannelStats {
- peak_linear: 0.0,
- peak_db: 0.0,
- rms_linear: 0.0,
- rms_db: 0.0,
- vu_db: 0.0,
- })
- .collect();
- let mut summary = MeterStats {
- max_peak_linear: 0.0,
- integrated_lufs: 0.0,
- silence: 0,
- };
- let r = unsafe {
- oakaudio_levelmeter_analyze(
- ptrs.as_ptr(),
- planes.len() as i32,
- planes.first().map_or(0, |p| p.len()) as i32,
- channels.as_mut_ptr(),
- channels.len() as i32,
- &mut summary,
- )
- };
- assert_eq!(r, OAKAUDIO_OK);
- (channels, summary)
+fn analyze(planes: &[Vec]) -> Stats {
+ let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
+ analyze_sample_buffer(&refs)
}
-/// A silence buffer reports silence=1, all-zero linear fields, and dB
+/// A silence buffer reports silence=true, all-zero linear fields, and dB
/// fields floored at -200.
#[test]
fn silence_analysis() {
let planes = common::silence_planar(2, 64);
- let (channels, summary) = analyze(&planes);
- assert_eq!(summary.silence, 1);
- assert_eq!(summary.max_peak_linear, 0.0);
- assert_eq!(summary.integrated_lufs, -200.0);
- for ch in &channels {
+ let stats = analyze(&planes);
+ assert!(stats.silence);
+ assert_eq!(stats.max_peak_linear, 0.0);
+ assert_eq!(stats.integrated_lufs, -200.0);
+ for ch in &stats.channels {
assert_eq!(ch.peak_linear, 0.0);
assert_eq!(ch.rms_linear, 0.0);
assert_eq!(ch.peak_db, -200.0);
@@ -70,21 +45,22 @@ fn silence_analysis() {
}
/// A constant-amplitude tone reports peak_linear == rms_linear == that
-/// amplitude (power terms), peak_db matches 20*log10(amp), and silence=0.
+/// amplitude (power terms), peak_db matches 20*log10(amp), and silence is
+/// false.
#[test]
fn constant_tone_stats() {
let planes = common::planar_from(&[0.5f32; 64], 1);
- let (channels, summary) = analyze(&planes);
- assert_eq!(summary.silence, 0);
- assert!((channels[0].peak_linear - 0.5).abs() < 1e-9);
- assert!((channels[0].rms_linear - 0.5).abs() < 1e-9);
+ let stats = analyze(&planes);
+ assert!(!stats.silence);
+ assert!((stats.channels[0].peak_linear - 0.5).abs() < 1e-9);
+ assert!((stats.channels[0].rms_linear - 0.5).abs() < 1e-9);
let expected_db = 20.0 * 0.5f64.log10();
- assert!((channels[0].peak_db - expected_db).abs() < 1e-9);
- assert!((channels[0].rms_db - expected_db).abs() < 1e-9);
+ assert!((stats.channels[0].peak_db - expected_db).abs() < 1e-9);
+ assert!((stats.channels[0].rms_db - expected_db).abs() < 1e-9);
}
/// A full-scale square wave yields max_peak_linear == 1.0 and a peak_db
-/// near 0 dB; per-channel channels array is filled for each channel.
+/// near 0 dB; per-channel stats are filled for each channel.
#[test]
fn full_scale_peak() {
let ch0: Vec = (0..64)
@@ -93,13 +69,13 @@ fn full_scale_peak() {
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);
- assert!((channels[0].peak_db - 0.0).abs() < 1e-9);
- assert!((channels[1].peak_db - 0.0).abs() < 1e-9);
- assert!((channels[0].rms_linear - 1.0).abs() < 1e-9);
- assert!((channels[1].rms_linear - 1.0).abs() < 1e-9);
+ let stats = analyze(&[ch0, ch1]);
+ assert_eq!(stats.max_peak_linear, 1.0);
+ assert!(!stats.silence);
+ assert!((stats.channels[0].peak_db - 0.0).abs() < 1e-9);
+ assert!((stats.channels[1].peak_db - 0.0).abs() < 1e-9);
+ assert!((stats.channels[0].rms_linear - 1.0).abs() < 1e-9);
+ assert!((stats.channels[1].rms_linear - 1.0).abs() < 1e-9);
}
/// integrated_lufs stays -200 for silence and matches the BS.1770
@@ -107,57 +83,27 @@ fn full_scale_peak() {
#[test]
fn integrated_lufs_silence_vs_tone() {
let silence = common::silence_planar(2, 64);
- let (_, summary) = analyze(&silence);
- assert_eq!(summary.integrated_lufs, -200.0);
+ let stats = analyze(&silence);
+ assert_eq!(stats.integrated_lufs, -200.0);
let tone = common::planar_from(&[0.5f32; 64], 2);
- let (_, summary) = analyze(&tone);
+ let stats = analyze(&tone);
// mean square over all channels = 0.25; -0.691 + 10*log10(0.25)
let expected = -0.691 + 10.0 * 0.25f64.log10();
- assert!((summary.integrated_lufs - expected).abs() < 1e-9);
+ assert!((stats.integrated_lufs - expected).abs() < 1e-9);
}
-/// channel_count of 0, a NULL planar pointer, or NULL for both outputs
-/// returns OAKAUDIO_E_INVALID.
+/// Empty or zero-length inputs yield a default stats struct (no channels,
+/// silence).
#[test]
-fn invalid_input() {
- let planes = common::planar_from(&[0.5f32; 8], 1);
- let ptr = planes[0].as_ptr();
- let mut summary = MeterStats {
- max_peak_linear: 0.0,
- integrated_lufs: 0.0,
- silence: 0,
- };
+fn empty_input() {
+ let stats = analyze_sample_buffer(&[]);
+ assert!(stats.channels.is_empty());
+ assert!(stats.silence);
- // channel_count 0.
- assert_eq!(
- unsafe { oakaudio_levelmeter_analyze(&ptr, 0, 8, std::ptr::null_mut(), 0, &mut summary) },
- OAKAUDIO_E_INVALID
- );
- // NULL planar.
- assert_eq!(
- unsafe {
- oakaudio_levelmeter_analyze(
- std::ptr::null(),
- 1,
- 8,
- std::ptr::null_mut(),
- 0,
- &mut summary,
- )
- },
- OAKAUDIO_E_INVALID
- );
- // Both outputs NULL.
- assert_eq!(
- unsafe {
- oakaudio_levelmeter_analyze(&ptr, 1, 8, std::ptr::null_mut(), 0, std::ptr::null_mut())
- },
- OAKAUDIO_E_INVALID
- );
- // Negative frame count.
- assert_eq!(
- unsafe { oakaudio_levelmeter_analyze(&ptr, 1, -1, std::ptr::null_mut(), 0, &mut summary) },
- OAKAUDIO_E_INVALID
- );
+ let silence = common::silence_planar(1, 0);
+ let refs: Vec<&[f32]> = silence.iter().map(Vec::as_slice).collect();
+ let stats = analyze_sample_buffer(&refs);
+ assert!(stats.silence);
+ assert_eq!(stats.max_peak_linear, 0.0);
}
diff --git a/crates/oakaudio/tests/manager_test.rs b/crates/oakaudio/tests/manager_test.rs
index b5e910430..b6438af58 100644
--- a/crates/oakaudio/tests/manager_test.rs
+++ b/crates/oakaudio/tests/manager_test.rs
@@ -14,171 +14,96 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! AudioManager contract tests (manager.rs), through the C ABI. The
-//! manager is a process-wide singleton, so every test holds the shared
+//! AudioManager contract tests (manager.rs), calling the public Rust API.
+//! The manager is a process-wide singleton, so every test holds the shared
//! `MANAGER_LOCK`.
mod common;
-use std::ffi::c_char;
+use common::lock_manager;
+use oakcodec::encodingparams::EncodingParams;
+use oakaudio::error::{Error, OAKAUDIO_E_INVALID};
+use oakaudio::manager::instance;
+use oakaudio::params::{AudioParams, SampleFormat};
-use common::MANAGER_LOCK;
-use oakaudio::bridge::codec::EncodingParams;
-use oakaudio::error::{OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK};
-use oakaudio::ffi::manager::{
- oakaudio_debug_alive_count, oakaudio_manager_clear_buffered_output,
- oakaudio_manager_create_instance, oakaudio_manager_destroy_instance,
- oakaudio_manager_find_config_device_by_name_s, oakaudio_manager_find_device_by_name_s,
- oakaudio_manager_free, oakaudio_manager_get_input_device, oakaudio_manager_get_output_device,
- oakaudio_manager_hard_reset, oakaudio_manager_instance, oakaudio_manager_push_to_output,
- oakaudio_manager_reset_output_clock, oakaudio_manager_seconds,
- oakaudio_manager_set_input_device, oakaudio_manager_set_output_device,
- oakaudio_manager_output_levels, oakaudio_manager_set_output_notify_interval,
- oakaudio_manager_start_recording,
- oakaudio_manager_stop_output, oakaudio_manager_stop_recording,
-};
-
-fn instance() -> oakaudio::handle::CHandle {
- unsafe { oakaudio_manager_instance() }
-}
-
-/// Lock the manager singleton for a test. The manager state persists across
-/// tests (the `OnceLock` cannot be reset), so a panicked test must not
-/// poison the lock for the rest of the binary.
-fn lock() -> std::sync::MutexGuard<'static, ()> {
- MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner())
-}
-
-fn encoding_params() -> EncodingParams {
- let mut filename = [0u8; 1024];
- for (i, b) in b"oakaudio_test.wav\0".iter().enumerate() {
- filename[i] = *b;
- }
- EncodingParams {
- filename,
- format: 0,
- video_enabled: 0,
- video_codec: 0,
- video_width: 0,
- video_height: 0,
- video_time_base_num: 0,
- video_time_base_den: 0,
- video_pixel_format: 0,
- video_interlacing: 0,
- video_pixel_aspect_num: 0,
- video_pixel_aspect_den: 0,
- video_bit_rate: 0,
- video_min_bit_rate: 0,
- video_max_bit_rate: 0,
- video_buffer_size: 0,
- video_threads: 0,
- video_pix_fmt: [0u8; 64],
- video_is_image_sequence: 0,
- video_scaling_method: 0,
- audio_enabled: 1,
- audio_codec: 13, // PCM_S16LE (the .wav recording codec)
- audio_sample_rate: 48000,
- audio_channel_layout: 3,
- audio_sample_format: 8,
- audio_bit_rate: 128000,
- subtitles_enabled: 0,
- subtitles_codec: 0,
- subtitles_are_sidecar: 0,
- subtitles_sidecar_format: 0,
- color_transform_output: [0u8; 256],
- export_length_num: 0,
- export_length_den: 0,
- has_custom_range: 0,
- custom_range_in_num: 0,
- custom_range_in_den: 0,
- custom_range_out_num: 0,
- custom_range_out_den: 0,
+/// Audio params for the tests: stereo f32 at 48 kHz.
+fn stereo() -> AudioParams {
+ AudioParams {
+ sample_rate: 48000,
+ channel_layout: 0x3,
+ format: SampleFormat::F32,
}
}
-/// create_instance/destroy_instance toggle the singleton; instance() returns
-/// a valid borrowed handle between them and NULL after destroy.
+/// A WAV recording config (format 7 = WAV).
+fn wav_params(filename: &str) -> EncodingParams {
+ let mut params = EncodingParams::default();
+ params.format = 7;
+ params.audio_enabled = 1;
+ params.audio_codec = 13; // PCM_S16LE
+ params.audio_sample_rate = 48000;
+ params.audio_channel_layout = 0x3;
+ params.audio_sample_format = SampleFormat::S16;
+ params.audio_bit_rate = 128000;
+ let bytes = filename.as_bytes();
+ params.filename[..bytes.len()].copy_from_slice(bytes);
+ params
+}
+
+/// create_instance/destroy_instance toggle the singleton; instance()
+/// returns a live guard between them and None after destroy.
#[test]
fn singleton_lifecycle() {
- let _guard = lock();
- unsafe { oakaudio_manager_destroy_instance() };
- assert!(instance().ctx.is_null());
+ let _guard = lock_manager();
+ oakaudio::manager::ManagerInner::destroy_instance();
+ assert!(instance().is_none());
- unsafe { oakaudio_manager_create_instance() };
- let m = instance();
- assert!(!m.ctx.is_null());
- unsafe { oakaudio_manager_free(&mut m.clone()) };
+ oakaudio::manager::ManagerInner::create_instance().unwrap();
+ assert!(instance().is_some());
- unsafe { oakaudio_manager_destroy_instance() };
- assert!(instance().ctx.is_null());
- unsafe { oakaudio_manager_create_instance() };
- assert!(!instance().ctx.is_null());
+ oakaudio::manager::ManagerInner::destroy_instance();
+ assert!(instance().is_none());
+ oakaudio::manager::ManagerInner::create_instance().unwrap();
+ assert!(instance().is_some());
+}
+
+/// A fresh singleton for a test: destroy resets the playback state
+/// (`output_started`, buffered params) that earlier tests in this binary
+/// may have left behind.
+fn fresh_instance() {
+ oakaudio::manager::ManagerInner::destroy_instance();
+ oakaudio::manager::ManagerInner::create_instance().unwrap();
}
/// push_to_output accepts raw interleaved bytes and starts the virtual
-/// playback clock; without a device it fails with a message in error_buf.
+/// playback clock; without a device the push still succeeds and keeps the
+/// samples buffered (unavailable devices play silence instead of failing).
///
-/// The virtual device never consumes frames (PortAudio is not bridged), so
-/// the clock reads 0.0 rather than advancing.
+/// The clock reads 0.0 after the push starts; a real audio device may have
+/// consumed a few frames by the time the assertion runs, so the bound is
+/// `>= 0` rather than exact (the callback-advances-clock behavior is pinned
+/// by manager.rs' own unit test).
#[test]
-fn push_output_advances_clock() {
- let _guard = lock();
- unsafe { oakaudio_manager_create_instance() };
- let m = instance();
+fn push_output_starts_clock() {
+ let _guard = lock_manager();
+ fresh_instance();
+ let mut m = instance().unwrap();
// No stream yet: seconds() reports -1.
let mut secs = 0.0f64;
- assert_eq!(
- unsafe { oakaudio_manager_seconds(m, &mut secs) },
- OAKAUDIO_OK
- );
+ m.seconds(&mut secs).unwrap();
assert_eq!(secs, -1.0);
- // M12 P1: with no explicit device the push still succeeds — the
- // samples buffer for the default output device (unavailable devices
- // keep playback silent instead of failing the push).
- assert_eq!(
- unsafe { oakaudio_manager_set_output_device(m, -1) },
- OAKAUDIO_OK
- );
+ // With no explicit device the push still succeeds (M12 P1).
+ m.set_output_device(-1).unwrap();
let samples = vec![0u8; 480 * 2 * 4];
- let r = unsafe {
- oakaudio_manager_push_to_output(
- m,
- 48000,
- 3,
- 4,
- samples.as_ptr() as *const c_char,
- samples.len() as i64,
- std::ptr::null_mut(),
- 0,
- )
- };
- assert_eq!(r, OAKAUDIO_OK);
+ m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
// After selecting a device the push succeeds and the clock starts at 0.
- assert_eq!(
- unsafe { oakaudio_manager_set_output_device(m, 0) },
- OAKAUDIO_OK
- );
- let mut err = [0 as c_char; 64];
- let r = unsafe {
- oakaudio_manager_push_to_output(
- m,
- 48000,
- 3,
- 4,
- samples.as_ptr() as *const c_char,
- samples.len() as i64,
- err.as_mut_ptr(),
- err.len() as i32,
- )
- };
- assert_eq!(r, OAKAUDIO_OK);
- unsafe { oakaudio_manager_seconds(m, &mut secs) };
- assert_eq!(secs, 0.0);
-
- unsafe { oakaudio_manager_destroy_instance() };
+ m.set_output_device(0).unwrap();
+ m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
+ m.seconds(&mut secs).unwrap();
+ assert!(secs >= 0.0, "clock started (got {secs})");
}
/// set/get output & input device: getters report a device, setters persist
@@ -186,183 +111,102 @@ fn push_output_advances_clock() {
/// clears buffers).
#[test]
fn device_selection_roundtrip() {
- let _guard = lock();
- unsafe { oakaudio_manager_create_instance() };
- let m = instance();
+ let _guard = lock_manager();
+ fresh_instance();
+ let mut m = instance().unwrap();
- assert_eq!(
- unsafe { oakaudio_manager_set_output_device(m, 42) },
- OAKAUDIO_OK
- );
- assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
- assert_eq!(
- unsafe { oakaudio_manager_set_input_device(m, 7) },
- OAKAUDIO_OK
- );
- assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
+ m.set_output_device(42).unwrap();
+ assert_eq!(m.get_output_device().unwrap(), 42);
+ m.set_input_device(7).unwrap();
+ assert_eq!(m.get_input_device().unwrap(), 7);
- assert_eq!(unsafe { oakaudio_manager_hard_reset(m) }, OAKAUDIO_OK);
- assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
- assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
+ m.hard_reset().unwrap();
+ assert_eq!(m.get_output_device().unwrap(), 42);
+ assert_eq!(m.get_input_device().unwrap(), 7);
// The stream stopped, so the clock is back at -1.
let mut secs = 0.0f64;
- unsafe { oakaudio_manager_seconds(m, &mut secs) };
+ m.seconds(&mut secs).unwrap();
assert_eq!(secs, -1.0);
-
- unsafe { oakaudio_manager_destroy_instance() };
}
-/// set_output_notify_interval stores the interval; clear_buffered_output
-/// drops queued bytes, stop_output halts the stream, and reset_output_clock
-/// restarts the counter.
+/// set_output_notify_interval stores the interval (negative is rejected);
+/// clear_buffered_output drops queued bytes, stop_output halts the stream,
+/// and reset_output_clock restarts the counter.
#[test]
fn output_control_flags() {
- let _guard = lock();
- unsafe { oakaudio_manager_create_instance() };
- let m = instance();
+ let _guard = lock_manager();
+ fresh_instance();
+ let mut m = instance().unwrap();
+ m.set_output_notify_interval(1024).unwrap();
+ let err = m.set_output_notify_interval(-1).unwrap_err();
assert_eq!(
- unsafe { oakaudio_manager_set_output_notify_interval(m, 1024) },
- OAKAUDIO_OK
- );
- assert_eq!(
- unsafe { oakaudio_manager_set_output_notify_interval(m, -1) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_manager_clear_buffered_output(m) },
- OAKAUDIO_OK
- );
- assert_eq!(
- unsafe { oakaudio_manager_reset_output_clock(m) },
- OAKAUDIO_OK
+ err.downcast_ref::().map(|e| e.code()),
+ Some(OAKAUDIO_E_INVALID)
);
+ m.clear_buffered_output().unwrap();
+ m.reset_output_clock().unwrap();
// Push starts the stream, then stop_output halts it (clock -> -1).
- unsafe { oakaudio_manager_set_output_device(m, 0) };
+ m.set_output_device(0).unwrap();
let samples = vec![0u8; 480 * 2 * 4];
- assert_eq!(
- unsafe {
- oakaudio_manager_push_to_output(
- m,
- 48000,
- 3,
- 4,
- samples.as_ptr() as *const c_char,
- samples.len() as i64,
- std::ptr::null_mut(),
- 0,
- )
- },
- OAKAUDIO_OK
- );
- assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
+ m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
+ m.stop_output().unwrap();
let mut secs = 1.0f64;
- unsafe { oakaudio_manager_seconds(m, &mut secs) };
+ m.seconds(&mut secs).unwrap();
assert_eq!(secs, -1.0);
-
- unsafe { oakaudio_manager_destroy_instance() };
}
-/// start_recording validates its parameters: NULL params or a disabled
-/// audio track return OAKAUDIO_E_INVALID with an error string. The full
-/// encoder-open success path (oakcodec writes a real file via ffmpeg) is
-/// an end-to-end concern covered by the codec crate's own tests; with the
-/// real oakcodec linked, `start_recording` either opens the encoder
-/// (OAKAUDIO_OK, environment-dependent) or reports the encoder's
-/// last-error string — both are correct manager behavior, so this test
-/// pins the manager's own validation only.
+/// start_recording validates its state: with no input device it fails with
+/// a reason; with a device the encoder open either succeeds (OAKAUDIO_OK,
+/// environment-dependent) or reports the encoder's diagnostic — both are
+/// correct manager behavior, so the test pins the manager's own handling
+/// only.
#[test]
fn recording_start_stop() {
- let _guard = lock();
- unsafe { oakaudio_manager_create_instance() };
- let m = instance();
- unsafe { oakaudio_manager_set_input_device(m, 0) };
+ let _guard = lock_manager();
+ fresh_instance();
+ let mut m = instance().unwrap();
- let mut err = [0 as c_char; 64];
- let params = encoding_params();
- // With a real encoder, the attempt must at least reach the encoder
- // (a failure must surface a diagnostic in error_buf, not crash).
- let r =
- unsafe { oakaudio_manager_start_recording(m, ¶ms, err.as_mut_ptr(), err.len() as i32) };
- if r != 0 {
- assert!(
- err.iter().any(|&b| b != 0),
- "failed start_recording must report a reason"
- );
- let _ = std::fs::remove_file("oakaudio_test.wav");
- } else {
- assert_eq!(unsafe { oakaudio_manager_stop_recording(m) }, OAKAUDIO_OK);
- // The real encoder writes the output file during open; clean it up.
- let _ = std::fs::remove_file("oakaudio_test.wav");
+ // No input device -> explainable failure, no crash.
+ m.set_input_device(-1).unwrap();
+ assert!(m.start_recording(&wav_params("unused.wav"), &mut vec![0u8; 64]).is_err());
+
+ m.set_input_device(0).unwrap();
+ let path = std::env::temp_dir().join(format!("oakaudio_rec_{}.wav", std::process::id()));
+ let filename = path.to_str().unwrap();
+ let mut err = vec![0u8; 256];
+ match m.start_recording(&wav_params(filename), &mut err) {
+ Ok(()) => {
+ assert!(m.stop_recording().is_ok());
+ }
+ Err(e) => {
+ assert!(!e.to_string().is_empty(), "failure must carry a reason");
+ }
}
-
- // NULL params is invalid and reports the reason in error_buf.
- let mut err = [0 as c_char; 64];
- let r = unsafe {
- oakaudio_manager_start_recording(m, std::ptr::null(), err.as_mut_ptr(), err.len() as i32)
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
- assert!(err.iter().any(|&b| b != 0));
-
- // A disabled audio track is likewise invalid.
- let mut disabled = encoding_params();
- disabled.audio_enabled = 0;
- let mut err = [0 as c_char; 64];
- let r = unsafe {
- oakaudio_manager_start_recording(m, &disabled, err.as_mut_ptr(), err.len() as i32)
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
- assert!(err.iter().any(|&b| b != 0));
-
- unsafe { oakaudio_manager_destroy_instance() };
+ let _ = std::fs::remove_file(path);
}
-/// Device enumeration is not bridged: every name/config lookup falls back to
-/// paNoDevice (-1); a NULL name is OAKAUDIO_E_INVALID. The config-backed
-/// buffer size/name helpers degrade to their defaults.
+/// Config defaults are read from the (empty) oakcommon store: buffer size
+/// falls back to 0 and device names are absent.
#[test]
-fn device_name_lookup() {
- let _guard = lock();
-
- assert_eq!(
- unsafe { oakaudio_manager_find_device_by_name_s(std::ptr::null(), 1) },
- OAKAUDIO_E_INVALID
- );
- let name = c"anything";
- assert_eq!(
- unsafe { oakaudio_manager_find_device_by_name_s(name.as_ptr(), 1) },
- -1
- );
- assert_eq!(
- unsafe { oakaudio_manager_find_config_device_by_name_s(1) },
- -1
- );
- assert_eq!(
- unsafe { oakaudio_manager_find_config_device_by_name_s(0) },
- -1
- );
-
- // config::output_buffer_size() reads its default (0) from the stub;
- // device_name degrades to the empty string.
+fn config_defaults() {
assert_eq!(oakaudio::config::output_buffer_size(), 0);
- assert!(oakaudio::config::device_name(true).as_c_str().is_empty());
- assert!(oakaudio::config::device_name(false).as_c_str().is_empty());
+ assert!(oakaudio::config::device_name(true).is_err(), "no configured output device");
+ assert!(oakaudio::config::device_name(false).is_err(), "no configured input device");
}
-/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock) that
-/// the manager path only touches indirectly.
+/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock).
#[test]
fn preview_device_pull_side() {
- use oakaudio::params::AudioParams;
use oakaudio::previewdevice::PreviewAudioDevice;
let mut dev = PreviewAudioDevice::new();
dev.set_params(AudioParams {
sample_rate: 48000,
channel_layout: 3,
- format: oakaudio::params::SampleFormat::F32,
+ format: SampleFormat::F32,
});
assert_eq!(dev.bytes_per_frame(), 8);
@@ -389,85 +233,65 @@ fn preview_device_pull_side() {
assert_eq!(dev.output_frames_consumed(), 0);
}
-/// free(NULL)/free(empty) are no-ops on the manager handle.
-#[test]
-fn free_null_noop() {
- let _guard = lock();
- unsafe { oakaudio_manager_create_instance() };
- let before = unsafe { oakaudio_debug_alive_count() };
-
- let mut empty = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_manager_free(&mut empty) };
- unsafe { oakaudio_manager_free(std::ptr::null_mut()) };
- assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
-
- unsafe { oakaudio_manager_destroy_instance() };
-}
-
-/// output_levels: validation, the no-output case, and a real peak
-/// readback over pushed packed-F32 stereo samples (left ramps to 0.25,
-/// right to ~1.0 — the per-channel linear peaks).
+/// output_levels: the no-output case, and a real peak readback over pushed
+/// packed-F32 stereo samples (left ramps to 0.05, right to ~0.2 — the
+/// per-channel linear peaks).
+///
+/// The push can open a real cpal stream whose callback consumes queued
+/// samples concurrently, so a long ramp is pushed and the peaks are
+/// asserted with a tolerance that absorbs partial consumption (the
+/// analysis window is the most recent 8192 frames of the queue).
#[test]
fn output_levels_reports_buffered_peaks() {
- let _guard = lock();
- unsafe { oakaudio_manager_destroy_instance() };
- assert_eq!(unsafe { oakaudio_manager_create_instance() }, OAKAUDIO_OK);
- let m = instance();
+ let _guard = lock_manager();
+ fresh_instance();
+ let mut m = instance().unwrap();
+
+ // Nothing configured yet: no channels.
+ assert_eq!(m.output_levels(&mut [0.0f32; 4]).unwrap(), 0);
+
+ m.set_output_device(42).unwrap();
+ m.clear_buffered_output().unwrap();
+
+ // Push 2 seconds of packed F32 stereo ramp (format 10), stereo layout
+ // 0x3. Left ramps 0 -> 0.05, right 0 -> 0.2.
+ let frames = 96000usize;
+ let mut packed = Vec::with_capacity(frames * 2 * 4);
+ for i in 0..frames {
+ let t = i as f32 / frames as f32;
+ packed.extend_from_slice(&(0.05f32 * t).to_le_bytes());
+ packed.extend_from_slice(&(0.2f32 * t).to_le_bytes());
+ }
+ m.push_to_output(stereo(), &packed, &mut vec![0u8; 64]).unwrap();
- // Invalid out args.
let mut peaks = [0.0f32; 4];
- assert_eq!(
- unsafe { oakaudio_manager_output_levels(m, std::ptr::null_mut(), 4) },
- OAKAUDIO_E_INVALID
+ let n = m.output_levels(&mut peaks).unwrap();
+ assert_eq!(n, 2);
+ assert!(
+ (peaks[0] - 0.05).abs() < 1e-3 && peaks[0] > 0.04,
+ "left peak: {}",
+ peaks[0]
+ );
+ assert!(
+ (peaks[1] - 0.2).abs() < 1e-3 && peaks[1] > 0.19,
+ "right peak: {}",
+ peaks[1]
);
- assert_eq!(unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 0) }, OAKAUDIO_E_INVALID);
- // The manager singleton retains output params across tests in this
- // binary, so the "no output" case is not reachable here; instead
- // verify a cleared buffer reports zeroed peaks (channel count from
- // the configured layout, 0 only when nothing was ever configured).
- assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK);
- assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
- let cleared = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
- assert!(cleared >= 0);
- for p in &peaks[..cleared as usize] {
+ // A cleared buffer reports zeroed peaks over the configured channels.
+ m.clear_buffered_output().unwrap();
+ let mut cleared = [0.0f32; 4];
+ assert_eq!(m.output_levels(&mut cleared).unwrap(), 2);
+ for p in &cleared[..2] {
assert_eq!(*p, 0.0, "cleared buffer must have silent peaks");
}
- // Push 480 frames of packed F32 stereo (format 10), stereo layout 0x3.
- let frames = 480usize;
- let mut samples = Vec::with_capacity(frames * 2);
- for i in 0..frames {
- let t = i as f32 / frames as f32;
- samples.push(0.25f32 * t);
- samples.push(t);
- }
- let rc = unsafe {
- oakaudio_manager_push_to_output(
- m,
- 48000,
- 0x3,
- 10,
- samples.as_ptr() as *const c_char,
- (samples.len() * 4) as i64,
- std::ptr::null_mut(),
- 0,
- )
- };
- assert_eq!(rc, OAKAUDIO_OK);
-
- let n = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
- assert_eq!(n, 2);
- let last = (frames - 1) as f32 / frames as f32;
- assert!((peaks[0] - 0.25 * last).abs() < 1e-6, "left peak: {}", peaks[0]);
- assert!((peaks[1] - last).abs() < 1e-6, "right peak: {}", peaks[1]);
-
- // Undersized buffer truncates the write, not the reported count.
- let mut one = [0.0f32; 1];
- assert_eq!(unsafe { oakaudio_manager_output_levels(m, one.as_mut_ptr(), 1) }, 2);
+ // The error mapping is intact.
+ assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
+ assert_eq!(Error::Failed("x".to_string()).code(), oakaudio::error::OAKAUDIO_E_FAILED);
// Leave the singleton as we found it: the push flipped
// `output_started`, which other tests' seconds() assertions depend on.
- assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
- assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
+ m.stop_output().unwrap();
+ m.clear_buffered_output().unwrap();
}
diff --git a/crates/oakaudio/tests/processor_test.rs b/crates/oakaudio/tests/processor_test.rs
index bf6a8801c..08b8894f0 100644
--- a/crates/oakaudio/tests/processor_test.rs
+++ b/crates/oakaudio/tests/processor_test.rs
@@ -14,20 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! AudioProcessor contract tests (processor.rs), through the C ABI. The
-//! conversion runs a real FFmpeg filter graph (aresample/aformat/atempo),
-//! so resampling and time-stretch have filter latency: the exact frame
-//! counts are drained after `flush`, while identity conversion is an
-//! immediate passthrough.
+//! AudioProcessor contract tests (processor.rs), calling the public Rust
+//! API. The conversion runs a real FFmpeg filter graph
+//! (aresample/aformat/atempo), so resampling and time-stretch have filter
+//! latency: the exact frame counts are drained after `flush`, while
+//! identity conversion is an immediate passthrough.
-mod common;
-
-use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
-use oakaudio::ffi::processor::{
- oakaudio_processor_close, oakaudio_processor_convert, oakaudio_processor_flush,
- oakaudio_processor_free, oakaudio_processor_init, oakaudio_processor_is_open,
- oakaudio_processor_open,
-};
+use oakaudio::error::{Error, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
+use oakaudio::params::{AudioParams, SampleFormat};
+use oakaudio::processor::Processor;
/// Stereo f32_p planes of `frames` ramp samples.
fn ramp_planes(frames: usize) -> Vec> {
@@ -37,37 +32,61 @@ fn ramp_planes(frames: usize) -> Vec> {
]
}
-fn open_identity(h: oakaudio::handle::CHandle) -> i32 {
- unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 1.0) }
+/// Stereo planar-f32 params at `rate`.
+fn params(rate: i32) -> AudioParams {
+ AudioParams {
+ sample_rate: rate,
+ channel_layout: 0x3,
+ format: SampleFormat::F32Planar,
+ }
}
-/// init yields a valid handle; is_open is false before open and true after;
-/// close returns it to closed without error.
+fn open_identity(p: &Processor) -> Result<(), Box> {
+ p.open(params(48000), params(48000), 1.0)
+}
+
+/// Pointer arrays for the convert call.
+fn plane_ptrs(planes: &[Vec]) -> Vec<*const f32> {
+ planes.iter().map(|p| p.as_ptr()).collect()
+}
+
+fn plane_mut_ptrs(planes: &mut [Vec]) -> Vec<*mut f32> {
+ planes.iter_mut().map(|p| p.as_mut_ptr()).collect()
+}
+
+/// The crate error code of a failed call (the API surfaces
+/// `Box`).
+fn code(err: Box) -> i32 {
+ err.downcast_ref::()
+ .map(|e| e.code())
+ .unwrap_or(-1)
+}
+
+/// init yields a closed processor; is_open is false before open and true
+/// after; close returns it to closed without error.
#[test]
fn processor_open_isopen_close() {
- let mut h = unsafe { oakaudio_processor_init() };
- assert!(!h.ctx.is_null());
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
- assert_eq!(open_identity(h), OAKAUDIO_OK);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
- assert_eq!(unsafe { oakaudio_processor_close(h) }, OAKAUDIO_OK);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
- unsafe { oakaudio_processor_free(&mut h) };
+ let p = Processor::init();
+ assert_eq!(p.is_open().unwrap(), false);
+ open_identity(&p).unwrap();
+ assert_eq!(p.is_open().unwrap(), true);
+ p.close().unwrap();
+ assert_eq!(p.is_open().unwrap(), false);
}
/// open with matching in/out rate and format is an identity passthrough:
/// convert returns the same frame count and samples within 1e-6.
#[test]
fn identity_convert_passthrough() {
- let mut h = unsafe { oakaudio_processor_init() };
- assert_eq!(open_identity(h), OAKAUDIO_OK);
+ let p = Processor::init();
+ open_identity(&p).unwrap();
let planes = ramp_planes(32);
- let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
+ let in_ptrs = plane_ptrs(&planes);
let mut out = vec![vec![0f32; 32]; 2];
- let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
+ let mut out_ptrs = plane_mut_ptrs(&mut out);
- let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32) };
+ let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap();
assert_eq!(n, 32);
for ch in 0..2 {
for i in 0..32 {
@@ -79,22 +98,21 @@ fn identity_convert_passthrough() {
);
}
}
- unsafe { oakaudio_processor_free(&mut h) };
}
/// convert with an output capacity smaller than the produced frames returns
/// the produced count clamped to capacity and fills up to capacity.
#[test]
fn convert_capacity_truncation() {
- let mut h = unsafe { oakaudio_processor_init() };
- assert_eq!(open_identity(h), OAKAUDIO_OK);
+ let p = Processor::init();
+ open_identity(&p).unwrap();
let planes = ramp_planes(32);
- let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
+ let in_ptrs = plane_ptrs(&planes);
let mut out = vec![vec![9.9f32; 10]; 2];
- let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
+ let mut out_ptrs = plane_mut_ptrs(&mut out);
- let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10) };
+ let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap();
assert_eq!(n, 10);
for ch in 0..2 {
for i in 0..10 {
@@ -104,56 +122,40 @@ fn convert_capacity_truncation() {
// The graph has already drained; nothing further to pull.
let mut out2 = vec![vec![0f32; 32]; 2];
- let mut out2_ptrs: Vec<*mut f32> = out2.iter_mut().map(|p| p.as_mut_ptr()).collect();
- let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32) };
+ let mut out2_ptrs = plane_mut_ptrs(&mut out2);
+ let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap();
assert_eq!(n, 0);
-
- unsafe { oakaudio_processor_free(&mut h) };
}
-/// open with a zero/negative rate or a wrong output format returns
-/// OAKAUDIO_E_INVALID and leaves the processor closed; an empty handle is
-/// OAKAUDIO_E_INVALID everywhere.
+/// open with a zero/negative rate or a wrong output format is rejected and
+/// leaves the processor closed; convert before open is a state error.
#[test]
fn open_invalid_params() {
- let mut h = unsafe { oakaudio_processor_init() };
+ let p = Processor::init();
assert_eq!(
- unsafe { oakaudio_processor_open(h, 0, 3, 4, 48000, 3, 4, 1.0) },
+ code(p.open(params(0), params(48000), 1.0).unwrap_err()),
OAKAUDIO_E_INVALID
);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
+ assert_eq!(p.is_open().unwrap(), false);
+ // The output format is forced to planar f32 by the processor contract.
+ let mut wrong_out = params(48000);
+ wrong_out.format = SampleFormat::F32;
assert_eq!(
- unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 0, 1.0) },
+ code(p.open(params(48000), wrong_out, 1.0).unwrap_err()),
OAKAUDIO_E_INVALID
);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
+ assert_eq!(p.is_open().unwrap(), false);
- let empty = oakaudio::handle::CHandle::null();
- assert_eq!(open_identity(empty), OAKAUDIO_E_INVALID);
+ // convert before open is a state error; a non-positive speed is
+ // rejected on open.
assert_eq!(
- unsafe { oakaudio_processor_is_open(empty) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_processor_close(empty) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_processor_flush(empty) },
- OAKAUDIO_E_INVALID
- );
- let mut out_ptrs: Vec<*mut f32> = Vec::new();
- assert_eq!(
- unsafe { oakaudio_processor_convert(empty, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
- OAKAUDIO_E_INVALID
- );
-
- // convert before open is a state error.
- assert_eq!(
- unsafe { oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
+ code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()),
OAKAUDIO_E_STATE
);
- unsafe { oakaudio_processor_free(&mut h) };
+ assert_eq!(
+ code(p.open(params(48000), params(48000), 0.0).unwrap_err()),
+ OAKAUDIO_E_INVALID
+ );
}
/// Resampling to half rate halves the frame count (44100 -> 22050). The
@@ -161,34 +163,23 @@ fn open_invalid_params() {
/// drained after `flush`; flush then keeps the processor open.
#[test]
fn resample_and_flush() {
- let mut h = unsafe { oakaudio_processor_init() };
- assert_eq!(
- unsafe { oakaudio_processor_open(h, 44100, 3, 4, 22050, 3, 4, 1.0) },
- OAKAUDIO_OK
- );
+ let p = Processor::init();
+ p.open(params(44100), params(22050), 1.0).unwrap();
// 1 second of input keeps the resampler delay well below the signal.
let frames = 44100;
let planes = ramp_planes(frames);
- let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
+ let in_ptrs = plane_ptrs(&planes);
let mut out = vec![vec![0f32; frames]; 2];
- let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
+ let mut out_ptrs = plane_mut_ptrs(&mut out);
- let mut total = unsafe {
- oakaudio_processor_convert(
- h,
- in_ptrs.as_ptr(),
- frames as i32,
- out_ptrs.as_ptr(),
- frames as i32,
- )
- };
- assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
+ let mut total = p
+ .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
+ .unwrap();
+ p.flush().unwrap();
// Drain the resampler delay after end-of-input.
while total < frames as i32 {
- let n = unsafe {
- oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
- };
+ let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
if n == 0 {
break;
}
@@ -198,8 +189,7 @@ fn resample_and_flush() {
(total - 22050).abs() <= 2,
"half-rate output must halve the frame count (got {total})"
);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
- unsafe { oakaudio_processor_free(&mut h) };
+ assert_eq!(p.is_open().unwrap(), true);
}
/// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count
@@ -207,33 +197,22 @@ fn resample_and_flush() {
/// produces output) and the processor stays open.
#[test]
fn tempo_stretch() {
- let mut h = unsafe { oakaudio_processor_init() };
- assert_eq!(
- unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 2.0) },
- OAKAUDIO_OK
- );
+ let p = Processor::init();
+ p.open(params(48000), params(48000), 2.0).unwrap();
// 1 second of input: many atempo windows (1024 samples at 48 kHz).
let frames = 48000;
let planes = ramp_planes(frames);
- let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
+ let in_ptrs = plane_ptrs(&planes);
let mut out = vec![vec![0f32; frames]; 2];
- let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
+ let mut out_ptrs = plane_mut_ptrs(&mut out);
- let mut total = unsafe {
- oakaudio_processor_convert(
- h,
- in_ptrs.as_ptr(),
- frames as i32,
- out_ptrs.as_ptr(),
- frames as i32,
- )
- };
- assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
+ let mut total = p
+ .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
+ .unwrap();
+ p.flush().unwrap();
while total < frames as i32 {
- let n = unsafe {
- oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
- };
+ let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
if n == 0 {
break;
}
@@ -243,24 +222,20 @@ fn tempo_stretch() {
(total - 24000).abs() <= 2400,
"tempo 2.0 must halve the frame count (got {total})"
);
- assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
- unsafe { oakaudio_processor_close(h) };
+ assert_eq!(p.is_open().unwrap(), true);
+ p.close().unwrap();
- // A non-positive speed is rejected (on a closed processor).
- assert_eq!(
- unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 0.0) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(open_identity(h), OAKAUDIO_OK);
- // Already open -> state error.
- assert_eq!(open_identity(h), OAKAUDIO_E_STATE);
- unsafe { oakaudio_processor_free(&mut h) };
+ // Re-opening a closed processor works; opening an open one is a state
+ // error.
+ open_identity(&p).unwrap();
+ assert_eq!(code(open_identity(&p).unwrap_err()), OAKAUDIO_E_STATE);
}
-/// free(NULL)/free(empty) are no-ops.
+/// The error mapping is intact.
#[test]
-fn free_null_noop() {
- let mut h = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_processor_free(&mut h) };
- unsafe { oakaudio_processor_free(std::ptr::null_mut()) };
+fn error_codes() {
+ assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
+ assert_eq!(Error::State.code(), OAKAUDIO_E_STATE);
+ assert_eq!(Error::Failed("x".to_string()).code(), -60003);
+ assert_eq!(oakaudio::error::OAKAUDIO_OK, OAKAUDIO_OK);
}
diff --git a/crates/oakaudio/tests/sync_test.rs b/crates/oakaudio/tests/sync_test.rs
index a1b0a0020..b4b86f07d 100644
--- a/crates/oakaudio/tests/sync_test.rs
+++ b/crates/oakaudio/tests/sync_test.rs
@@ -15,24 +15,22 @@
// along with this program. If not, see .
//! AudioSynchronizer + AudioWaveformSync contract tests
-//! (synchronizer.rs, waveformsync.rs), through the C ABI.
+//! (synchronizer.rs, waveformsync.rs), calling the public Rust API.
mod common;
-use oakaudio::error::OAKAUDIO_E_INVALID;
-use oakaudio::ffi::sync::{
- oakaudio_sync_estimate_envelope_offset, oakaudio_sync_estimate_stretch_and_offset,
- oakaudio_sync_extract_rms_envelope, oakaudio_sync_place_by_source_time,
- oakaudio_sync_place_by_waveform_offset, OffsetResult, SourceClip, StretchOffsetResult,
+use oakcore_rs::Rational;
+use oakaudio::synchronizer::{place_by_source_time, place_by_waveform_offset, SourceClip};
+use oakaudio::waveformsync::{
+ estimate_envelope_offset, estimate_envelope_offset_valid, estimate_stretch_and_offset,
+ extract_rms_envelope,
};
fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip {
SourceClip {
- source_start_time_num: source,
- source_start_time_den: 1,
- media_in_num: media_in,
- media_in_den: 1,
- has_source_start_time: has_source as i32,
+ source_start_time: Rational::new(source, 1),
+ media_in: Rational::new(media_in, 1),
+ has_source_start_time: has_source,
}
}
@@ -43,28 +41,15 @@ fn clip(source: i64, media_in: i64, has_source: bool) -> SourceClip {
fn place_by_source_time_matching() {
let reference = clip(0, 0, true);
let candidate = clip(0, 0, true);
- let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
- let r = unsafe {
- oakaudio_sync_place_by_source_time(
- &reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(num, 5);
- assert_eq!(den, 1);
- assert_eq!(valid, 1);
+ let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1));
+ assert!(placement.valid);
+ assert_eq!(placement.timeline_in, Rational::new(5, 1));
- // A source-less candidate is invalid: valid=0, null rational.
+ // A source-less candidate is invalid: valid=false, null rational.
let candidate = clip(0, 0, false);
- let r = unsafe {
- oakaudio_sync_place_by_source_time(
- &reference, &candidate, 5, 1, &mut num, &mut den, &mut valid,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(valid, 0);
- assert_eq!(num, 0);
- assert_eq!(den, 0);
+ let placement = place_by_source_time(&reference, &candidate, Rational::new(5, 1));
+ assert!(!placement.valid);
+ assert!(placement.timeline_in.is_null());
}
/// place_by_source_time: when source times disagree by a known delta, the
@@ -73,62 +58,28 @@ fn place_by_source_time_matching() {
fn place_by_source_time_delta() {
let reference = clip(5, 0, true);
let candidate = clip(12, 0, true);
- let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
- let r = unsafe {
- oakaudio_sync_place_by_source_time(
- &reference, &candidate, 0, 1, &mut num, &mut den, &mut valid,
- )
- };
- assert_eq!(r, 0);
+ let placement = place_by_source_time(&reference, &candidate, Rational::new(0, 1));
// 0 + (12 + 0) - (5 + 0) = 7
- assert_eq!(num, 7);
- assert_eq!(den, 1);
- assert_eq!(valid, 1);
-
- // A zero denominator is rejected up front.
- let r = unsafe {
- oakaudio_sync_place_by_source_time(
- &reference, &candidate, 0, 0, &mut num, &mut den, &mut valid,
- )
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
+ assert!(placement.valid);
+ assert_eq!(placement.timeline_in, Rational::new(7, 1));
}
/// place_by_waveform_offset converts a sample offset at a sample rate into
-/// a timeline-in shift; out_valid is 1 on success and 0 for a null rate.
+/// a timeline-in shift; out_valid is false for a null rate.
#[test]
fn place_by_waveform_offset_conversion() {
- let (mut num, mut den, mut valid) = (0i64, 0i64, 0i32);
- let r = unsafe {
- oakaudio_sync_place_by_waveform_offset(0, 1, 48000, 48000, &mut num, &mut den, &mut valid)
- };
- assert_eq!(r, 0);
- assert_eq!(num, 1);
- assert_eq!(den, 1);
- assert_eq!(valid, 1);
+ let placement = place_by_waveform_offset(Rational::new(0, 1), 48000, 48000);
+ assert!(placement.valid);
+ assert_eq!(placement.timeline_in, Rational::new(1, 1));
- let r = unsafe {
- oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 48000, &mut num, &mut den, &mut valid)
- };
- assert_eq!(r, 0);
- assert_eq!(num, 3);
- assert_eq!(den, 2);
- assert_eq!(valid, 1);
+ let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 48000);
+ assert!(placement.valid);
+ assert_eq!(placement.timeline_in, Rational::new(3, 2));
- // A null rate is invalid (valid=0, null rational), and the FFI rejects
- // a zero timeline denominator.
- let r = unsafe {
- oakaudio_sync_place_by_waveform_offset(1, 2, 48000, 0, &mut num, &mut den, &mut valid)
- };
- assert_eq!(r, 0);
- assert_eq!(valid, 0);
- assert_eq!(num, 0);
- assert_eq!(den, 0);
-
- let r = unsafe {
- oakaudio_sync_place_by_waveform_offset(1, 0, 48000, 48000, &mut num, &mut den, &mut valid)
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
+ // A null rate is invalid (valid=false, null rational).
+ let placement = place_by_waveform_offset(Rational::new(1, 2), 48000, 0);
+ assert!(!placement.valid);
+ assert!(placement.timeline_in.is_null());
}
/// extract_rms_envelope produces one value per window; a window larger than
@@ -137,85 +88,31 @@ fn place_by_waveform_offset_conversion() {
fn extract_rms_envelope_shape() {
let data: Vec = (0..100).map(|i| i as f32).collect();
let planes = common::planar_from(&data, 2);
- let ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
+ let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
- let mut out = vec![0.0f64; 16];
- let n = unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), 16)
- };
- assert_eq!(n, 10);
- assert!(out.iter().take(10).all(|&v| v > 0.0));
+ let env = extract_rms_envelope(&refs, 10);
+ assert_eq!(env.len(), 10);
+ assert!(env.iter().all(|&v| v > 0.0));
- let n = unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 200, out.as_mut_ptr(), 16)
- };
- assert_eq!(n, 1);
-
- // Invalid inputs.
- assert_eq!(
- unsafe {
- oakaudio_sync_extract_rms_envelope(std::ptr::null(), 2, 100, 10, out.as_mut_ptr(), 16)
- },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 0, 100, 10, out.as_mut_ptr(), 16)
- },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 0, out.as_mut_ptr(), 16)
- },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, out.as_mut_ptr(), -1)
- },
- OAKAUDIO_E_INVALID
- );
- // Two-stage: NULL out returns the required count.
- let n = unsafe {
- oakaudio_sync_extract_rms_envelope(ptrs.as_ptr(), 2, 100, 10, std::ptr::null_mut(), 0)
- };
- assert_eq!(n, 10);
+ let env = extract_rms_envelope(&refs, 200);
+ assert_eq!(env.len(), 1);
}
/// estimate_envelope_offset: for a candidate delayed by N windows relative
-/// to the reference, the returned offset is +N windows and valid=1.
+/// to the reference, the returned offset is +N windows and valid=true.
#[test]
fn envelope_offset_recovers_delay() {
let reference: Vec = (0..10).map(|i| i as f64).collect();
let mut candidate = vec![0.0f64; 10];
candidate[2..].copy_from_slice(&reference[..8]);
- let mut out = OffsetResult {
- offset_samples: 0,
- confidence: 0.0,
- valid: 0,
- };
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- reference.as_ptr(),
- 10,
- candidate.as_ptr(),
- 10,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 1);
+ let out = estimate_envelope_offset(&reference, &candidate, 100, 10);
+ assert!(out.valid);
assert_eq!(out.offset_samples, 200);
assert!((out.confidence - 1.0).abs() < 1e-9);
}
-/// estimate_envelope_offset: windows masked invalid on either side are
-/// excluded from correlation; empty masks are treated as all-valid.
+/// estimate_envelope_offset_valid: windows masked invalid on either side
+/// are excluded from correlation; empty masks are treated as all-valid.
#[test]
fn envelope_offset_respects_valid_masks() {
let reference: Vec = (0..10).map(|i| i as f64).collect();
@@ -224,68 +121,35 @@ fn envelope_offset_respects_valid_masks() {
// Only the last reference window is valid -> no lag has >= 2 valid
// overlap windows, so the estimate is invalid.
- let mut ref_valid = [1u8; 10];
- ref_valid[..9].fill(0);
- let mut out = OffsetResult {
- offset_samples: 0,
- confidence: 0.0,
- valid: 0,
- };
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- reference.as_ptr(),
- 10,
- candidate.as_ptr(),
- 10,
- ref_valid.as_ptr(),
- std::ptr::null(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 0);
+ let mut ref_valid = vec![false; 10];
+ ref_valid[9] = true;
+ let out = estimate_envelope_offset_valid(
+ &reference,
+ &candidate,
+ &ref_valid,
+ &[],
+ 100,
+ 10,
+ );
+ assert!(!out.valid);
assert_eq!(out.confidence, 0.0);
// Fully-valid masks behave like the unmasked call.
- let mut valid = [1u8; 10];
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- reference.as_ptr(),
- 10,
- candidate.as_ptr(),
- 10,
- valid.as_ptr(),
- valid.as_ptr(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 1);
+ let valid = vec![true; 10];
+ let out = estimate_envelope_offset_valid(
+ &reference,
+ &candidate,
+ &valid,
+ &valid,
+ 100,
+ 10,
+ );
+ assert!(out.valid);
assert_eq!(out.offset_samples, 200);
-
- // NULL arrays / non-positive lengths are invalid.
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- std::ptr::null(),
- 10,
- candidate.as_ptr(),
- 10,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
}
/// estimate_stretch_and_offset: a candidate sampled at 2x the reference
-/// rate reports rate ~2.0 (>1 = speed up) with a valid=1 result. A
+/// rate reports rate ~2.0 (>1 = speed up) with a valid=true result. A
/// non-linear (sine) reference is used — normalized correlation of linear
/// ramps is degenerate (any rate correlates 1.0), but only the true rate
/// resamples the sine back onto the reference exactly.
@@ -302,78 +166,43 @@ fn stretch_offset_recovers_rate() {
candidate.push((reference[k] + reference[k + 1]) / 2.0);
}
}
- let mut out = StretchOffsetResult {
- rate: 0.0,
- offset_samples: 0,
- confidence: 0.0,
- valid: 0,
- };
- let r = unsafe {
- oakaudio_sync_estimate_stretch_and_offset(
- reference.as_ptr(),
- 10,
- candidate.as_ptr(),
- candidate.len() as i32,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- 0.5,
- 3.0,
- 0.1,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 1);
+ let out = estimate_stretch_and_offset(
+ &reference,
+ &candidate,
+ &[],
+ &[],
+ 100,
+ 10,
+ 0.5,
+ 3.0,
+ 0.1,
+ );
+ assert!(out.valid);
assert!((out.rate - 2.0).abs() < 0.15, "rate = {}", out.rate);
assert!(out.confidence > 0.99, "confidence = {}", out.confidence);
- // Invalid rate parameters are rejected.
- let r = unsafe {
- oakaudio_sync_estimate_stretch_and_offset(
- reference.as_ptr(),
- 10,
- candidate.as_ptr(),
- candidate.len() as i32,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- 0.0,
- 3.0,
- 0.1,
- &mut out,
- )
- };
- assert_eq!(r, OAKAUDIO_E_INVALID);
+ // Invalid rate parameters are rejected (invalid result, defaults).
+ let out = estimate_stretch_and_offset(
+ &reference,
+ &candidate,
+ &[],
+ &[],
+ 100,
+ 10,
+ 0.0,
+ 3.0,
+ 0.1,
+ );
+ assert!(!out.valid);
}
/// estimate_* on identical silent envelopes yields low/no confidence and
-/// valid=0 (no correlation peak).
+/// valid=false (no correlation peak).
#[test]
fn silent_inputs_invalid() {
let silence = vec![0.0f64; 10];
- let mut out = OffsetResult {
- offset_samples: 0,
- confidence: 0.0,
- valid: 0,
- };
- let r = unsafe {
- oakaudio_sync_estimate_envelope_offset(
- silence.as_ptr(),
- 10,
- silence.as_ptr(),
- 10,
- std::ptr::null(),
- std::ptr::null(),
- 100,
- 10,
- &mut out,
- )
- };
- assert_eq!(r, 0);
- assert_eq!(out.valid, 0);
+ let out = estimate_envelope_offset(&silence, &silence, 100, 10);
+ assert!(!out.valid);
assert_eq!(out.confidence, 0.0);
}
diff --git a/crates/oakaudio/tests/waveform_test.rs b/crates/oakaudio/tests/waveform_test.rs
index 930c9a75c..ebfb10514 100644
--- a/crates/oakaudio/tests/waveform_test.rs
+++ b/crates/oakaudio/tests/waveform_test.rs
@@ -14,56 +14,29 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! AudioVisualWaveform contract tests (waveform.rs), through the C ABI.
+//! AudioVisualWaveform contract tests (waveform.rs), calling the public
+//! Rust API.
mod common;
use std::ffi::CString;
-use common::{pair, write_wav};
-use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE};
-use oakaudio::ffi::waveform::{
- oakaudio_waveform_extract, oakaudio_waveform_free, oakaudio_waveform_get_channel_count,
- oakaudio_waveform_get_summary, oakaudio_waveform_init, oakaudio_waveform_length,
- oakaudio_waveform_overwrite_samples, oakaudio_waveform_overwrite_silence,
- oakaudio_waveform_overwrite_sums, oakaudio_waveform_re_sum_s, oakaudio_waveform_resize,
- oakaudio_waveform_set_channel_count, oakaudio_waveform_sum_samples_s,
- oakaudio_waveform_trim_in, oakaudio_waveform_trim_range, MinMax,
-};
+use common::write_wav;
+use oakcore_rs::Rational;
+use oakaudio::waveform::{extract, AudioVisualWaveform, SamplePerChannel};
/// Fill `w` with 100 samples/channel of a 0..0.99 ramp at 100 Hz (1 s).
-fn fill_ramp(w: oakaudio::handle::CHandle) {
+fn fill_ramp(w: &mut AudioVisualWaveform) {
let ch0: Vec = (0..100).map(|i| i as f32 * 0.01).collect();
let ch1: Vec = (0..100).map(|i| -(i as f32) * 0.01).collect();
- let planes = [ch0.as_ptr(), ch1.as_ptr()];
- assert_eq!(unsafe { oakaudio_waveform_set_channel_count(w, 2) }, 0);
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 100, 100, 0, 1) },
- 0
- );
+ let planes = [ch0.as_slice(), ch1.as_slice()];
+ w.set_channel_count(2);
+ w.overwrite_samples(&planes, 100, Rational::new(0, 1));
}
-fn summary(
- w: oakaudio::handle::CHandle,
- start: (i64, i64),
- length: (i64, i64),
- cap: i32,
-) -> Vec {
- let mut out = vec![MinMax { min: 0.0, max: 0.0 }; cap as usize * 2];
- let n = unsafe {
- oakaudio_waveform_get_summary(
- w,
- start.0,
- start.1,
- length.0,
- length.1,
- out.as_mut_ptr(),
- cap,
- )
- };
- assert_eq!(n, cap);
- out.truncate(cap as usize * 2);
- out
+/// A one-point-per-channel summary over `start..start+length`.
+fn summary(w: &AudioVisualWaveform, start: Rational, length: Rational) -> Vec {
+ w.get_summary_from_time(start, length)
}
/// set_channel_count then overwrite_samples writes planar data at the given
@@ -71,110 +44,52 @@ fn summary(
/// channel-interleaved min/max pairs.
#[test]
fn overwrite_samples_and_length() {
- let mut w = unsafe { oakaudio_waveform_init() };
- fill_ramp(w);
+ let mut w = AudioVisualWaveform::new();
+ fill_ramp(&mut w);
- let (mut num, mut den) = (0i64, 0i64);
- assert_eq!(
- unsafe { oakaudio_waveform_length(w, &mut num, &mut den) },
- 0
- );
- assert_eq!(num, 1);
- assert_eq!(den, 1);
+ assert_eq!(w.length(), Rational::new(1, 1));
- let out = summary(w, (0, 1), (1, 1), 1);
+ let out = summary(&w, Rational::new(0, 1), Rational::new(1, 1));
+ assert_eq!(out.len(), 2);
assert_eq!(out[0].min, 0.0);
assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max);
assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min);
assert_eq!(out[1].max, 0.0);
- unsafe { oakaudio_waveform_free(&mut w) };
-}
-
-/// get_summary with out_pairs NULL returns the required point count without
-/// writing; a too-small capacity returns the same count and leaves the
-/// buffer untouched (two-stage contract).
-#[test]
-fn summary_two_stage_query() {
- let mut w = unsafe { oakaudio_waveform_init() };
- fill_ramp(w);
-
- // NULL out: required count only.
- let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, std::ptr::null_mut(), 0) };
- assert_eq!(n, 1);
-
- // Too-small capacity: same count, buffer untouched.
- let mut out = [MinMax {
- min: -1.0,
- max: -1.0,
- }; 2];
- let n = unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 1, out.as_mut_ptr(), 0) };
- assert_eq!(n, 1);
- assert_eq!(out[0].min, -1.0);
- assert_eq!(out[0].max, -1.0);
-
- // A zero/negative length is invalid.
- assert_eq!(
- unsafe { oakaudio_waveform_get_summary(w, 0, 1, 0, 1, std::ptr::null_mut(), 0) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_waveform_get_summary(w, 0, 1, 1, 0, std::ptr::null_mut(), 0) },
- OAKAUDIO_E_INVALID
- );
- unsafe { oakaudio_waveform_free(&mut w) };
}
/// overwrite_sums copies channel-interleaved pairs from another waveform
-/// into a dest range; a 0/1 length copies all of src.
+/// into a dest range; a null length copies all of src.
#[test]
fn overwrite_sums_range_copy() {
- let mut src = unsafe { oakaudio_waveform_init() };
- fill_ramp(src);
+ let mut src = AudioVisualWaveform::new();
+ fill_ramp(&mut src);
- let mut dst = unsafe { oakaudio_waveform_init() };
- assert_eq!(unsafe { oakaudio_waveform_set_channel_count(dst, 2) }, 0);
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_sums(dst, src, 0, 1, 0, 1, 0, 1) },
- 0
- );
- // A 0/1 length means "copy everything": dst matches src exactly.
- let out = summary(dst, (0, 1), (1, 1), 1);
+ let mut dst = AudioVisualWaveform::new();
+ dst.set_channel_count(2);
+ dst.overwrite_sums(&src, Rational::new(0, 1), Rational::new(0, 1), Rational::NULL);
+ // A null length means "copy everything": dst matches src exactly.
+ let out = summary(&dst, Rational::new(0, 1), Rational::new(1, 1));
+ assert_eq!(out.len(), 2);
assert_eq!(out[0].min, 0.0);
assert!((out[0].max - 0.99).abs() < 1e-5, "max = {}", out[0].max);
assert!((out[1].min + 0.99).abs() < 1e-5, "min = {}", out[1].min);
assert_eq!(out[1].max, 0.0);
-
- let (mut num, mut den) = (0i64, 0i64);
- unsafe { oakaudio_waveform_length(dst, &mut num, &mut den) };
- assert_eq!(num, 1);
-
- // Empty src handle is invalid.
- let empty = oakaudio::handle::CHandle::null();
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_sums(dst, empty, 0, 1, 0, 1, 0, 1) },
- OAKAUDIO_E_INVALID
- );
-
- unsafe { oakaudio_waveform_free(&mut dst) };
- unsafe { oakaudio_waveform_free(&mut src) };
+ assert_eq!(dst.length(), Rational::new(1, 1));
}
/// overwrite_silence zeroes min/max over a range without changing length.
#[test]
fn overwrite_silence() {
- let mut w = unsafe { oakaudio_waveform_init() };
- fill_ramp(w);
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_silence(w, 0, 1, 1, 2) },
- 0
- );
+ let mut w = AudioVisualWaveform::new();
+ fill_ramp(&mut w);
+ w.overwrite_silence(Rational::new(0, 1), Rational::new(1, 2));
// First half is silenced; second half retains the ramp data.
- let out = summary(w, (0, 1), (1, 2), 1);
- assert_eq!(pair(out[0].min, out[0].max), pair(0.0, 0.0));
- assert_eq!(pair(out[1].min, out[1].max), pair(0.0, 0.0));
+ let out = summary(&w, Rational::new(0, 1), Rational::new(1, 2));
+ assert_eq!((out[0].min, out[0].max), (0.0, 0.0));
+ assert_eq!((out[1].min, out[1].max), (0.0, 0.0));
- let out = summary(w, (1, 2), (1, 2), 1);
+ let out = summary(&w, Rational::new(1, 2), Rational::new(1, 2));
assert!(
out[0].max > 0.5,
"second half must keep ramp data, got {:?}",
@@ -182,94 +97,61 @@ fn overwrite_silence() {
);
assert!(out[1].min < -0.5);
- let (mut num, mut den) = (0i64, 0i64);
- unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
assert_eq!(
- (num, den),
- (1, 1),
+ w.length(),
+ Rational::new(1, 1),
"overwrite_silence must not change length"
);
- unsafe { oakaudio_waveform_free(&mut w) };
}
/// trim_in/trim_range/resize adjust length and drop or pad data; a negative
/// trim_in prepends silence (C++ semantics).
#[test]
fn trim_and_resize() {
- let mut w = unsafe { oakaudio_waveform_init() };
- fill_ramp(w);
+ let mut w = AudioVisualWaveform::new();
+ fill_ramp(&mut w);
// Negative trim_in prepends silence: absolute end (length) unchanged.
- assert_eq!(unsafe { oakaudio_waveform_trim_in(w, -1, 2) }, 0);
- let (mut num, mut den) = (0i64, 0i64);
- unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
- assert_eq!((num, den), (1, 1));
+ w.trim_in(Rational::new(-1, 2));
+ assert_eq!(w.length(), Rational::new(1, 1));
// Resize extends to 2 s.
- assert_eq!(unsafe { oakaudio_waveform_resize(w, 2, 1) }, 0);
- unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
- assert_eq!((num, den), (2, 1));
+ w.resize(Rational::new(2, 1));
+ assert_eq!(w.length(), Rational::new(2, 1));
// trim_range keeps 0.5 s from the (prepended) start.
- assert_eq!(unsafe { oakaudio_waveform_trim_range(w, 0, 1, 1, 2) }, 0);
- unsafe { oakaudio_waveform_length(w, &mut num, &mut den) };
- assert_eq!((num, den), (1, 2));
-
- // A negative resize target or a zero denominator is invalid.
- assert_eq!(
- unsafe { oakaudio_waveform_resize(w, -1, 2) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_waveform_resize(w, 1, 0) },
- OAKAUDIO_E_INVALID
- );
- unsafe { oakaudio_waveform_free(&mut w) };
+ w.trim_range(Rational::new(0, 1), Rational::new(1, 2));
+ assert_eq!(w.length(), Rational::new(1, 2));
}
-/// sum_samples_s reduces planar samples into one min/max pair per channel;
-/// re_sum_s merges channel-interleaved entries into one pair per channel.
-/// Both match golden vectors from the C++ implementation.
+/// sum_samples reduces planar samples into one min/max pair per channel;
+/// re_sum_samples merges channel-interleaved entries into one pair per
+/// channel. Both match golden vectors from the C++ implementation.
#[test]
fn sum_and_resum_golden() {
let ch0 = [1.0f32, -2.0, 3.0];
let ch1 = [4.0f32, -5.0, 6.0];
- let planes = [ch0.as_ptr(), ch1.as_ptr()];
- let mut out = [MinMax { min: 0.0, max: 0.0 }; 2];
- let r = unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 3, out.as_mut_ptr()) };
- assert_eq!(r, 0);
- assert_eq!(pair(out[0].min, out[0].max), pair(-2.0, 3.0));
- assert_eq!(pair(out[1].min, out[1].max), pair(-5.0, 6.0));
+ let planes = [ch0.as_slice(), ch1.as_slice()];
+ let out = AudioVisualWaveform::sum_samples(&planes, 0, 3);
+ assert_eq!((out[0].min, out[0].max), (-2.0, 3.0));
+ assert_eq!((out[1].min, out[1].max), (-5.0, 6.0));
- // re_sum_s over 4 interleaved entries, 2 channels -> one pair per
+ // re_sum_samples over 4 interleaved entries, 2 channels -> one pair per
// channel merging both points.
let input = [
- MinMax { min: 1.0, max: 2.0 },
- MinMax { min: 3.0, max: 4.0 },
- MinMax { min: 5.0, max: 6.0 },
- MinMax { min: 7.0, max: 8.0 },
+ SamplePerChannel { min: 1.0, max: 2.0 },
+ SamplePerChannel { min: 3.0, max: 4.0 },
+ SamplePerChannel { min: 5.0, max: 6.0 },
+ SamplePerChannel { min: 7.0, max: 8.0 },
];
- let mut out = [MinMax { min: 0.0, max: 0.0 }; 2];
- let r = unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 4, 2, out.as_mut_ptr()) };
- assert_eq!(r, 0);
- assert_eq!(pair(out[0].min, out[0].max), pair(1.0, 6.0));
- assert_eq!(pair(out[1].min, out[1].max), pair(3.0, 8.0));
-
- // Invalid arguments.
- assert_eq!(
- unsafe { oakaudio_waveform_sum_samples_s(planes.as_ptr(), 2, 0, 0, out.as_mut_ptr()) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_waveform_re_sum_s(input.as_ptr(), 0, 2, out.as_mut_ptr()) },
- OAKAUDIO_E_INVALID
- );
+ let out = AudioVisualWaveform::re_sum_samples(&input, 4, 2);
+ assert_eq!((out[0].min, out[0].max), (1.0, 6.0));
+ assert_eq!((out[1].min, out[1].max), (3.0, 8.0));
}
-/// extract probes through the oakcodec decoder C ABI; a missing file
-/// returns OAKAUDIO_E_NOT_FOUND. The valid-file path decodes the fixture
-/// WAV with oakcodec's in-process FFmpeg decoder and reduces it to min/max
-/// points.
+/// extract probes through oakcodec's decoder registry; a missing file
+/// returns NotFound. The valid-file path decodes the fixture WAV with
+/// oakcodec's in-process FFmpeg decoder and reduces it to min/max points.
#[test]
fn extract_file_and_notfound() {
let path = std::env::temp_dir().join(format!("oakaudio_extract_{}.wav", std::process::id()));
@@ -281,55 +163,19 @@ fn extract_file_and_notfound() {
}
write_wav(&path, 2, 48000, &samples).unwrap();
- // Missing file -> NOT_FOUND.
- let missing = CString::new(
- std::env::temp_dir()
- .join(format!("oakaudio_missing_{}.wav", std::process::id()))
- .to_str()
- .unwrap(),
- )
- .unwrap();
- let _ = std::fs::remove_file(std::path::Path::new(missing.to_str().unwrap()));
- let r = unsafe {
- oakaudio_waveform_extract(
- missing.as_ptr(),
- 0,
- 4,
- std::ptr::null_mut(),
- 0,
- std::ptr::null_mut(),
- )
- };
- assert_eq!(r, -60004);
+ // Missing file -> NotFound.
+ let missing = std::env::temp_dir().join(format!("oakaudio_missing_{}.wav", std::process::id()));
+ let _ = std::fs::remove_file(&missing);
+ let missing_c = CString::new(missing.to_str().unwrap()).unwrap();
+ assert!(extract(&missing_c, 0, 4).is_err());
// Real decode: 8 frames at 4 samples/point -> 2 points, 2 channels.
let c_path = CString::new(path.to_str().unwrap()).unwrap();
- let mut channel_count = 0i32;
- let n = unsafe {
- oakaudio_waveform_extract(
- c_path.as_ptr(),
- 0,
- 4,
- std::ptr::null_mut(),
- 0,
- &mut channel_count,
- )
- };
- assert_eq!(channel_count, 2);
- assert_eq!(n, 2, "8 frames at 4 samples/point must yield 2 points");
+ let outcome = extract(&c_path, 0, 4).unwrap();
+ assert_eq!(outcome.channels, 2);
+ assert_eq!(outcome.points.len(), 4, "2 points x 2 channels");
+ let out = &outcome.points;
- let mut out = vec![MinMax { min: 0.0, max: 0.0 }; 4];
- let n = unsafe {
- oakaudio_waveform_extract(
- c_path.as_ptr(),
- 0,
- 4,
- out.as_mut_ptr(),
- 2,
- &mut channel_count,
- )
- };
- assert_eq!(n, 2);
// s16 -> f32 is /32768; the ramp is exact in both formats.
let eps = 1e-6;
// Point 0 covers frames 0..4: left 0..3000, right 0..-3000.
@@ -346,60 +192,18 @@ fn extract_file_and_notfound() {
std::fs::remove_file(&path).ok();
}
-/// FFI validation and empty-handle error paths.
+/// Overwriting with channel count 0 is a logged no-op (the C ABI's state
+/// error was FFI-level validation; the Rust API keeps the C++ semantics of
+/// returning without touching the data).
#[test]
-fn ffi_error_paths() {
- let empty = oakaudio::handle::CHandle::null();
- assert_eq!(
- unsafe { oakaudio_waveform_get_channel_count(empty) },
- OAKAUDIO_E_INVALID
- );
- assert_eq!(
- unsafe { oakaudio_waveform_set_channel_count(empty, 2) },
- OAKAUDIO_E_INVALID
- );
- let (mut num, mut den) = (0i64, 0i64);
- assert_eq!(
- unsafe { oakaudio_waveform_length(empty, &mut num, &mut den) },
- OAKAUDIO_E_INVALID
- );
-
- // Negative channel count is invalid.
- let mut w = unsafe { oakaudio_waveform_init() };
- assert_eq!(
- unsafe { oakaudio_waveform_set_channel_count(w, -1) },
- OAKAUDIO_E_INVALID
- );
-
- // overwrite_samples before set_channel_count is a state error.
+fn no_channel_count_is_noop() {
+ let mut w = AudioVisualWaveform::new();
let data = [0.5f32; 8];
- let planes = [data.as_ptr()];
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 1) },
- OAKAUDIO_E_STATE
- );
- // A zero denominator is rejected.
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 8, 48000, 0, 0) },
- OAKAUDIO_E_INVALID
- );
- // A non-positive frame count is invalid.
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, planes.as_ptr(), 0, 48000, 0, 1) },
- OAKAUDIO_E_INVALID
- );
- // NULL planes are invalid.
- assert_eq!(
- unsafe { oakaudio_waveform_overwrite_samples(w, std::ptr::null(), 8, 48000, 0, 1) },
- OAKAUDIO_E_INVALID
- );
- unsafe { oakaudio_waveform_free(&mut w) };
-}
+ let planes = [data.as_slice()];
+ w.overwrite_samples(&planes, 48000, Rational::new(0, 1));
+ assert!(w.length().is_null());
+ assert_eq!(w.channel_count(), 0);
-/// free(NULL)/free(empty) are no-ops.
-#[test]
-fn free_null_noop() {
- let mut w = oakaudio::handle::CHandle::null();
- unsafe { oakaudio_waveform_free(&mut w) };
- unsafe { oakaudio_waveform_free(std::ptr::null_mut()) };
+ w.set_channel_count(2);
+ assert_eq!(w.channel_count(), 2);
}
diff --git a/crates/oakcodec/Cargo.toml b/crates/oakcodec/Cargo.toml
index 9f1488d00..8fb5eb5ff 100644
--- a/crates/oakcodec/Cargo.toml
+++ b/crates/oakcodec/Cargo.toml
@@ -8,13 +8,8 @@ license = "GPL-3.0-or-later"
[lib]
crate-type = ["staticlib", "rlib"]
-[features]
-# Compile the oakcore_*/oakrender_* host-mocks in src/bridge/test_stubs.rs so
-# consumer test binaries that link this crate (e.g. oaknode's) can resolve
-# those cross-crate C-ABI symbols without the host dylibs.
-test-stubs = []
-
[dependencies]
+oakcommon = { path = "../oakcommon" }
oakffmpeg-link = { path = "../oakffmpeg-link" }
oakcore-rs = { path = "../oakcore" }
# Real media decode/encode. The C++ ffmpeg_bridge library existed only to
diff --git a/crates/oakcodec/src/audioparams.rs b/crates/oakcodec/src/audioparams.rs
new file mode 100644
index 000000000..f368f2bdf
--- /dev/null
+++ b/crates/oakcodec/src/audioparams.rs
@@ -0,0 +1,60 @@
+// Oak Video Editor - Non-Linear Video Editor
+// Copyright (C) 2026 Oak Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+//! `olive::AudioParams` — the audio stream parameter value type.
+//!
+//! Single-lib unification (see `docs/zh/plans/riir/single-lib.md`): the
+//! codec crate used to create audio parameter sets through the oakcore C
+//! ABI (`oakcore_audioparams_*`, host-provided symbols) and store them
+//! behind refcounted handles. The parameters are plain values now — the
+//! FFmpeg probe fills them in directly and the footage description stores
+//! them by value.
+
+/// `olive::AudioParams` — the audio stream description recorded at probe
+/// time.
+///
+/// Mirrors `core/include/olive/core/oakcore/audioparams.h`: sample rate,
+/// ffmpeg channel-layout mask, sample format, stream index, duration and
+/// time base.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct AudioParams {
+ /// Sample rate in Hz.
+ pub sample_rate: i32,
+ /// ffmpeg-style channel layout mask (e.g. 0x3 = stereo).
+ pub channel_layout: u64,
+ /// `olive::core::SampleFormat::Format` value.
+ pub format: i32,
+ /// Stream index within the source container.
+ pub stream_index: i32,
+ /// Stream length in time-base units.
+ pub duration: i64,
+ /// Time base (num/den seconds per tick).
+ pub time_base: (i32, i32),
+}
+
+impl AudioParams {
+ /// Channel count derived from the layout mask, mirroring the C++
+ /// `AudioParams::channel_count()`.
+ pub fn channel_count(&self) -> i32 {
+ self.channel_layout.count_ones() as i32
+ }
+
+ /// Whether the parameter set describes a usable stream, mirroring the
+ /// C++ `AudioParams::is_valid()` (positive rate and a non-zero layout).
+ pub fn is_valid(&self) -> bool {
+ self.sample_rate > 0 && self.channel_layout != 0
+ }
+}
diff --git a/crates/oakcodec/src/bridge/common.rs b/crates/oakcodec/src/bridge/common.rs
deleted file mode 100644
index c3a547764..000000000
--- a/crates/oakcodec/src/bridge/common.rs
+++ /dev/null
@@ -1,686 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,
-//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,
-//! colortransform).
-//!
-//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,
-//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,
-//! release, abi_version}` layout from `include/common/handle.h`, so the
-//! codec module can store them by value and pass them straight across
-//! the FFI boundary.
-//!
-//! The oakcore audio parameters use a pointer-based C ABI instead of the
-//! by-value handle convention: `oakcore_audioparams_*` take and return
-//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/
-//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw
-//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`
-//! returns a newly allocated rational the caller must release with
-//! `oakcore_rational_free`.
-//!
-//! # ABI discipline (single-lib era)
-//!
-//! The frozen `oakcommon` C ABI (see `crates/oakengine/include/common/`)
-//! uses **out-pointer getters** and **instance handles** for the module
-//! families. Earlier codec-era bridges declared value-style signatures
-//! (C++-era headers); those declarations were wrong against the frozen
-//! contract and are fixed here. To keep the crate's call sites
-//! unchanged, each true ABI symbol is declared under `#[link_name]` with
-//! its real signature and wrapped in an adapter of the old value-style
-//! shape (an M12 P0 decode-path fix: the mismatch made every decoded
-//! frame invalid).
-
-use std::ffi::{c_char, c_int, c_void};
-
-use crate::handle::CHandle;
-
-/// `OakVideoParams` — refcounted video-parameter handle.
-pub type OakVideoParams = CHandle;
-
-/// `OakAudioParams` — refcounted audio-parameter handle.
-pub type OakAudioParams = CHandle;
-
-/// `OakSubtitleParams` — refcounted subtitle-parameter handle.
-pub type OakSubtitleParams = CHandle;
-
-/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec
-/// only stores and forwards it).
-pub type OakNodeBlock = CHandle;
-
-/// Module instance handles (filefunctions / oiioutils take `self_`).
-pub type OakFileFunctions = CHandle;
-pub type OakOIIOUtils = CHandle;
-
-// The handle structs are opaque refcounted handles pointing into a C
-// library; the boxed objects are independently synchronized there, so
-// moving a handle between threads is sound.
-
-extern "C" {
- /// `oakcommon_videoparams_init`.
- pub fn oakcommon_videoparams_init() -> OakVideoParams;
- /// `oakcommon_videoparams_init_basic`.
- #[link_name = "oakcommon_videoparams_init_basic"]
- fn videoparams_init_basic_abi(
- width: c_int,
- height: c_int,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
- ) -> OakVideoParams;
- /// `oakcommon_videoparams_init_with_time_base`.
- #[link_name = "oakcommon_videoparams_init_with_time_base"]
- fn videoparams_init_with_time_base_abi(
- width: c_int,
- height: c_int,
- time_base_num: c_int,
- time_base_den: c_int,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
- ) -> OakVideoParams;
- /// `oakcommon_videoparams_free` (NULL/empty no-op).
- pub fn oakcommon_videoparams_free(params: *mut OakVideoParams);
- /// `oakcommon_videoparams_get_width`.
- #[link_name = "oakcommon_videoparams_get_width"]
- fn videoparams_get_width_abi(params: OakVideoParams, width: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_height`.
- #[link_name = "oakcommon_videoparams_get_height"]
- fn videoparams_get_height_abi(params: OakVideoParams, height: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_format`.
- #[link_name = "oakcommon_videoparams_get_format"]
- fn videoparams_get_format_abi(params: OakVideoParams, format: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_time_base` (num/den out).
- #[link_name = "oakcommon_videoparams_get_time_base"]
- fn videoparams_get_time_base_abi(
- params: OakVideoParams,
- numerator: *mut c_int,
- denominator: *mut c_int,
- ) -> c_int;
- /// `oakcommon_videoparams_set_width`.
- pub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);
- /// `oakcommon_videoparams_set_height`.
- pub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);
- /// `oakcommon_videoparams_set_format`.
- pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);
- /// `oakcommon_videoparams_get_is_valid`.
- #[link_name = "oakcommon_videoparams_get_is_valid"]
- fn videoparams_get_is_valid_abi(params: OakVideoParams, is_valid: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_equals`.
- #[link_name = "oakcommon_videoparams_equals"]
- fn videoparams_equals_abi(
- params: OakVideoParams,
- other: OakVideoParams,
- equal: *mut c_int,
- ) -> c_int;
- /// `oakcommon_videoparams_set_time_base`.
- pub fn oakcommon_videoparams_set_time_base(params: OakVideoParams, num: i64, den: i64);
- /// `oakcommon_videoparams_set_frame_rate`.
- pub fn oakcommon_videoparams_set_frame_rate(params: OakVideoParams, num: i64, den: i64);
- /// `oakcommon_videoparams_set_pixel_aspect_ratio`.
- pub fn oakcommon_videoparams_set_pixel_aspect_ratio(params: OakVideoParams, num: i64, den: i64);
- /// `oakcommon_videoparams_set_interlacing`.
- pub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);
- /// `oakcommon_videoparams_set_duration`.
- pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);
- /// `oakcommon_videoparams_set_start_time`.
- pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);
- /// `oakcommon_videoparams_set_color_range`.
- pub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);
- /// `oakcommon_videoparams_set_video_type`.
- pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);
- /// `oakcommon_videoparams_set_channel_count`.
- pub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);
- /// `oakcommon_videoparams_set_color_primaries`.
- pub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);
- /// `oakcommon_videoparams_set_color_transfer`.
- pub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);
- /// `oakcommon_videoparams_set_premultiplied_alpha`.
- pub fn oakcommon_videoparams_set_premultiplied_alpha(
- params: OakVideoParams,
- premultiplied: c_int,
- );
- /// `oakcommon_videoparams_set_enabled`.
- pub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);
- /// `oakcommon_videoparams_static_get_bytes_per_pixel`.
- #[link_name = "oakcommon_videoparams_static_get_bytes_per_pixel"]
- fn videoparams_static_get_bytes_per_pixel_abi(format: c_int, channels: c_int) -> c_int;
- /// `oakcommon_videoparams_frame_rate_as_time_base`.
- pub fn oakcommon_videoparams_frame_rate_as_time_base(
- frame_rate_num: i64,
- frame_rate_den: i64,
- out_num: *mut i64,
- out_den: *mut i64,
- );
- /// `oakcommon_videoparams_get_stream_index`.
- #[link_name = "oakcommon_videoparams_get_stream_index"]
- fn videoparams_get_stream_index_abi(params: OakVideoParams, index: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_set_stream_index`.
- pub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);
- /// `oakcommon_videoparams_get_divider`.
- #[link_name = "oakcommon_videoparams_get_divider"]
- fn videoparams_get_divider_abi(params: OakVideoParams, divider: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_set_divider`.
- pub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);
- /// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).
- #[link_name = "oakcommon_videoparams_get_frame_rate"]
- fn videoparams_get_frame_rate_abi(
- params: OakVideoParams,
- numerator: *mut c_int,
- denominator: *mut c_int,
- ) -> c_int;
- /// `oakcommon_videoparams_get_duration` (time-base units).
- #[link_name = "oakcommon_videoparams_get_duration"]
- fn videoparams_get_duration_abi(params: OakVideoParams, duration: *mut i64) -> c_int;
- /// `oakcommon_videoparams_get_channel_count`.
- #[link_name = "oakcommon_videoparams_get_channel_count"]
- fn videoparams_get_channel_count_abi(params: OakVideoParams, count: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_color_primaries`.
- #[link_name = "oakcommon_videoparams_get_color_primaries"]
- fn videoparams_get_color_primaries_abi(params: OakVideoParams, primaries: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_color_transfer`.
- #[link_name = "oakcommon_videoparams_get_color_transfer"]
- fn videoparams_get_color_transfer_abi(params: OakVideoParams, transfer: *mut c_int) -> c_int;
- /// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).
- #[link_name = "oakcommon_videoparams_get_interlacing"]
- fn videoparams_get_interlacing_abi(params: OakVideoParams, interlacing: *mut c_int) -> c_int;
- /// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).
- pub fn oakcore_audioparams_create(
- sample_rate: c_int,
- channel_layout: u64,
- format: c_int,
- ) -> *mut OakAudioParams;
- /// `oakcore_audioparams_free` (NULL no-op).
- pub fn oakcore_audioparams_free(params: *mut OakAudioParams);
- /// `oakcore_audioparams_sample_rate`.
- pub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int;
- /// `oakcore_audioparams_set_sample_rate`.
- pub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);
- /// `oakcore_audioparams_channel_layout`.
- pub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64;
- /// `oakcore_audioparams_set_channel_layout`.
- pub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);
- /// `oakcore_audioparams_set_time_base`.
- pub fn oakcore_audioparams_set_time_base(params: *mut OakAudioParams, num: c_int, den: c_int);
- /// `oakcore_audioparams_set_format`.
- pub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);
- /// `oakcore_audioparams_set_stream_index`.
- pub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);
- /// `oakcore_audioparams_set_duration`.
- pub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);
- /// `oakcore_audioparams_channel_count`.
- pub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int;
- /// `oakcore_audioparams_format`.
- pub fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int;
- /// `oakcore_audioparams_stream_index`.
- pub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int;
- /// `oakcore_audioparams_duration`.
- pub fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64;
- /// `oakcore_audioparams_is_valid`.
- pub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int;
- /// `oakcore_audioparams_time_base` (newly allocated rational; caller
- /// releases with `oakcore_rational_free`).
- pub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void;
- /// `oakcore_rational_numerator`.
- pub fn oakcore_rational_numerator(rational: *const c_void) -> c_int;
- /// `oakcore_rational_denominator`.
- pub fn oakcore_rational_denominator(rational: *const c_void) -> c_int;
- /// `oakcore_rational_free` (NULL no-op).
- pub fn oakcore_rational_free(rational: *mut c_void);
- /// `oakcommon_subtitleparams_get_stream_index`.
- #[link_name = "oakcommon_subtitleparams_get_stream_index"]
- fn subtitleparams_get_stream_index_abi(
- params: OakSubtitleParams,
- index: *mut c_int,
- ) -> c_int;
- /// `oakcommon_subtitleparams_generate_ass_header`.
- pub fn oakcommon_subtitleparams_generate_ass_header(
- params: OakSubtitleParams,
- width: c_int,
- height: c_int,
- );
- /// `oakcommon_subtitleparams_add_subtitle`.
- pub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);
- /// `oakcommon_config_get_int`.
- pub fn oakcommon_config_get_int(
- group: *const c_char,
- key: *const c_char,
- fallback: c_int,
- ) -> c_int;
- /// `oakcommon_config_get_bool`.
- pub fn oakcommon_config_get_bool(
- group: *const c_char,
- key: *const c_char,
- fallback: c_int,
- ) -> c_int;
- /// `oakcommon_config_get` (two-stage string access).
- pub fn oakcommon_config_get(
- group: *const c_char,
- key: *const c_char,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakcommon_filefunctions_init`.
- pub fn oakcommon_filefunctions_init() -> OakFileFunctions;
- /// `oakcommon_filefunctions_free`.
- pub fn oakcommon_filefunctions_free(self_: *mut OakFileFunctions);
- /// `oakcommon_filefunctions_get_configuration_location` (two-stage).
- #[link_name = "oakcommon_filefunctions_get_configuration_location"]
- fn filefunctions_get_configuration_location_abi(
- self_: OakFileFunctions,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakcommon_filefunctions_get_unique_file_identifier`.
- #[link_name = "oakcommon_filefunctions_get_unique_file_identifier"]
- fn filefunctions_get_unique_file_identifier_abi(
- self_: OakFileFunctions,
- filename: *const c_char,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakcommon_filefunctions_get_application_path` (two-stage).
- #[link_name = "oakcommon_filefunctions_get_application_path"]
- fn filefunctions_get_application_path_abi(
- self_: OakFileFunctions,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakcommon_colortransform_init_output`.
- pub fn oakcommon_colortransform_init_output(output: *const c_char) -> OakVideoParams;
- /// `oakcommon_colortransform_get_output` (two-stage string).
- pub fn oakcommon_colortransform_get_output(
- transform: OakVideoParams,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakcommon_colortransform_free`.
- pub fn oakcommon_colortransform_free(params: *mut OakVideoParams);
- /// `oakcommon_ffmpegutils_get_native_sample_format`.
- #[link_name = "oakcommon_ffmpegutils_get_native_sample_format"]
- fn ffmpegutils_get_native_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
- /// `oakcommon_ffmpegutils_get_compatible_pixel_format`.
- #[link_name = "oakcommon_ffmpegutils_get_compatible_pixel_format"]
- fn ffmpegutils_get_compatible_pixel_format_abi(pix_fmt: c_int, out: *mut c_int) -> c_int;
- /// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.
- #[link_name = "oakcommon_ffmpegutils_get_ffmpeg_pixel_format"]
- fn ffmpegutils_get_ffmpeg_pixel_format_abi(
- pix_fmt: c_int,
- channel_count: c_int,
- out: *mut c_int,
- ) -> c_int;
- /// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
- #[link_name = "oakcommon_ffmpegutils_get_ffmpeg_sample_format"]
- fn ffmpegutils_get_ffmpeg_sample_format_abi(smp_fmt: c_int, out: *mut c_int) -> c_int;
- /// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.
- #[link_name = "oakcommon_ffmpegutils_get_compatible_bridge_pixel_format"]
- fn ffmpegutils_get_compatible_bridge_pixel_format_abi(
- pix_fmt: c_int,
- maximum_pix_fmt: c_int,
- out: *mut c_int,
- ) -> c_int;
- /// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
- #[link_name = "oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space"]
- fn ffmpegutils_convert_jpeg_space_to_regular_space_abi(
- pix_fmt: c_int,
- out: *mut c_int,
- ) -> c_int;
- /// `oakcommon_oiioutils_init`.
- pub fn oakcommon_oiioutils_init() -> OakOIIOUtils;
- /// `oakcommon_oiioutils_free`.
- pub fn oakcommon_oiioutils_free(self_: *mut OakOIIOUtils);
- /// `oakcommon_oiioutils_get_oiio_base_type_from_format`.
- #[link_name = "oakcommon_oiioutils_get_oiio_base_type_from_format"]
- fn oiioutils_get_oiio_base_type_from_format_abi(
- self_: OakOIIOUtils,
- pixel_format: c_int,
- out_base_type: *mut c_int,
- ) -> c_int;
- /// `oakcommon_oiioutils_get_format_from_oiio_basetype`.
- #[link_name = "oakcommon_oiioutils_get_format_from_oiio_basetype"]
- fn oiioutils_get_format_from_oiio_basetype_abi(
- self_: OakOIIOUtils,
- base_type: c_int,
- out_pixel_format: *mut c_int,
- ) -> c_int;
- /// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).
- #[link_name = "oakcommon_oiioutils_get_pixel_aspect_ratio"]
- fn oiioutils_get_pixel_aspect_ratio_abi(
- self_: OakOIIOUtils,
- pixel_aspect_ratio: f64,
- out_numerator: *mut c_int,
- out_denominator: *mut c_int,
- ) -> c_int;
-}
-
-// ---------------------------------------------------------------------------
-// Value-style adapters over the frozen out-pointer ABI (old call shapes)
-// ---------------------------------------------------------------------------
-
-/// `oakcommon_videoparams_init_basic` — value-style shape kept for call
-/// sites; defaults: U8 format, 4 channels, 1:1 aspect, progressive,
-/// divider 1.
-pub fn oakcommon_videoparams_init_basic(
- width: c_int,
- height: c_int,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
-) -> OakVideoParams {
- unsafe { videoparams_init_basic_abi(width, height, pixel_format, nb_channels, pixel_aspect_num, pixel_aspect_den, interlacing, divider) }
-}
-
-/// `oakcommon_videoparams_init_with_time_base` — value-style shape kept
-/// for call sites.
-pub fn oakcommon_videoparams_init_with_time_base(
- width: c_int,
- height: c_int,
- time_base_num: c_int,
- time_base_den: c_int,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
-) -> OakVideoParams {
- unsafe {
- videoparams_init_with_time_base_abi(
- width,
- height,
- time_base_num,
- time_base_den,
- pixel_format,
- nb_channels,
- pixel_aspect_num,
- pixel_aspect_den,
- interlacing,
- divider,
- )
- }
-}
-
-/// Value-style `oakcommon_videoparams_get_width`.
-pub fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int {
- let mut w: c_int = 0;
- unsafe { videoparams_get_width_abi(params, &mut w) };
- w
-}
-
-/// Value-style `oakcommon_videoparams_get_height`.
-pub fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int {
- let mut h: c_int = 0;
- unsafe { videoparams_get_height_abi(params, &mut h) };
- h
-}
-
-/// Value-style `oakcommon_videoparams_get_format`.
-pub fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int {
- let mut f: c_int = -1;
- unsafe { videoparams_get_format_abi(params, &mut f) };
- f
-}
-
-/// Value-style `oakcommon_videoparams_get_is_valid`.
-pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int {
- let mut v: c_int = 0;
- unsafe { videoparams_get_is_valid_abi(params, &mut v) };
- v
-}
-
-/// Value-style `oakcommon_videoparams_equals` (1 when equal).
-pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int {
- let mut eq: c_int = 0;
- unsafe { videoparams_equals_abi(a, b, &mut eq) };
- eq
-}
-
-/// Value-style `oakcommon_videoparams_get_stream_index`.
-pub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int {
- let mut i: c_int = -1;
- unsafe { videoparams_get_stream_index_abi(params, &mut i) };
- i
-}
-
-/// Value-style `oakcommon_videoparams_get_divider`.
-pub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int {
- let mut d: c_int = 0;
- unsafe { videoparams_get_divider_abi(params, &mut d) };
- d
-}
-
-/// Value-style `oakcommon_videoparams_get_duration`.
-pub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64 {
- let mut d: i64 = 0;
- unsafe { videoparams_get_duration_abi(params, &mut d) };
- d
-}
-
-/// Value-style `oakcommon_videoparams_get_channel_count`.
-pub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int {
- let mut c: c_int = 0;
- unsafe { videoparams_get_channel_count_abi(params, &mut c) };
- c
-}
-
-/// Value-style `oakcommon_videoparams_get_color_primaries`.
-pub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int {
- let mut p: c_int = 0;
- unsafe { videoparams_get_color_primaries_abi(params, &mut p) };
- p
-}
-
-/// Value-style `oakcommon_videoparams_get_color_transfer`.
-pub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int {
- let mut t: c_int = 0;
- unsafe { videoparams_get_color_transfer_abi(params, &mut t) };
- t
-}
-
-/// Value-style `oakcommon_videoparams_get_interlacing`.
-pub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int {
- let mut i: c_int = 0;
- unsafe { videoparams_get_interlacing_abi(params, &mut i) };
- i
-}
-
-/// Value-style `oakcommon_videoparams_get_time_base` (num/den out as
-/// `i64`, widened from the frozen `i32` outs).
-pub fn oakcommon_videoparams_get_time_base(
- params: OakVideoParams,
- out_num: *mut i64,
- out_den: *mut i64,
-) -> c_int {
- let mut num: c_int = 0;
- let mut den: c_int = 0;
- let rc = unsafe { videoparams_get_time_base_abi(params, &mut num, &mut den) };
- if !out_num.is_null() {
- unsafe { *out_num = num as i64 };
- }
- if !out_den.is_null() {
- unsafe { *out_den = den as i64 };
- }
- rc
-}
-
-/// Value-style `oakcommon_videoparams_get_frame_rate` (num/den out).
-pub fn oakcommon_videoparams_get_frame_rate(
- params: OakVideoParams,
- out_num: *mut c_int,
- out_den: *mut c_int,
-) -> c_int {
- unsafe { videoparams_get_frame_rate_abi(params, out_num, out_den) }
-}
-
-/// Value-style `oakcommon_videoparams_static_get_bytes_per_pixel`
-/// (4 channels assumed; the frozen ABI takes channels explicitly).
-pub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
- unsafe { videoparams_static_get_bytes_per_pixel_abi(format, 4) }
-}
-
-/// Value-style `oakcommon_subtitleparams_get_stream_index`.
-pub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -> c_int {
- let mut i: c_int = -1;
- unsafe { subtitleparams_get_stream_index_abi(params, &mut i) };
- i
-}
-
-/// Value-style `oakcommon_ffmpegutils_get_native_sample_format`.
-pub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_get_native_sample_format_abi(sample_format, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_ffmpegutils_get_compatible_pixel_format`.
-pub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_get_compatible_pixel_format_abi(format, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`
-/// (4 channels assumed).
-pub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_get_ffmpeg_pixel_format_abi(format, 4, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
-pub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_get_ffmpeg_sample_format_abi(format, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`
-/// (no maximum constraint).
-pub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_get_compatible_bridge_pixel_format_abi(format, -1, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
-pub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { ffmpegutils_convert_jpeg_space_to_regular_space_abi(format, &mut out) };
- out
-}
-
-/// The process-wide filefunctions instance (lazily created; freed on
-/// process exit is not required — the singleton outlives all users).
-fn filefunctions_instance() -> OakFileFunctions {
- static FF: std::sync::OnceLock = std::sync::OnceLock::new();
- *FF.get_or_init(|| unsafe { oakcommon_filefunctions_init() })
-}
-
-/// Value-style `oakcommon_filefunctions_get_application_path`
-/// (two-stage string getter on the shared instance).
-pub fn oakcommon_filefunctions_get_application_path(buf: *mut c_char, buf_size: c_int) -> c_int {
- unsafe { filefunctions_get_application_path_abi(filefunctions_instance(), buf, buf_size) }
-}
-
-/// Value-style `oakcommon_filefunctions_get_configuration_location`.
-pub fn oakcommon_filefunctions_get_configuration_location(
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- unsafe {
- filefunctions_get_configuration_location_abi(filefunctions_instance(), buf, buf_size)
- }
-}
-
-/// Value-style `oakcommon_filefunctions_get_unique_file_identifier`
-/// (the frozen ABI writes a string; the value-style call sites expect an
-/// `i64`, so the string is folded into a stable 64-bit FNV hash).
-pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64 {
- let mut buf = [0 as c_char; 512];
- // Two-stage contract: 0 = written, positive = required size, negative
- // = error. The adapter's value-style call sites get the i64 fold.
- let n = unsafe {
- filefunctions_get_unique_file_identifier_abi(
- filefunctions_instance(),
- path,
- buf.as_mut_ptr(),
- buf.len() as c_int,
- )
- };
- if n < 0 {
- return 0;
- }
- let avail = if n == 0 { buf.len() } else { (n as usize).min(buf.len()) };
- let bytes: Vec = buf[..avail]
- .iter()
- .take_while(|&&b| b != 0)
- .map(|&b| b as u8)
- .collect();
- let mut hash: u64 = 0xcbf29ce484222325;
- for &b in &bytes {
- hash ^= b as u64;
- hash = hash.wrapping_mul(0x100000001b3);
- }
- hash as i64
-}
-
-/// The process-wide oiioutils instance (lazily created).
-fn oiioutils_instance() -> OakOIIOUtils {
- static OU: std::sync::OnceLock = std::sync::OnceLock::new();
- *OU.get_or_init(|| unsafe { oakcommon_oiioutils_init() })
-}
-
-/// Value-style `oakcommon_oiioutils_get_oiio_base_type_from_format`.
-pub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { oiioutils_get_oiio_base_type_from_format_abi(oiioutils_instance(), format, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_oiioutils_get_format_from_oiio_basetype`.
-pub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int {
- let mut out: c_int = -1;
- unsafe { oiioutils_get_format_from_oiio_basetype_abi(oiioutils_instance(), basetype, &mut out) };
- out
-}
-
-/// Value-style `oakcommon_oiioutils_get_pixel_aspect_ratio`
-/// (the frozen ABI takes the aspect ratio as `f64`; the value-style call
-/// sites pass width/height and get num/den back).
-pub fn oakcommon_oiioutils_get_pixel_aspect_ratio(
- width: c_int,
- height: c_int,
- out_num: *mut c_int,
- out_den: *mut c_int,
-) -> c_int {
- let ratio = if height != 0 {
- f64::from(width) / f64::from(height)
- } else {
- 1.0
- };
- unsafe {
- oiioutils_get_pixel_aspect_ratio_abi(oiioutils_instance(), ratio, out_num, out_den)
- }
-}
diff --git a/crates/oakcodec/src/bridge/mod.rs b/crates/oakcodec/src/bridge/mod.rs
deleted file mode 100644
index c6b396fe5..000000000
--- a/crates/oakcodec/src/bridge/mod.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! C ABI imports from the other oak modules.
-//!
-//! The codec module links against oakcommon and oakrender at the C ABI.
-//! Every signature below mirrors the corresponding public header
-//! verbatim and is resolved at link time. The by-value handle structs
-//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of
-//! the `{ctx, addref, release, abi_version}` layout so the codec crate
-//! can hold and hand them across the FFI boundary without translation.
-
-pub mod common;
-pub mod render;
-
-// In-memory mocks for the oakcommon/oakrender C ABI so the crate links
-// and is testable under `cargo test` (where those dylibs are absent).
-// The `test-stubs` feature additionally compiles the oakcore_*/oakrender_*
-// host-mocks for consumer test binaries (e.g. oaknode's) that link this
-// crate directly — those symbols are not provided by any Rust crate.
-#[cfg(any(test, feature = "test-stubs"))]
-pub mod test_stubs;
diff --git a/crates/oakcodec/src/bridge/render.rs b/crates/oakcodec/src/bridge/render.rs
deleted file mode 100644
index 11cc28fd5..000000000
--- a/crates/oakcodec/src/bridge/render.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! oakrender C ABI imports (display textures, renderers, cancel atoms).
-//!
-//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a
-//! `CancelAtom`; both are oakrender refcounted handles with the standard
-//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`
-//! is a flattened POD the decoders construct to describe the frame.
-
-use std::ffi::{c_char, c_int, c_void};
-
-use crate::handle::CHandle;
-
-/// `OakRenderTexture` — refcounted GPU texture handle.
-pub type OakRenderTexture = CHandle;
-
-/// `OakCancelAtom` — refcounted cancellation atom handle.
-pub type OakCancelAtom = CHandle;
-
-/// `OakRenderRenderer` — refcounted display-renderer handle.
-pub type OakRenderRenderer = CHandle;
-
-/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.
-pub type OakCodecFrame = CHandle;
-
-// Refcounted opaque handles; thread-safe in the C library.
-
-/// `oakrender_video_params` — flattened POD of `olive::VideoParams`
-/// passed into oakrender; see `include/render/renderer.h`.
-#[repr(C)]
-pub struct oakrender_video_params {
- /// Width in pixels.
- pub width: c_int,
- /// Height in pixels.
- pub height: c_int,
- /// Frame-duration numerator (e.g. 1001/30000 s).
- pub time_base_num: c_int,
- /// Frame-duration denominator.
- pub time_base_den: c_int,
- /// `olive::PixelFormat::Format`.
- pub format: c_int,
- /// Pixel-aspect numerator.
- pub pixel_aspect_num: c_int,
- /// Pixel-aspect denominator.
- pub pixel_aspect_den: c_int,
- /// `olive::VideoParams::Interlacing`.
- pub interlacing: c_int,
- /// `olive::VideoParams::ColorRange`.
- pub color_range: c_int,
- /// Preview-resolution divider (1 = full).
- pub divider: c_int,
- /// `olive::VideoParams::Type` (0 = video).
- pub video_type: c_int,
- /// 0/1 premultiplied alpha.
- pub premultiplied_alpha: c_int,
-}
-
-extern "C" {
- /// `oakrender_cancelatom_init`.
- pub fn oakrender_cancelatom_init() -> OakCancelAtom;
- /// `oakrender_cancelatom_free` (NULL/empty no-op).
- pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);
- /// `oakrender_cancelatom_is_cancelled`.
- pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int;
- /// `oakrender_cancelatom_heard_cancel`.
- pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int;
- /// `oakrender_cancelatom_cancel`.
- pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);
- /// `oakrender_cancelatom_get_native`.
- pub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void;
- /// `oakrender_display_texture_create`.
- pub fn oakrender_display_texture_create(
- renderer: OakRenderRenderer,
- params: *const oakrender_video_params,
- data: *const c_void,
- linesize: c_int,
- ) -> OakRenderTexture;
- /// `oakrender_display_texture_retain`.
- pub fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture;
- /// `oakrender_display_texture_free` (NULL/empty no-op).
- pub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);
- /// `oakrender_display_texture_upload`.
- pub fn oakrender_display_texture_upload(texture: OakRenderTexture) -> c_int;
- /// `oakrender_display_texture_download`.
- pub fn oakrender_display_texture_download(
- texture: OakRenderTexture,
- pixels: *mut c_void,
- linesize: c_int,
- ) -> c_int;
- /// `oakrender_display_texture_get_params`.
- pub fn oakrender_display_texture_get_params(
- texture: OakRenderTexture,
- out: *mut oakrender_video_params,
- ) -> c_int;
- /// `oakrender_display_texture_id`.
- pub fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int;
- /// `oakrender_display_texture_is_dummy`.
- pub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -> c_int;
- /// `oakrender_display_texture_get_frame` (two-stage frame access).
- pub fn oakrender_display_texture_get_frame(
- texture: OakRenderTexture,
- buf: *mut c_char,
- buf_size: c_int,
- ) -> c_int;
- /// `oakrender_codec_frame_width`.
- pub fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int;
- /// `oakrender_codec_frame_height`.
- pub fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int;
- /// `oakrender_codec_frame_fb_format`.
- pub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -> c_int;
- /// `oakrender_codec_frame_free` (NULL/empty no-op).
- pub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);
- /// `oakrender_codec_frame_allocate`.
- pub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int;
- /// `oakrender_codec_frame_linesize_bytes`.
- pub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int;
- /// `oakrender_codec_frame_is_allocated`.
- pub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int;
- /// `oakrender_display_renderer_blit_color_managed`.
- pub fn oakrender_display_renderer_blit_color_managed(
- renderer: OakRenderRenderer,
- job: *const c_void,
- dst_texture: OakRenderTexture,
- params: *const oakrender_video_params,
- ) -> c_int;
-}
diff --git a/crates/oakcodec/src/bridge/test_stubs.rs b/crates/oakcodec/src/bridge/test_stubs.rs
deleted file mode 100644
index 9a1d14f87..000000000
--- a/crates/oakcodec/src/bridge/test_stubs.rs
+++ /dev/null
@@ -1,1265 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! `#[cfg(test)]` in-memory mocks for the oakcommon / oakrender C ABI.
-//!
-//! The real symbols live in `liboakcommon` / `liboakrender` and are only
-//! linked when the crate is built against those dylibs (ctest). Under
-//! `cargo test` those libraries are not linked, so `extern "C"` call
-//! sites in the crate would otherwise fail to resolve. These
-//! `#[no_mangle] extern "C"` definitions provide the symbols and back the
-//! two stateful pieces the crate actually reads (video params and cancel
-//! atoms) with real in-memory state, so `Frame`, `FootageDescription`
-//! and friends are meaningfully testable. Everything else returns a
-//! deterministic neutral value.
-//!
-//! Kept strictly under `#[cfg(test)]`; never compiled into a real build.
-
-#![allow(dead_code, unused_variables)]
-
-use std::collections::HashMap;
-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, OakFileFunctions, OakNodeBlock, OakOIIOUtils, OakSubtitleParams, OakVideoParams,
-};
-use crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};
-use crate::handle::OAKCODEC_ABI_VERSION;
-
-/// Per-`OakVideoParams` backing state.
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-struct MockParams {
- width: i32,
- height: i32,
- format: i32,
- time_base_num: i64,
- time_base_den: i64,
- stream_index: i32,
- divider: i32,
- frame_rate_num: i32,
- frame_rate_den: i32,
- duration: i64,
- channel_count: i32,
- color_primaries: i32,
- color_trc: i32,
- interlacing: i32,
- pixel_aspect_num: i32,
- pixel_aspect_den: i32,
- start_time: i64,
- color_range: i32,
- video_type: i32,
- premultiplied_alpha: i32,
- enabled: i32,
-}
-
-fn params_store() -> &'static Mutex> {
- static S: OnceLock>> = OnceLock::new();
- S.get_or_init(|| Mutex::new(HashMap::new()))
-}
-
-/// Build a handle whose `ctx` owns a leaked `MockParams`; the map entry
-/// keeps the storage alive until `free` is called.
-fn new_params_handle(p: MockParams) -> OakVideoParams {
- let raw = Box::into_raw(Box::new(p.clone()));
- params_store().lock().unwrap().insert(raw as usize, p);
- OakVideoParams {
- ctx: raw as *mut c_void,
- addref: None,
- release: None,
- abi_version: OAKCODEC_ABI_VERSION,
- }
-}
-
-fn params_ref(ctx: *mut c_void) -> Option<&'static mut MockParams> {
- let store = params_store().lock().unwrap();
- store.get(&(ctx as usize))?;
- drop(store);
- // SAFETY: entries are only removed by oakcommon_videoparams_free while
- // the caller still holds the handle, so the box outlives this borrow.
- Some(unsafe { &mut *(ctx as *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()
-}
-
-fn params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockParams)) {
- let mut store = params_store().lock().unwrap();
- if let Some(p) = store.get_mut(&(ctx as usize)) {
- f(p);
- }
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_videoparams_*
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_init() -> OakVideoParams {
- new_params_handle(MockParams {
- channel_count: 4, // internal RGBA pipeline layout
- ..Default::default()
- })
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_init_basic(
- width: c_int,
- height: c_int,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
-) -> OakVideoParams {
- new_params_handle(MockParams {
- width,
- height,
- format: pixel_format,
- channel_count: nb_channels,
- pixel_aspect_num,
- pixel_aspect_den,
- interlacing,
- divider,
- ..Default::default()
- })
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_init_with_time_base(
- width: c_int,
- height: c_int,
- time_base_num: i64,
- time_base_den: i64,
- pixel_format: c_int,
- nb_channels: c_int,
- pixel_aspect_num: c_int,
- pixel_aspect_den: c_int,
- interlacing: c_int,
- divider: c_int,
-) -> OakVideoParams {
- new_params_handle(MockParams {
- width,
- height,
- time_base_num,
- time_base_den,
- format: pixel_format,
- channel_count: nb_channels,
- pixel_aspect_num,
- pixel_aspect_den,
- interlacing,
- divider,
- ..Default::default()
- })
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_free(params: *mut OakVideoParams) {
- if params.is_null() {
- return;
- }
- unsafe {
- let ctx = (*params).ctx;
- params_store().lock().unwrap().remove(&(ctx as usize));
- if !ctx.is_null() {
- drop(Box::from_raw(ctx as *mut MockParams));
- }
- }
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -> c_int {
- let w = params_get(params.ctx).width;
- if width.is_null() {
- return -1;
- }
- unsafe { *width = w };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -> c_int {
- let h = params_get(params.ctx).height;
- if height.is_null() {
- return -1;
- }
- unsafe { *height = h };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -> c_int {
- let f = params_get(params.ctx).format;
- if format.is_null() {
- return -1;
- }
- unsafe { *format = f };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_time_base(
- params: OakVideoParams,
- numerator: *mut c_int,
- denominator: *mut c_int,
-) -> c_int {
- let p = params_get(params.ctx);
- if !numerator.is_null() {
- unsafe { *numerator = p.time_base_num as c_int };
- }
- if !denominator.is_null() {
- unsafe { *denominator = p.time_base_den as c_int };
- }
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int) {
- params_set(params.ctx, |p| p.width = width);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int) {
- params_set(params.ctx, |p| p.height = height);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) {
- params_set(params.ctx, |p| p.format = format);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, is_valid: *mut c_int) -> c_int {
- let p = params_get(params.ctx);
- let v = (p.width > 0 && p.height > 0) as c_int;
- if is_valid.is_null() {
- return -1;
- }
- unsafe { *is_valid = v };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_equals(
- a: OakVideoParams,
- b: OakVideoParams,
- equal: *mut c_int,
-) -> c_int {
- let eq = (params_get(a.ctx) == params_get(b.ctx)) as c_int;
- if equal.is_null() {
- return -1;
- }
- unsafe { *equal = eq };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel(
- format: c_int,
- _channels: 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,
- }
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_frame_rate_as_time_base(
- frame_rate_num: i64,
- frame_rate_den: i64,
- out_num: *mut i64,
- out_den: *mut i64,
-) {
- if !out_num.is_null() {
- unsafe { *out_num = frame_rate_den };
- }
- if !out_den.is_null() {
- unsafe { *out_den = frame_rate_num };
- }
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_stream_index(
- params: OakVideoParams,
- index: *mut c_int,
-) -> c_int {
- let i = params_get(params.ctx).stream_index;
- if index.is_null() {
- return -1;
- }
- unsafe { *index = i };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int) {
- params_set(params.ctx, |p| p.stream_index = index);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_divider(
- params: OakVideoParams,
- divider: *mut c_int,
-) -> c_int {
- let d = params_get(params.ctx).divider;
- if divider.is_null() {
- return -1;
- }
- unsafe { *divider = d };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int) {
- params_set(params.ctx, |p| p.divider = divider);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_frame_rate(
- params: OakVideoParams,
- out_num: *mut c_int,
- out_den: *mut c_int,
-) -> c_int {
- let p = params_get(params.ctx);
- if !out_num.is_null() {
- unsafe { *out_num = p.frame_rate_num };
- }
- if !out_den.is_null() {
- unsafe { *out_den = p.frame_rate_den };
- }
- 1
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_duration(
- params: OakVideoParams,
- duration: *mut i64,
-) -> c_int {
- let d = params_get(params.ctx).duration;
- if duration.is_null() {
- return -1;
- }
- unsafe { *duration = d };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_channel_count(
- params: OakVideoParams,
- count: *mut c_int,
-) -> c_int {
- let c = params_get(params.ctx).channel_count;
- if count.is_null() {
- return -1;
- }
- unsafe { *count = c };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_color_primaries(
- params: OakVideoParams,
- primaries: *mut c_int,
-) -> c_int {
- let p = params_get(params.ctx).color_primaries;
- if primaries.is_null() {
- return -1;
- }
- unsafe { *primaries = p };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_color_transfer(
- params: OakVideoParams,
- transfer: *mut c_int,
-) -> c_int {
- let t = params_get(params.ctx).color_trc;
- if transfer.is_null() {
- return -1;
- }
- unsafe { *transfer = t };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_get_interlacing(
- params: OakVideoParams,
- interlacing: *mut c_int,
-) -> c_int {
- let i = params_get(params.ctx).interlacing;
- if interlacing.is_null() {
- return -1;
- }
- unsafe { *interlacing = i };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-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;
- });
-}
-
-#[no_mangle]
-#[cfg(test)]
-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;
- });
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio(
- params: OakVideoParams,
- num: i64,
- den: i64,
-) {
- params_set(params.ctx, |p| {
- p.pixel_aspect_num = num as i32;
- p.pixel_aspect_den = den as i32;
- });
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_interlacing(
- params: OakVideoParams,
- interlacing: c_int,
-) {
- params_set(params.ctx, |p| p.interlacing = interlacing);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) {
- params_set(params.ctx, |p| p.duration = duration);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64) {
- params_set(params.ctx, |p| p.start_time = start_time);
-}
-
-#[no_mangle]
-#[cfg(test)]
-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);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) {
- params_set(params.ctx, |p| p.video_type = video_type);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int) {
- params_set(params.ctx, |p| p.channel_count = channels);
-}
-
-#[no_mangle]
-#[cfg(test)]
-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,
-) {
- params_set(params.ctx, |p| p.color_trc = transfer);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_premultiplied_alpha(
- params: OakVideoParams,
- premultiplied: c_int,
-) {
- params_set(params.ctx, |p| p.premultiplied_alpha = premultiplied);
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int) {
- params_set(params.ctx, |p| p.enabled = enabled);
-}
-
-// ---------------------------------------------------------------------------
-// oakcore_audioparams_* / oakcore_rational_* (pointer-based in-memory state)
-// ---------------------------------------------------------------------------
-
-/// Per-`OakAudioParams` backing state. The real oakcore C ABI is
-/// pointer-based (`core/include/olive/core/oakcore/audioparams.h`), so
-/// these stubs own a boxed struct and return its raw pointer.
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-struct MockAudioParams {
- sample_rate: i32,
- channel_layout: u64,
- format: i32,
- stream_index: i32,
- duration: i64,
- time_base_num: i32,
- time_base_den: i32,
-}
-
-fn audio_params_store() -> &'static Mutex> {
- static S: OnceLock>> = OnceLock::new();
- S.get_or_init(|| Mutex::new(HashMap::new()))
-}
-
-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()
-}
-
-/// Per-`OakRational` backing state (an owned `(num, den)` pair).
-fn rational_store() -> &'static Mutex> {
- static S: OnceLock>> = OnceLock::new();
- S.get_or_init(|| Mutex::new(HashMap::new()))
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_create(
- sample_rate: c_int,
- channel_layout: u64,
- format: c_int,
-) -> *mut OakAudioParams {
- // The timebase starts at 1/sample_rate, mirroring the real header.
- let p = MockAudioParams {
- sample_rate,
- channel_layout,
- format,
- stream_index: 0,
- duration: 0,
- time_base_num: 1,
- time_base_den: sample_rate,
- };
- let raw = Box::into_raw(Box::new(p.clone()));
- audio_params_store().lock().unwrap().insert(raw as usize, p);
- raw as *mut OakAudioParams
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_free(params: *mut OakAudioParams) {
- if params.is_null() {
- return;
- }
- audio_params_store()
- .lock()
- .unwrap()
- .remove(&(params as usize));
- // SAFETY: `params` was produced by `oakcore_audioparams_create` as a
- // boxed `MockAudioParams`; we hold the only reference after removal.
- unsafe { drop(Box::from_raw(params as *mut MockAudioParams)) };
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int {
- audio_params_get(params as *const c_void).sample_rate
-}
-
-fn audio_params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockAudioParams)) {
- let mut store = audio_params_store().lock().unwrap();
- if let Some(p) = store.get_mut(&(ctx as usize)) {
- f(p);
- }
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_set_sample_rate(
- params: *mut OakAudioParams,
- sample_rate: c_int,
-) {
- audio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate);
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64 {
- audio_params_get(params as *const c_void).channel_layout
-}
-
-#[no_mangle]
-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);
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_set_time_base(
- params: *mut OakAudioParams,
- num: c_int,
- den: c_int,
-) {
- audio_params_set(params as *mut c_void, |p| {
- p.time_base_num = num;
- p.time_base_den = den;
- });
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) {
- audio_params_set(params as *mut c_void, |p| p.format = format);
-}
-
-#[no_mangle]
-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);
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) {
- audio_params_set(params as *mut c_void, |p| p.duration = duration);
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int {
- audio_params_get(params as *const c_void)
- .channel_layout
- .count_ones() as c_int
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int {
- audio_params_get(params as *const c_void).format
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int {
- audio_params_get(params as *const c_void).stream_index
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64 {
- audio_params_get(params as *const c_void).duration
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int {
- let p = audio_params_get(params as *const c_void);
- (p.sample_rate > 0 && p.channel_layout != 0 && p.format >= 0) as c_int
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void {
- let p = audio_params_get(params as *const c_void);
- let r = (p.time_base_num, p.time_base_den);
- let raw = Box::into_raw(Box::new(r));
- rational_store().lock().unwrap().insert(raw as usize, r);
- raw as *mut c_void
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_rational_numerator(rational: *const c_void) -> c_int {
- rational_store()
- .lock()
- .unwrap()
- .get(&(rational as usize))
- .map(|r| r.0)
- .unwrap_or(0)
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_rational_denominator(rational: *const c_void) -> c_int {
- rational_store()
- .lock()
- .unwrap()
- .get(&(rational as usize))
- .map(|r| r.1)
- .unwrap_or(0)
-}
-
-#[no_mangle]
-pub extern "C" fn oakcore_rational_free(rational: *mut c_void) {
- if rational.is_null() {
- return;
- }
- 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))) };
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_subtitleparams_*
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_subtitleparams_get_stream_index(
- _params: OakSubtitleParams,
- index: *mut c_int,
-) -> c_int {
- if index.is_null() {
- return -1;
- }
- unsafe { *index = 0 };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_subtitleparams_generate_ass_header(
- _params: OakSubtitleParams,
- _width: c_int,
- _height: c_int,
-) {
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_subtitleparams_add_subtitle(
- _params: OakSubtitleParams,
- _text: *const c_char,
-) {
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_config_*
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_config_get_int(
- _group: *const c_char,
- _key: *const c_char,
- default: c_int,
-) -> c_int {
- default
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_config_get_bool(
- _group: *const c_char,
- _key: *const c_char,
- default: c_int,
-) -> c_int {
- default
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_config_get(
- _group: *const c_char,
- _key: *const c_char,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- // Empty value: report the size needed (1 for NUL) and write NUL.
- if buf_size <= 0 {
- return 1;
- }
- if !buf.is_null() {
- unsafe { *buf = 0 };
- }
- 1
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_filefunctions_*
-// ---------------------------------------------------------------------------
-
-static FAKE_PATH: &[u8] = b"/mock/config/oak\0";
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_filefunctions_init() -> OakFileFunctions {
- OakFileFunctions::null()
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_filefunctions_get_configuration_location(
- _self_: OakFileFunctions,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- copy_cstr(FAKE_PATH, buf, buf_size)
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier(
- _self_: OakFileFunctions,
- path: *const c_char,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- if path.is_null() || buf.is_null() {
- return -1;
- }
- let hash = unsafe { CStr::from_ptr(path) }
- .to_bytes()
- .iter()
- .fold(14695981039346656037u64, |acc, &b| {
- (acc ^ b as u64).wrapping_mul(1099511628211)
- });
- let s = format!("{}", hash as i64);
- let bytes = s.as_bytes();
- let n = bytes.len() + 1;
- if buf_size < n as c_int {
- return n as c_int;
- }
- unsafe {
- std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
- *buf.add(bytes.len()) = 0;
- }
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_filefunctions_get_application_path(
- _self_: OakFileFunctions,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- copy_cstr(b"/mock/app/oak\0", buf, buf_size)
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_filefunctions_free(_ptr: *mut c_void) {}
-
-// ---------------------------------------------------------------------------
-// oakcommon_colortransform_*
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_colortransform_init_output(
- _output: *const c_char,
-) -> OakVideoParams {
- oakcommon_videoparams_init()
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_colortransform_get_output(
- params: OakVideoParams,
- out: *mut OakVideoParams,
-) {
- if !out.is_null() {
- unsafe { *out = params.clone() };
- }
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_colortransform_free(params: *mut OakVideoParams) {
- oakcommon_videoparams_free(params);
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_ffmpegutils_* (pure enum mapping; identity is a safe default)
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_get_native_sample_format(
- smp_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = smp_fmt };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_get_compatible_pixel_format(
- pix_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = pix_fmt };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(
- pix_fmt: c_int,
- _channel_count: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = pix_fmt };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(
- smp_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = smp_fmt };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(
- pix_fmt: c_int,
- _maximum_pix_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = pix_fmt };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(
- pix_fmt: c_int,
- out: *mut c_int,
-) -> c_int {
- if out.is_null() {
- return -1;
- }
- unsafe { *out = pix_fmt };
- 0
-}
-
-// ---------------------------------------------------------------------------
-// oakcommon_oiioutils_*
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_oiioutils_init() -> OakOIIOUtils {
- OakOIIOUtils::null()
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_oiioutils_get_oiio_base_type_from_format(
- _self_: OakOIIOUtils,
- pixel_format: c_int,
- out_base_type: *mut c_int,
-) -> c_int {
- if out_base_type.is_null() {
- return -1;
- }
- unsafe { *out_base_type = pixel_format };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_oiioutils_get_format_from_oiio_basetype(
- _self_: OakOIIOUtils,
- base_type: c_int,
- out_pixel_format: *mut c_int,
-) -> c_int {
- if out_pixel_format.is_null() {
- return -1;
- }
- unsafe { *out_pixel_format = base_type };
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_oiioutils_get_pixel_aspect_ratio(
- _self_: OakOIIOUtils,
- _pixel_aspect_ratio: f64,
- out_numerator: *mut c_int,
- out_denominator: *mut c_int,
-) -> c_int {
- if !out_numerator.is_null() {
- unsafe { *out_numerator = 1 };
- }
- if !out_denominator.is_null() {
- unsafe { *out_denominator = 1 };
- }
- 0
-}
-
-#[no_mangle]
-#[cfg(test)]
-pub extern "C" fn oakcommon_oiioutils_free(_self_: *mut OakOIIOUtils) {}
-
-// ---------------------------------------------------------------------------
-// oakrender_cancelatom_* (real in-memory cancel state)
-// ---------------------------------------------------------------------------
-
-static CANCEL_FLAGS: OnceLock>> = OnceLock::new();
-
-fn cancel_flags() -> &'static Mutex> {
- CANCEL_FLAGS.get_or_init(|| Mutex::new(HashMap::new()))
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_cancelatom_init() -> OakCancelAtom {
- static COUNTER: AtomicU64 = AtomicU64::new(1);
- let id = COUNTER.fetch_add(1, Ordering::SeqCst) as usize;
- cancel_flags().lock().unwrap().insert(id, false);
- OakCancelAtom {
- ctx: id as *mut c_void,
- addref: None,
- release: None,
- abi_version: OAKCODEC_ABI_VERSION,
- }
-}
-
-#[no_mangle]
-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))
- };
-}
-
-#[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
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int {
- oakrender_cancelatom_is_cancelled(atom)
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) {
- if let Some(f) = cancel_flags().lock().unwrap().get_mut(&(atom.ctx as usize)) {
- *f = true;
- }
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void {
- atom.ctx
-}
-
-// ---------------------------------------------------------------------------
-// oakrender_display_texture_* / codec_frame_* (neutral)
-// ---------------------------------------------------------------------------
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_create(
- _renderer: OakRenderRenderer,
- _params: *const crate::bridge::render::oakrender_video_params,
- _data: *const c_void,
- _linesize: c_int,
-) -> OakRenderTexture {
- OakRenderTexture {
- ctx: std::ptr::null_mut(),
- addref: None,
- release: None,
- abi_version: OAKCODEC_ABI_VERSION,
- }
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture {
- texture
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_free(_texture: *mut OakRenderTexture) {}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_upload(_texture: OakRenderTexture) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_download(
- _texture: OakRenderTexture,
- _pixels: *mut c_void,
- _linesize: c_int,
-) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_get_params(
- _texture: OakRenderTexture,
- _out: *mut crate::bridge::render::oakrender_video_params,
-) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_id(_texture: OakRenderTexture) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_is_dummy(_texture: OakRenderTexture) -> c_int {
- 1
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_texture_get_frame(
- _texture: OakRenderTexture,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- if buf_size <= 0 {
- return 1;
- }
- if !buf.is_null() {
- unsafe { *buf = 0 };
- }
- 1
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_width(_frame: OakCodecFrame) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_height(_frame: OakCodecFrame) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_free(_frame: *mut OakCodecFrame) {}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_allocate(_frame: OakCodecFrame) -> c_int {
- 1
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_linesize_bytes(_frame: OakCodecFrame) -> c_int {
- 0
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_codec_frame_is_allocated(_frame: OakCodecFrame) -> c_int {
- 1
-}
-
-#[no_mangle]
-pub extern "C" fn oakrender_display_renderer_blit_color_managed(
- _renderer: OakRenderRenderer,
- _job: *const c_void,
- _dst_texture: OakRenderTexture,
- _params: *const crate::bridge::render::oakrender_video_params,
-) -> c_int {
- 0
-}
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-/// Two-stage string copy helper used by the mock getters. Returns the
-/// total size needed (including the trailing NUL), or truncates the buffer
-/// and writes a NUL terminator when the buffer is too small.
-fn copy_cstr(src: &[u8], buf: *mut c_char, buf_size: c_int) -> c_int {
- let needed = src.len() as c_int;
- if buf.is_null() || buf_size < needed {
- return needed;
- }
- unsafe {
- for (i, &b) in src.iter().enumerate() {
- *buf.add(i) = b as c_char;
- }
- }
- needed
-}
-
-// Silence unused-import warnings when the by-value handle types are not
-// referenced by every build; they are part of the mock's public surface.
-#[allow(unused)]
-fn _keep(_: OakAudioParams, _: OakNodeBlock) {}
diff --git a/crates/oakcodec/src/conformmanager.rs b/crates/oakcodec/src/conformmanager.rs
index 8a56f424e..33be4b153 100644
--- a/crates/oakcodec/src/conformmanager.rs
+++ b/crates/oakcodec/src/conformmanager.rs
@@ -22,7 +22,7 @@
//! `Unavailable`. Deterministic per-channel filenames derive from the
//! source + target audio params.
-use std::ffi::CString;
+use oakcommon::filefunctions::FileFunctions;
use std::path::Path;
/// Conform state of one audio stream.
@@ -184,18 +184,11 @@ fn conform_filenames(
out
}
-/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
-/// returns a 64-bit id directly).
+/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper.
fn unique_file_identifier(filename: &str) -> String {
- let c = match CString::new(filename) {
- Ok(c) => c,
- Err(_) => return String::new(),
- };
- // # Safety: `c` is a valid NUL-terminated C string alive for the call.
- let id = unsafe {
- crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
- };
- format!("{}", id)
+ FileFunctions::new()
+ .get_unique_file_identifier(filename)
+ .unwrap_or_default()
}
/// True when every conform filename already exists on disk.
@@ -234,34 +227,23 @@ mod tests {
dir.to_string_lossy().into_owned()
}
- fn fnv1a64(bytes: &[u8]) -> u64 {
- let mut h: u64 = 14695981039346656037;
- for &b in bytes {
- h ^= b as u64;
- h = h.wrapping_mul(1099511628211);
- }
- h
- }
-
#[test]
- fn unique_identifier_matches_bridge_hash() {
- // The frozen ABI returns the identifier as a string; the
- // value-style adapter folds it into an FNV-1a-64. The test stub
- // produces the decimal FNV of the path bytes, so the expected
- // value is FNV of that decimal string.
- let stub_id = fnv1a64(b"media.mp4") as i64;
- let expected = format!("{}", fnv1a64(stub_id.to_string().as_bytes()) as i64);
- assert_eq!(unique_file_identifier("media.mp4"), expected);
+ fn unique_identifier_uses_file_metadata() {
+ // The identifier is the 16-digit FNV-1a hex of (absolute path +
+ // mtime); a missing file has no identifier.
+ assert_eq!(unique_file_identifier("no-such-file.mp4"), "");
+ let dir = temp_subdir("id");
+ let file = std::path::Path::new(&dir).join("media.mp4");
+ std::fs::write(&file, b"x").unwrap();
+ let id = unique_file_identifier(&file.to_string_lossy());
+ assert_eq!(id.len(), 16);
+ assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
// Deterministic: same input, same id.
- assert_eq!(
- unique_file_identifier("media.mp4"),
- unique_file_identifier("media.mp4")
- );
+ assert_eq!(id, unique_file_identifier(&file.to_string_lossy()));
+ let other = std::path::Path::new(&dir).join("other.mp4");
+ std::fs::write(&other, b"y").unwrap();
// Different input, different id.
- assert_ne!(
- unique_file_identifier("media.mp4"),
- unique_file_identifier("other.mp4")
- );
+ assert_ne!(id, unique_file_identifier(&other.to_string_lossy()));
}
#[test]
@@ -279,21 +261,23 @@ mod tests {
#[test]
fn conform_filename_derivation_and_range() {
let m = ConformManager::instance();
+ let src_dir = temp_subdir("names");
+ let src = std::path::Path::new(&src_dir).join("media.mp4");
+ std::fs::write(&src, b"x").unwrap();
let cache = temp_subdir("names");
- let stub_id = fnv1a64(b"media.mp4") as i64;
- let id = fnv1a64(stub_id.to_string().as_bytes()) as i64;
+ let id = unique_file_identifier(&src.to_string_lossy());
let base = format!("{}-0.48000.0.3", id);
let f0 = m
- .get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0)
+ .get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 0)
.unwrap();
let f1 = m
- .get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 1)
+ .get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 1)
.unwrap();
assert_eq!(f0, format!("{}/{}.0.pcm", cache, base));
assert_eq!(f1, format!("{}/{}.1.pcm", cache, base));
// Out of range.
assert!(matches!(
- m.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 5),
+ m.get_conform_filename(&cache, &src.to_string_lossy(), 0, 48000, 0x3, 0, 5),
Err(crate::error::Error::NotFound)
));
}
diff --git a/crates/oakcodec/src/decoder.rs b/crates/oakcodec/src/decoder.rs
index 9327e7704..9c19f477e 100644
--- a/crates/oakcodec/src/decoder.rs
+++ b/crates/oakcodec/src/decoder.rs
@@ -26,12 +26,22 @@
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
+use oakcommon::cancelatom::CancelAtom;
use oakcore_rs::{Rational, TimeRange};
-use crate::bridge::render::{OakCancelAtom, OakRenderTexture};
use crate::footagedescription::FootageDescription;
use crate::frame::Frame;
+/// `OakRenderTexture` — refcounted GPU texture handle (an oakrender type,
+/// opaque to oakcodec). The codec crate cannot depend on oakrender (the
+/// dependency cycle), so it only ever produces an empty handle — the
+/// shared [`crate::handle::CHandle`] carries that value unchanged.
+pub type OakRenderTexture = crate::handle::CHandle;
+
+/// `OakNodeBlock` — opaque node-block handle owned elsewhere; the codec
+/// only stores and forwards it (borrowed, never dereferenced).
+pub type OakNodeBlock = crate::handle::CHandle;
+
/// `oakcodec_video_stream_info` — POD probe output describing one video
/// stream; see `include/codec/decoder.h`.
#[repr(C)]
@@ -158,7 +168,7 @@ pub enum RetrieveState {
pub struct CodecStream {
filename: String,
stream: i32,
- block: Option,
+ block: Option,
}
impl CodecStream {
@@ -175,7 +185,7 @@ impl CodecStream {
pub fn with_block(
filename: String,
stream: i32,
- block: Option,
+ block: Option,
) -> Self {
CodecStream {
filename,
@@ -212,7 +222,7 @@ impl CodecStream {
}
/// Associated timeline block (borrowed; only compared, never used).
- pub fn block(&self) -> Option {
+ pub fn block(&self) -> Option {
self.block.clone()
}
}
@@ -241,7 +251,7 @@ pub trait Decoder: Send + Sync {
fn probe(
&self,
filename: &str,
- cancelled: Option<&OakCancelAtom>,
+ cancelled: Option<&CancelAtom>,
) -> Option;
/// Open `stream` for decoding. Thread-safe.
@@ -281,7 +291,7 @@ pub trait Decoder: Send + Sync {
sample_rate: i32,
channel_layout: u64,
sample_format: i32,
- cancelled: Option<&OakCancelAtom>,
+ cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()>;
/// Offset of the audio start relative to the video (rational seconds).
@@ -326,7 +336,7 @@ impl Decoder for UnimplementedDecoder {
fn probe(
&self,
_filename: &str,
- _cancelled: Option<&OakCancelAtom>,
+ _cancelled: Option<&CancelAtom>,
) -> Option {
None
}
@@ -377,7 +387,7 @@ impl Decoder for UnimplementedDecoder {
_sample_rate: i32,
_channel_layout: u64,
_sample_format: i32,
- _cancelled: Option<&OakCancelAtom>,
+ _cancelled: Option<&CancelAtom>,
) -> crate::error::Result<()> {
Err(crate::error::Error::Failed(
"decoder not yet implemented".to_string(),
@@ -400,13 +410,13 @@ pub fn create_from_id(id: &str) -> Option> {
/// not injected, in which case the built-in list below is used.
static TEST_DECODERS: OnceLock>>> = OnceLock::new();
-/// Serializes every test that reads the built-in decoder registry. The ffi
-/// decoder tests inject through `crate::ffi::lock_tests()` (the shared
-/// `TEST_LOCK`), so the registry assertions below take that same lock to
-/// never race with an injected list.
+/// Serializes every test that reads the built-in decoder registry. Tests
+/// inject through [`set_test_decoders`] under [`crate::lock_tests`] (the
+/// shared test lock), so the registry assertions below take that same lock
+/// to never race with an injected list.
#[cfg(test)]
-fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
- crate::ffi::lock_tests()
+fn registry_guard() -> crate::TestLock {
+ crate::lock_tests()
}
/// Replace the decoder registry with `list`; pass an empty list to restore
diff --git a/crates/oakcodec/src/ffi/conform.rs b/crates/oakcodec/src/ffi/conform.rs
deleted file mode 100644
index eb503a877..000000000
--- a/crates/oakcodec/src/ffi/conform.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! `include/codec/conform.h` exports.
-//!
-//! Complete inventory: create_instance / destroy_instance / get_state /
-//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the
-//! states.
-//!
-//! # CPP-PARITY
-//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the
-//! singleton is absent (`!ConformManager::instance()`); the Rust
-//! [`crate::conformmanager::ConformManager::instance`] is a lazy
-//! `'static` singleton that can never be absent, so that branch cannot
-//! trigger.
-
-use std::ffi::{c_char, c_int};
-
-use crate::conformmanager::{ConformManager, ConformState};
-use crate::handle;
-
-/// `OAKCODEC_CONFORM_EXISTS`.
-pub const OAKCODEC_CONFORM_EXISTS: c_int = 0;
-/// `OAKCODEC_CONFORM_GENERATING`.
-pub const OAKCODEC_CONFORM_GENERATING: c_int = 1;
-/// `OAKCODEC_CONFORM_UNAVAILABLE`.
-pub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;
-
-/// `oakcodec_conform_create_instance`: create the singleton (always
-/// present here, so a no-op).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_conform_create_instance() -> c_int {
- handle::guard_raw(|| {
- let _ = ConformManager::instance();
- crate::error::OAKCODEC_OK
- })
-}
-
-/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust
-/// manager is stateless, so a no-op).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_conform_destroy_instance() -> c_int {
- handle::guard_raw(|| crate::error::OAKCODEC_OK)
-}
-
-/// `oakcodec_conform_get_state`: query the conform state of one audio
-/// stream.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_conform_get_state(
- cache_path: *const c_char,
- source_filename: *const c_char,
- stream_index: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- sample_format: c_int,
- wait: c_int,
-) -> c_int {
- handle::guard_raw(|| {
- let cache = match crate::ffi::c_str(cache_path) {
- Some(c) if !c.is_empty() => c,
- _ => return crate::error::OAKCODEC_E_INVALID,
- };
- let source = match crate::ffi::c_str(source_filename) {
- Some(s) if !s.is_empty() => s,
- _ => return crate::error::OAKCODEC_E_INVALID,
- };
- let m = ConformManager::instance();
- match m.get_conform_state(
- &cache,
- &source,
- stream_index,
- sample_rate,
- channel_layout,
- sample_format,
- wait != 0,
- ) {
- Ok(ConformState::Exists) => OAKCODEC_CONFORM_EXISTS,
- Ok(ConformState::Generating) => OAKCODEC_CONFORM_GENERATING,
- Ok(ConformState::Unavailable) | Err(_) => OAKCODEC_CONFORM_UNAVAILABLE,
- }
- })
-}
-
-/// `oakcodec_conform_filename_count`: number of conform files for the
-/// given stream/params; 0 on invalid arguments.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_conform_filename_count(
- cache_path: *const c_char,
- source_filename: *const c_char,
- stream_index: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- sample_format: c_int,
-) -> c_int {
- handle::guard_raw(|| {
- let cache = match crate::ffi::c_str(cache_path) {
- Some(c) if !c.is_empty() => c,
- _ => return 0,
- };
- let source = match crate::ffi::c_str(source_filename) {
- Some(s) if !s.is_empty() => s,
- _ => return 0,
- };
- let m = ConformManager::instance();
- m.get_conform_filename_count(
- &cache,
- &source,
- stream_index,
- sample_rate,
- channel_layout,
- sample_format,
- ) as c_int
- })
-}
-
-/// `oakcodec_conform_filename_at`: the `index`-th conform filename
-/// (two-stage string).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_conform_filename_at(
- cache_path: *const c_char,
- source_filename: *const c_char,
- stream_index: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- sample_format: c_int,
- index: c_int,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- handle::guard_raw(|| {
- let cache = match crate::ffi::c_str(cache_path) {
- Some(c) if !c.is_empty() => c,
- _ => return crate::error::OAKCODEC_E_INVALID,
- };
- let source = match crate::ffi::c_str(source_filename) {
- Some(s) if !s.is_empty() => s,
- _ => return crate::error::OAKCODEC_E_INVALID,
- };
- let m = ConformManager::instance();
- match m.get_conform_filename(
- &cache,
- &source,
- stream_index,
- sample_rate,
- channel_layout,
- sample_format,
- index as usize,
- ) {
- Ok(f) => super::string_out(&f, buf, buf_size),
- Err(crate::error::Error::NotFound) => crate::error::OAKCODEC_E_NOT_FOUND,
- Err(_) => crate::error::OAKCODEC_E_FAILED,
- }
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::conformmanager::test_util::REG_LOCK;
- use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
-
- fn cstr(s: &str) -> std::ffi::CString {
- std::ffi::CString::new(s).unwrap()
- }
-
- fn temp_cache(name: &str) -> String {
- let dir = std::env::temp_dir().join(format!(
- "oakcodec_ffi_conform_{}_{}",
- name,
- std::process::id()
- ));
- let _ = std::fs::create_dir_all(&dir);
- dir.to_string_lossy().into_owned()
- }
-
- #[test]
- fn create_destroy_instance_ok() {
- let _g = crate::ffi::lock_tests();
- assert_eq!(
- unsafe { oakcodec_conform_create_instance() },
- crate::error::OAKCODEC_OK
- );
- assert_eq!(
- unsafe { oakcodec_conform_destroy_instance() },
- crate::error::OAKCODEC_OK
- );
- }
-
- #[test]
- fn get_state_maps_states() {
- let _g = crate::ffi::lock_tests();
- let _g = REG_LOCK.lock().unwrap();
- // No registrar and no files -> UNAVAILABLE.
- let cache = cstr(&temp_cache("state"));
- let src = cstr("media.mp4");
- let rc = unsafe {
- oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
- };
- assert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);
-
- // Invalid arguments -> E_INVALID.
- let rc = unsafe {
- oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
- };
- assert_eq!(rc, OAKCODEC_E_INVALID);
- let empty = cstr("");
- let rc = unsafe {
- oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
- };
- assert_eq!(rc, OAKCODEC_E_INVALID);
-
- // Write the conform files -> EXISTS.
- let m = ConformManager::instance();
- for i in 0..2 {
- let f = m
- .get_conform_filename(&temp_cache("state"), "media.mp4", 0, 48000, 0x3, 0, i)
- .unwrap();
- std::fs::write(&f, b"pcm").unwrap();
- }
- let rc = unsafe {
- oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0)
- };
- assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
- }
-
- #[test]
- fn filename_count_and_at() {
- let _g = crate::ffi::lock_tests();
- let cache = cstr(&temp_cache("names"));
- let src = cstr("media.mp4");
-
- // Stereo -> 2 files.
- let rc = unsafe {
- oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0)
- };
- assert_eq!(rc, 2);
-
- // Invalid args -> 0 (not an error).
- let rc = unsafe {
- oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0)
- };
- assert_eq!(rc, 0);
-
- // filename_at round-trips the deterministic name.
- let mut buf = [0i8; 512];
- let rc = unsafe {
- oakcodec_conform_filename_at(
- cache.as_ptr(),
- src.as_ptr(),
- 0,
- 48000,
- 0x3,
- 0,
- 0,
- buf.as_mut_ptr(),
- 512,
- )
- };
- assert!(rc > 0);
- let name = crate::ffi::c_str(buf.as_ptr()).unwrap();
- assert!(name.ends_with(".0.pcm"));
-
- // Out-of-range index -> E_NOT_FOUND.
- let rc = unsafe {
- oakcodec_conform_filename_at(
- cache.as_ptr(),
- src.as_ptr(),
- 0,
- 48000,
- 0x3,
- 0,
- 5,
- buf.as_mut_ptr(),
- 512,
- )
- };
- assert_eq!(rc, OAKCODEC_E_NOT_FOUND);
-
- // Invalid args -> E_INVALID.
- let rc = unsafe {
- oakcodec_conform_filename_at(
- std::ptr::null(),
- src.as_ptr(),
- 0,
- 48000,
- 0x3,
- 0,
- 0,
- buf.as_mut_ptr(),
- 512,
- )
- };
- assert_eq!(rc, OAKCODEC_E_INVALID);
- }
-}
diff --git a/crates/oakcodec/src/ffi/decoder.rs b/crates/oakcodec/src/ffi/decoder.rs
deleted file mode 100644
index 99f98b8f2..000000000
--- a/crates/oakcodec/src/ffi/decoder.rs
+++ /dev/null
@@ -1,1329 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! `include/codec/decoder.h` exports.
-//!
-//! Complete inventory: probe / probe_last_error / probe_decoder_name /
-//! probe_video_stream_count / probe_audio_stream_count /
-//! probe_subtitle_stream_count / probe_get_video_stream /
-//! probe_get_audio_stream / init / free / open / close / is_open /
-//! decode_video / decode_audio / conform_audio /
-//! get_image_sequence_digit_count / get_image_sequence_index /
-//! transform_image_sequence_file_name / last_error.
-//!
-//! Two handle box types share the `OakDecoder` handle, mirroring the C++
-//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports
-//! read a plain [`ProbeBox`], session exports read a `Mutex`.
-
-#[cfg(test)]
-use std::ffi::c_void;
-use std::ffi::{c_char, c_int};
-use std::path::Path;
-use std::sync::{Arc, Mutex};
-
-use oakcore_rs::{Rational, TimeRange};
-
-use crate::bridge::common::{
- oakcommon_videoparams_get_channel_count, oakcommon_videoparams_get_color_primaries,
- oakcommon_videoparams_get_color_transfer, oakcommon_videoparams_get_duration,
- oakcommon_videoparams_get_format, oakcommon_videoparams_get_frame_rate,
- oakcommon_videoparams_get_height, oakcommon_videoparams_get_interlacing,
- oakcommon_videoparams_get_stream_index, oakcommon_videoparams_get_time_base,
- oakcommon_videoparams_get_width, oakcore_audioparams_channel_count,
- oakcore_audioparams_channel_layout, oakcore_audioparams_duration,
- oakcore_audioparams_sample_rate, oakcore_audioparams_stream_index,
- oakcore_audioparams_time_base, oakcore_rational_denominator, oakcore_rational_free,
- oakcore_rational_numerator, OakAudioParams, OakVideoParams,
-};
-use crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom};
-use crate::decoder::{
- CodecStream, Decoder, OakCodecAudioStreamInfo, OakCodecVideoStreamInfo, RenderMode,
- RetrieveAudioStatus, RetrieveVideoParams, K_COLOR_RANGE_DEFAULT,
-};
-use crate::footagedescription::FootageDescription;
-#[cfg(test)]
-use crate::frame::Frame;
-use crate::handle::{self, CHandle};
-
-/// Box behind a probe handle: the recognized decoder name plus the
-/// probed stream inventory (`ProbeBox` in `c_api/decoder.cpp`).
-struct ProbeBox {
- decoder_name: String,
- desc: FootageDescription,
-}
-
-/// Box behind a decode-session handle (`DecoderBox` in `c_api/decoder.cpp`).
-struct DecoderBox {
- decoder: Option>,
- last_error: String,
- open_filename: String,
- open_stream: i32,
- open: bool,
-}
-
-/// Last probe failure reason. The C++ uses a `thread_local`; the crate
-/// serializes through a global mutex instead (tests are single-threaded
-/// per binary, so behavior is equivalent).
-static PROBE_ERROR: Mutex = Mutex::new(String::new());
-
-/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).
-const OAKCOMMON_VIDEO_INTERLACE_NONE: c_int = 0;
-
-/// Probe with every available decoder, returning the first valid
-/// description (mirrors `probe_with_any_decoder` in `c_api/decoder.cpp`).
-///
-/// `is_valid` follows the C++ `FootageDescription::is_valid`: a non-empty
-/// decoder name and at least one stream.
-fn probe_with_any_decoder(filename: &str) -> Option<(String, FootageDescription)> {
- for d in crate::decoder::receive_list_of_all_decoders() {
- let desc = match d.probe(filename, None) {
- Some(desc) => desc,
- None => continue,
- };
- if !desc.decoder().is_empty()
- && (desc.video_stream_count() > 0
- || desc.audio_stream_count() > 0
- || desc.subtitle_stream_count() > 0)
- {
- return Some((desc.decoder().to_string(), desc));
- }
- }
- None
-}
-
-/// Fill an `oakcodec_video_stream_info` from a probed video params handle
-/// (mirrors `fill_video_info` in `c_api/decoder.cpp`).
-fn fill_video_info(vp: &OakVideoParams, out: &mut OakCodecVideoStreamInfo) {
- *out = OakCodecVideoStreamInfo {
- stream_index: 0,
- width: 0,
- height: 0,
- frame_rate_num: 0,
- frame_rate_den: 0,
- duration_ts: 0,
- time_base_num: 0,
- time_base_den: 0,
- format: 0,
- channel_count: 0,
- color_primaries: 0,
- color_trc: 0,
- interlaced: 0,
- };
- out.stream_index = unsafe { oakcommon_videoparams_get_stream_index(vp.clone()) };
- out.width = unsafe { oakcommon_videoparams_get_width(vp.clone()) };
- out.height = unsafe { oakcommon_videoparams_get_height(vp.clone()) };
-
- let mut fr_num: c_int = 0;
- let mut fr_den: c_int = 0;
- unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut fr_num, &mut fr_den) };
- out.frame_rate_num = fr_num;
- out.frame_rate_den = fr_den;
-
- let mut tb_num: i64 = 0;
- let mut tb_den: i64 = 0;
- unsafe { oakcommon_videoparams_get_time_base(vp.clone(), &mut tb_num, &mut tb_den) };
- out.time_base_num = tb_num as c_int;
- out.time_base_den = tb_den as c_int;
-
- out.duration_ts = unsafe { oakcommon_videoparams_get_duration(vp.clone()) };
- out.format = unsafe { oakcommon_videoparams_get_format(vp.clone()) };
- out.channel_count = unsafe { oakcommon_videoparams_get_channel_count(vp.clone()) };
- out.color_primaries = unsafe { oakcommon_videoparams_get_color_primaries(vp.clone()) };
- out.color_trc = unsafe { oakcommon_videoparams_get_color_transfer(vp.clone()) };
-
- let interlacing = unsafe { oakcommon_videoparams_get_interlacing(vp.clone()) };
- out.interlaced = (interlacing != OAKCOMMON_VIDEO_INTERLACE_NONE) as c_int;
-}
-
-/// Fill an `oakcodec_audio_stream_info` from a probed audio params handle
-/// (mirrors `fill_audio_info` in `c_api/decoder.cpp`).
-fn fill_audio_info(ap: &OakAudioParams, out: &mut OakCodecAudioStreamInfo) {
- *out = OakCodecAudioStreamInfo {
- stream_index: 0,
- sample_rate: 0,
- channel_layout: 0,
- channel_count: 0,
- duration_ts: 0,
- time_base_num: 0,
- time_base_den: 0,
- };
- let ctx = ap.ctx as *const OakAudioParams;
- out.stream_index = unsafe { oakcore_audioparams_stream_index(ctx) };
- out.sample_rate = unsafe { oakcore_audioparams_sample_rate(ctx) };
- out.channel_layout = unsafe { oakcore_audioparams_channel_layout(ctx) };
- out.channel_count = unsafe { oakcore_audioparams_channel_count(ctx) };
- out.duration_ts = unsafe { oakcore_audioparams_duration(ctx) };
-
- // `time_base` returns a newly allocated rational the caller owns.
- let tb = unsafe { oakcore_audioparams_time_base(ctx) };
- if !tb.is_null() {
- out.time_base_num = unsafe { oakcore_rational_numerator(tb) };
- out.time_base_den = unsafe { oakcore_rational_denominator(tb) };
- unsafe { oakcore_rational_free(tb) };
- }
-}
-
-/// Whether a media file exists on disk (mirrors the C++ `file_exists`).
-fn file_exists(filename: &str) -> bool {
- Path::new(filename).exists()
-}
-
-/* ---- Probe ---------------------------------------------------------------- */
-
-/// `oakcodec_decoder_probe`: probe a media file with every available
-/// decoder; empty handle plus `oakcodec_probe_last_error` on failure.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe(filename: *const c_char) -> CHandle {
- handle::guard_handle(|| {
- let filename = match crate::ffi::c_str(filename) {
- Some(f) if !f.is_empty() => f,
- _ => {
- *PROBE_ERROR.lock().unwrap() = "no filename given".to_string();
- return Ok(CHandle::null());
- }
- };
- if !file_exists(&filename) {
- *PROBE_ERROR.lock().unwrap() = format!("file not found: {}", filename);
- return Ok(CHandle::null());
- }
- match probe_with_any_decoder(&filename) {
- Some((decoder_name, desc)) => {
- PROBE_ERROR.lock().unwrap().clear();
- Ok(handle::make_owned(ProbeBox { decoder_name, desc }))
- }
- None => {
- *PROBE_ERROR.lock().unwrap() =
- format!("no decoder recognizes this file: {}", filename);
- Ok(CHandle::null())
- }
- }
- })
-}
-
-/// `oakcodec_probe_last_error`: last probe failure reason (two-stage).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_probe_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
- handle::guard_raw(|| {
- let s = PROBE_ERROR.lock().unwrap().clone();
- super::string_out(&s, buf, buf_size)
- })
-}
-
-/// `oakcodec_decoder_probe_decoder_name`: recognized decoder id
-/// (two-stage).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_decoder_name(
- probe: CHandle,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- handle::guard_raw(|| match super::get_box::(&probe) {
- Some(b) => super::string_out(&b.decoder_name, buf, buf_size),
- None => crate::error::OAKCODEC_E_INVALID,
- })
-}
-
-/// `oakcodec_decoder_probe_video_stream_count`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_video_stream_count(probe: CHandle) -> c_int {
- handle::guard_raw(|| match super::get_box::(&probe) {
- Some(b) => b.desc.video_stream_count() as c_int,
- None => 0,
- })
-}
-
-/// `oakcodec_decoder_probe_audio_stream_count`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -> c_int {
- handle::guard_raw(|| match super::get_box::(&probe) {
- Some(b) => b.desc.audio_stream_count() as c_int,
- None => 0,
- })
-}
-
-/// `oakcodec_decoder_probe_subtitle_stream_count`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_subtitle_stream_count(probe: CHandle) -> c_int {
- handle::guard_raw(|| match super::get_box::(&probe) {
- Some(b) => b.desc.subtitle_stream_count() as c_int,
- None => 0,
- })
-}
-
-/// `oakcodec_decoder_probe_get_video_stream`: fill `out` with the video
-/// stream at `index` (video ordinal).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_get_video_stream(
- probe: CHandle,
- index: c_int,
- out: *mut OakCodecVideoStreamInfo,
-) -> c_int {
- handle::guard(|| {
- if out.is_null() {
- return Err(crate::error::Error::Invalid);
- }
- let b = super::get_box::(&probe).ok_or(crate::error::Error::Invalid)?;
- let vp = b
- .desc
- .get_video_stream(index as usize)
- .ok_or(crate::error::Error::NotFound)?;
- fill_video_info(vp, unsafe { &mut *out });
- Ok(())
- })
-}
-
-/// `oakcodec_decoder_probe_get_audio_stream`: fill `out` with the audio
-/// stream at `index` (audio ordinal).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_probe_get_audio_stream(
- probe: CHandle,
- index: c_int,
- out: *mut OakCodecAudioStreamInfo,
-) -> c_int {
- handle::guard(|| {
- if out.is_null() {
- return Err(crate::error::Error::Invalid);
- }
- let b = super::get_box::(&probe).ok_or(crate::error::Error::Invalid)?;
- let ap = b
- .desc
- .get_audio_stream(index as usize)
- .ok_or(crate::error::Error::NotFound)?;
- fill_audio_info(ap, unsafe { &mut *out });
- Ok(())
- })
-}
-
-/* ---- Decode session -------------------------------------------------------- */
-
-/// `oakcodec_decoder_init`: new closed decoder handle, count 1.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_init() -> CHandle {
- handle::guard_handle(|| {
- Ok(handle::make_owned(Mutex::new(DecoderBox {
- decoder: None,
- last_error: String::new(),
- open_filename: String::new(),
- open_stream: -1,
- open: false,
- })))
- })
-}
-
-/// `oakcodec_decoder_free`: NULL/empty no-op; nulls `ctx` afterwards.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_free(decoder: *mut CHandle) {
- handle::guard_void(|| super::free_handle(decoder));
-}
-
-/// `oakcodec_decoder_open`: open `filename`'s stream `stream_index` for
-/// decoding; opening the same stream again is a successful no-op.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_open(
- decoder: CHandle,
- filename: *const c_char,
- stream_index: c_int,
-) -> c_int {
- handle::guard(|| {
- let b =
- super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?;
- let filename = match crate::ffi::c_str(filename) {
- Some(f) => f,
- None => return Err(crate::error::Error::Invalid),
- };
- if stream_index < 0 {
- return Err(crate::error::Error::Invalid);
- }
- let mut b = b.lock().unwrap();
-
- if b.open && b.decoder.is_some() {
- if b.open_filename == filename && b.open_stream == stream_index {
- return Ok(()); // already open on this stream
- }
- let _ = b.decoder.as_ref().unwrap().close();
- b.open = false;
- }
-
- if !file_exists(&filename) {
- b.last_error = format!("file not found: {}", filename);
- return Err(crate::error::Error::NotFound);
- }
-
- let (decoder_name, _desc) = match probe_with_any_decoder(&filename) {
- Some(x) => x,
- None => {
- b.last_error = format!("no decoder recognizes this file: {}", filename);
- return Err(crate::error::Error::Failed(
- "no decoder recognizes this file".to_string(),
- ));
- }
- };
-
- let decoder = match crate::decoder::create_from_id(&decoder_name) {
- Some(d) => d,
- None => {
- b.last_error = format!("failed to create decoder: {}", decoder_name);
- return Err(crate::error::Error::Failed(
- "failed to create decoder".to_string(),
- ));
- }
- };
-
- let stream = CodecStream::with_block(filename.clone(), stream_index, None);
- if decoder.open(&stream).is_err() {
- b.last_error = "failed to open stream".to_string();
- return Err(crate::error::Error::Failed(
- "failed to open stream".to_string(),
- ));
- }
-
- b.last_error.clear();
- b.decoder = Some(decoder);
- b.open_filename = filename;
- b.open_stream = stream_index;
- b.open = true;
- Ok(())
- })
-}
-
-/// `oakcodec_decoder_close`: close the current stream (safe when closed).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_close(decoder: CHandle) -> c_int {
- handle::guard(|| {
- let b =
- super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?;
- let mut b = b.lock().unwrap();
- if b.open {
- if let Some(d) = &b.decoder {
- let _ = d.close();
- }
- }
- b.open = false;
- Ok(())
- })
-}
-
-/// `oakcodec_decoder_is_open`: 1 when a stream is open.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_is_open(decoder: CHandle) -> c_int {
- handle::guard_raw(|| match super::get_box::>(&decoder) {
- Some(b) => {
- let b = b.lock().unwrap();
- if b.open {
- 1
- } else {
- 0
- }
- }
- None => 0,
- })
-}
-
-/// `oakcodec_decoder_decode_video`: decode the frame at
-/// `numerator/denominator` seconds into a new frame handle (count 1).
-///
-/// # CPP-PARITY
-/// The C++ boxes an `olive::FramePtr` and happily aliases a frame the
-/// decoder still holds. The Rust interim boxes `Mutex` (see
-/// `ffi::frame`), so a decode that returns a still-shared `Arc`
-/// cannot be aliased and reports an empty handle plus `last_error`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_decode_video(
- decoder: CHandle,
- numerator: c_int,
- denominator: c_int,
-) -> CHandle {
- handle::guard_handle(|| {
- let b =
- super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?;
- let d = {
- let b = b.lock().unwrap();
- if !b.open || b.decoder.is_none() {
- // C++ returns an empty frame without touching last_error.
- return Ok(CHandle::null());
- }
- b.decoder.as_ref().unwrap().clone()
- };
- let p = RetrieveVideoParams {
- // C++ default-constructs the params and only sets `time`.
- stream: CodecStream::new(),
- time: Rational::new(numerator as i64, denominator as i64),
- length: TimeRange::default(),
- force_range: K_COLOR_RANGE_DEFAULT,
- is_image_sequence: false,
- image_sequence_digits: 0,
- image_sequence_number: 0,
- mode: RenderMode::Offline,
- alpha_is_premultiplied: false,
- };
- match d.retrieve_video_frame(&p) {
- Ok(arc) => match Arc::try_unwrap(arc) {
- Ok(frame) => Ok(handle::make_owned(Mutex::new(frame))),
- Err(_) => {
- b.lock().unwrap().last_error = "failed to decode video frame".to_string();
- Ok(CHandle::null())
- }
- },
- Err(_) => {
- b.lock().unwrap().last_error = "failed to decode video frame".to_string();
- Ok(CHandle::null())
- }
- }
- })
-}
-
-/// `oakcodec_decoder_decode_audio`: decode interleaved float audio
-/// covering [in, out) into `buf` (at least `buf_frames` frames).
-///
-/// Returns the number of frames written, or a negative `OAKCODEC_E_*`.
-/// # CPP-PARITY
-/// The C++ retrieves into an allocated `SampleBuffer` and copies
-/// `min(available, buf_frames)` frames. The Rust trait fills a caller-size
-/// destination, so `Success` is treated as "`buf_frames` were written".
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_decode_audio(
- decoder: CHandle,
- in_num: c_int,
- in_den: c_int,
- out_num: c_int,
- out_den: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- buf: *mut f32,
- buf_frames: c_int,
-) -> c_int {
- handle::guard_raw(|| unsafe {
- decode_audio_inner(
- decoder,
- in_num,
- in_den,
- out_num,
- out_den,
- sample_rate,
- channel_layout,
- buf,
- buf_frames,
- )
- })
-}
-
-unsafe fn decode_audio_inner(
- decoder: CHandle,
- in_num: c_int,
- in_den: c_int,
- out_num: c_int,
- out_den: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- buf: *mut f32,
- buf_frames: c_int,
-) -> c_int {
- let b = match super::get_box::>(&decoder) {
- Some(b) => b,
- None => return crate::error::OAKCODEC_E_INVALID,
- };
- if (buf.is_null() && buf_frames > 0) || buf_frames < 0 {
- return crate::error::OAKCODEC_E_INVALID;
- }
-
- let d = {
- let b = b.lock().unwrap();
- if !b.open || b.decoder.is_none() {
- return crate::error::OAKCODEC_E_STATE;
- }
- b.decoder.as_ref().unwrap().clone()
- };
-
- let range = TimeRange::new(
- Rational::new(in_num as i64, in_den as i64),
- Rational::new(out_num as i64, out_den as i64),
- );
- let channels = channel_layout.count_ones() as usize;
- let mut dest = vec![0f32; buf_frames as usize * channels];
-
- let status = match d.retrieve_audio(&mut dest, &range, sample_rate, channel_layout) {
- Ok(s) => s,
- Err(_) => {
- b.lock().unwrap().last_error = "failed to decode audio".to_string();
- return crate::error::OAKCODEC_E_FAILED;
- }
- };
-
- match status {
- RetrieveAudioStatus::Success => {
- // SAFETY: the caller guarantees `buf` holds at least `buf_frames`
- // interleaved floats; `dest` is exactly that size.
- unsafe { std::ptr::copy_nonoverlapping(dest.as_ptr(), buf, dest.len()) };
- buf_frames
- }
- RetrieveAudioStatus::ConformNeeded => {
- b.lock().unwrap().last_error =
- "audio requires a conform, but no task submit callback is registered (see oakcodec_set_task_submit_cb)"
- .to_string();
- crate::error::OAKCODEC_E_STATE
- }
- _ => {
- b.lock().unwrap().last_error = "failed to decode audio".to_string();
- crate::error::OAKCODEC_E_FAILED
- }
- }
-}
-
-/// `oakcodec_decoder_conform_audio`: conform the open stream's audio into
-/// the given per-channel pcm files.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_conform_audio(
- decoder: CHandle,
- output_filenames: *const *const c_char,
- filename_count: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- sample_format: c_int,
- cancelled: OakCancelAtom,
-) -> c_int {
- handle::guard(|| unsafe {
- conform_audio_inner(
- decoder,
- output_filenames,
- filename_count,
- sample_rate,
- channel_layout,
- sample_format,
- cancelled,
- )
- })
-}
-
-unsafe fn conform_audio_inner(
- decoder: CHandle,
- output_filenames: *const *const c_char,
- filename_count: c_int,
- sample_rate: c_int,
- channel_layout: u64,
- sample_format: c_int,
- cancelled: OakCancelAtom,
-) -> crate::error::Result<()> {
- let b = super::get_box::>(&decoder).ok_or(crate::error::Error::Invalid)?;
- if (output_filenames.is_null() && filename_count > 0) || filename_count < 0 {
- return Err(crate::error::Error::Invalid);
- }
-
- let d = {
- let b = b.lock().unwrap();
- if !b.open || b.decoder.is_none() {
- return Err(crate::error::Error::State);
- }
- b.decoder.as_ref().unwrap().clone()
- };
-
- let mut filenames: Vec = Vec::with_capacity(filename_count as usize);
- for i in 0..filename_count {
- let p = unsafe { *output_filenames.add(i as usize) };
- let s = crate::ffi::c_str(p).ok_or(crate::error::Error::Invalid)?;
- filenames.push(s);
- }
-
- // The trait takes the target audio params flattened; no handle
- // construction is needed.
- let cancelled_ref = if cancelled.ctx.is_null() {
- None
- } else {
- Some(&cancelled)
- };
- let ret = d.conform_audio(
- &filenames,
- sample_rate,
- channel_layout,
- sample_format,
- cancelled_ref,
- );
-
- match ret {
- Ok(()) => Ok(()),
- Err(_) => {
- let heard = if cancelled.ctx.is_null() {
- 0
- } else {
- unsafe { oakrender_cancelatom_heard_cancel(cancelled.clone()) }
- };
- if heard != 0 {
- Err(crate::error::Error::Cancelled)
- } else {
- Err(crate::error::Error::Failed("conform failed".to_string()))
- }
- }
- }
-}
-
-/// `oakcodec_decoder_get_image_sequence_digit_count`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_digit_count(
- filename: *const c_char,
-) -> c_int {
- handle::guard_raw(|| match crate::ffi::c_str(filename) {
- Some(f) => crate::decoder::get_image_sequence_digit_count(&f),
- None => crate::error::OAKCODEC_E_INVALID,
- })
-}
-
-/// `oakcodec_decoder_get_image_sequence_index`.
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -> i64 {
- handle::guard_i64(|| match crate::ffi::c_str(filename) {
- Some(f) => crate::decoder::get_image_sequence_index(&f),
- None => crate::error::OAKCODEC_E_INVALID as i64,
- })
-}
-
-/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_transform_image_sequence_file_name(
- filename: *const c_char,
- number: i64,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- handle::guard_raw(|| match crate::ffi::c_str(filename) {
- Some(f) => {
- let s = crate::decoder::transform_image_sequence_file_name(&f, number);
- super::string_out(&s, buf, buf_size)
- }
- None => crate::error::OAKCODEC_E_INVALID,
- })
-}
-
-/// `oakcodec_decoder_last_error` (two-stage).
-#[no_mangle]
-pub unsafe extern "C" fn oakcodec_decoder_last_error(
- decoder: CHandle,
- buf: *mut c_char,
- buf_size: c_int,
-) -> c_int {
- handle::guard_raw(|| match super::get_box::>(&decoder) {
- Some(b) => {
- let b = b.lock().unwrap();
- super::string_out(&b.last_error, buf, buf_size)
- }
- None => super::string_out("", buf, buf_size),
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::bridge::common::{
- oakcommon_videoparams_init_basic, oakcommon_videoparams_init_with_time_base,
- oakcommon_videoparams_set_stream_index, oakcore_audioparams_create,
- };
- use crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init};
- use crate::decoder::set_test_decoders;
- use crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
- use crate::footagedescription::StreamEntry;
-
- /// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes
- /// every test in this module (they share the global probe error, the
- /// injected decoder registry and the fake's state).
-
- /// Fake decoder driving the ffi export tests.
- struct FakeDecoder {
- retain_frame: bool,
- retained: Mutex