From 142aa3b511707447886760d336f2602350ed6d92 Mon Sep 17 00:00:00 2001 From: Denis Avvakumov Date: Sat, 20 Dec 2025 10:25:22 +0200 Subject: [PATCH] Add raw allocation APIs, split tests, and enforce MSRV in CI --- .github/workflows/ci.yml | 12 + Cargo.lock | 10 +- Cargo.toml | 3 +- README.md | 10 + src/alloc.rs | 29 ++ src/decoder.rs | 222 +++++++++---- src/dred.rs | 143 ++++++--- src/encoder.rs | 310 +++++++++++------- src/lib.rs | 23 +- src/multistream.rs | 648 +++++++++++++++++++++++++++++--------- src/packet.rs | 7 +- src/projection.rs | 411 ++++++++++++++++++++---- src/raw.rs | 32 ++ src/repacketizer.rs | 156 +++++++-- src/types.rs | 6 +- tests/encoder_decoder.rs | 104 ++++++ tests/ffmpeg_roundtrip.rs | 6 +- tests/multistream.rs | 145 +++++++++ tests/opus_tests.rs | 212 ------------- tests/packet_utils.rs | 64 ++++ tests/projection.rs | 101 ++++++ tests/raw_alloc.rs | 14 + tests/repacketizer.rs | 83 +++++ 23 files changed, 2082 insertions(+), 669 deletions(-) create mode 100644 src/alloc.rs create mode 100644 src/raw.rs create mode 100644 tests/encoder_decoder.rs create mode 100644 tests/multistream.rs delete mode 100755 tests/opus_tests.rs create mode 100644 tests/packet_utils.rs create mode 100644 tests/projection.rs create mode 100644 tests/raw_alloc.rs create mode 100644 tests/repacketizer.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a020f6..c627743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,16 @@ jobs: - name: cargo check run: cargo check --all-targets + msrv: + name: MSRV Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@1.87.0 + - name: cargo check + run: cargo check --all-targets + tests: name: Tests runs-on: ${{ matrix.os }} @@ -101,6 +111,8 @@ jobs: run: | sudo apt-get update sudo apt-get install -y ffmpeg wget + - name: Clippy with dred + run: cargo clippy --features dred --all-targets -- -D warnings - name: Build with dred run: cargo build --features dred - name: Test with dred diff --git a/Cargo.lock b/Cargo.lock index 88aca2a..f1f2e3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "shlex", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -195,7 +195,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opus-codec" -version = "0.1.1" +version = "0.1.2" dependencies = [ "bindgen", "cmake", diff --git a/Cargo.toml b/Cargo.toml index 2f21a4c..fb11bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "opus-codec" -version = "0.1.1" +version = "0.1.2" edition = "2024" authors = ["Denis Avvakumov"] description = "Safe Rust bindings for the Opus audio codec" +rust-version = "1.87" license = "MIT OR Apache-2.0" repository = "https://github.com/Deniskore/opus-codec" homepage = "https://github.com/Deniskore" diff --git a/README.md b/README.md index f9b4773..82ef0bf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # opus-codec +[![Build Status](https://github.com/Deniskore/opus-codec/actions/workflows/ci.yml/badge.svg)](https://github.com/Deniskore/opus-codec/actions/workflows/ci.yml) +[![Crates.io](https://img.shields.io/crates/v/opus-codec.svg)](https://crates.io/crates/opus-codec) +[![API reference](https://docs.rs/opus-codec/badge.svg)](https://docs.rs/opus-codec) +[![MSRV](https://img.shields.io/badge/MSRV-1.87.0-blue)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) +[![License](https://img.shields.io/crates/l/opus-codec.svg)](https://crates.io/crates/opus-codec) + Safe Rust wrappers around libopus for encoding/decoding Opus audio, with tests that validate core functionality against ffmpeg. ## Features @@ -8,6 +14,10 @@ Safe Rust wrappers around libopus for encoding/decoding Opus audio, with tests t - `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. - `system-lib`: Link against a system-provided libopus instead of the bundled sources. +## MSRV + +Minimum Supported Rust Version (MSRV): **1.87.0**. + ## License This crate is licensed under either of diff --git a/src/alloc.rs b/src/alloc.rs new file mode 100644 index 0000000..6959267 --- /dev/null +++ b/src/alloc.rs @@ -0,0 +1,29 @@ +/// Aligned storage for libopus state structures. +#[derive(Debug)] +pub struct AlignedBuffer { + buf: Vec, + bytes: usize, +} + +impl AlignedBuffer { + /// Allocate at least `bytes` of pointer-aligned storage. + #[must_use] + pub fn with_capacity_bytes(bytes: usize) -> Self { + let word = std::mem::size_of::(); + let len = bytes.div_ceil(word); + let buf = vec![0usize; len]; + let bytes = len * word; + Self { buf, bytes } + } + + /// Total available capacity in bytes. + #[must_use] + pub fn capacity_bytes(&self) -> usize { + self.bytes + } + + /// Borrow the underlying buffer as a mutable pointer. + pub fn as_mut_ptr(&mut self) -> *mut T { + self.buf.as_mut_ptr().cast() + } +} diff --git a/src/decoder.rs b/src/decoder.rs index a6c41db..3fb4e83 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -10,16 +10,21 @@ use crate::bindings::{ OPUS_GET_SAMPLE_RATE_REQUEST, OPUS_RESET_STATE, OPUS_SET_GAIN_REQUEST, OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, OpusDecoder, opus_decode, opus_decode_float, opus_decoder_create, opus_decoder_ctl, opus_decoder_destroy, opus_decoder_get_nb_samples, + opus_decoder_get_size, opus_decoder_init, }; use crate::constants::max_frame_samples_for; use crate::error::{Error, Result}; use crate::packet; use crate::types::{Bandwidth, Channels, SampleRate}; -use std::ptr; +use crate::{AlignedBuffer, Ownership, RawHandle}; +use std::marker::PhantomData; +use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; +use std::ptr::{self, NonNull}; /// Safe wrapper around a libopus `OpusDecoder`. pub struct Decoder { - raw: *mut OpusDecoder, + raw: RawHandle, sample_rate: SampleRate, channels: Channels, } @@ -27,7 +32,68 @@ pub struct Decoder { unsafe impl Send for Decoder {} unsafe impl Sync for Decoder {} +/// Borrowed wrapper around a decoder state. +pub struct DecoderRef<'a> { + inner: Decoder, + _marker: PhantomData<&'a mut OpusDecoder>, +} + +unsafe impl Send for DecoderRef<'_> {} +unsafe impl Sync for DecoderRef<'_> {} + impl Decoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: Channels, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_decoder_destroy), + sample_rate, + channels, + } + } + + /// Size in bytes of a decoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the channel count is invalid or libopus reports + /// an impossible size. + pub fn size(channels: Channels) -> Result { + let raw = unsafe { opus_decoder_get_size(channels.as_i32()) }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated decoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `Decoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] if `ptr` is null, or a mapped libopus error. + pub unsafe fn init_in_place( + ptr: *mut OpusDecoder, + sample_rate: SampleRate, + channels: Channels, + ) -> Result<()> { + if ptr.is_null() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + let r = unsafe { opus_decoder_init(ptr, sample_rate.as_i32(), channels.as_i32()) }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } + /// Create a new decoder for a given sample rate and channel layout. /// /// # Errors @@ -51,15 +117,14 @@ impl Decoder { return Err(Error::from_code(error)); } - if decoder.is_null() { - return Err(Error::AllocFail); - } + let decoder = NonNull::new(decoder).ok_or(Error::AllocFail)?; - Ok(Self { - raw: decoder, + Ok(Self::from_raw( + decoder, sample_rate, channels, - }) + Ownership::Owned, + )) } /// Decode a packet into 16-bit PCM. @@ -74,10 +139,6 @@ impl Decoder { /// [`Error::from_code`]. pub fn decode(&mut self, input: &[u8], output: &mut [i16], fec: bool) -> Result { // Errors: InvalidState, BadArg, or libopus error mapped. - if self.raw.is_null() { - return Err(Error::InvalidState); - } - // Validate buffer sizes up-front if !input.is_empty() && input.len() > i32::MAX as usize { return Err(Error::BadArg); @@ -89,8 +150,9 @@ impl Decoder { return Err(Error::BadArg); } let frame_size = output.len() / self.channels.as_usize(); + let frame_size = NonZeroUsize::new(frame_size).ok_or(Error::BadArg)?; let max_frame = max_frame_samples_for(self.sample_rate); - if frame_size == 0 || frame_size > max_frame { + if frame_size.get() > max_frame { return Err(Error::BadArg); } @@ -99,11 +161,11 @@ impl Decoder { } else { i32::try_from(input.len()).map_err(|_| Error::BadArg)? }; - let frame_size_i32 = i32::try_from(frame_size).map_err(|_| Error::BadArg)?; + let frame_size_i32 = i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)?; let result = unsafe { opus_decode( - self.raw, + self.raw.as_ptr(), if input.is_empty() { ptr::null() } else { @@ -132,10 +194,6 @@ impl Decoder { /// for invalid buffer sizes or frame sizes, or a mapped libopus error via /// [`Error::from_code`]. pub fn decode_float(&mut self, input: &[u8], output: &mut [f32], fec: bool) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - // Validate buffer sizes up-front if !input.is_empty() && input.len() > i32::MAX as usize { return Err(Error::BadArg); @@ -147,8 +205,9 @@ impl Decoder { return Err(Error::BadArg); } let frame_size = output.len() / self.channels.as_usize(); + let frame_size = NonZeroUsize::new(frame_size).ok_or(Error::BadArg)?; let max_frame = max_frame_samples_for(self.sample_rate); - if frame_size == 0 || frame_size > max_frame { + if frame_size.get() > max_frame { return Err(Error::BadArg); } @@ -157,11 +216,11 @@ impl Decoder { } else { i32::try_from(input.len()).map_err(|_| Error::BadArg)? }; - let frame_size_i32 = i32::try_from(frame_size).map_err(|_| Error::BadArg)?; + let frame_size_i32 = i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)?; let result = unsafe { opus_decode_float( - self.raw, + self.raw.as_ptr(), if input.is_empty() { ptr::null() } else { @@ -188,15 +247,12 @@ impl Decoder { /// overlong input, or a mapped libopus error. pub fn packet_samples(&self, packet: &[u8]) -> Result { // Errors: InvalidState or libopus error mapped. - if self.raw.is_null() { - return Err(Error::InvalidState); - } - if packet.len() > i32::MAX as usize { return Err(Error::BadArg); } let len_i32 = i32::try_from(packet.len()).map_err(|_| Error::BadArg)?; - let result = unsafe { opus_decoder_get_nb_samples(self.raw, packet.as_ptr(), len_i32) }; + let result = + unsafe { opus_decoder_get_nb_samples(self.raw.as_ptr(), packet.as_ptr(), len_i32) }; if result < 0 { return Err(Error::from_code(result)); @@ -212,10 +268,6 @@ impl Decoder { /// if the packet cannot be parsed. pub fn packet_bandwidth(&self, packet: &[u8]) -> Result { // Errors: InvalidState or InvalidPacket. - if self.raw.is_null() { - return Err(Error::InvalidState); - } - packet::packet_bandwidth(packet) } @@ -226,10 +278,6 @@ impl Decoder { /// if the packet cannot be parsed. pub fn packet_channels(&self, packet: &[u8]) -> Result { // Errors: InvalidState or InvalidPacket. - if self.raw.is_null() { - return Err(Error::InvalidState); - } - packet::packet_channels(packet) } @@ -240,12 +288,8 @@ impl Decoder { /// if resetting fails. pub fn reset(&mut self) -> Result<()> { // Errors: InvalidState or request failure. - if self.raw.is_null() { - return Err(Error::InvalidState); - } - // OPUS_RESET_STATE takes no additional argument. Passing extras is undefined behavior. - let result = unsafe { opus_decoder_ctl(self.raw, OPUS_RESET_STATE as i32) }; + let result = unsafe { opus_decoder_ctl(self.raw.as_ptr(), OPUS_RESET_STATE as i32) }; if result != 0 { return Err(Error::from_code(result)); @@ -268,7 +312,7 @@ impl Decoder { #[cfg_attr(not(feature = "dred"), allow(dead_code))] pub(crate) fn as_mut_ptr(&mut self) -> *mut OpusDecoder { - self.raw + self.raw.as_ptr() } /// Query decoder output sample rate. @@ -300,11 +344,14 @@ impl Decoder { /// # Errors /// Returns [`Error::InvalidState`] if the decoder is invalid, or a mapped libopus error. pub fn final_range(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: u32 = 0; - let r = unsafe { opus_decoder_ctl(self.raw, OPUS_GET_FINAL_RANGE_REQUEST as i32, &mut v) }; + let r = unsafe { + opus_decoder_ctl( + self.raw.as_ptr(), + OPUS_GET_FINAL_RANGE_REQUEST as i32, + &mut v, + ) + }; if r != 0 { return Err(Error::from_code(r)); } @@ -371,13 +418,17 @@ impl Decoder { /// # Errors /// Returns [`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<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } if ptr.is_null() || len <= 0 { return Err(Error::BadArg); } - let r = unsafe { opus_decoder_ctl(self.raw, OPUS_SET_DNN_BLOB_REQUEST as i32, ptr, len) }; + let r = unsafe { + opus_decoder_ctl( + self.raw.as_ptr(), + OPUS_SET_DNN_BLOB_REQUEST as i32, + ptr, + len, + ) + }; if r != 0 { return Err(Error::from_code(r)); } @@ -386,21 +437,15 @@ impl Decoder { // --- internal helpers for CTLs --- fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_decoder_ctl(self.raw, req, val) }; + let r = unsafe { opus_decoder_ctl(self.raw.as_ptr(), req, val) }; if r != 0 { return Err(Error::from_code(r)); } Ok(()) } fn get_int_ctl(&mut self, req: i32) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: i32 = 0; - let r = unsafe { opus_decoder_ctl(self.raw, req, &mut v) }; + let r = unsafe { opus_decoder_ctl(self.raw.as_ptr(), req, &mut v) }; if r != 0 { return Err(Error::from_code(r)); } @@ -408,10 +453,65 @@ impl Decoder { } } -impl Drop for Decoder { - fn drop(&mut self) { - unsafe { - opus_decoder_destroy(self.raw); +impl<'a> DecoderRef<'a> { + /// Wrap an externally-initialized decoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`Decoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`Decoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw( + ptr: *mut OpusDecoder, + sample_rate: SampleRate, + channels: Channels, + ) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let decoder = Decoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sample_rate, + channels, + Ownership::Borrowed, + ); + Self { + inner: decoder, + _marker: PhantomData, + } + } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sample_rate: SampleRate, + channels: Channels, + ) -> Result { + let required = Decoder::size(channels)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); } + let ptr = buf.as_mut_ptr::(); + unsafe { Decoder::init_in_place(ptr, sample_rate, channels)? }; + Ok(unsafe { Self::from_raw(ptr, sample_rate, channels) }) + } +} + +impl Deref for DecoderRef<'_> { + type Target = Decoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DecoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } diff --git a/src/dred.rs b/src/dred.rs index d764212..7a9f188 100644 --- a/src/dred.rs +++ b/src/dred.rs @@ -11,16 +11,35 @@ use crate::constants::max_frame_samples_for; use crate::decoder::Decoder; 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::ptr::NonNull; /// Managed handle for libopus `OpusDREDDecoder`. pub struct DredDecoder { - raw: *mut OpusDREDDecoder, + raw: RawHandle, } unsafe impl Send for DredDecoder {} unsafe impl Sync for DredDecoder {} +/// Borrowed wrapper around an externally allocated DRED decoder. +pub struct DredDecoderRef<'a> { + inner: DredDecoder, + _marker: PhantomData<&'a mut OpusDREDDecoder>, +} + +unsafe impl Send for DredDecoderRef<'_> {} +unsafe impl Sync for DredDecoderRef<'_> {} + impl DredDecoder { + fn from_raw(ptr: NonNull, ownership: Ownership) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_dred_decoder_destroy), + } + } + /// Allocate a new DRED decoder. /// /// # Errors @@ -33,10 +52,8 @@ impl DredDecoder { if err != 0 { return Err(Error::from_code(err)); } - if ptr.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { raw: ptr }) + let ptr = NonNull::new(ptr).ok_or(Error::AllocFail)?; + Ok(Self::from_raw(ptr, Ownership::Owned)) } /// Initialize an externally allocated decoder buffer. @@ -48,10 +65,13 @@ impl DredDecoder { /// # Errors /// /// Returns a mapped libopus error if initialization fails. - pub unsafe fn init_raw(ptr: *mut OpusDREDDecoder) -> Result<()> { + pub unsafe fn init_in_place(ptr: *mut OpusDREDDecoder) -> Result<()> { if ptr.is_null() { return Err(Error::BadArg); } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } let r = unsafe { opus_dred_decoder_init(ptr) }; if r != 0 { return Err(Error::from_code(r)); @@ -61,7 +81,7 @@ impl DredDecoder { /// Borrow the raw decoder pointer. pub fn as_mut_ptr(&mut self) -> *mut OpusDREDDecoder { - self.raw + self.raw.as_ptr() } /// Size of a decoder object in bytes. @@ -86,14 +106,11 @@ impl DredDecoder { /// /// Returns [`Error::InvalidState`] if the decoder is invalid, or a mapped libopus /// error when the control call fails. - pub unsafe fn ctl(&mut self, request: i32, arg: T) -> Result<()> + pub unsafe fn control(&mut self, request: i32, arg: T) -> Result<()> where T: Copy, { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_dred_decoder_ctl(self.raw, request, arg) }; + let r = unsafe { opus_dred_decoder_ctl(self.raw.as_ptr(), request, arg) }; if r != 0 { return Err(Error::from_code(r)); } @@ -115,15 +132,12 @@ impl DredDecoder { dred_end: &mut i32, defer_processing: bool, ) -> Result { - if self.raw.is_null() || state.raw.is_null() { - return Err(Error::InvalidState); - } let len = i32::try_from(data.len()).map_err(|_| Error::BadArg)?; let max_samples = i32::try_from(max_dred_samples).map_err(|_| Error::BadArg)?; let result = unsafe { opus_dred_parse( - self.raw, - state.raw, + self.raw.as_ptr(), + state.raw.as_ptr(), data.as_ptr(), len, max_samples, @@ -145,10 +159,7 @@ impl DredDecoder { /// Returns [`Error::InvalidState`] if pointers are invalid, or a mapped libopus /// error when [`opus_dred_process`] fails. pub fn process(&mut self, src: &DredState, dst: &mut DredState) -> Result<()> { - if self.raw.is_null() || src.raw.is_null() || dst.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_dred_process(self.raw, src.raw, dst.raw) }; + let r = unsafe { opus_dred_process(self.raw.as_ptr(), src.raw.as_ptr(), dst.raw.as_ptr()) }; if r != 0 { return Err(Error::from_code(r)); } @@ -169,15 +180,12 @@ impl DredDecoder { dred_offset: i32, pcm: &mut [i16], ) -> Result { - if self.raw.is_null() || state.raw.is_null() { - return Err(Error::InvalidState); - } let channel_count = decoder.channels().as_usize(); let frame_size = validate_pcm_frame_len(pcm, channel_count, decoder.sample_rate())?; let result = unsafe { opus_decoder_dred_decode( decoder.as_mut_ptr(), - state.raw, + state.raw.as_ptr(), dred_offset, pcm.as_mut_ptr(), frame_size, @@ -203,15 +211,12 @@ impl DredDecoder { dred_offset: i32, pcm: &mut [f32], ) -> Result { - if self.raw.is_null() || state.raw.is_null() { - return Err(Error::InvalidState); - } let channel_count = decoder.channels().as_usize(); let frame_size = validate_pcm_frame_len(pcm, channel_count, decoder.sample_rate())?; let result = unsafe { opus_decoder_dred_decode_float( decoder.as_mut_ptr(), - state.raw, + state.raw.as_ptr(), dred_offset, pcm.as_mut_ptr(), frame_size, @@ -224,12 +229,54 @@ impl DredDecoder { } } -impl Drop for DredDecoder { - fn drop(&mut self) { - if !self.raw.is_null() { - unsafe { opus_dred_decoder_destroy(self.raw) }; +impl<'a> DredDecoderRef<'a> { + /// Wrap an externally-initialized DRED decoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`DredDecoder::size()`] bytes + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`DredDecoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw(ptr: *mut OpusDREDDecoder) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let decoder = + DredDecoder::from_raw(unsafe { NonNull::new_unchecked(ptr) }, Ownership::Borrowed); + Self { + inner: decoder, + _marker: PhantomData, } } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in(buf: &'a mut AlignedBuffer) -> Result { + let required = DredDecoder::size()?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { DredDecoder::init_in_place(ptr)? }; + Ok(unsafe { Self::from_raw(ptr) }) + } +} + +impl Deref for DredDecoderRef<'_> { + type Target = DredDecoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DredDecoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } fn validate_pcm_frame_len( @@ -243,7 +290,7 @@ fn validate_pcm_frame_len( if pcm.is_empty() { return Err(Error::BadArg); } - if pcm.len() % channel_count != 0 { + if !pcm.len().is_multiple_of(channel_count) { return Err(Error::BadArg); } let frame_size_per_ch = pcm.len() / channel_count; @@ -255,7 +302,7 @@ fn validate_pcm_frame_len( /// Managed handle for libopus `OpusDRED` state. pub struct DredState { - raw: *mut OpusDRED, + raw: NonNull, } unsafe impl Send for DredState {} @@ -274,40 +321,36 @@ impl DredState { if err != 0 { return Err(Error::from_code(err)); } - if ptr.is_null() { - return Err(Error::AllocFail); - } + let ptr = NonNull::new(ptr).ok_or(Error::AllocFail)?; Ok(Self { raw: ptr }) } - /// Size of a DRED state in bytes. - /// - /// # Panics - /// - /// Panics if libopus reports a negative size, which would indicate a - /// mismatch with the bundled headers. /// Size of a DRED state in bytes. /// /// # Errors /// - /// Returns [`Error::InternalError`] if libopus reports an invalid (negative) - /// size, indicating a mismatch with the bundled headers. + /// Returns [`Error::Unimplemented`] if DRED is disabled in the linked + /// libopus, or [`Error::InternalError`] if libopus reports an invalid size. pub fn size() -> Result { let raw = unsafe { opus_dred_get_size() }; + if raw == 0 { + return Err(Error::Unimplemented); + } + if raw < 0 { + return Err(Error::InternalError); + } usize::try_from(raw).map_err(|_| Error::InternalError) } /// Borrow the raw pointer. pub fn as_mut_ptr(&mut self) -> *mut OpusDRED { - self.raw + self.raw.as_ptr() } } impl Drop for DredState { fn drop(&mut self) { - if !self.raw.is_null() { - unsafe { opus_dred_free(self.raw) }; - } + unsafe { opus_dred_free(self.raw.as_ptr()) }; } } diff --git a/src/encoder.rs b/src/encoder.rs index 435ed70..0b65bd9 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -15,16 +15,22 @@ use crate::bindings::{ OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, OPUS_SET_PREDICTION_DISABLED_REQUEST, OPUS_SET_SIGNAL_REQUEST, OPUS_SET_VBR_CONSTRAINT_REQUEST, OPUS_SET_VBR_REQUEST, OpusEncoder, opus_encode, opus_encode_float, opus_encoder_create, opus_encoder_ctl, opus_encoder_destroy, + opus_encoder_get_size, opus_encoder_init, }; use crate::constants::max_frame_samples_for; use crate::error::{Error, Result}; use crate::types::{ Application, Bandwidth, Bitrate, Channels, Complexity, ExpertFrameDuration, SampleRate, Signal, }; +use crate::{AlignedBuffer, Ownership, RawHandle}; +use std::marker::PhantomData; +use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; /// Safe wrapper around a libopus `OpusEncoder`. pub struct Encoder { - raw: *mut OpusEncoder, + raw: RawHandle, sample_rate: SampleRate, channels: Channels, } @@ -32,7 +38,76 @@ pub struct Encoder { unsafe impl Send for Encoder {} unsafe impl Sync for Encoder {} +/// Borrowed wrapper around an encoder state. +pub struct EncoderRef<'a> { + inner: Encoder, + _marker: PhantomData<&'a mut OpusEncoder>, +} + +unsafe impl Send for EncoderRef<'_> {} +unsafe impl Sync for EncoderRef<'_> {} + impl Encoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: Channels, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_encoder_destroy), + sample_rate, + channels, + } + } + + /// Size in bytes of an encoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the channel count is invalid or libopus reports + /// an impossible size. + pub fn size(channels: Channels) -> Result { + let raw = unsafe { opus_encoder_get_size(channels.as_i32()) }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated encoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `Encoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] if `ptr` is null, or a mapped libopus error. + pub unsafe fn init_in_place( + ptr: *mut OpusEncoder, + sample_rate: SampleRate, + channels: Channels, + application: Application, + ) -> Result<()> { + if ptr.is_null() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + let r = unsafe { + opus_encoder_init( + ptr, + sample_rate.as_i32(), + channels.as_i32(), + application as i32, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } + /// Create a new encoder. /// /// # Errors @@ -61,15 +136,14 @@ impl Encoder { return Err(Error::from_code(error)); } - if encoder.is_null() { - return Err(Error::AllocFail); - } + let encoder = NonNull::new(encoder).ok_or(Error::AllocFail)?; - Ok(Self { - raw: encoder, + Ok(Self::from_raw( + encoder, sample_rate, channels, - }) + Ownership::Owned, + )) } /// Encode 16-bit PCM into an Opus packet. @@ -78,10 +152,6 @@ impl Encoder { /// Returns [`Error::InvalidState`] if the encoder is invalid, [`Error::BadArg`] for /// invalid buffer sizes or frame size, or a mapped libopus error. pub fn encode(&mut self, input: &[i16], output: &mut [u8]) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - // Validate input buffer size if input.is_empty() { return Err(Error::BadArg); @@ -93,8 +163,9 @@ impl Encoder { } let frame_size = input.len() / self.channels.as_usize(); + let frame_size = NonZeroUsize::new(frame_size).ok_or(Error::BadArg)?; // Validate frame size is within Opus limits for the configured sample rate - if frame_size == 0 || frame_size > max_frame_samples_for(self.sample_rate) { + if frame_size.get() > max_frame_samples_for(self.sample_rate) { return Err(Error::BadArg); } @@ -106,11 +177,11 @@ impl Encoder { return Err(Error::BadArg); } - let frame_size_i32 = i32::try_from(frame_size).map_err(|_| Error::BadArg)?; + let frame_size_i32 = i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)?; let out_len_i32 = i32::try_from(output.len()).map_err(|_| Error::BadArg)?; let result = unsafe { opus_encode( - self.raw, + self.raw.as_ptr(), input.as_ptr(), frame_size_i32, output.as_mut_ptr(), @@ -139,10 +210,6 @@ impl Encoder { output: &mut [u8], max_data_bytes: usize, ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - // Validate input buffer size if input.is_empty() { return Err(Error::BadArg); @@ -154,8 +221,9 @@ impl Encoder { } let frame_size = input.len() / self.channels.as_usize(); + let frame_size = NonZeroUsize::new(frame_size).ok_or(Error::BadArg)?; // Validate frame size is within Opus limits for the configured sample rate - if frame_size == 0 || frame_size > max_frame_samples_for(self.sample_rate) { + if frame_size.get() > max_frame_samples_for(self.sample_rate) { return Err(Error::BadArg); } @@ -171,11 +239,11 @@ impl Encoder { return Err(Error::BadArg); } - let frame_size_i32 = i32::try_from(frame_size).map_err(|_| Error::BadArg)?; + let frame_size_i32 = i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)?; let max_bytes_i32 = i32::try_from(max_data_bytes).map_err(|_| Error::BadArg)?; let result = unsafe { opus_encode( - self.raw, + self.raw.as_ptr(), input.as_ptr(), frame_size_i32, output.as_mut_ptr(), @@ -190,32 +258,12 @@ impl Encoder { usize::try_from(result).map_err(|_| Error::InternalError) } - /// Deprecated alias for `encode_limited` (does not itself enable FEC). - /// - /// # Errors - /// Returns [`Error::InvalidState`] if the encoder is invalid, [`Error::BadArg`] for - /// invalid buffer sizes or frame size, or a mapped libopus error. - #[deprecated( - note = "Renamed to encode_limited; enabling FEC requires set_inband_fec + set_packet_loss_perc" - )] - pub fn encode_with_fec( - &mut self, - input: &[i16], - output: &mut [u8], - max_data_bytes: usize, - ) -> Result { - self.encode_limited(input, output, max_data_bytes) - } - /// Encode f32 PCM into an Opus packet. /// /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, [`Error::BadArg`] for /// invalid buffer sizes or frame size, or a mapped libopus error. pub fn encode_float(&mut self, input: &[f32], output: &mut [u8]) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } if input.is_empty() { return Err(Error::BadArg); } @@ -223,17 +271,18 @@ impl Encoder { return Err(Error::BadArg); } let frame_size = input.len() / self.channels.as_usize(); - if frame_size == 0 || frame_size > max_frame_samples_for(self.sample_rate) { + let frame_size = NonZeroUsize::new(frame_size).ok_or(Error::BadArg)?; + if frame_size.get() > max_frame_samples_for(self.sample_rate) { return Err(Error::BadArg); } if output.is_empty() || output.len() > i32::MAX as usize { return Err(Error::BadArg); } - let frame_i32 = i32::try_from(frame_size).map_err(|_| Error::BadArg)?; + let frame_i32 = i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)?; let out_len_i32 = i32::try_from(output.len()).map_err(|_| Error::BadArg)?; let n = unsafe { opus_encode_float( - self.raw, + self.raw.as_ptr(), input.as_ptr(), frame_i32, output.as_mut_ptr(), @@ -387,10 +436,12 @@ impl Encoder { /// Query current signal hint. /// /// # Errors - /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. + /// Returns [`Error::InvalidState`] if the encoder is invalid, [`Error::InternalError`] if the + /// response is not recognized, or a mapped libopus error. pub fn signal(&mut self) -> Result { let v = self.get_int_ctl(OPUS_GET_SIGNAL_REQUEST as i32)?; match v { + x if x == OPUS_AUTO => Ok(Signal::Auto), x if x == crate::bindings::OPUS_SIGNAL_VOICE as i32 => Ok(Signal::Voice), x if x == crate::bindings::OPUS_SIGNAL_MUSIC as i32 => Ok(Signal::Music), _ => Err(Error::InternalError), @@ -410,8 +461,13 @@ impl Encoder { /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn final_range(&mut self) -> Result { let mut val: u32 = 0; - let r = - unsafe { opus_encoder_ctl(self.raw, OPUS_GET_FINAL_RANGE_REQUEST as i32, &mut val) }; + let r = unsafe { + opus_encoder_ctl( + self.raw.as_ptr(), + OPUS_GET_FINAL_RANGE_REQUEST as i32, + &mut val, + ) + }; if r != 0 { return Err(Error::from_code(r)); } @@ -447,21 +503,24 @@ impl Encoder { /// Query expert frame duration. /// /// # Errors - /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. + /// Returns [`Error::InvalidState`] if the encoder is invalid, [`Error::InternalError`] if the + /// response is not recognized, or a mapped libopus error. pub fn expert_frame_duration(&mut self) -> Result { let v = self.get_int_ctl(OPUS_GET_EXPERT_FRAME_DURATION_REQUEST as i32)?; let vu = u32::try_from(v).map_err(|_| Error::InternalError)?; - Ok(match vu { - x if x == crate::bindings::OPUS_FRAMESIZE_2_5_MS => ExpertFrameDuration::Ms2_5, - x if x == crate::bindings::OPUS_FRAMESIZE_5_MS => ExpertFrameDuration::Ms5, - x if x == crate::bindings::OPUS_FRAMESIZE_10_MS => ExpertFrameDuration::Ms10, - x if x == crate::bindings::OPUS_FRAMESIZE_20_MS => ExpertFrameDuration::Ms20, - x if x == crate::bindings::OPUS_FRAMESIZE_40_MS => ExpertFrameDuration::Ms40, - x if x == crate::bindings::OPUS_FRAMESIZE_60_MS => ExpertFrameDuration::Ms60, - x if x == crate::bindings::OPUS_FRAMESIZE_80_MS => ExpertFrameDuration::Ms80, - x if x == crate::bindings::OPUS_FRAMESIZE_100_MS => ExpertFrameDuration::Ms100, - _ => ExpertFrameDuration::Ms120, - }) + match vu { + x if x == crate::bindings::OPUS_FRAMESIZE_ARG => Ok(ExpertFrameDuration::Auto), + x if x == crate::bindings::OPUS_FRAMESIZE_2_5_MS => Ok(ExpertFrameDuration::Ms2_5), + x if x == crate::bindings::OPUS_FRAMESIZE_5_MS => Ok(ExpertFrameDuration::Ms5), + x if x == crate::bindings::OPUS_FRAMESIZE_10_MS => Ok(ExpertFrameDuration::Ms10), + x if x == crate::bindings::OPUS_FRAMESIZE_20_MS => Ok(ExpertFrameDuration::Ms20), + x if x == crate::bindings::OPUS_FRAMESIZE_40_MS => Ok(ExpertFrameDuration::Ms40), + x if x == crate::bindings::OPUS_FRAMESIZE_60_MS => Ok(ExpertFrameDuration::Ms60), + x if x == crate::bindings::OPUS_FRAMESIZE_80_MS => Ok(ExpertFrameDuration::Ms80), + x if x == crate::bindings::OPUS_FRAMESIZE_100_MS => Ok(ExpertFrameDuration::Ms100), + x if x == crate::bindings::OPUS_FRAMESIZE_120_MS => Ok(ExpertFrameDuration::Ms120), + _ => Err(Error::InternalError), + } } /// Disable/enable inter-frame prediction (expert option). @@ -502,10 +561,7 @@ impl Encoder { // --- internal helpers --- fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_encoder_ctl(self.raw, req, val) }; + let r = unsafe { opus_encoder_ctl(self.raw.as_ptr(), req, val) }; if r != 0 { return Err(Error::from_code(r)); } @@ -515,11 +571,8 @@ impl Encoder { Ok(self.get_int_ctl(req)? != 0) } fn get_int_ctl(&mut self, req: i32) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: i32 = 0; - let r = unsafe { opus_encoder_ctl(self.raw, req, &mut v) }; + let r = unsafe { opus_encoder_ctl(self.raw.as_ptr(), req, &mut v) }; if r != 0 { return Err(Error::from_code(r)); } @@ -543,12 +596,13 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn set_bitrate(&mut self, bitrate: Bitrate) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - - let result = - unsafe { opus_encoder_ctl(self.raw, OPUS_SET_BITRATE_REQUEST as i32, bitrate.value()) }; + let result = unsafe { + opus_encoder_ctl( + self.raw.as_ptr(), + OPUS_SET_BITRATE_REQUEST as i32, + bitrate.value(), + ) + }; if result != 0 { return Err(Error::from_code(result)); @@ -562,13 +616,14 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn bitrate(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let mut bitrate = 0i32; - let result = - unsafe { opus_encoder_ctl(self.raw, OPUS_GET_BITRATE_REQUEST as i32, &mut bitrate) }; + let result = unsafe { + opus_encoder_ctl( + self.raw.as_ptr(), + OPUS_GET_BITRATE_REQUEST as i32, + &mut bitrate, + ) + }; if result != 0 { return Err(Error::from_code(result)); @@ -586,13 +641,9 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn set_complexity(&mut self, complexity: Complexity) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let result = unsafe { opus_encoder_ctl( - self.raw, + self.raw.as_ptr(), OPUS_SET_COMPLEXITY_REQUEST as i32, complexity.value() as i32, ) @@ -610,14 +661,10 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn complexity(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let mut complexity = 0i32; let result = unsafe { opus_encoder_ctl( - self.raw, + self.raw.as_ptr(), OPUS_GET_COMPLEXITY_REQUEST as i32, &mut complexity, ) @@ -637,12 +684,9 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn set_vbr(&mut self, enabled: bool) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let vbr = i32::from(enabled); - let result = unsafe { opus_encoder_ctl(self.raw, OPUS_SET_VBR_REQUEST as i32, vbr) }; + let result = + unsafe { opus_encoder_ctl(self.raw.as_ptr(), OPUS_SET_VBR_REQUEST as i32, vbr) }; if result != 0 { return Err(Error::from_code(result)); @@ -656,12 +700,9 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn vbr(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let mut vbr = 0i32; - let result = unsafe { opus_encoder_ctl(self.raw, OPUS_GET_VBR_REQUEST as i32, &mut vbr) }; + let result = + unsafe { opus_encoder_ctl(self.raw.as_ptr(), OPUS_GET_VBR_REQUEST as i32, &mut vbr) }; if result != 0 { return Err(Error::from_code(result)); @@ -687,10 +728,9 @@ impl Encoder { /// # Errors /// Returns [`Error::InvalidState`] if the encoder is invalid, or a mapped libopus error. pub fn reset(&mut self) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_encoder_ctl(self.raw, crate::bindings::OPUS_RESET_STATE as i32) }; + let r = unsafe { + opus_encoder_ctl(self.raw.as_ptr(), crate::bindings::OPUS_RESET_STATE as i32) + }; if r != 0 { return Err(Error::from_code(r)); } @@ -698,10 +738,66 @@ impl Encoder { } } -impl Drop for Encoder { - fn drop(&mut self) { - unsafe { - opus_encoder_destroy(self.raw); +impl<'a> EncoderRef<'a> { + /// Wrap an externally-initialized encoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`Encoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`Encoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw( + ptr: *mut OpusEncoder, + sample_rate: SampleRate, + channels: Channels, + ) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let encoder = Encoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sample_rate, + channels, + Ownership::Borrowed, + ); + Self { + inner: encoder, + _marker: PhantomData, } } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sample_rate: SampleRate, + channels: Channels, + application: Application, + ) -> Result { + let required = Encoder::size(channels)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { Encoder::init_in_place(ptr, sample_rate, channels, application)? }; + Ok(unsafe { Self::from_raw(ptr, sample_rate, channels) }) + } +} + +impl Deref for EncoderRef<'_> { + type Target = Encoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for EncoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } diff --git a/src/lib.rs b/src/lib.rs index b8ceba8..d5480fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod bindings { include!("bindings.rs"); } +mod alloc; pub mod constants; pub mod decoder; #[cfg(feature = "dred")] @@ -23,19 +24,21 @@ pub mod error; pub mod multistream; pub mod packet; pub mod projection; +mod raw; pub mod repacketizer; pub mod types; +pub use alloc::AlignedBuffer; pub use constants::{MAX_FRAME_SAMPLES_48KHZ, MAX_PACKET_DURATION_MS, max_frame_samples_for}; pub use decoder::Decoder; #[cfg(feature = "dred")] pub use dred::{DredDecoder, DredState}; pub use encoder::Encoder; pub use error::{Error, Result}; -pub use multistream::{MSDecoder, MSEncoder, Mapping}; +pub use multistream::{Mapping, MultistreamDecoder, MultistreamEncoder}; pub use packet::{ - packet_bandwidth, packet_channels, packet_has_lbrr, packet_nb_frames, packet_nb_samples, - packet_parse, packet_samples_per_frame, soft_clip, + packet_bandwidth, packet_channels, packet_frame_count, packet_has_lbrr, packet_parse, + packet_sample_count, packet_samples_per_frame, soft_clip, }; pub use projection::{ProjectionDecoder, ProjectionEncoder}; pub use repacketizer::Repacketizer; @@ -47,6 +50,20 @@ pub use types::{ #[doc(hidden)] pub use bindings::*; +pub(crate) use raw::RawHandle; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Ownership { + Owned, + Borrowed, +} + +#[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::()) +} + /// Returns the bundled libopus version string of this crate. #[must_use] pub fn version() -> &'static str { diff --git a/src/multistream.rs b/src/multistream.rs index 3c4d642..21e00db 100644 --- a/src/multistream.rs +++ b/src/multistream.rs @@ -19,19 +19,28 @@ use crate::bindings::{ OPUS_SET_VBR_CONSTRAINT_REQUEST, OPUS_SET_VBR_REQUEST, OPUS_SIGNAL_MUSIC, OPUS_SIGNAL_VOICE, OpusDecoder, OpusEncoder, OpusMSDecoder, OpusMSEncoder, opus_multistream_decode, opus_multistream_decode_float, opus_multistream_decoder_create, opus_multistream_decoder_ctl, - opus_multistream_decoder_destroy, opus_multistream_encode, opus_multistream_encode_float, + opus_multistream_decoder_destroy, opus_multistream_decoder_get_size, + opus_multistream_decoder_init, opus_multistream_encode, opus_multistream_encode_float, opus_multistream_encoder_create, opus_multistream_encoder_ctl, - opus_multistream_encoder_destroy, opus_multistream_surround_encoder_create, + opus_multistream_encoder_destroy, opus_multistream_encoder_get_size, + opus_multistream_encoder_init, opus_multistream_surround_encoder_create, + opus_multistream_surround_encoder_get_size, opus_multistream_surround_encoder_init, }; +use crate::constants::max_frame_samples_for; use crate::error::{Error, Result}; use crate::types::{Application, Bandwidth, Bitrate, Channels, Complexity, SampleRate, Signal}; +use crate::{AlignedBuffer, Ownership, RawHandle}; +use std::marker::PhantomData; +use std::num::{NonZeroU8, NonZeroUsize}; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; /// Describes the multistream mapping configuration. #[derive(Debug, Clone, Copy)] pub struct Mapping<'a> { /// Total input/output channels. pub channels: u8, - /// Number of uncoupled mono streams. + /// Total number of streams (coupled + uncoupled). pub streams: u8, /// Number of coupled stereo streams (each counts as 2 channels). pub coupled_streams: u8, @@ -40,63 +49,233 @@ pub struct Mapping<'a> { } impl Mapping<'_> { - /// Validate that mapping length matches channels. - fn validate(&self) -> Result<()> { - let channel_count = usize::from(self.channels); - if channel_count == 0 { - return Err(Error::BadArg); - } + fn validate_common(&self) -> Result<(usize, usize, usize)> { + let channel_count = NonZeroU8::new(self.channels).ok_or(Error::BadArg)?; + let channel_count = usize::from(channel_count.get()); if self.mapping.len() != channel_count { return Err(Error::BadArg); } - let streams = usize::from(self.streams); + let streams = NonZeroU8::new(self.streams).ok_or(Error::BadArg)?; + let streams = usize::from(streams.get()); let coupled = usize::from(self.coupled_streams); - if streams + coupled == 0 { - return Err(Error::BadArg); - } - if streams > channel_count { + if coupled > streams { return Err(Error::BadArg); } - if coupled > channel_count / 2 { + if streams + coupled > u8::MAX as usize { return Err(Error::BadArg); } let total_streams = streams + coupled; - let mut assignments = vec![0usize; total_streams]; for &entry in self.mapping { if entry == u8::MAX { continue; } - let idx = usize::from(entry); - if idx >= total_streams { + if usize::from(entry) >= total_streams { return Err(Error::BadArg); } - assignments[idx] += 1; - if idx < streams { - if assignments[idx] > 1 { - return Err(Error::BadArg); + } + Ok((channel_count, streams, coupled)) + } + + /// Validate mapping for use with libopus multistream decoder. + fn validate_for_decoder(&self) -> Result<()> { + self.validate_common()?; + Ok(()) + } + + /// Validate mapping for use with libopus multistream encoder. + fn validate_for_encoder(&self) -> Result<()> { + let (channel_count, streams, coupled) = self.validate_common()?; + if streams + coupled > channel_count { + return Err(Error::BadArg); + } + + let mut has_left = vec![false; coupled]; + let mut has_right = vec![false; coupled]; + let mut has_mono = vec![false; streams.saturating_sub(coupled)]; + for &entry in self.mapping { + if entry == u8::MAX { + continue; + } + let idx = usize::from(entry); + if idx < 2 * coupled { + let stream = idx / 2; + if idx % 2 == 0 { + has_left[stream] = true; + } else { + has_right[stream] = true; + } + } else { + let stream = idx - coupled; + if stream >= coupled && stream < streams { + has_mono[stream - coupled] = true; } - } else if assignments[idx] > 2 { - return Err(Error::BadArg); } } + + if has_left.iter().any(|has| !has) || has_right.iter().any(|has| !has) { + return Err(Error::BadArg); + } + if has_mono.iter().any(|has| !has) { + return Err(Error::BadArg); + } Ok(()) } } /// Safe wrapper around `OpusMSEncoder`. -pub struct MSEncoder { - raw: *mut OpusMSEncoder, +pub struct MultistreamEncoder { + raw: RawHandle, sample_rate: SampleRate, channels: u8, streams: u8, coupled_streams: u8, } -unsafe impl Send for MSEncoder {} -unsafe impl Sync for MSEncoder {} +unsafe impl Send for MultistreamEncoder {} +unsafe impl Sync for MultistreamEncoder {} + +/// Borrowed wrapper around a multistream encoder. +pub struct MultistreamEncoderRef<'a> { + inner: MultistreamEncoder, + _marker: PhantomData<&'a mut OpusMSEncoder>, +} + +unsafe impl Send for MultistreamEncoderRef<'_> {} +unsafe impl Sync for MultistreamEncoderRef<'_> {} + +impl MultistreamEncoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_multistream_encoder_destroy), + sample_rate, + channels, + streams, + coupled_streams, + } + } + + /// Size in bytes of a multistream encoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the stream counts are invalid or libopus reports + /// an impossible size. + pub fn size(streams: u8, coupled_streams: u8) -> Result { + let raw = unsafe { + opus_multistream_encoder_get_size(i32::from(streams), i32::from(coupled_streams)) + }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Size in bytes of a surround multistream encoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the channel/mapping configuration is invalid. + pub fn surround_size(channels: u8, mapping_family: i32) -> Result { + let raw = unsafe { + opus_multistream_surround_encoder_get_size(i32::from(channels), mapping_family) + }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated multistream encoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `MultistreamEncoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] if the mapping is invalid or `ptr` is null, or a + /// mapped libopus error on failure. + pub unsafe fn init_in_place( + ptr: *mut OpusMSEncoder, + sr: SampleRate, + app: Application, + mapping: Mapping<'_>, + ) -> Result<()> { + if ptr.is_null() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + mapping.validate_for_encoder()?; + let r = unsafe { + opus_multistream_encoder_init( + ptr, + sr as i32, + i32::from(mapping.channels), + i32::from(mapping.streams), + i32::from(mapping.coupled_streams), + mapping.mapping.as_ptr(), + app as i32, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } + + /// Initialize a previously allocated surround multistream encoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `MultistreamEncoder::surround_size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] for invalid channel counts or a mapped libopus error. + pub unsafe fn init_surround_in_place( + ptr: *mut OpusMSEncoder, + sr: SampleRate, + channels: u8, + mapping_family: i32, + app: Application, + ) -> Result<(u8, u8, Vec)> { + if ptr.is_null() || channels == 0 { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + let mut streams = 0i32; + let mut coupled = 0i32; + let mut mapping = vec![0u8; channels as usize]; + let r = unsafe { + opus_multistream_surround_encoder_init( + ptr, + sr as i32, + i32::from(channels), + mapping_family, + std::ptr::addr_of_mut!(streams), + std::ptr::addr_of_mut!(coupled), + mapping.as_mut_ptr(), + app as i32, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(( + u8::try_from(streams).map_err(|_| Error::BadArg)?, + u8::try_from(coupled).map_err(|_| Error::BadArg)?, + mapping, + )) + } -impl MSEncoder { /// Create a new multistream encoder. /// /// The `mapping.mapping` array describes how input channels are assigned to streams. @@ -106,7 +285,7 @@ impl MSEncoder { /// Returns [`Error::BadArg`] when the mapping dimensions are inconsistent, or /// propagates allocation/configuration failures from libopus. pub fn new(sr: SampleRate, app: Application, mapping: Mapping<'_>) -> Result { - mapping.validate()?; + mapping.validate_for_encoder()?; let mut err = 0i32; let enc = unsafe { opus_multistream_encoder_create( @@ -122,16 +301,15 @@ impl MSEncoder { if err != 0 { return Err(Error::from_code(err)); } - if enc.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { - raw: enc, - sample_rate: sr, - channels: mapping.channels, - streams: mapping.streams, - coupled_streams: mapping.coupled_streams, - }) + let enc = NonNull::new(enc).ok_or(Error::AllocFail)?; + Ok(Self::from_raw( + enc, + sr, + mapping.channels, + mapping.streams, + mapping.coupled_streams, + Ownership::Owned, + )) } /// Encode interleaved i16 PCM into a multistream Opus packet. @@ -146,10 +324,11 @@ impl MSEncoder { frame_size_per_ch: usize, out: &mut [u8], ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); + let frame_size_per_ch = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size_per_ch.get() > max_frame_samples_for(self.sample_rate) { + return Err(Error::BadArg); } - if pcm.len() != frame_size_per_ch * self.channels as usize { + if pcm.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } if out.is_empty() || out.len() > i32::MAX as usize { @@ -157,9 +336,9 @@ impl MSEncoder { } let n = unsafe { opus_multistream_encode( - self.raw, + self.raw.as_ptr(), pcm.as_ptr(), - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg)?, + i32::try_from(frame_size_per_ch.get()).map_err(|_| Error::BadArg)?, out.as_mut_ptr(), i32::try_from(out.len()).map_err(|_| Error::BadArg)?, ) @@ -181,10 +360,11 @@ impl MSEncoder { frame_size_per_ch: usize, out: &mut [u8], ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); + let frame_size_per_ch = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size_per_ch.get() > max_frame_samples_for(self.sample_rate) { + return Err(Error::BadArg); } - if pcm.len() != frame_size_per_ch * self.channels as usize { + if pcm.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } if out.is_empty() || out.len() > i32::MAX as usize { @@ -192,9 +372,9 @@ impl MSEncoder { } let n = unsafe { opus_multistream_encode_float( - self.raw, + self.raw.as_ptr(), pcm.as_ptr(), - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg)?, + i32::try_from(frame_size_per_ch.get()).map_err(|_| Error::BadArg)?, out.as_mut_ptr(), i32::try_from(out.len()).map_err(|_| Error::BadArg)?, ) @@ -211,12 +391,13 @@ impl MSEncoder { /// Returns [`Error::InvalidState`] when the encoder handle is null or /// propagates the libopus error. pub fn final_range(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: u32 = 0; let r = unsafe { - opus_multistream_encoder_ctl(self.raw, OPUS_GET_FINAL_RANGE_REQUEST as i32, &mut v) + opus_multistream_encoder_ctl( + self.raw.as_ptr(), + OPUS_GET_FINAL_RANGE_REQUEST as i32, + &mut v, + ) }; if r != 0 { return Err(Error::from_code(r)); @@ -459,6 +640,7 @@ impl MSEncoder { pub fn signal(&mut self) -> Result { let v = self.get_int_ctl(OPUS_GET_SIGNAL_REQUEST as i32)?; match v { + x if x == OPUS_AUTO => Ok(Signal::Auto), x if x == OPUS_SIGNAL_VOICE as i32 => Ok(Signal::Voice), x if x == OPUS_SIGNAL_MUSIC as i32 => Ok(Signal::Music), _ => Err(Error::InternalError), @@ -480,10 +662,7 @@ impl MSEncoder { /// Returns [`Error::InvalidState`] if the encoder handle is null or propagates any error /// reported by libopus. pub fn reset(&mut self) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_multistream_encoder_ctl(self.raw, OPUS_RESET_STATE as i32) }; + let r = unsafe { opus_multistream_encoder_ctl(self.raw.as_ptr(), OPUS_RESET_STATE as i32) }; if r != 0 { return Err(Error::from_code(r)); } @@ -544,19 +723,11 @@ impl MSEncoder { if err != 0 { return Err(Error::from_code(err)); } - if enc.is_null() { - return Err(Error::AllocFail); - } + let enc = NonNull::new(enc).ok_or(Error::AllocFail)?; let streams_u8 = u8::try_from(streams).map_err(|_| Error::BadArg)?; let coupled_u8 = u8::try_from(coupled).map_err(|_| Error::BadArg)?; Ok(( - Self { - raw: enc, - sample_rate: sr, - channels, - streams: streams_u8, - coupled_streams: coupled_u8, - }, + Self::from_raw(enc, sr, channels, streams_u8, coupled_u8, Ownership::Owned), mapping, )) } @@ -571,13 +742,10 @@ impl MSEncoder { /// Returns [`Error::InvalidState`] if the encoder handle is invalid or propagates the /// libopus error if retrieving the state fails. pub unsafe fn encoder_state_ptr(&mut self, stream_index: i32) -> Result<*mut OpusEncoder> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut state: *mut OpusEncoder = std::ptr::null_mut(); let r = unsafe { opus_multistream_encoder_ctl( - self.raw, + self.raw.as_ptr(), OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST as i32, stream_index, &mut state, @@ -593,10 +761,7 @@ impl MSEncoder { } fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_multistream_encoder_ctl(self.raw, req, val) }; + let r = unsafe { opus_multistream_encoder_ctl(self.raw.as_ptr(), req, val) }; if r != 0 { return Err(Error::from_code(r)); } @@ -604,11 +769,8 @@ impl MSEncoder { } fn get_int_ctl(&mut self, req: i32) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: i32 = 0; - let r = unsafe { opus_multistream_encoder_ctl(self.raw, req, &mut v) }; + let r = unsafe { opus_multistream_encoder_ctl(self.raw.as_ptr(), req, &mut v) }; if r != 0 { return Err(Error::from_code(r)); } @@ -632,30 +794,190 @@ impl MSEncoder { } } -impl Drop for MSEncoder { - fn drop(&mut self) { - unsafe { opus_multistream_encoder_destroy(self.raw) } +impl<'a> MultistreamEncoderRef<'a> { + /// Wrap an externally-initialized multistream encoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`MultistreamEncoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`MultistreamEncoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw(ptr: *mut OpusMSEncoder, sr: SampleRate, mapping: Mapping<'_>) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + debug_assert!(mapping.validate_for_encoder().is_ok()); + let encoder = MultistreamEncoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sr, + mapping.channels, + mapping.streams, + mapping.coupled_streams, + Ownership::Borrowed, + ); + Self { + inner: encoder, + _marker: PhantomData, + } + } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sr: SampleRate, + app: Application, + mapping: Mapping<'_>, + ) -> Result { + let required = MultistreamEncoder::size(mapping.streams, mapping.coupled_streams)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { MultistreamEncoder::init_in_place(ptr, sr, app, mapping)? }; + Ok(unsafe { Self::from_raw(ptr, sr, mapping) }) + } + + /// Initialize a surround encoder in an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in_surround( + buf: &'a mut AlignedBuffer, + sr: SampleRate, + channels: u8, + mapping_family: i32, + app: Application, + ) -> Result<(Self, Vec)> { + let required = MultistreamEncoder::surround_size(channels, mapping_family)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + let (streams, coupled, mapping) = unsafe { + MultistreamEncoder::init_surround_in_place(ptr, sr, channels, mapping_family, app)? + }; + let mapping_ref = Mapping { + channels, + streams, + coupled_streams: coupled, + mapping: &mapping, + }; + let encoder = unsafe { Self::from_raw(ptr, sr, mapping_ref) }; + Ok((encoder, mapping)) + } +} + +impl Deref for MultistreamEncoderRef<'_> { + type Target = MultistreamEncoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MultistreamEncoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } /// Safe wrapper around `OpusMSDecoder`. -pub struct MSDecoder { - raw: *mut OpusMSDecoder, +pub struct MultistreamDecoder { + raw: RawHandle, sample_rate: SampleRate, channels: u8, } -unsafe impl Send for MSDecoder {} -unsafe impl Sync for MSDecoder {} +unsafe impl Send for MultistreamDecoder {} +unsafe impl Sync for MultistreamDecoder {} + +/// Borrowed wrapper around a multistream decoder. +pub struct MultistreamDecoderRef<'a> { + inner: MultistreamDecoder, + _marker: PhantomData<&'a mut OpusMSDecoder>, +} + +unsafe impl Send for MultistreamDecoderRef<'_> {} +unsafe impl Sync for MultistreamDecoderRef<'_> {} + +impl MultistreamDecoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: u8, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_multistream_decoder_destroy), + sample_rate, + channels, + } + } + + /// Size in bytes of a multistream decoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the stream counts are invalid or libopus reports + /// an impossible size. + pub fn size(streams: u8, coupled_streams: u8) -> Result { + let raw = unsafe { + opus_multistream_decoder_get_size(i32::from(streams), i32::from(coupled_streams)) + }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated multistream decoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `MultistreamDecoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] if the mapping is invalid or `ptr` is null, or a + /// mapped libopus error on failure. + pub unsafe fn init_in_place( + ptr: *mut OpusMSDecoder, + sr: SampleRate, + mapping: Mapping<'_>, + ) -> Result<()> { + if ptr.is_null() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + mapping.validate_for_decoder()?; + let r = unsafe { + opus_multistream_decoder_init( + ptr, + sr as i32, + i32::from(mapping.channels), + i32::from(mapping.streams), + i32::from(mapping.coupled_streams), + mapping.mapping.as_ptr(), + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } -impl MSDecoder { /// Create a new multistream decoder. /// /// # Errors /// Returns [`Error::BadArg`] when the mapping dimensions are inconsistent, or /// propagates allocation/configuration failures from libopus. pub fn new(sr: SampleRate, mapping: Mapping<'_>) -> Result { - mapping.validate()?; + mapping.validate_for_decoder()?; let mut err = 0i32; let dec = unsafe { opus_multistream_decoder_create( @@ -670,14 +992,8 @@ impl MSDecoder { if err != 0 { return Err(Error::from_code(err)); } - if dec.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { - raw: dec, - sample_rate: sr, - channels: mapping.channels, - }) + let dec = NonNull::new(dec).ok_or(Error::AllocFail)?; + Ok(Self::from_raw(dec, sr, mapping.channels, Ownership::Owned)) } /// Decode into interleaved i16 PCM (`frame_size` is per-channel). @@ -692,15 +1008,16 @@ impl MSDecoder { frame_size_per_ch: usize, fec: bool, ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); + let frame_size_per_ch = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size_per_ch.get() > max_frame_samples_for(self.sample_rate) { + return Err(Error::BadArg); } - if out.len() != frame_size_per_ch * self.channels as usize { + if out.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } let n = unsafe { opus_multistream_decode( - self.raw, + self.raw.as_ptr(), if packet.is_empty() { std::ptr::null() } else { @@ -712,7 +1029,7 @@ impl MSDecoder { i32::try_from(packet.len()).map_err(|_| Error::BadArg)? }, out.as_mut_ptr(), - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg)?, + i32::try_from(frame_size_per_ch.get()).map_err(|_| Error::BadArg)?, i32::from(fec), ) }; @@ -734,15 +1051,16 @@ impl MSDecoder { frame_size_per_ch: usize, fec: bool, ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); + let frame_size_per_ch = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size_per_ch.get() > max_frame_samples_for(self.sample_rate) { + return Err(Error::BadArg); } - if out.len() != frame_size_per_ch * self.channels as usize { + if out.len() != frame_size_per_ch.get() * self.channels as usize { return Err(Error::BadArg); } let n = unsafe { opus_multistream_decode_float( - self.raw, + self.raw.as_ptr(), if packet.is_empty() { std::ptr::null() } else { @@ -754,7 +1072,7 @@ impl MSDecoder { i32::try_from(packet.len()).map_err(|_| Error::BadArg)? }, out.as_mut_ptr(), - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg)?, + i32::try_from(frame_size_per_ch.get()).map_err(|_| Error::BadArg)?, i32::from(fec), ) }; @@ -770,12 +1088,13 @@ impl MSDecoder { /// Returns [`Error::InvalidState`] when the decoder handle is null or /// propagates the libopus error. pub fn final_range(&mut self) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: u32 = 0; let r = unsafe { - opus_multistream_decoder_ctl(self.raw, OPUS_GET_FINAL_RANGE_REQUEST as i32, &mut v) + opus_multistream_decoder_ctl( + self.raw.as_ptr(), + OPUS_GET_FINAL_RANGE_REQUEST as i32, + &mut v, + ) }; if r != 0 { return Err(Error::from_code(r)); @@ -789,10 +1108,7 @@ impl MSDecoder { /// Returns [`Error::InvalidState`] if the decoder handle is null or propagates any error /// reported by libopus. pub fn reset(&mut self) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_multistream_decoder_ctl(self.raw, OPUS_RESET_STATE as i32) }; + let r = unsafe { opus_multistream_decoder_ctl(self.raw.as_ptr(), OPUS_RESET_STATE as i32) }; if r != 0 { return Err(Error::from_code(r)); } @@ -926,15 +1242,9 @@ impl MSDecoder { if err != 0 { return Err(Error::from_code(err)); } - if dec.is_null() { - return Err(Error::AllocFail); - } + let dec = NonNull::new(dec).ok_or(Error::AllocFail)?; Ok(( - Self { - raw: dec, - sample_rate: sr, - channels, - }, + Self::from_raw(dec, sr, channels, Ownership::Owned), mapping, u8::try_from(streams).map_err(|_| Error::BadArg)?, u8::try_from(coupled).map_err(|_| Error::BadArg)?, @@ -951,13 +1261,10 @@ impl MSDecoder { /// Returns [`Error::InvalidState`] if the decoder handle is invalid or propagates the /// libopus error when retrieving the per-stream state fails. pub unsafe fn decoder_state_ptr(&mut self, stream_index: i32) -> Result<*mut OpusDecoder> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut state: *mut OpusDecoder = std::ptr::null_mut(); let r = unsafe { opus_multistream_decoder_ctl( - self.raw, + self.raw.as_ptr(), OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST as i32, stream_index, &mut state, @@ -973,10 +1280,7 @@ impl MSDecoder { } fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_multistream_decoder_ctl(self.raw, req, val) }; + let r = unsafe { opus_multistream_decoder_ctl(self.raw.as_ptr(), req, val) }; if r != 0 { return Err(Error::from_code(r)); } @@ -984,11 +1288,8 @@ impl MSDecoder { } fn get_int_ctl(&mut self, req: i32) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v: i32 = 0; - let r = unsafe { opus_multistream_decoder_ctl(self.raw, req, &mut v) }; + let r = unsafe { opus_multistream_decoder_ctl(self.raw.as_ptr(), req, &mut v) }; if r != 0 { return Err(Error::from_code(r)); } @@ -1000,9 +1301,63 @@ impl MSDecoder { } } -impl Drop for MSDecoder { - fn drop(&mut self) { - unsafe { opus_multistream_decoder_destroy(self.raw) } +impl<'a> MultistreamDecoderRef<'a> { + /// Wrap an externally-initialized multistream decoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`MultistreamDecoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`MultistreamDecoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw(ptr: *mut OpusMSDecoder, sr: SampleRate, mapping: Mapping<'_>) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + debug_assert!(mapping.validate_for_decoder().is_ok()); + let decoder = MultistreamDecoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sr, + mapping.channels, + Ownership::Borrowed, + ); + Self { + inner: decoder, + _marker: PhantomData, + } + } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sr: SampleRate, + mapping: Mapping<'_>, + ) -> Result { + let required = MultistreamDecoder::size(mapping.streams, mapping.coupled_streams)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { MultistreamDecoder::init_in_place(ptr, sr, mapping)? }; + Ok(unsafe { Self::from_raw(ptr, sr, mapping) }) + } +} + +impl Deref for MultistreamDecoderRef<'_> { + type Target = MultistreamDecoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MultistreamDecoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } @@ -1014,21 +1369,22 @@ mod tests { fn mapping_allows_dropped_channels() { let mapping = Mapping { channels: 6, - streams: 1, - coupled_streams: 2, - mapping: &[0, 1, 1, 2, 2, u8::MAX], + streams: 2, + coupled_streams: 1, + mapping: &[0, 1, 2, u8::MAX, u8::MAX, u8::MAX], }; - assert!(mapping.validate().is_ok()); + assert!(mapping.validate_for_encoder().is_ok()); } #[test] - fn mapping_rejects_duplicate_mono_assignments() { + fn mapping_requires_encoder_stream_coverage() { let mapping = Mapping { - channels: 3, + channels: 2, streams: 1, coupled_streams: 1, - mapping: &[0, 0, 1], + mapping: &[0, 0], }; - assert!(mapping.validate().is_err()); + assert!(mapping.validate_for_decoder().is_ok()); + assert!(mapping.validate_for_encoder().is_err()); } } diff --git a/src/packet.rs b/src/packet.rs index e41bc65..d1f4246 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -53,7 +53,7 @@ pub fn packet_channels(packet: &[u8]) -> Result { /// /// # Errors /// Returns an error if the packet cannot be parsed. -pub fn packet_nb_frames(packet: &[u8]) -> Result { +pub fn packet_frame_count(packet: &[u8]) -> Result { if packet.is_empty() { return Err(Error::BadArg); } @@ -69,7 +69,7 @@ pub fn packet_nb_frames(packet: &[u8]) -> Result { /// /// # Errors /// Returns an error if the packet cannot be parsed. -pub fn packet_nb_samples(packet: &[u8], sample_rate: SampleRate) -> Result { +pub fn packet_sample_count(packet: &[u8], sample_rate: SampleRate) -> Result { if packet.is_empty() { return Err(Error::BadArg); } @@ -186,9 +186,6 @@ pub fn packet_parse(packet: &[u8]) -> Result<(u8, usize, Vec<&[u8]>)> { let mut frames = Vec::with_capacity(count); for i in 0..count { let size = usize::try_from(sizes[i]).map_err(|_| Error::InternalError)?; - if size == 0 { - continue; - } let ptr = frames_ptrs[i]; if ptr.is_null() { return Err(Error::InvalidPacket); diff --git a/src/projection.rs b/src/projection.rs index d7bbc9e..0318366 100644 --- a/src/projection.rs +++ b/src/projection.rs @@ -4,18 +4,24 @@ use crate::bindings::{ OPUS_BITRATE_MAX, OPUS_GET_BITRATE_REQUEST, OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST, OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST, OPUS_SET_BITRATE_REQUEST, OpusProjectionDecoder, OpusProjectionEncoder, - opus_projection_ambisonics_encoder_create, opus_projection_decode, - opus_projection_decode_float, opus_projection_decoder_create, opus_projection_decoder_destroy, - opus_projection_encode, opus_projection_encode_float, opus_projection_encoder_ctl, - opus_projection_encoder_destroy, + opus_projection_ambisonics_encoder_create, opus_projection_ambisonics_encoder_get_size, + opus_projection_ambisonics_encoder_init, opus_projection_decode, opus_projection_decode_float, + opus_projection_decoder_create, opus_projection_decoder_destroy, + opus_projection_decoder_get_size, opus_projection_decoder_init, opus_projection_encode, + opus_projection_encode_float, opus_projection_encoder_ctl, opus_projection_encoder_destroy, }; use crate::constants::max_frame_samples_for; use crate::error::{Error, Result}; use crate::types::{Application, Bitrate, SampleRate}; +use crate::{AlignedBuffer, Ownership, RawHandle}; +use std::marker::PhantomData; +use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; /// Safe wrapper around `OpusProjectionEncoder`. pub struct ProjectionEncoder { - raw: *mut OpusProjectionEncoder, + raw: RawHandle, sample_rate: SampleRate, channels: u8, streams: u8, @@ -25,7 +31,90 @@ pub struct ProjectionEncoder { unsafe impl Send for ProjectionEncoder {} unsafe impl Sync for ProjectionEncoder {} +/// Borrowed wrapper around a projection encoder state. +pub struct ProjectionEncoderRef<'a> { + inner: ProjectionEncoder, + _marker: PhantomData<&'a mut OpusProjectionEncoder>, +} + +unsafe impl Send for ProjectionEncoderRef<'_> {} +unsafe impl Sync for ProjectionEncoderRef<'_> {} + impl ProjectionEncoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_projection_encoder_destroy), + sample_rate, + channels, + streams, + coupled_streams, + } + } + + /// Size in bytes of a projection encoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the channel/mapping configuration is invalid. + pub fn size(channels: u8, mapping_family: i32) -> Result { + let raw = unsafe { + opus_projection_ambisonics_encoder_get_size(i32::from(channels), mapping_family) + }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated projection encoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `ProjectionEncoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] for invalid inputs or a mapped libopus error. + pub unsafe fn init_in_place( + ptr: *mut OpusProjectionEncoder, + sample_rate: SampleRate, + channels: u8, + mapping_family: i32, + application: Application, + ) -> Result<(u8, u8)> { + if ptr.is_null() || channels == 0 { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + let mut streams = 0i32; + let mut coupled = 0i32; + let r = unsafe { + opus_projection_ambisonics_encoder_init( + ptr, + sample_rate as i32, + i32::from(channels), + mapping_family, + std::ptr::addr_of_mut!(streams), + std::ptr::addr_of_mut!(coupled), + application as i32, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(( + u8::try_from(streams).map_err(|_| Error::BadArg)?, + u8::try_from(coupled).map_err(|_| Error::BadArg)?, + )) + } + /// Create a new projection encoder using the ambisonics helper. /// /// Returns [`Error::BadArg`] for unsupported channel/mapping combinations @@ -57,23 +146,25 @@ impl ProjectionEncoder { if err != 0 { return Err(Error::from_code(err)); } - if enc.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { - raw: enc, + let enc = NonNull::new(enc).ok_or(Error::AllocFail)?; + let streams_u8 = u8::try_from(streams).map_err(|_| Error::BadArg)?; + let coupled_u8 = u8::try_from(coupled).map_err(|_| Error::BadArg)?; + Ok(Self::from_raw( + enc, sample_rate, channels, - streams: u8::try_from(streams).map_err(|_| Error::BadArg)?, - coupled_streams: u8::try_from(coupled).map_err(|_| Error::BadArg)?, - }) + streams_u8, + coupled_u8, + Ownership::Owned, + )) } fn validate_frame_size(&self, frame_size_per_ch: usize) -> Result { - if frame_size_per_ch == 0 || frame_size_per_ch > max_frame_samples_for(self.sample_rate) { + let frame_size = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size.get() > max_frame_samples_for(self.sample_rate) { return Err(Error::BadArg); } - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg) + i32::try_from(frame_size.get()).map_err(|_| Error::BadArg) } fn ensure_pcm_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> { @@ -95,9 +186,6 @@ impl ProjectionEncoder { frame_size_per_ch: usize, out: &mut [u8], ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } if out.is_empty() || out.len() > i32::MAX as usize { return Err(Error::BadArg); } @@ -106,7 +194,7 @@ impl ProjectionEncoder { let out_len = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; let n = unsafe { opus_projection_encode( - self.raw, + self.raw.as_ptr(), pcm.as_ptr(), frame_size, out.as_mut_ptr(), @@ -131,9 +219,6 @@ impl ProjectionEncoder { frame_size_per_ch: usize, out: &mut [u8], ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } if out.is_empty() || out.len() > i32::MAX as usize { return Err(Error::BadArg); } @@ -142,7 +227,7 @@ impl ProjectionEncoder { let out_len = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; let n = unsafe { opus_projection_encode_float( - self.raw, + self.raw.as_ptr(), pcm.as_ptr(), frame_size, out.as_mut_ptr(), @@ -209,7 +294,7 @@ impl ProjectionEncoder { } let r = unsafe { opus_projection_encoder_ctl( - self.raw, + self.raw.as_ptr(), OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST as i32, out.as_mut_ptr(), size, @@ -259,10 +344,7 @@ impl ProjectionEncoder { } fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> { - if self.raw.is_null() { - return Err(Error::InvalidState); - } - let r = unsafe { opus_projection_encoder_ctl(self.raw, req, val) }; + let r = unsafe { opus_projection_encoder_ctl(self.raw.as_ptr(), req, val) }; if r != 0 { return Err(Error::from_code(r)); } @@ -270,11 +352,8 @@ impl ProjectionEncoder { } fn get_int_ctl(&mut self, req: i32) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } let mut v = 0i32; - let r = unsafe { opus_projection_encoder_ctl(self.raw, req, &mut v) }; + let r = unsafe { opus_projection_encoder_ctl(self.raw.as_ptr(), req, &mut v) }; if r != 0 { return Err(Error::from_code(r)); } @@ -282,17 +361,86 @@ impl ProjectionEncoder { } } -impl Drop for ProjectionEncoder { - fn drop(&mut self) { - if !self.raw.is_null() { - unsafe { opus_projection_encoder_destroy(self.raw) }; +impl<'a> ProjectionEncoderRef<'a> { + /// Wrap an externally-initialized projection encoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`ProjectionEncoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`ProjectionEncoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw( + ptr: *mut OpusProjectionEncoder, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + ) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let encoder = ProjectionEncoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sample_rate, + channels, + streams, + coupled_streams, + Ownership::Borrowed, + ); + Self { + inner: encoder, + _marker: PhantomData, } } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sample_rate: SampleRate, + channels: u8, + mapping_family: i32, + application: Application, + ) -> Result { + let required = ProjectionEncoder::size(channels, mapping_family)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + let (streams, coupled) = unsafe { + ProjectionEncoder::init_in_place( + ptr, + sample_rate, + channels, + mapping_family, + application, + )? + }; + Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled) }) + } +} + +impl Deref for ProjectionEncoderRef<'_> { + type Target = ProjectionEncoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for ProjectionEncoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } /// Safe wrapper around `OpusProjectionDecoder`. pub struct ProjectionDecoder { - raw: *mut OpusProjectionDecoder, + raw: RawHandle, sample_rate: SampleRate, channels: u8, streams: u8, @@ -302,7 +450,91 @@ pub struct ProjectionDecoder { unsafe impl Send for ProjectionDecoder {} unsafe impl Sync for ProjectionDecoder {} +/// Borrowed wrapper around a projection decoder state. +pub struct ProjectionDecoderRef<'a> { + inner: ProjectionDecoder, + _marker: PhantomData<&'a mut OpusProjectionDecoder>, +} + +unsafe impl Send for ProjectionDecoderRef<'_> {} +unsafe impl Sync for ProjectionDecoderRef<'_> {} + impl ProjectionDecoder { + fn from_raw( + ptr: NonNull, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + ownership: Ownership, + ) -> Self { + Self { + raw: RawHandle::new(ptr, ownership, opus_projection_decoder_destroy), + sample_rate, + channels, + streams, + coupled_streams, + } + } + + /// Size in bytes of a projection decoder state for external allocation. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the channel/stream configuration is invalid. + pub fn size(channels: u8, streams: u8, coupled_streams: u8) -> Result { + let raw = unsafe { + opus_projection_decoder_get_size( + i32::from(channels), + i32::from(streams), + i32::from(coupled_streams), + ) + }; + if raw <= 0 { + return Err(Error::BadArg); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated projection decoder state. + /// + /// # Safety + /// The caller must provide a valid pointer to `ProjectionDecoder::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] for invalid inputs or a mapped libopus error. + pub unsafe fn init_in_place( + ptr: *mut OpusProjectionDecoder, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + demixing_matrix: &[u8], + ) -> Result<()> { + if ptr.is_null() || demixing_matrix.is_empty() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + let matrix_len = i32::try_from(demixing_matrix.len()).map_err(|_| Error::BadArg)?; + let r = unsafe { + opus_projection_decoder_init( + ptr, + sample_rate as i32, + i32::from(channels), + i32::from(streams), + i32::from(coupled_streams), + demixing_matrix.as_ptr().cast_mut(), + matrix_len, + ) + }; + if r != 0 { + return Err(Error::from_code(r)); + } + Ok(()) + } + /// Create a projection decoder given the demixing matrix provided by the encoder. /// /// # Errors @@ -334,23 +566,23 @@ impl ProjectionDecoder { if err != 0 { return Err(Error::from_code(err)); } - if dec.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { - raw: dec, + let dec = NonNull::new(dec).ok_or(Error::AllocFail)?; + Ok(Self::from_raw( + dec, sample_rate, channels, streams, coupled_streams, - }) + Ownership::Owned, + )) } fn validate_frame_size(&self, frame_size_per_ch: usize) -> Result { - if frame_size_per_ch == 0 || frame_size_per_ch > max_frame_samples_for(self.sample_rate) { + let frame_size = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?; + if frame_size.get() > max_frame_samples_for(self.sample_rate) { return Err(Error::BadArg); } - i32::try_from(frame_size_per_ch).map_err(|_| Error::BadArg) + i32::try_from(frame_size.get()).map_err(|_| Error::BadArg) } fn ensure_output_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> { @@ -373,9 +605,6 @@ impl ProjectionDecoder { frame_size_per_ch: usize, fec: bool, ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } self.ensure_output_layout(out.len(), frame_size_per_ch)?; let frame_size = self.validate_frame_size(frame_size_per_ch)?; let packet_len = if packet.is_empty() { @@ -385,7 +614,7 @@ impl ProjectionDecoder { }; let n = unsafe { opus_projection_decode( - self.raw, + self.raw.as_ptr(), if packet.is_empty() { std::ptr::null() } else { @@ -416,9 +645,6 @@ impl ProjectionDecoder { frame_size_per_ch: usize, fec: bool, ) -> Result { - if self.raw.is_null() { - return Err(Error::InvalidState); - } self.ensure_output_layout(out.len(), frame_size_per_ch)?; let frame_size = self.validate_frame_size(frame_size_per_ch)?; let packet_len = if packet.is_empty() { @@ -428,7 +654,7 @@ impl ProjectionDecoder { }; let n = unsafe { opus_projection_decode_float( - self.raw, + self.raw.as_ptr(), if packet.is_empty() { std::ptr::null() } else { @@ -471,10 +697,81 @@ impl ProjectionDecoder { } } -impl Drop for ProjectionDecoder { - fn drop(&mut self) { - if !self.raw.is_null() { - unsafe { opus_projection_decoder_destroy(self.raw) }; +impl<'a> ProjectionDecoderRef<'a> { + /// Wrap an externally-initialized projection decoder without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`ProjectionDecoder::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`ProjectionDecoder::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw( + ptr: *mut OpusProjectionDecoder, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + ) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let decoder = ProjectionDecoder::from_raw( + unsafe { NonNull::new_unchecked(ptr) }, + sample_rate, + channels, + streams, + coupled_streams, + Ownership::Borrowed, + ); + Self { + inner: decoder, + _marker: PhantomData, + } + } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error. + pub fn init_in( + buf: &'a mut AlignedBuffer, + sample_rate: SampleRate, + channels: u8, + streams: u8, + coupled_streams: u8, + demixing_matrix: &[u8], + ) -> Result { + let required = ProjectionDecoder::size(channels, streams, coupled_streams)?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { + ProjectionDecoder::init_in_place( + ptr, + sample_rate, + channels, + streams, + coupled_streams, + demixing_matrix, + )?; } + Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled_streams) }) + } +} + +impl Deref for ProjectionDecoderRef<'_> { + type Target = ProjectionDecoder; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for ProjectionDecoderRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } diff --git a/src/raw.rs b/src/raw.rs new file mode 100644 index 0000000..975a1ff --- /dev/null +++ b/src/raw.rs @@ -0,0 +1,32 @@ +use crate::Ownership; +use std::ptr::NonNull; + +pub(crate) type DropFn = unsafe extern "C" fn(*mut T); + +pub(crate) struct RawHandle { + ptr: NonNull, + ownership: Ownership, + drop_fn: DropFn, +} + +impl RawHandle { + pub(crate) fn new(ptr: NonNull, ownership: Ownership, drop_fn: DropFn) -> Self { + Self { + ptr, + ownership, + drop_fn, + } + } + + pub(crate) fn as_ptr(&self) -> *mut T { + self.ptr.as_ptr() + } +} + +impl Drop for RawHandle { + fn drop(&mut self) { + if self.ownership == Ownership::Owned { + unsafe { (self.drop_fn)(self.ptr.as_ptr()) }; + } + } +} diff --git a/src/repacketizer.rs b/src/repacketizer.rs index dc2303b..07adf18 100644 --- a/src/repacketizer.rs +++ b/src/repacketizer.rs @@ -2,39 +2,61 @@ use crate::bindings::{ OpusRepacketizer, opus_repacketizer_cat, opus_repacketizer_create, opus_repacketizer_destroy, - opus_repacketizer_get_nb_frames, opus_repacketizer_init, opus_repacketizer_out, - opus_repacketizer_out_range, + opus_repacketizer_get_nb_frames, opus_repacketizer_get_size, opus_repacketizer_init, + opus_repacketizer_out, opus_repacketizer_out_range, }; use crate::error::{Error, Result}; +use crate::{AlignedBuffer, Ownership, RawHandle}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; /// Repackages Opus frames into packets. pub struct Repacketizer { - rp: *mut OpusRepacketizer, + rp: RawHandle, + packets: Vec>, } unsafe impl Send for Repacketizer {} unsafe impl Sync for Repacketizer {} +/// Borrowed wrapper around a repacketizer state. +pub struct RepacketizerRef<'a> { + inner: Repacketizer, + _marker: PhantomData<&'a mut OpusRepacketizer>, +} + +unsafe impl Send for RepacketizerRef<'_> {} +unsafe impl Sync for RepacketizerRef<'_> {} + impl Repacketizer { + fn from_raw(ptr: NonNull, ownership: Ownership) -> Self { + Self { + rp: RawHandle::new(ptr, ownership, opus_repacketizer_destroy), + packets: Vec::new(), + } + } + /// Create a new repacketizer. /// /// # Errors /// Returns `AllocFail` if allocation fails. pub fn new() -> Result { let rp = unsafe { opus_repacketizer_create() }; - if rp.is_null() { - return Err(Error::AllocFail); - } - Ok(Self { rp }) + let rp = NonNull::new(rp).ok_or(Error::AllocFail)?; + Ok(Self::from_raw(rp, Ownership::Owned)) } /// Reset internal state. pub fn reset(&mut self) { - unsafe { opus_repacketizer_init(self.rp) }; + unsafe { opus_repacketizer_init(self.rp.as_ptr()) }; + self.packets.clear(); } /// Add a packet to the current state. /// + /// The packet data is copied and retained until the next call to [`Self::reset`]. + /// /// # Errors /// Returns an error if the packet is invalid for the current state. pub fn push(&mut self, packet: &[u8]) -> Result<()> { @@ -42,24 +64,46 @@ impl Repacketizer { return Err(Error::BadArg); } let len_i32 = i32::try_from(packet.len()).map_err(|_| Error::BadArg)?; - let r = unsafe { opus_repacketizer_cat(self.rp, packet.as_ptr(), len_i32) }; + self.packets.push(packet.to_vec()); + let idx = self.packets.len() - 1; + let r = + unsafe { opus_repacketizer_cat(self.rp.as_ptr(), self.packets[idx].as_ptr(), len_i32) }; if r != 0 { + self.packets.pop(); return Err(Error::from_code(r)); } + // libopus stores pointers into packet data; keep owned buffers alive. Ok(()) } /// Number of frames currently queued. #[must_use] - pub fn frames(&mut self) -> i32 { - unsafe { opus_repacketizer_get_nb_frames(self.rp) } + pub fn frame_count(&self) -> i32 { + unsafe { opus_repacketizer_get_nb_frames(self.rp.as_ptr()) } + } + + /// Number of frames currently queued as a `usize`. + #[must_use] + pub fn len(&self) -> usize { + let frames = self.frame_count(); + debug_assert!( + frames >= 0, + "repacketizer frame count should be non-negative" + ); + usize::try_from(frames).unwrap_or(0) + } + + /// Returns true when there are no queued frames. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 } /// Emit a packet containing frames in range [begin, end). /// /// # Errors /// Returns an error if range is invalid or output buffer is too small. - pub fn out_range(&mut self, begin: i32, end: i32, out: &mut [u8]) -> Result { + pub fn emit_range(&mut self, begin: i32, end: i32, out: &mut [u8]) -> Result { if out.is_empty() { return Err(Error::BadArg); } @@ -68,7 +112,7 @@ impl Repacketizer { } let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; let n = unsafe { - opus_repacketizer_out_range(self.rp, begin, end, out.as_mut_ptr(), out_len_i32) + opus_repacketizer_out_range(self.rp.as_ptr(), begin, end, out.as_mut_ptr(), out_len_i32) }; if n < 0 { return Err(Error::from_code(n)); @@ -80,21 +124,97 @@ impl Repacketizer { /// /// # Errors /// Returns an error if the output buffer is too small. - pub fn out(&mut self, out: &mut [u8]) -> Result { + pub fn emit(&mut self, out: &mut [u8]) -> Result { if out.is_empty() { return Err(Error::BadArg); } let out_len_i32 = i32::try_from(out.len()).map_err(|_| Error::BadArg)?; - let n = unsafe { opus_repacketizer_out(self.rp, out.as_mut_ptr(), out_len_i32) }; + let n = unsafe { opus_repacketizer_out(self.rp.as_ptr(), out.as_mut_ptr(), out_len_i32) }; if n < 0 { return Err(Error::from_code(n)); } usize::try_from(n).map_err(|_| Error::InternalError) } + + /// Size of a repacketizer state in bytes for external allocation. + /// + /// # Errors + /// Returns [`Error::InternalError`] if libopus reports an invalid size. + pub fn size() -> Result { + let raw = unsafe { opus_repacketizer_get_size() }; + if raw <= 0 { + return Err(Error::InternalError); + } + usize::try_from(raw).map_err(|_| Error::InternalError) + } + + /// Initialize a previously allocated repacketizer state. + /// + /// # Safety + /// The caller must provide a valid pointer to `Repacketizer::size()` bytes, + /// aligned to at least `align_of::()` (malloc-style alignment). + /// + /// # Errors + /// Returns [`Error::BadArg`] if `ptr` is null. + pub unsafe fn init_in_place(ptr: *mut OpusRepacketizer) -> Result<()> { + if ptr.is_null() { + return Err(Error::BadArg); + } + if !crate::opus_ptr_is_aligned(ptr.cast()) { + return Err(Error::BadArg); + } + unsafe { opus_repacketizer_init(ptr) }; + Ok(()) + } +} + +impl<'a> RepacketizerRef<'a> { + /// Wrap an externally-initialized repacketizer without taking ownership. + /// + /// # Safety + /// - `ptr` must point to valid, initialized memory of at least [`Repacketizer::size()`] bytes + /// - `ptr` must be aligned to at least `align_of::()` (malloc-style alignment) + /// - The memory must remain valid for the lifetime `'a` + /// - Caller is responsible for freeing the memory after this wrapper is dropped + /// + /// Use [`Repacketizer::init_in_place`] to initialize the memory before calling this. + #[must_use] + pub unsafe fn from_raw(ptr: *mut OpusRepacketizer) -> Self { + debug_assert!(!ptr.is_null(), "from_raw called with null ptr"); + debug_assert!(crate::opus_ptr_is_aligned(ptr.cast())); + let repacketizer = + Repacketizer::from_raw(unsafe { NonNull::new_unchecked(ptr) }, Ownership::Borrowed); + Self { + inner: repacketizer, + _marker: PhantomData, + } + } + + /// Initialize and wrap an externally allocated buffer. + /// + /// # Errors + /// Returns [`Error::BadArg`] if the buffer is too small. + pub fn init_in(buf: &'a mut AlignedBuffer) -> Result { + let required = Repacketizer::size()?; + if buf.capacity_bytes() < required { + return Err(Error::BadArg); + } + let ptr = buf.as_mut_ptr::(); + unsafe { Repacketizer::init_in_place(ptr)? }; + Ok(unsafe { Self::from_raw(ptr) }) + } +} + +impl Deref for RepacketizerRef<'_> { + type Target = Repacketizer; + + fn deref(&self) -> &Self::Target { + &self.inner + } } -impl Drop for Repacketizer { - fn drop(&mut self) { - unsafe { opus_repacketizer_destroy(self.rp) }; +impl DerefMut for RepacketizerRef<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } diff --git a/src/types.rs b/src/types.rs index e71c477..3f37f99 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,7 +6,7 @@ use crate::bindings::{ OPUS_BANDWIDTH_SUPERWIDEBAND, OPUS_BANDWIDTH_WIDEBAND, OPUS_BITRATE_MAX, OPUS_FRAMESIZE_2_5_MS, OPUS_FRAMESIZE_5_MS, OPUS_FRAMESIZE_10_MS, OPUS_FRAMESIZE_20_MS, OPUS_FRAMESIZE_40_MS, OPUS_FRAMESIZE_60_MS, OPUS_FRAMESIZE_80_MS, OPUS_FRAMESIZE_100_MS, OPUS_FRAMESIZE_120_MS, - OPUS_SIGNAL_MUSIC, OPUS_SIGNAL_VOICE, + OPUS_FRAMESIZE_ARG, OPUS_SIGNAL_MUSIC, OPUS_SIGNAL_VOICE, }; /// Encoder application mode. @@ -121,6 +121,8 @@ impl FrameSize { /// Hint the encoder about the type of content. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Signal { + /// Automatic selection (default). + Auto = OPUS_AUTO as isize, /// Voice-optimized mode. Voice = OPUS_SIGNAL_VOICE as isize, /// Music/general audio optimized mode. @@ -130,6 +132,8 @@ pub enum Signal { /// Expert frame duration settings for the encoder. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExpertFrameDuration { + /// Select frame size from the argument (default). + Auto = OPUS_FRAMESIZE_ARG as isize, /// 2.5 ms. Ms2_5 = OPUS_FRAMESIZE_2_5_MS as isize, /// 5 ms. diff --git a/tests/encoder_decoder.rs b/tests/encoder_decoder.rs new file mode 100644 index 0000000..6c9939b --- /dev/null +++ b/tests/encoder_decoder.rs @@ -0,0 +1,104 @@ +use opus_codec::AlignedBuffer; +use opus_codec::decoder::{Decoder, DecoderRef}; +use opus_codec::encoder::{Encoder, EncoderRef}; +use opus_codec::error::Error; +use opus_codec::types::{Application, Channels, SampleRate}; + +#[test] +fn test_encode_decode() { + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); + + let frame_size = 960; // 20ms + let pcm_in = vec![0i16; frame_size]; + let mut packet = [0u8; 500]; + let mut pcm_out = vec![0i16; frame_size]; + + let len = encoder.encode(&pcm_in, &mut packet).unwrap(); + assert!(len > 0); + + let decoded_len = decoder.decode(&packet[..len], &mut pcm_out, false).unwrap(); + assert_eq!(decoded_len, frame_size); +} + +#[test] +fn test_float_api() { + let mut encoder = + Encoder::new(SampleRate::Hz48000, Channels::Stereo, Application::Audio).unwrap(); + let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Stereo).unwrap(); + + let frame_size = 480; // 10ms + let pcm_in = vec![0.0f32; frame_size * 2]; + let mut packet = [0u8; 500]; + let mut pcm_out = vec![0.0f32; frame_size * 2]; + + let len = encoder.encode_float(&pcm_in, &mut packet).unwrap(); + assert!(len > 0); + + let decoded_len = decoder + .decode_float(&packet[..len], &mut pcm_out, false) + .unwrap(); + assert_eq!(decoded_len, frame_size); +} + +#[test] +fn test_buffer_empty() { + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let pcm = vec![0i16; 960]; + let mut empty_buf = [0u8; 0]; + + // The wrapper should catch this and return BadArg before calling libopus + let result = encoder.encode(&pcm, &mut empty_buf); + assert_eq!(result, Err(Error::BadArg)); +} + +#[test] +fn test_init_in_place_alignment_checks() { + let sr = SampleRate::Hz48000; + let channels = Channels::Mono; + + let enc_size = Encoder::size(channels).unwrap(); + let mut enc_buf = vec![0u8; enc_size + 1]; + let enc_ptr = unsafe { enc_buf.as_mut_ptr().add(1) }; + let err = unsafe { Encoder::init_in_place(enc_ptr.cast(), sr, channels, Application::Voip) } + .unwrap_err(); + assert_eq!(err, Error::BadArg); + + let dec_size = Decoder::size(channels).unwrap(); + let mut dec_buf = vec![0u8; dec_size + 1]; + let dec_ptr = unsafe { dec_buf.as_mut_ptr().add(1) }; + let err = unsafe { Decoder::init_in_place(dec_ptr.cast(), sr, channels) }.unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn test_init_in_place_unowned_encoder_decoder() { + let sr = SampleRate::Hz48000; + let channels = Channels::Mono; + let frame_size = 960; + + let enc_size = Encoder::size(channels).unwrap(); + let mut enc_buf = AlignedBuffer::with_capacity_bytes(enc_size); + let enc_ptr = enc_buf.as_mut_ptr(); + unsafe { + Encoder::init_in_place(enc_ptr, sr, channels, Application::Voip).unwrap(); + } + let mut encoder = unsafe { EncoderRef::from_raw(enc_ptr, sr, channels) }; + + let dec_size = Decoder::size(channels).unwrap(); + let mut dec_buf = AlignedBuffer::with_capacity_bytes(dec_size); + let dec_ptr = dec_buf.as_mut_ptr(); + unsafe { + Decoder::init_in_place(dec_ptr, sr, channels).unwrap(); + } + let mut decoder = unsafe { DecoderRef::from_raw(dec_ptr, sr, channels) }; + + let pcm = vec![0i16; frame_size]; + let mut packet = [0u8; 500]; + let len = encoder.encode(&pcm, &mut packet).unwrap(); + assert!(len > 0); + + let mut out = vec![0i16; frame_size]; + let decoded = decoder.decode(&packet[..len], &mut out, false).unwrap(); + assert_eq!(decoded, frame_size); +} diff --git a/tests/ffmpeg_roundtrip.rs b/tests/ffmpeg_roundtrip.rs index e72e287..29c93c3 100644 --- a/tests/ffmpeg_roundtrip.rs +++ b/tests/ffmpeg_roundtrip.rs @@ -5,7 +5,7 @@ use std::process::Command; use tempfile::NamedTempFile; use opus_codec::{Application, Channels, Decoder, Encoder, SampleRate}; -use opus_codec::{MSDecoder, MSEncoder, Mapping}; +use opus_codec::{Mapping, MultistreamDecoder, MultistreamEncoder}; fn ffmpeg_available() -> bool { Command::new("ffmpeg").arg("-version").output().is_ok() @@ -185,14 +185,14 @@ fn test_multistream_basic_stereo_roundtrip() { coupled_streams: 1, mapping: &[0, 1], }; - let mut enc = MSEncoder::new(sr, Application::Audio, mapping).expect("ms encoder"); + let mut enc = MultistreamEncoder::new(sr, Application::Audio, mapping).expect("ms encoder"); let mapping_dec = Mapping { channels, streams: 1, coupled_streams: 1, mapping: &[0, 1], }; - let mut dec = MSDecoder::new(sr, mapping_dec).expect("ms decoder"); + let mut dec = MultistreamDecoder::new(sr, mapping_dec).expect("ms decoder"); // Generate 20 ms stereo sine let frame = 960usize; // per channel diff --git a/tests/multistream.rs b/tests/multistream.rs new file mode 100644 index 0000000..2b234a7 --- /dev/null +++ b/tests/multistream.rs @@ -0,0 +1,145 @@ +use opus_codec::AlignedBuffer; +use opus_codec::error::Error; +use opus_codec::max_frame_samples_for; +use opus_codec::multistream::{ + Mapping, MultistreamDecoder, MultistreamDecoderRef, MultistreamEncoder, MultistreamEncoderRef, +}; +use opus_codec::types::{Application, SampleRate}; + +#[test] +fn test_multistream_surround() { + // 5.1 Surround: 6 channels + let channels = 6; + let mapping_family = 1; // Family 1 is for surround + let (mut encoder, _) = MultistreamEncoder::new_surround( + SampleRate::Hz48000, + channels, + mapping_family, + Application::Audio, + ) + .unwrap(); + + let streams = encoder.streams(); + let coupled = encoder.coupled_streams(); + let mapping_table = [0, 1, 2, 3, 4, 5]; // Standard identity mapping for the streams + + let mapping = Mapping { + channels, + streams, + coupled_streams: coupled, + mapping: &mapping_table, + }; + + let mut decoder = MultistreamDecoder::new(SampleRate::Hz48000, mapping).unwrap(); + + let frame_size = 960; + let pcm_in = vec![0i16; frame_size * channels as usize]; + let mut packet = [0u8; 1500]; + let mut pcm_out = vec![0i16; frame_size * channels as usize]; + + let len = encoder.encode(&pcm_in, frame_size, &mut packet).unwrap(); + assert!(len > 0); + + let decoded_len = decoder + .decode(&packet[..len], &mut pcm_out, frame_size, false) + .unwrap(); + assert_eq!(decoded_len, frame_size); +} + +#[test] +fn test_multistream_frame_size_validation() { + let mapping_table = [0u8]; + let mapping = Mapping { + channels: 1, + streams: 1, + coupled_streams: 0, + mapping: &mapping_table, + }; + let mut encoder = + MultistreamEncoder::new(SampleRate::Hz48000, Application::Audio, mapping).unwrap(); + let mut decoder = MultistreamDecoder::new(SampleRate::Hz48000, mapping).unwrap(); + let mut out = [0u8; 100]; + + assert_eq!(encoder.encode(&[], 0, &mut out), Err(Error::BadArg)); + let too_large = max_frame_samples_for(SampleRate::Hz48000) + 1; + assert_eq!(encoder.encode(&[], too_large, &mut out), Err(Error::BadArg)); + + let mut pcm_out: [i16; 0] = []; + assert_eq!( + decoder.decode(&[], &mut pcm_out, 0, false), + Err(Error::BadArg) + ); + assert_eq!( + decoder.decode(&[], &mut pcm_out, too_large, false), + Err(Error::BadArg) + ); +} + +#[test] +fn test_init_in_place_unowned_multistream() { + let sr = SampleRate::Hz48000; + let frame_size = 960; + let mapping_table = [0u8, 1u8]; + let mapping = Mapping { + channels: 2, + streams: 1, + coupled_streams: 1, + mapping: &mapping_table, + }; + + let enc_size = MultistreamEncoder::size(mapping.streams, mapping.coupled_streams).unwrap(); + let mut enc_buf = AlignedBuffer::with_capacity_bytes(enc_size); + let enc_ptr = enc_buf.as_mut_ptr(); + unsafe { + MultistreamEncoder::init_in_place(enc_ptr, sr, Application::Audio, mapping).unwrap(); + } + let mut encoder = unsafe { MultistreamEncoderRef::from_raw(enc_ptr, sr, mapping) }; + + let dec_size = MultistreamDecoder::size(mapping.streams, mapping.coupled_streams).unwrap(); + let mut dec_buf = AlignedBuffer::with_capacity_bytes(dec_size); + let dec_ptr = dec_buf.as_mut_ptr(); + unsafe { + MultistreamDecoder::init_in_place(dec_ptr, sr, mapping).unwrap(); + } + let mut decoder = unsafe { MultistreamDecoderRef::from_raw(dec_ptr, sr, mapping) }; + + let mut pcm = vec![0i16; frame_size * mapping.channels as usize]; + for (i, sample) in pcm.iter_mut().enumerate() { + *sample = ((i as i32 * 17) % 2000) as i16; + } + let mut packet = vec![0u8; 4000]; + let len = encoder.encode(&pcm, frame_size, &mut packet).unwrap(); + assert!(len > 0); + + let mut out = vec![0i16; frame_size * mapping.channels as usize]; + let decoded = decoder + .decode(&packet[..len], &mut out, frame_size, false) + .unwrap(); + assert_eq!(decoded, frame_size); +} + +#[test] +fn test_init_in_place_invalid_mapping() { + let sr = SampleRate::Hz48000; + let mapping_table = [0u8]; + let mapping = Mapping { + channels: 2, + streams: 1, + coupled_streams: 1, + mapping: &mapping_table, + }; + + let enc_size = MultistreamEncoder::size(mapping.streams, mapping.coupled_streams).unwrap(); + let mut enc_buf = AlignedBuffer::with_capacity_bytes(enc_size); + let enc_ptr = enc_buf.as_mut_ptr(); + let err = + unsafe { MultistreamEncoder::init_in_place(enc_ptr, sr, Application::Audio, mapping) } + .unwrap_err(); + assert_eq!(err, Error::BadArg); + + let dec_size = MultistreamDecoder::size(mapping.streams, mapping.coupled_streams).unwrap(); + let mut dec_buf = AlignedBuffer::with_capacity_bytes(dec_size); + let dec_ptr = dec_buf.as_mut_ptr(); + let err = unsafe { MultistreamDecoder::init_in_place(dec_ptr, sr, mapping) }.unwrap_err(); + assert_eq!(err, Error::BadArg); +} diff --git a/tests/opus_tests.rs b/tests/opus_tests.rs deleted file mode 100755 index e0bf002..0000000 --- a/tests/opus_tests.rs +++ /dev/null @@ -1,212 +0,0 @@ -use opus_codec::decoder::Decoder; -use opus_codec::encoder::Encoder; -use opus_codec::error::Error; -use opus_codec::multistream::{MSDecoder, MSEncoder, Mapping}; -use opus_codec::packet::{ - packet_bandwidth, packet_channels, packet_nb_frames, packet_nb_samples, packet_parse, soft_clip, -}; -use opus_codec::repacketizer::Repacketizer; -use opus_codec::types::{Application, Bandwidth, Channels, SampleRate}; - -#[test] -fn test_packet_analysis() { - // Create a silent packet - let mut encoder = - Encoder::new(SampleRate::Hz48000, Channels::Stereo, Application::Audio).unwrap(); - let pcm = vec![0i16; 960 * 2]; // 20ms stereo - let mut output = [0u8; 100]; - let len = encoder.encode(&pcm, &mut output).unwrap(); - let packet = &output[..len]; - - // Analyze - assert!(packet_nb_frames(packet).unwrap() > 0); - assert_eq!(packet_nb_samples(packet, SampleRate::Hz48000).unwrap(), 960); - assert_eq!(packet_channels(packet).unwrap(), Channels::Stereo); - assert!(packet_bandwidth(packet).unwrap() != Bandwidth::Narrowband); // Likely Fullband for Audio app - - // Parse - let (_toc, _offset, frames) = packet_parse(packet).unwrap(); - assert!(!frames.is_empty()); -} - -#[test] -fn test_encode_decode() { - let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); - let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); - - let frame_size = 960; // 20ms - let pcm_in = vec![0i16; frame_size]; - let mut packet = [0u8; 500]; - let mut pcm_out = vec![0i16; frame_size]; - - let len = encoder.encode(&pcm_in, &mut packet).unwrap(); - assert!(len > 0); - - let decoded_len = decoder.decode(&packet[..len], &mut pcm_out, false).unwrap(); - assert_eq!(decoded_len, frame_size); -} - -#[test] -fn test_float_api() { - let mut encoder = - Encoder::new(SampleRate::Hz48000, Channels::Stereo, Application::Audio).unwrap(); - let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Stereo).unwrap(); - - let frame_size = 480; // 10ms - let pcm_in = vec![0.0f32; frame_size * 2]; - let mut packet = [0u8; 500]; - let mut pcm_out = vec![0.0f32; frame_size * 2]; - - let len = encoder.encode_float(&pcm_in, &mut packet).unwrap(); - assert!(len > 0); - - let decoded_len = decoder - .decode_float(&packet[..len], &mut pcm_out, false) - .unwrap(); - assert_eq!(decoded_len, frame_size); -} - -#[test] -fn test_multistream_surround() { - // 5.1 Surround: 6 channels - let channels = 6; - let mapping_family = 1; // Family 1 is for surround - let (mut encoder, _) = MSEncoder::new_surround( - SampleRate::Hz48000, - channels, - mapping_family, - Application::Audio, - ) - .unwrap(); - - let streams = encoder.streams(); - let coupled = encoder.coupled_streams(); - let mapping_table = [0, 1, 2, 3, 4, 5]; // Standard identity mapping for the streams - - let mapping = Mapping { - channels, - streams, - coupled_streams: coupled, - mapping: &mapping_table, - }; - - let mut decoder = MSDecoder::new(SampleRate::Hz48000, mapping).unwrap(); - - let frame_size = 960; - let pcm_in = vec![0i16; frame_size * channels as usize]; - let mut packet = [0u8; 1500]; - let mut pcm_out = vec![0i16; frame_size * channels as usize]; - - let len = encoder.encode(&pcm_in, frame_size, &mut packet).unwrap(); - assert!(len > 0); - - let decoded_len = decoder - .decode(&packet[..len], &mut pcm_out, frame_size, false) - .unwrap(); - assert_eq!(decoded_len, frame_size); -} - -#[test] -fn test_repacketizer() { - let mut rp = Repacketizer::new().unwrap(); - let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); - - // Create two 20ms frames - let frame_size = 960; - let pcm = vec![0i16; frame_size]; - let mut packet1 = [0u8; 200]; - let mut packet2 = [0u8; 200]; - - let len1 = encoder.encode(&pcm, &mut packet1).unwrap(); - let len2 = encoder.encode(&pcm, &mut packet2).unwrap(); - - // Add them to repacketizer - rp.push(&packet1[..len1]).unwrap(); - rp.push(&packet2[..len2]).unwrap(); - - // Verify we have 2 frames - assert_eq!(rp.frames(), 2); - - // Merge into one packet - let mut merged = [0u8; 500]; - let merged_len = rp.out(&mut merged).unwrap(); - assert!(merged_len > 0); - - // Verify the merged packet has 2 frames - assert_eq!(packet_nb_frames(&merged[..merged_len]).unwrap(), 2); -} - -#[test] -fn test_buffer_empty() { - let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); - let pcm = vec![0i16; 960]; - let mut empty_buf = [0u8; 0]; - - // The wrapper should catch this and return BadArg before calling libopus - let result = encoder.encode(&pcm, &mut empty_buf); - assert_eq!(result, Err(Error::BadArg)); -} - -#[test] -fn test_projection_ambisonics() { - use opus_codec::projection::{ProjectionDecoder, ProjectionEncoder}; - - // First Order Ambisonics (4 channels) with Family 3 (Ambisonics) - let channels = 4; - let mapping_family = 3; - let mut encoder = ProjectionEncoder::new( - SampleRate::Hz48000, - channels, - mapping_family, - Application::Audio, - ) - .unwrap(); - - let demixing_matrix = encoder.demixing_matrix_bytes().unwrap(); - assert!(!demixing_matrix.is_empty()); - - let mut decoder = ProjectionDecoder::new( - SampleRate::Hz48000, - channels, - encoder.streams(), - encoder.coupled_streams(), - &demixing_matrix, - ) - .unwrap(); - - let frame_size = 960; - let pcm_in = vec![0i16; frame_size * channels as usize]; - let mut packet = [0u8; 1500]; - let mut pcm_out = vec![0i16; frame_size * channels as usize]; - - let len = encoder.encode(&pcm_in, frame_size, &mut packet).unwrap(); - assert!(len > 0); - - let decoded_len = decoder - .decode(&packet[..len], &mut pcm_out, frame_size, false) - .unwrap(); - assert_eq!(decoded_len, frame_size); -} - -#[test] -fn test_soft_clip_validations() { - let mut pcm = vec![1.5f32; 4]; - let mut state = vec![0f32; 2]; - assert!(soft_clip(&mut pcm, 2, 2, &mut state).is_ok()); - - let mut short_pcm = vec![1.5f32; 3]; - assert_eq!( - soft_clip(&mut short_pcm, 2, 2, &mut state), - Err(Error::BadArg) - ); - - let mut pcm = vec![1.5f32; 4]; - let mut too_small_state = vec![0f32; 1]; - assert_eq!( - soft_clip(&mut pcm, 2, 2, &mut too_small_state), - Err(Error::BadArg) - ); - - let mut pcm = vec![1.5f32; 4]; - assert_eq!(soft_clip(&mut pcm, 2, -1, &mut state), Err(Error::BadArg)); -} diff --git a/tests/packet_utils.rs b/tests/packet_utils.rs new file mode 100644 index 0000000..b95d28c --- /dev/null +++ b/tests/packet_utils.rs @@ -0,0 +1,64 @@ +use opus_codec::encoder::Encoder; +use opus_codec::error::Error; +use opus_codec::packet::{ + packet_bandwidth, packet_channels, packet_frame_count, packet_parse, packet_sample_count, + soft_clip, +}; +use opus_codec::types::{Application, Bandwidth, Channels, SampleRate}; + +#[test] +fn test_packet_analysis() { + // Create a silent packet + let mut encoder = + Encoder::new(SampleRate::Hz48000, Channels::Stereo, Application::Audio).unwrap(); + let pcm = vec![0i16; 960 * 2]; // 20ms stereo + let mut output = [0u8; 100]; + let len = encoder.encode(&pcm, &mut output).unwrap(); + let packet = &output[..len]; + + // Analyze + assert!(packet_frame_count(packet).unwrap() > 0); + assert_eq!( + packet_sample_count(packet, SampleRate::Hz48000).unwrap(), + 960 + ); + assert_eq!(packet_channels(packet).unwrap(), Channels::Stereo); + assert!(packet_bandwidth(packet).unwrap() != Bandwidth::Narrowband); // Likely Fullband for Audio app + + // Parse + let (_toc, _offset, frames) = packet_parse(packet).unwrap(); + assert!(!frames.is_empty()); +} + +#[test] +fn test_packet_parse_keeps_zero_length_frames() { + let packet = [0x02u8, 0x01, 0x00]; + let (_toc, offset, frames) = packet_parse(&packet).unwrap(); + assert_eq!(offset, 2); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].len(), 1); + assert!(frames[1].is_empty()); +} + +#[test] +fn test_soft_clip_validations() { + let mut pcm = vec![1.5f32; 4]; + let mut state = vec![0f32; 2]; + assert!(soft_clip(&mut pcm, 2, 2, &mut state).is_ok()); + + let mut short_pcm = vec![1.5f32; 3]; + assert_eq!( + soft_clip(&mut short_pcm, 2, 2, &mut state), + Err(Error::BadArg) + ); + + let mut pcm = vec![1.5f32; 4]; + let mut too_small_state = vec![0f32; 1]; + assert_eq!( + soft_clip(&mut pcm, 2, 2, &mut too_small_state), + Err(Error::BadArg) + ); + + let mut pcm = vec![1.5f32; 4]; + assert_eq!(soft_clip(&mut pcm, 2, -1, &mut state), Err(Error::BadArg)); +} diff --git a/tests/projection.rs b/tests/projection.rs new file mode 100644 index 0000000..0eeb656 --- /dev/null +++ b/tests/projection.rs @@ -0,0 +1,101 @@ +use opus_codec::AlignedBuffer; +use opus_codec::error::Error; +use opus_codec::projection::{ + ProjectionDecoder, ProjectionDecoderRef, ProjectionEncoder, ProjectionEncoderRef, +}; +use opus_codec::types::{Application, SampleRate}; + +#[test] +fn test_projection_ambisonics() { + // First Order Ambisonics (4 channels) with Family 3 (Ambisonics) + let channels = 4; + let mapping_family = 3; + let mut encoder = ProjectionEncoder::new( + SampleRate::Hz48000, + channels, + mapping_family, + Application::Audio, + ) + .unwrap(); + + let demixing_matrix = encoder.demixing_matrix_bytes().unwrap(); + assert!(!demixing_matrix.is_empty()); + + let mut decoder = ProjectionDecoder::new( + SampleRate::Hz48000, + channels, + encoder.streams(), + encoder.coupled_streams(), + &demixing_matrix, + ) + .unwrap(); + + let frame_size = 960; + let pcm_in = vec![0i16; frame_size * channels as usize]; + let mut packet = [0u8; 1500]; + let mut pcm_out = vec![0i16; frame_size * channels as usize]; + + let len = encoder.encode(&pcm_in, frame_size, &mut packet).unwrap(); + assert!(len > 0); + + let decoded_len = decoder + .decode(&packet[..len], &mut pcm_out, frame_size, false) + .unwrap(); + assert_eq!(decoded_len, frame_size); +} + +#[test] +fn test_init_in_place_unowned_projection() { + let sr = SampleRate::Hz48000; + let channels = 4; + let mapping_family = 3; + let frame_size = 960; + + let enc_size = ProjectionEncoder::size(channels, mapping_family).unwrap(); + let mut enc_buf = AlignedBuffer::with_capacity_bytes(enc_size); + let enc_ptr = enc_buf.as_mut_ptr(); + let (streams, coupled) = unsafe { + ProjectionEncoder::init_in_place(enc_ptr, sr, channels, mapping_family, Application::Audio) + .unwrap() + }; + let mut encoder = + unsafe { ProjectionEncoderRef::from_raw(enc_ptr, sr, channels, streams, coupled) }; + + let demixing = encoder.demixing_matrix_bytes().unwrap(); + let dec_size = ProjectionDecoder::size(channels, streams, coupled).unwrap(); + let mut dec_buf = AlignedBuffer::with_capacity_bytes(dec_size); + let dec_ptr = dec_buf.as_mut_ptr(); + unsafe { + ProjectionDecoder::init_in_place(dec_ptr, sr, channels, streams, coupled, &demixing) + .unwrap(); + } + let mut decoder = + unsafe { ProjectionDecoderRef::from_raw(dec_ptr, sr, channels, streams, coupled) }; + + let pcm_in = vec![0i16; frame_size * channels as usize]; + let mut packet = vec![0u8; 4000]; + let len = encoder.encode(&pcm_in, frame_size, &mut packet).unwrap(); + assert!(len > 0); + + let mut pcm_out = vec![0i16; frame_size * channels as usize]; + let decoded = decoder + .decode(&packet[..len], &mut pcm_out, frame_size, false) + .unwrap(); + assert_eq!(decoded, frame_size); +} + +#[test] +fn test_init_in_place_invalid_demixing_matrix() { + let sr = SampleRate::Hz48000; + let channels = 4; + let streams = 2; + let coupled_streams = 2; + let size = ProjectionDecoder::size(channels, streams, coupled_streams).unwrap(); + let mut buf = AlignedBuffer::with_capacity_bytes(size); + let ptr = buf.as_mut_ptr(); + let err = unsafe { + ProjectionDecoder::init_in_place(ptr, sr, channels, streams, coupled_streams, &[]) + } + .unwrap_err(); + assert_eq!(err, Error::BadArg); +} diff --git a/tests/raw_alloc.rs b/tests/raw_alloc.rs new file mode 100644 index 0000000..6d32ddd --- /dev/null +++ b/tests/raw_alloc.rs @@ -0,0 +1,14 @@ +use opus_codec::decoder::Decoder; +use opus_codec::encoder::Encoder; +use opus_codec::multistream::{MultistreamDecoder, MultistreamEncoder}; +use opus_codec::repacketizer::Repacketizer; +use opus_codec::types::Channels; + +#[test] +fn test_raw_size_helpers() { + assert!(Encoder::size(Channels::Mono).unwrap() > 0); + assert!(Decoder::size(Channels::Stereo).unwrap() > 0); + assert!(MultistreamEncoder::size(1, 0).unwrap() > 0); + assert!(MultistreamDecoder::size(1, 0).unwrap() > 0); + assert!(Repacketizer::size().unwrap() > 0); +} diff --git a/tests/repacketizer.rs b/tests/repacketizer.rs new file mode 100644 index 0000000..4ab16e2 --- /dev/null +++ b/tests/repacketizer.rs @@ -0,0 +1,83 @@ +use opus_codec::AlignedBuffer; +use opus_codec::encoder::Encoder; +use opus_codec::error::Error; +use opus_codec::packet::packet_frame_count; +use opus_codec::repacketizer::{Repacketizer, RepacketizerRef}; +use opus_codec::types::{Application, Channels, SampleRate}; + +#[test] +fn test_repacketizer() { + let mut rp = Repacketizer::new().unwrap(); + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + + // Create two 20ms frames + let frame_size = 960; + let pcm = vec![0i16; frame_size]; + let mut packet1 = [0u8; 200]; + let mut packet2 = [0u8; 200]; + + let len1 = encoder.encode(&pcm, &mut packet1).unwrap(); + let len2 = encoder.encode(&pcm, &mut packet2).unwrap(); + + // Add them to repacketizer + rp.push(&packet1[..len1]).unwrap(); + rp.push(&packet2[..len2]).unwrap(); + + // Verify we have 2 frames + assert_eq!(rp.len(), 2); + + // Merge into one packet + let mut merged = [0u8; 500]; + let merged_len = rp.emit(&mut merged).unwrap(); + assert!(merged_len > 0); + + // Verify the merged packet has 2 frames + assert_eq!(packet_frame_count(&merged[..merged_len]).unwrap(), 2); +} + +#[test] +fn test_init_in_place_null_repacketizer() { + let err = unsafe { Repacketizer::init_in_place(std::ptr::null_mut()) }.unwrap_err(); + assert_eq!(err, Error::BadArg); +} + +#[test] +fn test_init_in_place_unowned_repacketizer() { + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let frame_size = 960; + let pcm = vec![0i16; frame_size]; + let mut packet = [0u8; 500]; + let len = encoder.encode(&pcm, &mut packet).unwrap(); + + let rp_size = Repacketizer::size().unwrap(); + let mut rp_buf = AlignedBuffer::with_capacity_bytes(rp_size); + let rp_ptr = rp_buf.as_mut_ptr(); + unsafe { + Repacketizer::init_in_place(rp_ptr).unwrap(); + } + let mut rp = unsafe { RepacketizerRef::from_raw(rp_ptr) }; + + rp.push(&packet[..len]).unwrap(); + let mut out = [0u8; 500]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], &packet[..len]); +} + +#[test] +fn test_repacketizer_owns_packet_data() { + let mut encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).unwrap(); + let mut rp = Repacketizer::new().unwrap(); + + let frame_size = 960; + let pcm = vec![0i16; frame_size]; + let mut packet = [0u8; 200]; + let len = encoder.encode(&pcm, &mut packet).unwrap(); + let original = packet[..len].to_vec(); + + rp.push(&packet[..len]).unwrap(); + packet[..len].fill(0); + + let mut out = [0u8; 200]; + let out_len = rp.emit(&mut out).unwrap(); + assert_eq!(&out[..out_len], original.as_slice()); +}