diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05ecb7..f8828fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,8 @@ jobs: uses: dtolnay/rust-toolchain@1.87.0 - name: cargo check run: cargo check --all-targets + - name: cargo check (DRED/external weights) + run: cargo check --all-targets --features external-weights tests: name: Tests @@ -166,6 +168,13 @@ jobs: run: | sudo apt-get update sudo apt-get install -y clang llvm ffmpeg wget + - name: Test external DNN weights end to end + env: + CARGO_TARGET_DIR: target/ci-asan-external-weights + run: >- + python scripts/verify_external_weights.py + --toolchain nightly + --target x86_64-unknown-linux-gnu - name: Test Rust and bundled C with ASan and leak detection run: >- cargo +nightly test diff --git a/Cargo.lock b/Cargo.lock index 3f2d1a5..48d685c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -48,9 +48,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex 2.0.1", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "errno" @@ -155,15 +155,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "getrandom" @@ -179,15 +179,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "prettyplease" @@ -287,18 +287,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 67dbed6..5903459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ sha2 = "0.11.0" [features] default = [] dred = [] +external-weights = ["dred"] system-lib = [] presume-avx2 = [] diff --git a/README.md b/README.md index 81d545b..f094222 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Safe Rust wrappers around libopus for encoding/decoding Opus audio, with tests t ## Features - `presume-avx2`: Build the bundled libopus with `OPUS_X86_PRESUME_AVX2` on x86/x86_64 targets, assuming AVX/AVX2/FMA support. Ignored when linking against a system libopus. -- `dred`: Enable libopus DRED support (downloads the model when building the bundled library). The bundled DRED build currently assumes a Unix-like host with `sh`, `wget`, and `tar`, it is not supported on Windows. +- `dred`: Enable libopus DRED support (downloads the model when building the bundled library). The bundled DRED build currently assumes a Unix-like host with `sh`, `tar`, and either `wget` or `curl`, it is not supported on Windows. +- `external-weights`: Enable DRED with runtime-loaded DNN weights instead of embedded weights. This implies `dred` and builds bundled libopus with `USE_WEIGHTS_FILE`. With `system-lib`, the installed libopus must already support external weights. - `system-lib`: Link against a system-provided libopus instead of the bundled sources. ## MSRV diff --git a/build.rs b/build.rs index 84f74a3..004b738 100644 --- a/build.rs +++ b/build.rs @@ -24,6 +24,7 @@ const BUNDLED_PACKET_OPS_FINGERPRINTS: &[SourceFingerprint] = &[ struct BuildOptions { use_system_lib: bool, dred_enabled: bool, + external_weights: bool, presume_avx: bool, target_arch: String, avx_allowed: bool, @@ -34,6 +35,7 @@ impl BuildOptions { fn from_env() -> Self { let use_system_lib = env::var("CARGO_FEATURE_SYSTEM_LIB").is_ok(); let dred_enabled = env::var("CARGO_FEATURE_DRED").is_ok(); + let external_weights = env::var("CARGO_FEATURE_EXTERNAL_WEIGHTS").is_ok(); let presume_avx = env::var("CARGO_FEATURE_PRESUME_AVX2").is_ok(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let avx_allowed = presume_avx && matches!(target_arch.as_str(), "x86" | "x86_64"); @@ -42,6 +44,7 @@ impl BuildOptions { Self { use_system_lib, dred_enabled, + external_weights, presume_avx, target_arch, avx_allowed, @@ -128,6 +131,7 @@ fn emit_rerun_directives() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=opus/opus_data-735117b.tar.gz"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SYSTEM_LIB"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_EXTERNAL_WEIGHTS"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_PRESUME_AVX2"); println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ENV"); println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FAMILY"); @@ -140,6 +144,11 @@ fn handle_system_lib(opts: &BuildOptions) { "cargo:warning=system-lib feature enabled; ensure the system libopus includes DRED support" ); } + if opts.external_weights { + println!( + "cargo:warning=external-weights cannot configure a system libopus; ensure it was built with USE_WEIGHTS_FILE" + ); + } if opts.presume_avx { println!( "cargo:warning=presume-avx2 feature enabled; ensure the system libopus was built with OPUS_X86_PRESUME_AVX2" @@ -198,6 +207,10 @@ fn build_bundled(opts: &BuildOptions, opus_source: &Path) -> std::path::PathBuf .define("OPUS_X86_MAY_HAVE_AVX2", "ON"); } + if opts.external_weights { + config.cflag("-DUSE_WEIGHTS_FILE"); + } + config.build() } @@ -369,6 +382,40 @@ fn should_skip_dred_generated_path(path: &Path) -> bool { ) || rel.starts_with("dnn/models/") } +fn download_dred_archive(archive_path: &Path, url: &str) { + use std::process::Command; + + let mut failures = Vec::new(); + let wget = Command::new("wget") + .arg("-O") + .arg(archive_path) + .arg(url) + .status(); + match wget { + Ok(status) if status.success() => return, + Ok(status) => failures.push(format!("wget exited with {status}")), + Err(err) => failures.push(format!("wget could not be started: {err}")), + } + + let curl = Command::new("curl") + .arg("--fail") + .arg("--location") + .arg("--output") + .arg(archive_path) + .arg(url) + .status(); + match curl { + Ok(status) if status.success() => return, + Ok(status) => failures.push(format!("curl exited with {status}")), + Err(err) => failures.push(format!("curl could not be started: {err}")), + } + + panic!( + "failed to download DRED model archive with wget or curl: {}", + failures.join("; ") + ); +} + fn ensure_dred_assets(opus_source: &Path, out_dir: &Path) { use std::path::Component; use std::process::Command; @@ -393,18 +440,8 @@ fn ensure_dred_assets(opus_source: &Path, out_dir: &Path) { out_dir.join(MODEL_ARCHIVE) }; if !archive_path.exists() { - let status = Command::new("wget") - .arg("-O") - .arg(&archive_path) - .arg(format!( - "https://media.xiph.org/opus/models/opus_data-{MODEL_REV}.tar.gz" - )) - .status() - .expect("failed to spawn wget for DRED model download"); - - if !status.success() { - panic!("downloading DRED model assets failed (exit status: {status})"); - } + let url = format!("https://media.xiph.org/opus/models/opus_data-{MODEL_REV}.tar.gz"); + download_dred_archive(&archive_path, &url); } let actual = sha256_hex(&archive_path); diff --git a/scripts/verify_external_weights.py b/scripts/verify_external_weights.py new file mode 100644 index 0000000..be2d086 --- /dev/null +++ b/scripts/verify_external_weights.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Generate a DNN blob and test runtime-loaded DRED weights end to end.""" + +import argparse +import hashlib +import os +import shlex +import shutil +import tempfile +from pathlib import Path + +import ci_utils + + +ROOT = Path(__file__).resolve().parent.parent +TARGET_DIR = os.environ.get("CARGO_TARGET_DIR", "target/ci-external-weights") +MODEL_ARCHIVE = "opus_data-735117b.tar.gz" +MODEL_SHA256 = "8f34305a299183509d22c7ba66790f67916a0fc56028ebd4c8f7b938458f2801" +MODEL_URL = f"https://media.xiph.org/opus/models/{MODEL_ARCHIVE}" + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def download_model_archive(archive: Path) -> None: + failures = [] + wget = shutil.which("wget") + if wget: + result = ci_utils.run([wget, "-O", str(archive), MODEL_URL], check=False) + if result.returncode == 0: + return + failures.append(f"wget exited with {result.returncode}") + else: + failures.append("wget was not found") + + curl = shutil.which("curl") + if curl: + result = ci_utils.run( + [curl, "--fail", "--location", "--output", str(archive), MODEL_URL], + check=False, + ) + if result.returncode == 0: + return + failures.append(f"curl exited with {result.returncode}") + else: + failures.append("curl was not found") + + ci_utils.fail( + "failed to download the DRED model archive with wget or curl: " + + "; ".join(failures) + ) + + +def ensure_model_archive() -> Path: + archive = ROOT / "opus" / MODEL_ARCHIVE + if not archive.exists(): + download_model_archive(archive) + + actual = sha256(archive) + if actual != MODEL_SHA256: + ci_utils.fail( + f"DRED model archive checksum mismatch: expected {MODEL_SHA256}, got {actual}" + ) + return archive + + +def generate_weights_blob(archive: Path, temp: Path) -> Path: + ci_utils.run(["tar", "xzf", str(archive), "-C", str(temp)]) + + compiler = shutil.which(os.environ.get("CC", "cc")) + if not compiler: + ci_utils.fail(f"C compiler {os.environ.get('CC', 'cc')!r} was not found") + + cflags = shlex.split(os.environ.get("CFLAGS", "")) + dnn = ROOT / "opus" / "dnn" + command = [ + compiler, + *cflags, + "-O2", + f"-I{ROOT / 'opus'}", + f"-I{ROOT / 'opus' / 'include'}", + f"-I{ROOT / 'opus' / 'celt'}", + f"-I{dnn}", + f"-I{temp / 'dnn'}", + str(dnn / "write_lpcnet_weights.c"), + str(dnn / "parse_lpcnet_weights.c"), + "-lm", + "-o", + str(temp / "dump_weights_blob"), + ] + ci_utils.run(command) + ci_utils.run([str(temp / "dump_weights_blob")], cwd=temp) + + blob = temp / "weights_blob.bin" + if not blob.is_file() or blob.stat().st_size == 0: + ci_utils.fail("weight blob generator produced no data") + print(f"Generated external DNN blob: {blob.stat().st_size} bytes") + return blob + + +def run_test(blob: Path, toolchain: str | None, target: str | None) -> None: + command = ["cargo"] + if toolchain: + command.append(f"+{toolchain}") + command += ["test"] + if target: + command += ["--target", target] + command += [ + "--features", + "external-weights", + "--test", + "external_weights", + "--", + "--ignored", + "--exact", + "external_weight_dred_round_trip", + ] + + env = { + "CARGO_TARGET_DIR": TARGET_DIR, + "OPUS_CODEC_DNN_BLOB": str(blob), + } + ci_utils.run(command, cwd=ROOT, env=env) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--toolchain") + parser.add_argument("--target") + args = parser.parse_args() + + archive = ensure_model_archive() + with tempfile.TemporaryDirectory(prefix="opus-external-weights-") as temp_dir: + blob = generate_weights_blob(archive, Path(temp_dir)) + run_test(blob, args.toolchain, args.target) + + +if __name__ == "__main__": + main() diff --git a/src/decoder.rs b/src/decoder.rs index 52783ed..e8abb9e 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -17,21 +17,54 @@ use crate::types::{Bandwidth, Channels, SampleRate}; use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; use std::num::NonZeroUsize; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::{self, NonNull}; +#[cfg(feature = "dred")] +struct RetainedDnnBlob { + data: Box<[u32]>, + len: i32, +} + +#[cfg(feature = "dred")] +impl RetainedDnnBlob { + fn parts(&self) -> (*const u8, i32) { + (self.data.as_ptr().cast::(), self.len) + } +} + /// Safe wrapper around a libopus `OpusDecoder`. pub struct Decoder { raw: RawHandle, sample_rate: SampleRate, channels: Channels, + // External-weight builds retain pointers into DNN blobs. Keep each copy, + // including copies used by failed non-transactional load attempts, until + // after the C decoder has been destroyed. Field declaration order makes + // `raw` drop before this storage. + #[cfg(feature = "dred")] + dnn_blobs: Vec, + #[cfg(feature = "dred")] + active_dnn_blob: Option, } unsafe impl Send for Decoder {} /// Borrowed wrapper around a decoder state. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::decoder::DecoderRef; +/// use opus_codec::Decoder; +/// fn extract<'a>(state: &mut DecoderRef<'a>, replacement: Decoder) -> Decoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct DecoderRef<'a> { inner: Decoder, + #[cfg(feature = "dred")] + active_dnn_blob: Option<(*const u8, i32)>, _marker: PhantomData<&'a mut OpusDecoder>, } @@ -48,6 +81,10 @@ impl Decoder { raw: RawHandle::new(ptr, ownership, opus_decoder_destroy), sample_rate, channels, + #[cfg(feature = "dred")] + dnn_blobs: Vec::new(), + #[cfg(feature = "dred")] + active_dnn_blob: None, } } @@ -306,6 +343,8 @@ impl Decoder { return Err(Error::from_code(result)); } + #[cfg(feature = "dred")] + self.reload_active_dnn_blob()?; Ok(()) } @@ -407,15 +446,48 @@ impl Decoder { /// Set DNN blob for DRED (feature-gated; will error if unsupported). /// /// # Safety - /// Caller must ensure `ptr` is valid for reads as expected by libopus for the duration of the call - /// and points to a properly formatted DNN blob. Passing an invalid or dangling pointer is UB. + /// `ptr` must be valid for reads of `len` bytes for the duration of this call and point to a + /// complete, correctly formatted libopus DNN blob. The bytes are copied into aligned storage + /// owned by the decoder, so the caller's allocation need not remain alive after this returns. + /// Some external-weight libopus builds do not safely handle malformed model records. /// /// # Errors - /// Returns [`Error::InvalidState`] if the decoder is invalid, or a mapped libopus error. + /// Returns [`Error::BadArg`] if `ptr` is null or `len` is non-positive, + /// [`Error::InvalidState`] if the decoder is invalid, or a mapped libopus error. pub unsafe fn set_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { + let blob_index = unsafe { self.retain_dnn_blob_copy(ptr, len)? }; + let (owned_ptr, owned_len) = self.dnn_blobs[blob_index].parts(); + if let Err(error) = unsafe { self.apply_dnn_blob(owned_ptr, owned_len) } { + if error == Error::Unimplemented { + // An unsupported CTL never inspected or retained the pointer. + // Other failures may leave model fields pointing into the blob. + let removed = self.dnn_blobs.pop(); + debug_assert!(removed.is_some()); + } + return Err(error); + } + self.active_dnn_blob = Some(blob_index); + Ok(()) + } + + #[cfg(feature = "dred")] + unsafe fn retain_dnn_blob_copy(&mut self, ptr: *const u8, len: i32) -> Result { if ptr.is_null() || len <= 0 { return Err(Error::BadArg); } + let byte_len = usize::try_from(len).map_err(|_| Error::BadArg)?; + let word_len = byte_len.div_ceil(std::mem::size_of::()); + let mut blob = vec![0u32; word_len].into_boxed_slice(); + unsafe { + std::ptr::copy_nonoverlapping(ptr, blob.as_mut_ptr().cast::(), byte_len); + } + let index = self.dnn_blobs.len(); + self.dnn_blobs.push(RetainedDnnBlob { data: blob, len }); + Ok(index) + } + + #[cfg(feature = "dred")] + unsafe fn apply_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { let r = unsafe { opus_decoder_ctl( self.raw.as_ptr(), @@ -430,6 +502,15 @@ impl Decoder { Ok(()) } + #[cfg(feature = "dred")] + fn reload_active_dnn_blob(&mut self) -> Result<()> { + let Some(index) = self.active_dnn_blob else { + return Ok(()); + }; + let (ptr, len) = self.dnn_blobs[index].parts(); + unsafe { self.apply_dnn_blob(ptr, len) } + } + // --- internal helpers for CTLs --- fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { let r = unsafe { opus_decoder_ctl(self.raw.as_ptr(), req, val) }; @@ -457,6 +538,8 @@ impl<'a> DecoderRef<'a> { /// - `sample_rate` and `channels` must exactly match the decoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// - If the external state already uses runtime-loaded DNN weights, register that blob again + /// through `DecoderRef::set_dnn_blob` before calling `DecoderRef::reset` /// /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer /// sizes against the wrong channel/rate and then call libopus with out-of-bounds buffers. @@ -479,6 +562,8 @@ impl<'a> DecoderRef<'a> { ); Self { inner: decoder, + #[cfg(feature = "dred")] + active_dnn_blob: None, _marker: PhantomData, } } @@ -500,6 +585,57 @@ impl<'a> DecoderRef<'a> { unsafe { Decoder::init_in_place(ptr, sample_rate, channels)? }; Ok(unsafe { Self::from_raw(ptr, sample_rate, channels) }) } + + delegate_ref_mut_methods! { + fn decode(input: &[u8], output: &mut [i16], fec: bool) -> Result; + fn decode_float(input: &[u8], output: &mut [f32], fec: bool) -> Result; + fn get_sample_rate() -> Result; + fn get_pitch() -> Result; + fn get_last_packet_duration() -> Result; + fn final_range() -> Result; + fn set_gain(q8_db: i32) -> Result<()>; + fn gain() -> Result; + fn phase_inversion_disabled() -> Result; + fn set_phase_inversion_disabled(disabled: bool) -> Result<()>; + } + + /// Reset the decoder and restore the last successfully registered external DNN model. + /// + /// # Errors + /// Returns a mapped libopus error if the reset or model restoration fails. + pub fn reset(&mut self) -> Result<()> { + self.inner.reset()?; + #[cfg(feature = "dred")] + if let Some((ptr, len)) = self.active_dnn_blob { + unsafe { self.inner.apply_dnn_blob(ptr, len)? }; + } + Ok(()) + } + + #[cfg(feature = "dred")] + /// Load an external DNN blob into this borrowed decoder state. + /// + /// Unlike [`Decoder::set_dnn_blob`], a borrowed wrapper cannot attach owned storage to the + /// external state. This method therefore passes the caller's allocation directly to libopus. + /// + /// # Safety + /// - `ptr` must point to `len` readable bytes containing a complete, correctly formatted + /// libopus DNN blob, and must be aligned to at least `align_of::()`. + /// - The allocation must remain fixed and readable until the external decoder state is + /// destroyed or will never be used again, even if this method returns an error. Dropping + /// this Rust wrapper alone does not end that requirement. + /// + /// # Errors + /// Returns [`Error::BadArg`] for invalid pointer metadata or alignment, or a mapped libopus + /// error when loading fails. + pub unsafe fn set_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { + if ptr.is_null() || len <= 0 || !ptr.addr().is_multiple_of(std::mem::align_of::()) { + return Err(Error::BadArg); + } + unsafe { self.inner.apply_dnn_blob(ptr, len)? }; + self.active_dnn_blob = Some((ptr, len)); + Ok(()) + } } impl Deref for DecoderRef<'_> { @@ -510,8 +646,26 @@ impl Deref for DecoderRef<'_> { } } -impl DerefMut for DecoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner +#[cfg(all(test, feature = "dred"))] +mod tests { + use super::*; + use crate::types::{Channels, SampleRate}; + + #[test] + fn dnn_blob_is_copied_into_retained_aligned_storage() { + let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); + let source = [0u8, 1, 2, 3, 4]; + let unaligned = unsafe { source.as_ptr().add(1) }; + + let index = unsafe { decoder.retain_dnn_blob_copy(unaligned, 4) }.unwrap(); + let (retained, len) = decoder.dnn_blobs[index].parts(); + + assert_eq!(len, 4); + assert_eq!((retained as usize) % std::mem::align_of::(), 0); + assert_eq!( + unsafe { std::slice::from_raw_parts(retained, 4) }, + &source[1..] + ); + assert_eq!(decoder.dnn_blobs.len(), 1); } } diff --git a/src/dred.rs b/src/dred.rs index 14f96fc..c9b2624 100644 --- a/src/dred.rs +++ b/src/dred.rs @@ -13,7 +13,7 @@ use crate::error::{Error, Result}; use crate::types::SampleRate; use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::NonNull; // libopus computes `100 * max_dred_samples / sampling_rate` in signed 32-bit math. @@ -22,11 +22,25 @@ const MAX_SAFE_DRED_SAMPLES: usize = (i32::MAX as usize) / 100; /// Managed handle for libopus `OpusDREDDecoder`. pub struct DredDecoder { raw: RawHandle, + // External-weight builds retain pointers into DNN blobs. Keep each copy, + // including copies used by failed non-transactional load attempts, until + // after the C decoder has been destroyed. Field declaration order makes + // `raw` drop before this storage. + dnn_blobs: Vec>, } unsafe impl Send for DredDecoder {} /// Borrowed wrapper around an externally allocated DRED decoder. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::dred::{DredDecoder, DredDecoderRef}; +/// fn extract<'a>(state: &mut DredDecoderRef<'a>, replacement: DredDecoder) -> DredDecoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct DredDecoderRef<'a> { inner: DredDecoder, _marker: PhantomData<&'a mut OpusDREDDecoder>, @@ -38,6 +52,7 @@ impl DredDecoder { fn from_raw(ptr: NonNull, ownership: Ownership) -> Self { Self { raw: RawHandle::new(ptr, ownership, opus_dred_decoder_destroy), + dnn_blobs: Vec::new(), } } @@ -103,37 +118,58 @@ impl DredDecoder { /// /// # Safety /// - /// - `data` must contain a complete, correctly formatted libopus DNN weights blob. Some - /// external-weight libopus builds do not safely handle malformed model records. - /// - The backing allocation must remain at the same address and valid for reads until this - /// decoder is dropped, even when this method returns an error. Libopus model layers may - /// retain pointers into the blob and model loading is not guaranteed to be transactional. + /// `data` must contain a complete, correctly formatted libopus DNN weights blob. Some + /// external-weight libopus builds do not safely handle malformed model records. The bytes are + /// copied into aligned storage owned by the decoder, so the caller's allocation need not + /// remain alive after this returns. /// /// # Errors /// - /// Returns [`Error::BadArg`] for an empty, misaligned, or overlong blob, + /// Returns [`Error::BadArg`] for an empty or overlong blob, /// [`Error::Unimplemented`] when the linked libopus was not built for external model /// weights, or another mapped libopus error when loading fails. pub unsafe fn set_dnn_blob(&mut self, data: &[u8]) -> Result<()> { - if data.is_empty() || !(data.as_ptr() as usize).is_multiple_of(std::mem::align_of::()) - { - return Err(Error::BadArg); - } - let len = i32::try_from(data.len()).map_err(|_| Error::BadArg)?; + let (owned_ptr, len) = self.retain_dnn_blob_copy(data)?; let r = unsafe { opus_dred_decoder_ctl( self.raw.as_ptr(), OPUS_SET_DNN_BLOB_REQUEST as i32, - data.as_ptr(), + owned_ptr, len, ) }; if r != 0 { - return Err(Error::from_code(r)); + let error = Error::from_code(r); + if error == Error::Unimplemented { + // An unsupported CTL never inspected or retained the pointer. + // Other failures may leave model fields pointing into the blob. + let removed = self.dnn_blobs.pop(); + debug_assert!(removed.is_some()); + } + return Err(error); } Ok(()) } + fn retain_dnn_blob_copy(&mut self, data: &[u8]) -> Result<(*const u8, i32)> { + if data.is_empty() { + return Err(Error::BadArg); + } + let len = i32::try_from(data.len()).map_err(|_| Error::BadArg)?; + let word_len = data.len().div_ceil(std::mem::size_of::()); + let mut blob = vec![0u32; word_len].into_boxed_slice(); + let blob_bytes = unsafe { + std::slice::from_raw_parts_mut( + blob.as_mut_ptr().cast::(), + std::mem::size_of_val(&*blob), + ) + }; + blob_bytes[..data.len()].copy_from_slice(data); + let owned_ptr = blob.as_ptr().cast::(); + self.dnn_blobs.push(blob); + Ok((owned_ptr, len)) + } + /// Parse DRED payload and update `state`. /// /// # Errors @@ -290,6 +326,51 @@ impl<'a> DredDecoderRef<'a> { unsafe { DredDecoder::init_in_place(ptr)? }; Ok(unsafe { Self::from_raw(ptr) }) } + + /// Borrow the raw external decoder pointer. + #[must_use] + pub fn as_mut_ptr(&mut self) -> *mut OpusDREDDecoder { + self.inner.as_mut_ptr() + } + + delegate_ref_mut_methods! { + fn parse(state: &mut DredState, data: &[u8], max_dred_samples: usize, sampling_rate: SampleRate, dred_end: &mut i32, defer_processing: bool) -> Result; + fn process(src: &DredState, dst: &mut DredState) -> Result<()>; + fn decode_into_i16(decoder: &mut Decoder, state: &DredState, dred_offset: i32, pcm: &mut [i16]) -> Result; + fn decode_into_f32(decoder: &mut Decoder, state: &DredState, dred_offset: i32, pcm: &mut [f32]) -> Result; + } + + /// Load an external DNN model blob into the borrowed DRED decoder state. + /// + /// # Safety + /// - `data` must contain a complete, correctly formatted libopus DNN weights blob and its + /// backing allocation must be aligned to at least `align_of::()`. + /// - The allocation must remain fixed and readable until the external decoder state is + /// destroyed or will never be used again, even if this method returns an error. Dropping + /// this Rust wrapper alone does not end that requirement. + /// + /// # Errors + /// Returns [`Error::BadArg`] for an empty, misaligned, or overlong blob, or a mapped libopus + /// error when loading fails. + pub unsafe fn set_dnn_blob(&mut self, data: &[u8]) -> Result<()> { + if data.is_empty() || !(data.as_ptr() as usize).is_multiple_of(std::mem::align_of::()) + { + return Err(Error::BadArg); + } + let len = i32::try_from(data.len()).map_err(|_| Error::BadArg)?; + let r = unsafe { + opus_dred_decoder_ctl( + self.inner.raw.as_ptr(), + OPUS_SET_DNN_BLOB_REQUEST as i32, + data.as_ptr(), + len, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } } impl Deref for DredDecoderRef<'_> { @@ -300,12 +381,6 @@ impl Deref for DredDecoderRef<'_> { } } -impl DerefMut for DredDecoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - fn validate_pcm_frame_len( pcm: &[T], channel_count: usize, @@ -443,6 +518,23 @@ mod tests { #[cfg(not(opus_codec_system_lib))] #[test] + fn dnn_blob_is_copied_into_retained_aligned_storage() { + let mut decoder = DredDecoder::new().expect("create bundled DRED decoder"); + let source = [0u8, 1, 2, 3, 4]; + + let (retained, len) = decoder.retain_dnn_blob_copy(&source[1..]).unwrap(); + + assert_eq!(len, 4); + assert_eq!((retained as usize) % std::mem::align_of::(), 0); + assert_eq!( + unsafe { std::slice::from_raw_parts(retained, 4) }, + &source[1..] + ); + assert_eq!(decoder.dnn_blobs.len(), 1); + } + + #[cfg(all(not(opus_codec_system_lib), not(feature = "external-weights")))] + #[test] fn typed_dnn_blob_ctl_has_checked_input_and_exact_abi() { let model_word = 0u32; let mut decoder = DredDecoder::new().expect("create bundled DRED decoder"); @@ -458,5 +550,6 @@ mod tests { unsafe { decoder.set_dnn_blob(aligned_blob) }, Err(Error::Unimplemented) ); + assert!(decoder.dnn_blobs.is_empty()); } } diff --git a/src/encoder.rs b/src/encoder.rs index b1f83e7..fa22296 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -18,7 +18,9 @@ use crate::bindings::{ opus_encoder_get_size, opus_encoder_init, }; #[cfg(feature = "dred")] -use crate::bindings::{OPUS_GET_DRED_DURATION_REQUEST, OPUS_SET_DRED_DURATION_REQUEST}; +use crate::bindings::{ + OPUS_GET_DRED_DURATION_REQUEST, OPUS_SET_DNN_BLOB_REQUEST, OPUS_SET_DRED_DURATION_REQUEST, +}; use crate::constants::max_frame_samples_for; use crate::error::{Error, Result}; use crate::types::{ @@ -27,21 +29,54 @@ use crate::types::{ use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; use std::num::NonZeroUsize; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::NonNull; +#[cfg(feature = "dred")] +struct RetainedDnnBlob { + data: Box<[u32]>, + len: i32, +} + +#[cfg(feature = "dred")] +impl RetainedDnnBlob { + fn parts(&self) -> (*const u8, i32) { + (self.data.as_ptr().cast::(), self.len) + } +} + /// Safe wrapper around a libopus `OpusEncoder`. pub struct Encoder { raw: RawHandle, sample_rate: SampleRate, channels: Channels, + // External-weight builds retain pointers into DNN blobs. Keep each copy, + // including copies used by failed non-transactional load attempts, until + // after the C encoder has been destroyed. Field declaration order makes + // `raw` drop before this storage. + #[cfg(feature = "dred")] + dnn_blobs: Vec, + #[cfg(feature = "dred")] + active_dnn_blob: Option, } unsafe impl Send for Encoder {} /// Borrowed wrapper around an encoder state. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::encoder::EncoderRef; +/// use opus_codec::Encoder; +/// fn extract<'a>(state: &mut EncoderRef<'a>, replacement: Encoder) -> Encoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct EncoderRef<'a> { inner: Encoder, + #[cfg(feature = "dred")] + active_dnn_blob: Option<(*const u8, i32)>, _marker: PhantomData<&'a mut OpusEncoder>, } @@ -58,6 +93,10 @@ impl Encoder { raw: RawHandle::new(ptr, ownership, opus_encoder_destroy), sample_rate, channels, + #[cfg(feature = "dred")] + dnn_blobs: Vec::new(), + #[cfg(feature = "dred")] + active_dnn_blob: None, } } @@ -582,6 +621,75 @@ impl Encoder { self.get_int_ctl(OPUS_GET_DRED_DURATION_REQUEST as i32) } + #[cfg(feature = "dred")] + /// Load an external DNN model blob into this encoder. + /// + /// # Safety + /// `ptr` must be valid for reads of `len` bytes for the duration of this call and point to a + /// complete, correctly formatted libopus DNN blob. The bytes are copied into aligned storage + /// owned by the encoder, so the caller's allocation need not remain alive after this returns. + /// Some external-weight libopus builds do not safely handle malformed model records. + /// + /// # Errors + /// Returns [`Error::BadArg`] if `ptr` is null or `len` is non-positive, or a mapped libopus + /// error when loading fails. Embedded-weight libopus builds return [`Error::Unimplemented`]. + pub unsafe fn set_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { + let blob_index = unsafe { self.retain_dnn_blob_copy(ptr, len)? }; + let (owned_ptr, owned_len) = self.dnn_blobs[blob_index].parts(); + if let Err(error) = unsafe { self.apply_dnn_blob(owned_ptr, owned_len) } { + if error == Error::Unimplemented { + // An unsupported CTL never inspected or retained the pointer. + // Other failures may leave model fields pointing into the blob. + let removed = self.dnn_blobs.pop(); + debug_assert!(removed.is_some()); + } + return Err(error); + } + self.active_dnn_blob = Some(blob_index); + Ok(()) + } + + #[cfg(feature = "dred")] + unsafe fn retain_dnn_blob_copy(&mut self, ptr: *const u8, len: i32) -> Result { + if ptr.is_null() || len <= 0 { + return Err(Error::BadArg); + } + let byte_len = usize::try_from(len).map_err(|_| Error::BadArg)?; + let word_len = byte_len.div_ceil(std::mem::size_of::()); + let mut blob = vec![0u32; word_len].into_boxed_slice(); + unsafe { + std::ptr::copy_nonoverlapping(ptr, blob.as_mut_ptr().cast::(), byte_len); + } + let index = self.dnn_blobs.len(); + self.dnn_blobs.push(RetainedDnnBlob { data: blob, len }); + Ok(index) + } + + #[cfg(feature = "dred")] + unsafe fn apply_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { + let r = unsafe { + opus_encoder_ctl( + self.raw.as_ptr(), + OPUS_SET_DNN_BLOB_REQUEST as i32, + ptr, + len, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } + + #[cfg(feature = "dred")] + fn reload_active_dnn_blob(&mut self) -> Result<()> { + let Some(index) = self.active_dnn_blob else { + return Ok(()); + }; + let (ptr, len) = self.dnn_blobs[index].parts(); + unsafe { self.apply_dnn_blob(ptr, len) } + } + // --- internal helpers --- fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { let r = unsafe { opus_encoder_ctl(self.raw.as_ptr(), req, val) }; @@ -756,6 +864,8 @@ impl Encoder { if r != 0 { return Err(Error::from_code(r)); } + #[cfg(feature = "dred")] + self.reload_active_dnn_blob()?; Ok(()) } } @@ -769,6 +879,8 @@ impl<'a> EncoderRef<'a> { /// - `sample_rate` and `channels` must exactly match the encoder state already stored at `ptr` /// - The memory must remain valid for the lifetime `'a` /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// - If the external state already uses runtime-loaded DNN weights, register that blob again + /// through `EncoderRef::set_dnn_blob` before calling `EncoderRef::reset` /// /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer /// sizes against the wrong channel/rate and then call libopus with out-of-bounds buffers. @@ -791,6 +903,8 @@ impl<'a> EncoderRef<'a> { ); Self { inner: encoder, + #[cfg(feature = "dred")] + active_dnn_blob: None, _marker: PhantomData, } } @@ -813,6 +927,87 @@ impl<'a> EncoderRef<'a> { unsafe { Encoder::init_in_place(ptr, sample_rate, channels, application)? }; Ok(unsafe { Self::from_raw(ptr, sample_rate, channels) }) } + + delegate_ref_mut_methods! { + fn encode(input: &[i16], output: &mut [u8]) -> Result; + fn encode_limited(input: &[i16], output: &mut [u8], max_data_bytes: usize) -> Result; + fn encode_float(input: &[f32], output: &mut [u8]) -> Result; + fn set_inband_fec(enabled: bool) -> Result<()>; + fn inband_fec() -> Result; + fn set_packet_loss_perc(perc: i32) -> Result<()>; + fn packet_loss_perc() -> Result; + fn set_dtx(enabled: bool) -> Result<()>; + fn dtx() -> Result; + fn in_dtx() -> Result; + fn set_vbr_constraint(constrained: bool) -> Result<()>; + fn vbr_constraint() -> Result; + fn set_max_bandwidth(bw: Bandwidth) -> Result<()>; + fn max_bandwidth() -> Result; + fn set_bandwidth(bw: Bandwidth) -> Result<()>; + fn bandwidth() -> Result; + fn set_force_channels(channels: Option) -> Result<()>; + fn force_channels() -> Result>; + fn set_signal(signal: Signal) -> Result<()>; + fn signal() -> Result; + fn lookahead() -> Result; + fn final_range() -> Result; + fn set_lsb_depth(bits: i32) -> Result<()>; + fn lsb_depth() -> Result; + fn set_expert_frame_duration(dur: ExpertFrameDuration) -> Result<()>; + fn expert_frame_duration() -> Result; + fn set_prediction_disabled(disabled: bool) -> Result<()>; + fn prediction_disabled() -> Result; + fn set_phase_inversion_disabled(disabled: bool) -> Result<()>; + fn phase_inversion_disabled() -> Result; + #[cfg(feature = "dred")] + fn set_dred_duration(frames_10ms: i32) -> Result<()>; + #[cfg(feature = "dred")] + fn dred_duration() -> Result; + fn set_bitrate(bitrate: Bitrate) -> Result<()>; + fn bitrate() -> Result; + fn set_complexity(complexity: Complexity) -> Result<()>; + fn complexity() -> Result; + fn set_vbr(enabled: bool) -> Result<()>; + fn vbr() -> Result; + } + + /// Reset the encoder and restore the last successfully registered external DNN model. + /// + /// # Errors + /// Returns a mapped libopus error if the reset or model restoration fails. + pub fn reset(&mut self) -> Result<()> { + self.inner.reset()?; + #[cfg(feature = "dred")] + if let Some((ptr, len)) = self.active_dnn_blob { + unsafe { self.inner.apply_dnn_blob(ptr, len)? }; + } + Ok(()) + } + + #[cfg(feature = "dred")] + /// Load an external DNN blob into this borrowed encoder state. + /// + /// Unlike [`Encoder::set_dnn_blob`], a borrowed wrapper cannot retain storage beyond the + /// wrapper's lifetime. This method therefore passes the caller's allocation to libopus. + /// + /// # Safety + /// - `ptr` must point to `len` readable bytes containing a complete, correctly formatted + /// libopus DNN blob, and must be aligned to at least `align_of::()`. + /// - The allocation must remain fixed and readable until the external encoder state is + /// destroyed or will never be used again, even if this method returns an error. Dropping + /// this Rust wrapper alone does not end that requirement. + /// + /// # Errors + /// Returns [`Error::BadArg`] for invalid pointer metadata or alignment, or a mapped libopus + /// error when loading fails. Embedded-weight libopus builds return [`Error::Unimplemented`]. + pub unsafe fn set_dnn_blob(&mut self, ptr: *const u8, len: i32) -> Result<()> { + if ptr.is_null() || len <= 0 || !ptr.addr().is_multiple_of(std::mem::align_of::()) { + return Err(Error::BadArg); + } + unsafe { self.inner.apply_dnn_blob(ptr, len)? }; + self.active_dnn_blob = Some((ptr, len)); + Ok(()) + } } impl Deref for EncoderRef<'_> { @@ -823,8 +1018,26 @@ impl Deref for EncoderRef<'_> { } } -impl DerefMut for EncoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner +#[cfg(all(test, feature = "dred"))] +mod tests { + use super::*; + + #[test] + fn dnn_blob_is_copied_into_retained_aligned_storage() { + let mut encoder = + Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Audio).unwrap(); + let source = [0u8, 1, 2, 3, 4]; + let unaligned = unsafe { source.as_ptr().add(1) }; + + let index = unsafe { encoder.retain_dnn_blob_copy(unaligned, 4) }.unwrap(); + let (retained, len) = encoder.dnn_blobs[index].parts(); + + assert_eq!(len, 4); + assert_eq!((retained as usize) % std::mem::align_of::(), 0); + assert_eq!( + unsafe { std::slice::from_raw_parts(retained, 4) }, + &source[1..] + ); + assert_eq!(encoder.dnn_blobs.len(), 1); } } diff --git a/src/lib.rs b/src/lib.rs index 5c480fa..5caf211 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,46 @@ #![allow(clippy::cast_possible_wrap)] #![allow(clippy::cast_possible_truncation)] +// Borrowed state wrappers must not expose `&mut` access to their lifetime-erased +// owning handle: doing so would let safe code move that handle out with +// `mem::replace`. Generate explicit forwarding methods instead. +macro_rules! delegate_ref_mut_methods { + ($( + $(#[$meta:meta])* + fn $name:ident($($arg:ident: $arg_ty:ty),* $(,)?) -> $ret:ty; + )*) => {$( + $(#[$meta])* + #[doc = "Calls the corresponding operation on this borrowed libopus state."] + /// + /// # Errors + /// Returns the same errors as the corresponding owning-handle method. + #[inline] + pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret { + self.inner.$name($($arg),*) + } + )*}; +} + +macro_rules! delegate_ref_unsafe_mut_methods { + ($( + $(#[$meta:meta])* + unsafe fn $name:ident($($arg:ident: $arg_ty:ty),* $(,)?) -> $ret:ty; + )*) => {$( + $(#[$meta])* + #[doc = "Calls the corresponding unsafe operation on this borrowed libopus state."] + /// + /// # Safety + /// The safety requirements of the corresponding owning-handle method apply. + /// + /// # Errors + /// Returns the same errors as the corresponding owning-handle method. + #[inline] + pub unsafe fn $name(&mut self, $($arg: $arg_ty),*) -> $ret { + unsafe { self.inner.$name($($arg),*) } + } + )*}; +} + // Include the generated bindings #[allow(warnings)] #[allow(clippy::all)] @@ -61,7 +101,7 @@ pub(crate) enum Ownership { #[inline] pub(crate) fn opus_ptr_is_aligned(ptr: *const u8) -> bool { // libopus aligns internal state to pointer-sized alignment (opus_private.h align()). - (ptr as usize).is_multiple_of(std::mem::align_of::()) + ptr.addr().is_multiple_of(std::mem::align_of::()) } /// Returns the bundled libopus version string of this crate. diff --git a/src/multistream.rs b/src/multistream.rs index 5e0e36c..a944a94 100644 --- a/src/multistream.rs +++ b/src/multistream.rs @@ -6,7 +6,7 @@ use crate::bindings::{ OPUS_GET_BANDWIDTH_REQUEST, OPUS_GET_BITRATE_REQUEST, OPUS_GET_COMPLEXITY_REQUEST, OPUS_GET_DTX_REQUEST, OPUS_GET_FINAL_RANGE_REQUEST, OPUS_GET_FORCE_CHANNELS_REQUEST, OPUS_GET_GAIN_REQUEST, OPUS_GET_IN_DTX_REQUEST, OPUS_GET_INBAND_FEC_REQUEST, - OPUS_GET_LAST_PACKET_DURATION_REQUEST, OPUS_GET_LOOKAHEAD_REQUEST, + OPUS_GET_LAST_PACKET_DURATION_REQUEST, OPUS_GET_LOOKAHEAD_REQUEST, OPUS_GET_LSB_DEPTH_REQUEST, OPUS_GET_MAX_BANDWIDTH_REQUEST, OPUS_GET_PACKET_LOSS_PERC_REQUEST, OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST, OPUS_GET_PITCH_REQUEST, OPUS_GET_SAMPLE_RATE_REQUEST, OPUS_GET_SIGNAL_REQUEST, OPUS_GET_VBR_CONSTRAINT_REQUEST, @@ -14,7 +14,7 @@ use crate::bindings::{ OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, OPUS_RESET_STATE, OPUS_SET_BANDWIDTH_REQUEST, OPUS_SET_BITRATE_REQUEST, OPUS_SET_COMPLEXITY_REQUEST, OPUS_SET_DTX_REQUEST, OPUS_SET_FORCE_CHANNELS_REQUEST, OPUS_SET_GAIN_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, - OPUS_SET_MAX_BANDWIDTH_REQUEST, OPUS_SET_PACKET_LOSS_PERC_REQUEST, + OPUS_SET_LSB_DEPTH_REQUEST, OPUS_SET_MAX_BANDWIDTH_REQUEST, OPUS_SET_PACKET_LOSS_PERC_REQUEST, OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, OPUS_SET_SIGNAL_REQUEST, OPUS_SET_VBR_CONSTRAINT_REQUEST, OPUS_SET_VBR_REQUEST, OPUS_SIGNAL_MUSIC, OPUS_SIGNAL_VOICE, OpusDecoder, OpusEncoder, OpusMSDecoder, OpusMSEncoder, opus_decoder_ctl, opus_encoder_ctl, @@ -32,7 +32,7 @@ use crate::types::{Application, Bandwidth, Bitrate, Channels, Complexity, Sample use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; use std::num::{NonZeroU8, NonZeroUsize}; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::NonNull; /// Describes the multistream mapping configuration. @@ -220,6 +220,18 @@ pub struct MultistreamEncoder { unsafe impl Send for MultistreamEncoder {} /// Borrowed wrapper around a multistream encoder. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::multistream::{MultistreamEncoder, MultistreamEncoderRef}; +/// fn extract<'a>( +/// state: &mut MultistreamEncoderRef<'a>, +/// replacement: MultistreamEncoder, +/// ) -> MultistreamEncoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct MultistreamEncoderRef<'a> { inner: MultistreamEncoder, _marker: PhantomData<&'a mut OpusMSEncoder>, @@ -767,6 +779,26 @@ impl MultistreamEncoder { self.get_int_ctl(OPUS_GET_LOOKAHEAD_REQUEST as i32) } + /// Set the effective input signal depth in bits. + /// + /// # Errors + /// Returns [`Error::BadArg`] when `bits` is outside `8..=24`, + /// [`Error::InvalidState`] if the encoder handle is null, or a mapped libopus error. + pub fn set_lsb_depth(&mut self, bits: i32) -> Result<()> { + if !(8..=24).contains(&bits) { + return Err(Error::BadArg); + } + self.simple_ctl(OPUS_SET_LSB_DEPTH_REQUEST as i32, bits) + } + + /// Query the effective input signal depth in bits. + /// + /// # Errors + /// Returns [`Error::InvalidState`] if the encoder handle is null or a mapped libopus error. + pub fn lsb_depth(&mut self) -> Result { + self.get_int_ctl(OPUS_GET_LSB_DEPTH_REQUEST as i32) + } + /// Reset the encoder state (retaining configuration). /// /// # Errors @@ -1021,6 +1053,43 @@ impl<'a> MultistreamEncoderRef<'a> { }; Ok((encoder, mapping)) } + + delegate_ref_mut_methods! { + fn encode(pcm: &[i16], frame_size_per_ch: usize, out: &mut [u8]) -> Result; + fn encode_float(pcm: &[f32], frame_size_per_ch: usize, out: &mut [u8]) -> Result; + fn final_range() -> Result; + fn set_bitrate(bitrate: Bitrate) -> Result<()>; + fn bitrate() -> Result; + fn set_complexity(complexity: Complexity) -> Result<()>; + fn complexity() -> Result; + fn set_dtx(enabled: bool) -> Result<()>; + fn dtx() -> Result; + fn in_dtx() -> Result; + fn set_inband_fec(enabled: bool) -> Result<()>; + fn inband_fec() -> Result; + fn set_packet_loss_perc(perc: i32) -> Result<()>; + fn packet_loss_perc() -> Result; + fn set_vbr(enabled: bool) -> Result<()>; + fn vbr() -> Result; + fn set_vbr_constraint(constrained: bool) -> Result<()>; + fn vbr_constraint() -> Result; + fn set_max_bandwidth(bw: Bandwidth) -> Result<()>; + fn max_bandwidth() -> Result; + fn set_bandwidth(bw: Bandwidth) -> Result<()>; + fn bandwidth() -> Result; + fn set_force_channels(channels: Option) -> Result<()>; + fn force_channels() -> Result>; + fn set_signal(signal: Signal) -> Result<()>; + fn signal() -> Result; + fn lookahead() -> Result; + fn set_lsb_depth(bits: i32) -> Result<()>; + fn lsb_depth() -> Result; + fn reset() -> Result<()>; + } + + delegate_ref_unsafe_mut_methods! { + unsafe fn encoder_state_ptr(stream_index: i32) -> Result<*mut OpusEncoder>; + } } impl Deref for MultistreamEncoderRef<'_> { @@ -1031,12 +1100,6 @@ impl Deref for MultistreamEncoderRef<'_> { } } -impl DerefMut for MultistreamEncoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - /// Safe wrapper around `OpusMSDecoder`. pub struct MultistreamDecoder { raw: RawHandle, @@ -1047,6 +1110,18 @@ pub struct MultistreamDecoder { unsafe impl Send for MultistreamDecoder {} /// Borrowed wrapper around a multistream decoder. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::multistream::{MultistreamDecoder, MultistreamDecoderRef}; +/// fn extract<'a>( +/// state: &mut MultistreamDecoderRef<'a>, +/// replacement: MultistreamDecoder, +/// ) -> MultistreamDecoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct MultistreamDecoderRef<'a> { inner: MultistreamDecoder, _marker: PhantomData<&'a mut OpusMSDecoder>, @@ -1507,6 +1582,24 @@ impl<'a> MultistreamDecoderRef<'a> { unsafe { MultistreamDecoder::init_in_place(ptr, sr, mapping)? }; Ok(unsafe { Self::from_raw(ptr, sr, mapping) }) } + + delegate_ref_mut_methods! { + fn decode(packet: &[u8], out: &mut [i16], frame_size_per_ch: usize, fec: bool) -> Result; + fn decode_float(packet: &[u8], out: &mut [f32], frame_size_per_ch: usize, fec: bool) -> Result; + fn final_range() -> Result; + fn reset() -> Result<()>; + fn set_gain(q8_db: i32) -> Result<()>; + fn gain() -> Result; + fn set_phase_inversion_disabled(disabled: bool) -> Result<()>; + fn phase_inversion_disabled() -> Result; + fn get_sample_rate() -> Result; + fn get_pitch() -> Result; + fn get_last_packet_duration() -> Result; + } + + delegate_ref_unsafe_mut_methods! { + unsafe fn decoder_state_ptr(stream_index: i32) -> Result<*mut OpusDecoder>; + } } impl Deref for MultistreamDecoderRef<'_> { @@ -1517,12 +1610,6 @@ impl Deref for MultistreamDecoderRef<'_> { } } -impl DerefMut for MultistreamDecoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/packet.rs b/src/packet.rs index 020a5c4..d836cdc 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -149,9 +149,7 @@ pub fn soft_clip( if softclip_mem.len() < channels_usize { return Err(Error::BadArg); } - let needed_samples = frame_size_per_ch - .checked_mul(channels_usize) - .ok_or(Error::BadArg)?; + let needed_samples = checked_soft_clip_sample_count(frame_size_per_ch, channels_usize)?; if pcm.len() < needed_samples { return Err(Error::BadArg); } @@ -167,6 +165,19 @@ pub fn soft_clip( Ok(()) } +fn checked_soft_clip_sample_count(frame_size_per_ch: usize, channels: usize) -> Result { + let needed_samples = frame_size_per_ch + .checked_mul(channels) + .ok_or(Error::BadArg)?; + // opus_pcm_soft_clip() evaluates N*C and all sample offsets in signed + // C `int` arithmetic. A larger Rust slice would therefore still make the + // C implementation overflow before it accessed the full slice. + if needed_samples > i32::MAX as usize { + return Err(Error::BadArg); + } + Ok(needed_samples) +} + /// Parse a packet into caller-provided frame storage. /// /// Returns `(toc, payload_offset, frame_count)`. The first `frame_count` @@ -203,6 +214,9 @@ pub fn packet_parse_into<'packet>( return Err(Error::from_code(n)); } let count = usize::try_from(n).map_err(|_| Error::InternalError)?; + if count > MAX_FRAMES_PER_PACKET { + return Err(Error::InternalError); + } if count > frames.len() { return Err(Error::BufferTooSmall); } @@ -228,14 +242,14 @@ pub fn packet_parse_into<'packet>( starts[i] = start; lengths[i] = size; } + let payload_offset = usize::try_from(payload_offset).map_err(|_| Error::InternalError)?; + if payload_offset > packet.len() { + return Err(Error::InvalidPacket); + } for i in 0..count { frames[i] = &packet[starts[i]..starts[i] + lengths[i]]; } - Ok(( - out_toc, - usize::try_from(payload_offset).map_err(|_| Error::InternalError)?, - count, - )) + Ok((out_toc, payload_offset, count)) } /// Parse packet into frame slices. Returns `(toc, payload_offset, frames)`. @@ -400,3 +414,21 @@ pub fn multistream_packet_unpad(packet: &mut [u8], len: usize, nb_streams: i32) } usize::try_from(n).map_err(|_| Error::InternalError) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn soft_clip_rejects_sample_products_that_overflow_c_int() { + let frame_size = i32::MAX as usize / 2 + 1; + assert_eq!( + checked_soft_clip_sample_count(frame_size, 2), + Err(Error::BadArg) + ); + assert_eq!( + checked_soft_clip_sample_count(i32::MAX as usize, 1), + Ok(i32::MAX as usize) + ); + } +} diff --git a/src/projection.rs b/src/projection.rs index 4bcf5ec..7236144 100644 --- a/src/projection.rs +++ b/src/projection.rs @@ -16,7 +16,7 @@ use crate::types::{Application, Bitrate, SampleRate}; use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; use std::num::{NonZeroU8, NonZeroUsize}; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::NonNull; fn validate_channels(channels: u8) -> Result<()> { @@ -55,6 +55,18 @@ pub struct ProjectionEncoder { unsafe impl Send for ProjectionEncoder {} /// Borrowed wrapper around a projection encoder state. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::projection::{ProjectionEncoder, ProjectionEncoderRef}; +/// fn extract<'a>( +/// state: &mut ProjectionEncoderRef<'a>, +/// replacement: ProjectionEncoder, +/// ) -> ProjectionEncoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct ProjectionEncoderRef<'a> { inner: ProjectionEncoder, _marker: PhantomData<&'a mut OpusProjectionEncoder>, @@ -470,6 +482,17 @@ impl<'a> ProjectionEncoderRef<'a> { }; Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled) }) } + + delegate_ref_mut_methods! { + fn encode(pcm: &[i16], frame_size_per_ch: usize, out: &mut [u8]) -> Result; + fn encode_float(pcm: &[f32], frame_size_per_ch: usize, out: &mut [u8]) -> Result; + fn set_bitrate(bitrate: Bitrate) -> Result<()>; + fn bitrate() -> Result; + fn demixing_matrix_size() -> Result; + fn demixing_matrix_gain() -> Result; + fn write_demixing_matrix(out: &mut [u8]) -> Result; + fn demixing_matrix_bytes() -> Result>; + } } impl Deref for ProjectionEncoderRef<'_> { @@ -480,12 +503,6 @@ impl Deref for ProjectionEncoderRef<'_> { } } -impl DerefMut for ProjectionEncoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - /// Safe wrapper around `OpusProjectionDecoder`. pub struct ProjectionDecoder { raw: RawHandle, @@ -498,6 +515,18 @@ pub struct ProjectionDecoder { unsafe impl Send for ProjectionDecoder {} /// Borrowed wrapper around a projection decoder state. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::projection::{ProjectionDecoder, ProjectionDecoderRef}; +/// fn extract<'a>( +/// state: &mut ProjectionDecoderRef<'a>, +/// replacement: ProjectionDecoder, +/// ) -> ProjectionDecoder { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct ProjectionDecoderRef<'a> { inner: ProjectionDecoder, _marker: PhantomData<&'a mut OpusProjectionDecoder>, @@ -848,6 +877,11 @@ impl<'a> ProjectionDecoderRef<'a> { } Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled_streams) }) } + + delegate_ref_mut_methods! { + fn decode(packet: &[u8], out: &mut [i16], frame_size_per_ch: usize, fec: bool) -> Result; + fn decode_float(packet: &[u8], out: &mut [f32], frame_size_per_ch: usize, fec: bool) -> Result; + } } impl Deref for ProjectionDecoderRef<'_> { @@ -857,9 +891,3 @@ impl Deref for ProjectionDecoderRef<'_> { &self.inner } } - -impl DerefMut for ProjectionDecoderRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} diff --git a/src/repacketizer.rs b/src/repacketizer.rs index 1887111..6cd37c9 100644 --- a/src/repacketizer.rs +++ b/src/repacketizer.rs @@ -10,7 +10,7 @@ use crate::error::{Error, Result}; use crate::packet; use crate::{AlignedBuffer, Ownership, RawHandle}; use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use std::ptr::NonNull; /// Repackages Opus frames into packets. @@ -28,6 +28,15 @@ struct RetainedPacket { unsafe impl Send for Repacketizer {} /// Borrowed wrapper around a repacketizer state. +/// +/// The owning handle cannot be moved out of this borrowed wrapper: +/// +/// ```compile_fail +/// use opus_codec::repacketizer::{Repacketizer, RepacketizerRef}; +/// fn extract<'a>(state: &mut RepacketizerRef<'a>, replacement: Repacketizer) -> Repacketizer { +/// std::mem::replace(&mut **state, replacement) +/// } +/// ``` pub struct RepacketizerRef<'a> { inner: Repacketizer, _marker: PhantomData<&'a mut OpusRepacketizer>, @@ -345,6 +354,14 @@ impl<'a> RepacketizerRef<'a> { unsafe { Repacketizer::init_in_place(ptr)? }; Ok(unsafe { Self::from_raw(ptr) }) } + + delegate_ref_mut_methods! { + fn reset() -> (); + fn push(packet: &[u8]) -> Result<()>; + fn push_owned(packet: Vec) -> Result<()>; + fn emit_range(begin: i32, end: i32, out: &mut [u8]) -> Result; + fn emit(out: &mut [u8]) -> Result; + } } impl Drop for RepacketizerRef<'_> { @@ -367,12 +384,6 @@ impl Deref for RepacketizerRef<'_> { } } -impl DerefMut for RepacketizerRef<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - #[cfg(all(test, opus_codec_rust_packet_ops))] mod tests { use super::*; diff --git a/tests/ctl_compatibility.rs b/tests/ctl_compatibility.rs index 2e343c0..559457c 100644 --- a/tests/ctl_compatibility.rs +++ b/tests/ctl_compatibility.rs @@ -85,8 +85,9 @@ fn every_single_stream_ctl_is_supported_by_bundled_opus() { Ok(()) }), ("lsb_depth", |encoder| { + assert_eq!(encoder.set_lsb_depth(7), Err(Error::BadArg)); encoder.set_lsb_depth(16)?; - let _ = encoder.lsb_depth()?; + assert_eq!(encoder.lsb_depth()?, 16); Ok(()) }), ("expert_frame_duration", |encoder| { @@ -167,6 +168,11 @@ fn every_multistream_ctl_is_supported_by_bundled_opus() { let _ = encoder.complexity()?; Ok(()) }), + ("lsb_depth", |encoder| { + encoder.set_lsb_depth(16)?; + let _ = encoder.lsb_depth()?; + Ok(()) + }), ("dtx", |encoder| { encoder.set_dtx(true)?; let _ = encoder.dtx()?; @@ -305,6 +311,10 @@ fn every_dred_ctl_uses_the_bundled_opus_signature() { Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Audio).unwrap(); encoder.set_dred_duration(2).unwrap(); assert_eq!(encoder.dred_duration().unwrap(), 2); + assert_eq!( + unsafe { encoder.set_dnn_blob(std::ptr::null(), 0) }, + Err(Error::BadArg) + ); let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); assert_eq!( diff --git a/tests/external_weights.rs b/tests/external_weights.rs new file mode 100644 index 0000000..c8f3ade --- /dev/null +++ b/tests/external_weights.rs @@ -0,0 +1,215 @@ +#![cfg(feature = "external-weights")] + +use opus_codec::decoder::DecoderRef; +use opus_codec::dred::{DredDecoder, DredState}; +use opus_codec::encoder::EncoderRef; +use opus_codec::{ + AlignedBuffer, Application, Bitrate, Channels, Decoder, Encoder, Result, SampleRate, +}; + +const SAMPLE_RATE: SampleRate = SampleRate::Hz48000; +const FRAME_SAMPLES: usize = 960; + +fn aligned_blob_copy(source: &[u8]) -> Box<[u32]> { + let word_len = source.len().div_ceil(std::mem::size_of::()); + let mut words = vec![0u32; word_len].into_boxed_slice(); + let bytes = unsafe { + std::slice::from_raw_parts_mut( + words.as_mut_ptr().cast::(), + std::mem::size_of_val(&*words), + ) + }; + bytes[..source.len()].copy_from_slice(source); + words +} + +fn encode_until_dred( + mut encode: impl FnMut(&[i16], &mut [u8]) -> Result, + dred_decoder: &mut DredDecoder, + dred_state: &mut DredState, + noise: &mut u32, +) -> Vec { + let mut packet_storage = vec![0u8; 4_000]; + for _ in 0..300 { + let mut pcm = vec![0i16; FRAME_SAMPLES]; + for sample in &mut pcm { + *noise = noise.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + *sample = ((*noise >> 17) as i16).wrapping_sub(16_384); + } + + let packet_len = encode(&pcm, &mut packet_storage).expect("encode packet"); + let packet = &packet_storage[..packet_len]; + let mut dred_end = 0; + let dred_amount = dred_decoder + .parse( + dred_state, + packet, + 48_000, + SAMPLE_RATE, + &mut dred_end, + false, + ) + .expect("parse DRED packet"); + if dred_amount >= FRAME_SAMPLES { + return packet.to_vec(); + } + } + panic!("encoder produced no decodable DRED payload"); +} + +fn decode_packet_and_loss( + mut decode: impl FnMut(&[u8], &mut [i16], bool) -> Result, + packet: &[u8], +) { + let mut pcm = vec![0i16; FRAME_SAMPLES]; + assert_eq!( + decode(packet, &mut pcm, false).expect("decode packet"), + FRAME_SAMPLES + ); + assert_eq!( + decode(&[], &mut pcm, false).expect("decode packet loss"), + FRAME_SAMPLES + ); +} + +fn configure_encoder(encoder: &mut Encoder) { + encoder + .set_bitrate(Bitrate::Custom(32_000)) + .expect("set bitrate"); + encoder + .set_packet_loss_perc(20) + .expect("set expected packet loss"); + encoder.set_dred_duration(100).expect("enable DRED"); +} + +fn configure_borrowed_encoder(encoder: &mut EncoderRef<'_>) { + encoder + .set_bitrate(Bitrate::Custom(32_000)) + .expect("set bitrate"); + encoder + .set_packet_loss_perc(20) + .expect("set expected packet loss"); + encoder.set_dred_duration(100).expect("enable DRED"); +} + +#[test] +#[ignore = "requires libopus built with USE_WEIGHTS_FILE and OPUS_CODEC_DNN_BLOB"] +fn external_weight_dred_round_trip() { + let blob_path = std::env::var_os("OPUS_CODEC_DNN_BLOB") + .expect("OPUS_CODEC_DNN_BLOB must name a valid weights blob"); + let source = std::fs::read(blob_path).expect("read weights blob"); + let blob_len = i32::try_from(source.len()).expect("blob length fits i32"); + let borrowed_blob = aligned_blob_copy(&source); + let borrowed_blob_ptr = borrowed_blob.as_ptr().cast::(); + + let mut dred_decoder = DredDecoder::new().expect("create DRED decoder"); + unsafe { + dred_decoder + .set_dnn_blob(&source) + .expect("load DRED decoder weights"); + } + + let mut noise = 0x1234_5678u32; + { + let mut encoder = + Encoder::new(SAMPLE_RATE, Channels::Mono, Application::Voip).expect("create encoder"); + let mut decoder = Decoder::new(SAMPLE_RATE, Channels::Mono).expect("create decoder"); + unsafe { + encoder + .set_dnn_blob(source.as_ptr(), blob_len) + .expect("load encoder weights"); + decoder + .set_dnn_blob(source.as_ptr(), blob_len) + .expect("load decoder weights"); + } + + // All owning handles retain aligned copies. + drop(source); + + configure_encoder(&mut encoder); + let mut dred_state = DredState::new().expect("create DRED state"); + let _packet = encode_until_dred( + |pcm, out| encoder.encode(pcm, out), + &mut dred_decoder, + &mut dred_state, + &mut noise, + ); + let mut dred_pcm = vec![0i16; FRAME_SAMPLES]; + assert_eq!( + dred_decoder + .decode_into_i16( + &mut decoder, + &dred_state, + FRAME_SAMPLES as i32, + &mut dred_pcm, + ) + .expect("decode DRED audio"), + FRAME_SAMPLES + ); + + encoder.reset().expect("reset encoder and restore weights"); + decoder.reset().expect("reset decoder and restore weights"); + // OPUS_RESET_STATE resets this runtime setting even though it preserves bitrate settings. + encoder.set_dred_duration(100).expect("re-enable DRED"); + + let packet = encode_until_dred( + |pcm, out| encoder.encode(pcm, out), + &mut dred_decoder, + &mut dred_state, + &mut noise, + ); + assert!(packet[0] >> 3 < 16, "deep-PLC test requires SILK/hybrid"); + assert_eq!( + dred_decoder + .decode_into_i16( + &mut decoder, + &dred_state, + FRAME_SAMPLES as i32, + &mut dred_pcm, + ) + .expect("decode DRED audio after reset"), + FRAME_SAMPLES + ); + decode_packet_and_loss(|data, out, fec| decoder.decode(data, out, fec), &packet); + } + + // Borrowed wrappers keep caller-owned pointer metadata and reapply it after reset. + let mut encoder_storage = + AlignedBuffer::with_capacity_bytes(Encoder::size(Channels::Mono).expect("encoder size")); + let mut decoder_storage = + AlignedBuffer::with_capacity_bytes(Decoder::size(Channels::Mono).expect("decoder size")); + let mut encoder = EncoderRef::init_in( + &mut encoder_storage, + SAMPLE_RATE, + Channels::Mono, + Application::Voip, + ) + .expect("create borrowed encoder"); + let mut decoder = DecoderRef::init_in(&mut decoder_storage, SAMPLE_RATE, Channels::Mono) + .expect("create borrowed decoder"); + unsafe { + encoder + .set_dnn_blob(borrowed_blob_ptr, blob_len) + .expect("load borrowed encoder weights"); + decoder + .set_dnn_blob(borrowed_blob_ptr, blob_len) + .expect("load borrowed decoder weights"); + } + encoder + .reset() + .expect("reset borrowed encoder and restore weights"); + decoder + .reset() + .expect("reset borrowed decoder and restore weights"); + configure_borrowed_encoder(&mut encoder); + + let mut dred_state = DredState::new().expect("create borrowed-path DRED state"); + let packet = encode_until_dred( + |pcm, out| encoder.encode(pcm, out), + &mut dred_decoder, + &mut dred_state, + &mut noise, + ); + assert!(packet[0] >> 3 < 16, "deep-PLC test requires SILK/hybrid"); + decode_packet_and_loss(|data, out, fec| decoder.decode(data, out, fec), &packet); +}