Skip to main content

opus_codec/
projection.rs

1//! Safe wrappers for the libopus projection (ambisonics) API
2
3use crate::bindings::{
4    OPUS_BITRATE_MAX, OPUS_GET_BITRATE_REQUEST, OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST,
5    OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST,
6    OPUS_SET_BITRATE_REQUEST, OpusProjectionDecoder, OpusProjectionEncoder,
7    opus_projection_ambisonics_encoder_create, opus_projection_ambisonics_encoder_get_size,
8    opus_projection_ambisonics_encoder_init, opus_projection_decode, opus_projection_decode_float,
9    opus_projection_decoder_create, opus_projection_decoder_destroy,
10    opus_projection_decoder_get_size, opus_projection_decoder_init, opus_projection_encode,
11    opus_projection_encode_float, opus_projection_encoder_ctl, opus_projection_encoder_destroy,
12};
13use crate::constants::{is_frame_size_2_5ms_aligned, max_frame_samples_for};
14use crate::error::{Error, Result};
15use crate::types::{Application, Bitrate, SampleRate};
16use crate::{AlignedBuffer, Ownership, RawHandle};
17use std::marker::PhantomData;
18use std::num::{NonZeroU8, NonZeroUsize};
19use std::ops::Deref;
20use std::ptr::NonNull;
21
22fn validate_channels(channels: u8) -> Result<()> {
23    NonZeroU8::new(channels).ok_or(Error::BadArg)?;
24    Ok(())
25}
26
27fn validate_projection_mapping_family(mapping_family: i32) -> Result<()> {
28    if mapping_family == 3 {
29        Ok(())
30    } else {
31        Err(Error::BadArg)
32    }
33}
34
35fn validate_stream_counts(streams: u8, coupled_streams: u8) -> Result<()> {
36    let streams = NonZeroU8::new(streams).ok_or(Error::BadArg)?;
37    if coupled_streams > streams.get() {
38        return Err(Error::BadArg);
39    }
40    if usize::from(streams.get()) + usize::from(coupled_streams) > u8::MAX as usize {
41        return Err(Error::BadArg);
42    }
43    Ok(())
44}
45
46/// Safe wrapper around `OpusProjectionEncoder`.
47pub struct ProjectionEncoder {
48    raw: RawHandle<OpusProjectionEncoder>,
49    sample_rate: SampleRate,
50    channels: u8,
51    streams: u8,
52    coupled_streams: u8,
53}
54
55unsafe impl Send for ProjectionEncoder {}
56
57/// Borrowed wrapper around a projection encoder state.
58///
59/// The owning handle cannot be moved out of this borrowed wrapper:
60///
61/// ```compile_fail
62/// use opus_codec::projection::{ProjectionEncoder, ProjectionEncoderRef};
63/// fn extract<'a>(
64///     state: &mut ProjectionEncoderRef<'a>,
65///     replacement: ProjectionEncoder,
66/// ) -> ProjectionEncoder {
67///     std::mem::replace(&mut **state, replacement)
68/// }
69/// ```
70pub struct ProjectionEncoderRef<'a> {
71    inner: ProjectionEncoder,
72    _marker: PhantomData<&'a mut OpusProjectionEncoder>,
73}
74
75unsafe impl Send for ProjectionEncoderRef<'_> {}
76
77impl ProjectionEncoder {
78    fn from_raw(
79        ptr: NonNull<OpusProjectionEncoder>,
80        sample_rate: SampleRate,
81        channels: u8,
82        streams: u8,
83        coupled_streams: u8,
84        ownership: Ownership,
85    ) -> Self {
86        Self {
87            raw: RawHandle::new(ptr, ownership, opus_projection_encoder_destroy),
88            sample_rate,
89            channels,
90            streams,
91            coupled_streams,
92        }
93    }
94
95    /// Size in bytes of a projection encoder state for external allocation.
96    ///
97    /// # Errors
98    /// Returns [`Error::BadArg`] if the channel/mapping configuration is invalid.
99    pub fn size(channels: u8, mapping_family: i32) -> Result<usize> {
100        validate_channels(channels)?;
101        validate_projection_mapping_family(mapping_family)?;
102        let raw = unsafe {
103            opus_projection_ambisonics_encoder_get_size(i32::from(channels), mapping_family)
104        };
105        if raw <= 0 {
106            return Err(Error::BadArg);
107        }
108        usize::try_from(raw).map_err(|_| Error::InternalError)
109    }
110
111    /// Initialize a previously allocated projection encoder state.
112    ///
113    /// # Safety
114    /// The caller must provide a valid pointer to `ProjectionEncoder::size()` bytes,
115    /// aligned to at least `align_of::<usize>()` (malloc-style alignment).
116    ///
117    /// # Errors
118    /// Returns [`Error::BadArg`] for invalid inputs or a mapped libopus error.
119    pub unsafe fn init_in_place(
120        ptr: *mut OpusProjectionEncoder,
121        sample_rate: SampleRate,
122        channels: u8,
123        mapping_family: i32,
124        application: Application,
125    ) -> Result<(u8, u8)> {
126        if ptr.is_null() || channels == 0 {
127            return Err(Error::BadArg);
128        }
129        if !crate::opus_ptr_is_aligned(ptr.cast()) {
130            return Err(Error::BadArg);
131        }
132        Self::size(channels, mapping_family)?;
133        let mut streams = 0i32;
134        let mut coupled = 0i32;
135        let r = unsafe {
136            opus_projection_ambisonics_encoder_init(
137                ptr,
138                sample_rate as i32,
139                i32::from(channels),
140                mapping_family,
141                std::ptr::addr_of_mut!(streams),
142                std::ptr::addr_of_mut!(coupled),
143                application as i32,
144            )
145        };
146        if r != 0 {
147            return Err(Error::from_code(r));
148        }
149        Ok((
150            u8::try_from(streams).map_err(|_| Error::BadArg)?,
151            u8::try_from(coupled).map_err(|_| Error::BadArg)?,
152        ))
153    }
154
155    /// Create a new projection encoder using the ambisonics helper.
156    ///
157    /// Returns [`Error::BadArg`] for unsupported channel/mapping combinations
158    /// or propagates libopus allocation failures.
159    ///
160    /// # Errors
161    /// Returns [`Error::BadArg`] for invalid arguments or the libopus error produced by
162    /// the underlying create call; [`Error::AllocFail`] if libopus returns a null handle.
163    pub fn new(
164        sample_rate: SampleRate,
165        channels: u8,
166        mapping_family: i32,
167        application: Application,
168    ) -> Result<Self> {
169        Self::size(channels, mapping_family)?;
170        let mut err = 0i32;
171        let mut streams = 0i32;
172        let mut coupled = 0i32;
173        let enc = unsafe {
174            opus_projection_ambisonics_encoder_create(
175                sample_rate as i32,
176                i32::from(channels),
177                mapping_family,
178                &raw mut streams,
179                &raw mut coupled,
180                application as i32,
181                &raw mut err,
182            )
183        };
184        if err != 0 {
185            return Err(Error::from_code(err));
186        }
187        let enc = NonNull::new(enc).ok_or(Error::AllocFail)?;
188        let streams_u8 = u8::try_from(streams).map_err(|_| Error::BadArg)?;
189        let coupled_u8 = u8::try_from(coupled).map_err(|_| Error::BadArg)?;
190        Ok(Self::from_raw(
191            enc,
192            sample_rate,
193            channels,
194            streams_u8,
195            coupled_u8,
196            Ownership::Owned,
197        ))
198    }
199
200    fn validate_frame_size(&self, frame_size_per_ch: usize) -> Result<i32> {
201        let frame_size = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?;
202        if frame_size.get() > max_frame_samples_for(self.sample_rate) {
203            return Err(Error::BadArg);
204        }
205        i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)
206    }
207
208    fn ensure_pcm_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> {
209        let expected = frame_size_per_ch
210            .checked_mul(self.channels as usize)
211            .ok_or(Error::BadArg)?;
212        if len != expected {
213            return Err(Error::BadArg);
214        }
215        Ok(())
216    }
217
218    /// Encode interleaved `i16` PCM.
219    ///
220    /// # Errors
221    /// Returns [`Error::InvalidState`] if the encoder handle was freed, [`Error::BadArg`] for
222    /// buffer/layout issues, the libopus error mapped via [`Error::from_code`], or
223    /// [`Error::InternalError`] if libopus reports an impossible packet length.
224    pub fn encode(
225        &mut self,
226        pcm: &[i16],
227        frame_size_per_ch: usize,
228        out: &mut [u8],
229    ) -> Result<usize> {
230        if out.is_empty() || out.len() > i32::MAX as usize {
231            return Err(Error::BadArg);
232        }
233        self.ensure_pcm_layout(pcm.len(), frame_size_per_ch)?;
234        let frame_size = self.validate_frame_size(frame_size_per_ch)?;
235        let out_len = i32::try_from(out.len()).map_err(|_| Error::BadArg)?;
236        let n = unsafe {
237            opus_projection_encode(
238                self.raw.as_ptr(),
239                pcm.as_ptr(),
240                frame_size,
241                out.as_mut_ptr(),
242                out_len,
243            )
244        };
245        if n < 0 {
246            return Err(Error::from_code(n));
247        }
248        usize::try_from(n).map_err(|_| Error::InternalError)
249    }
250
251    /// Encode interleaved `f32` PCM.
252    ///
253    /// # Errors
254    /// Returns [`Error::InvalidState`] if the encoder handle was freed, [`Error::BadArg`] for
255    /// buffer/layout issues, the libopus error mapped via [`Error::from_code`], or
256    /// [`Error::InternalError`] if libopus reports an impossible packet length.
257    pub fn encode_float(
258        &mut self,
259        pcm: &[f32],
260        frame_size_per_ch: usize,
261        out: &mut [u8],
262    ) -> Result<usize> {
263        if out.is_empty() || out.len() > i32::MAX as usize {
264            return Err(Error::BadArg);
265        }
266        self.ensure_pcm_layout(pcm.len(), frame_size_per_ch)?;
267        let frame_size = self.validate_frame_size(frame_size_per_ch)?;
268        let out_len = i32::try_from(out.len()).map_err(|_| Error::BadArg)?;
269        let n = unsafe {
270            opus_projection_encode_float(
271                self.raw.as_ptr(),
272                pcm.as_ptr(),
273                frame_size,
274                out.as_mut_ptr(),
275                out_len,
276            )
277        };
278        if n < 0 {
279            return Err(Error::from_code(n));
280        }
281        usize::try_from(n).map_err(|_| Error::InternalError)
282    }
283
284    /// Set target bitrate for the encoder.
285    ///
286    /// # Errors
287    /// Returns [`Error::InvalidState`] if the encoder handle is invalid or a mapped libopus error.
288    pub fn set_bitrate(&mut self, bitrate: Bitrate) -> Result<()> {
289        self.simple_ctl(OPUS_SET_BITRATE_REQUEST as i32, bitrate.value())
290    }
291
292    /// Query current bitrate configuration.
293    ///
294    /// # Errors
295    /// Returns [`Error::InvalidState`] if the encoder handle is invalid or a mapped libopus error.
296    pub fn bitrate(&mut self) -> Result<Bitrate> {
297        let v = self.get_int_ctl(OPUS_GET_BITRATE_REQUEST as i32)?;
298        Ok(match v {
299            x if x == crate::bindings::OPUS_AUTO => Bitrate::Auto,
300            x if x == OPUS_BITRATE_MAX => Bitrate::Max,
301            other => Bitrate::Custom(other),
302        })
303    }
304
305    /// Size in bytes of the current demixing matrix.
306    ///
307    /// # Errors
308    /// Returns [`Error::InvalidState`] if the encoder handle is invalid or a mapped libopus error.
309    pub fn demixing_matrix_size(&mut self) -> Result<i32> {
310        self.get_int_ctl(OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST as i32)
311    }
312
313    /// Gain (in Q8 dB) of the demixing matrix.
314    ///
315    /// # Errors
316    /// Returns [`Error::InvalidState`] if the encoder handle is invalid or a mapped libopus error.
317    pub fn demixing_matrix_gain(&mut self) -> Result<i32> {
318        self.get_int_ctl(OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST as i32)
319    }
320
321    /// Copy the demixing matrix into `out` and return the number of bytes written.
322    ///
323    /// # Errors
324    /// Returns [`Error::InvalidState`] if the encoder handle is invalid, [`Error::BufferTooSmall`]
325    /// when `out` cannot fit the matrix, a mapped libopus error, or [`Error::InternalError`]
326    /// when libopus reports an invalid matrix size.
327    pub fn write_demixing_matrix(&mut self, out: &mut [u8]) -> Result<usize> {
328        let size = self.demixing_matrix_size()?;
329        self.write_demixing_matrix_with_size(out, size)
330    }
331
332    fn write_demixing_matrix_with_size(&mut self, out: &mut [u8], size: i32) -> Result<usize> {
333        if size <= 0 {
334            return Err(Error::InternalError);
335        }
336        let needed = usize::try_from(size).map_err(|_| Error::InternalError)?;
337        if out.len() < needed {
338            return Err(Error::BufferTooSmall);
339        }
340        let r = unsafe {
341            opus_projection_encoder_ctl(
342                self.raw.as_ptr(),
343                OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST as i32,
344                out.as_mut_ptr(),
345                size,
346            )
347        };
348        if r != 0 {
349            return Err(Error::from_code(r));
350        }
351        Ok(needed)
352    }
353
354    /// Convenience helper returning the demixing matrix as a newly allocated buffer.
355    ///
356    /// # Errors
357    /// Propagates errors from [`Self::demixing_matrix_size`] and [`Self::write_demixing_matrix`],
358    /// including [`Error::InternalError`] if libopus reports impossible sizes.
359    pub fn demixing_matrix_bytes(&mut self) -> Result<Vec<u8>> {
360        let size = self.demixing_matrix_size()?;
361        let len = usize::try_from(size).map_err(|_| Error::InternalError)?;
362        let mut buf = vec![0u8; len];
363        self.write_demixing_matrix_with_size(&mut buf, size)?;
364        Ok(buf)
365    }
366
367    /// Number of coded streams.
368    #[must_use]
369    pub const fn streams(&self) -> u8 {
370        self.streams
371    }
372
373    /// Number of coupled (stereo) coded streams.
374    #[must_use]
375    pub const fn coupled_streams(&self) -> u8 {
376        self.coupled_streams
377    }
378
379    /// Input channels passed to the encoder.
380    #[must_use]
381    pub const fn channels(&self) -> u8 {
382        self.channels
383    }
384
385    /// Encoder sample rate.
386    #[must_use]
387    pub const fn sample_rate(&self) -> SampleRate {
388        self.sample_rate
389    }
390
391    fn simple_ctl(&mut self, req: i32, val: i32) -> Result<()> {
392        let r = unsafe { opus_projection_encoder_ctl(self.raw.as_ptr(), req, val) };
393        if r != 0 {
394            return Err(Error::from_code(r));
395        }
396        Ok(())
397    }
398
399    fn get_int_ctl(&mut self, req: i32) -> Result<i32> {
400        let mut v = 0i32;
401        let r = unsafe { opus_projection_encoder_ctl(self.raw.as_ptr(), req, &mut v) };
402        if r != 0 {
403            return Err(Error::from_code(r));
404        }
405        Ok(v)
406    }
407}
408
409impl<'a> ProjectionEncoderRef<'a> {
410    /// Wrap an externally-initialized projection encoder without taking ownership.
411    ///
412    /// # Safety
413    /// - `ptr` must point to valid, initialized memory of at least [`ProjectionEncoder::size()`] bytes
414    /// - `ptr` must be aligned to at least `align_of::<usize>()` (malloc-style alignment)
415    /// - `sample_rate`, `channels`, `streams`, and `coupled_streams` must exactly match
416    ///   the encoder state already stored at `ptr`
417    /// - The memory must remain valid for the lifetime `'a`
418    /// - Caller is responsible for freeing the memory after this wrapper is dropped
419    ///
420    /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer
421    /// sizes with the wrong layout and then call libopus with out-of-bounds buffers.
422    ///
423    /// # Panics
424    /// Panics if `ptr` is null or not pointer-aligned, or if the channel/stream
425    /// counts are invalid.
426    ///
427    /// Use [`ProjectionEncoder::init_in_place`] to initialize the memory before calling this.
428    #[must_use]
429    pub unsafe fn from_raw(
430        ptr: *mut OpusProjectionEncoder,
431        sample_rate: SampleRate,
432        channels: u8,
433        streams: u8,
434        coupled_streams: u8,
435    ) -> Self {
436        assert!(
437            validate_channels(channels).is_ok(),
438            "ProjectionEncoderRef::from_raw called with zero channels"
439        );
440        assert!(
441            validate_stream_counts(streams, coupled_streams).is_ok(),
442            "ProjectionEncoderRef::from_raw called with invalid stream counts"
443        );
444        let encoder = ProjectionEncoder::from_raw(
445            crate::checked_non_null(ptr, "ProjectionEncoderRef::from_raw"),
446            sample_rate,
447            channels,
448            streams,
449            coupled_streams,
450            Ownership::Borrowed,
451        );
452        Self {
453            inner: encoder,
454            _marker: PhantomData,
455        }
456    }
457
458    /// Initialize and wrap an externally allocated buffer.
459    ///
460    /// # Errors
461    /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error.
462    pub fn init_in(
463        buf: &'a mut AlignedBuffer,
464        sample_rate: SampleRate,
465        channels: u8,
466        mapping_family: i32,
467        application: Application,
468    ) -> Result<Self> {
469        let required = ProjectionEncoder::size(channels, mapping_family)?;
470        if buf.capacity_bytes() < required {
471            return Err(Error::BadArg);
472        }
473        let ptr = buf.as_mut_ptr::<OpusProjectionEncoder>();
474        let (streams, coupled) = unsafe {
475            ProjectionEncoder::init_in_place(
476                ptr,
477                sample_rate,
478                channels,
479                mapping_family,
480                application,
481            )?
482        };
483        Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled) })
484    }
485
486    delegate_ref_mut_methods! {
487        fn encode(pcm: &[i16], frame_size_per_ch: usize, out: &mut [u8]) -> Result<usize>;
488        fn encode_float(pcm: &[f32], frame_size_per_ch: usize, out: &mut [u8]) -> Result<usize>;
489        fn set_bitrate(bitrate: Bitrate) -> Result<()>;
490        fn bitrate() -> Result<Bitrate>;
491        fn demixing_matrix_size() -> Result<i32>;
492        fn demixing_matrix_gain() -> Result<i32>;
493        fn write_demixing_matrix(out: &mut [u8]) -> Result<usize>;
494        fn demixing_matrix_bytes() -> Result<Vec<u8>>;
495    }
496}
497
498impl Deref for ProjectionEncoderRef<'_> {
499    type Target = ProjectionEncoder;
500
501    fn deref(&self) -> &Self::Target {
502        &self.inner
503    }
504}
505
506/// Safe wrapper around `OpusProjectionDecoder`.
507pub struct ProjectionDecoder {
508    raw: RawHandle<OpusProjectionDecoder>,
509    sample_rate: SampleRate,
510    channels: u8,
511    streams: u8,
512    coupled_streams: u8,
513}
514
515unsafe impl Send for ProjectionDecoder {}
516
517/// Borrowed wrapper around a projection decoder state.
518///
519/// The owning handle cannot be moved out of this borrowed wrapper:
520///
521/// ```compile_fail
522/// use opus_codec::projection::{ProjectionDecoder, ProjectionDecoderRef};
523/// fn extract<'a>(
524///     state: &mut ProjectionDecoderRef<'a>,
525///     replacement: ProjectionDecoder,
526/// ) -> ProjectionDecoder {
527///     std::mem::replace(&mut **state, replacement)
528/// }
529/// ```
530pub struct ProjectionDecoderRef<'a> {
531    inner: ProjectionDecoder,
532    _marker: PhantomData<&'a mut OpusProjectionDecoder>,
533}
534
535unsafe impl Send for ProjectionDecoderRef<'_> {}
536
537impl ProjectionDecoder {
538    fn from_raw(
539        ptr: NonNull<OpusProjectionDecoder>,
540        sample_rate: SampleRate,
541        channels: u8,
542        streams: u8,
543        coupled_streams: u8,
544        ownership: Ownership,
545    ) -> Self {
546        Self {
547            raw: RawHandle::new(ptr, ownership, opus_projection_decoder_destroy),
548            sample_rate,
549            channels,
550            streams,
551            coupled_streams,
552        }
553    }
554
555    /// Size in bytes of a projection decoder state for external allocation.
556    ///
557    /// # Errors
558    /// Returns [`Error::BadArg`] if the channel/stream configuration is invalid.
559    pub fn size(channels: u8, streams: u8, coupled_streams: u8) -> Result<usize> {
560        validate_channels(channels)?;
561        validate_stream_counts(streams, coupled_streams)?;
562        let raw = unsafe {
563            opus_projection_decoder_get_size(
564                i32::from(channels),
565                i32::from(streams),
566                i32::from(coupled_streams),
567            )
568        };
569        if raw <= 0 {
570            return Err(Error::BadArg);
571        }
572        usize::try_from(raw).map_err(|_| Error::InternalError)
573    }
574
575    /// Initialize a previously allocated projection decoder state.
576    ///
577    /// # Safety
578    /// The caller must provide a valid pointer to `ProjectionDecoder::size()` bytes,
579    /// aligned to at least `align_of::<usize>()` (malloc-style alignment).
580    ///
581    /// # Errors
582    /// Returns [`Error::BadArg`] for invalid inputs or a mapped libopus error.
583    pub unsafe fn init_in_place(
584        ptr: *mut OpusProjectionDecoder,
585        sample_rate: SampleRate,
586        channels: u8,
587        streams: u8,
588        coupled_streams: u8,
589        demixing_matrix: &[u8],
590    ) -> Result<()> {
591        if ptr.is_null() || demixing_matrix.is_empty() {
592            return Err(Error::BadArg);
593        }
594        if !crate::opus_ptr_is_aligned(ptr.cast()) {
595            return Err(Error::BadArg);
596        }
597        Self::size(channels, streams, coupled_streams)?;
598        let matrix_len = i32::try_from(demixing_matrix.len()).map_err(|_| Error::BadArg)?;
599        let mut demixing_matrix = demixing_matrix.to_vec();
600        // SAFETY: libopus' C ABI takes a non-const pointer despite documenting this
601        // parameter as input-only. Pass a mutable scratch copy so C never receives a
602        // mutable pointer into the caller's immutable slice. libopus copies the
603        // bytes before returning and does not retain this pointer.
604        let r = unsafe {
605            opus_projection_decoder_init(
606                ptr,
607                sample_rate as i32,
608                i32::from(channels),
609                i32::from(streams),
610                i32::from(coupled_streams),
611                demixing_matrix.as_mut_ptr(),
612                matrix_len,
613            )
614        };
615        if r != 0 {
616            return Err(Error::from_code(r));
617        }
618        Ok(())
619    }
620
621    /// Create a projection decoder given the demixing matrix provided by the encoder.
622    ///
623    /// # Errors
624    /// Returns [`Error::BadArg`] for invalid inputs, `Error::from_code` for libopus failures,
625    /// or [`Error::AllocFail`] if libopus returns a null handle.
626    pub fn new(
627        sample_rate: SampleRate,
628        channels: u8,
629        streams: u8,
630        coupled_streams: u8,
631        demixing_matrix: &[u8],
632    ) -> Result<Self> {
633        if demixing_matrix.is_empty() {
634            return Err(Error::BadArg);
635        }
636        Self::size(channels, streams, coupled_streams)?;
637        let matrix_len = i32::try_from(demixing_matrix.len()).map_err(|_| Error::BadArg)?;
638        let mut demixing_matrix = demixing_matrix.to_vec();
639        let mut err = 0i32;
640        // SAFETY: see comment in init_in_place; libopus copies the scratch input
641        // before returning and does not retain this pointer.
642        let dec = unsafe {
643            opus_projection_decoder_create(
644                sample_rate as i32,
645                i32::from(channels),
646                i32::from(streams),
647                i32::from(coupled_streams),
648                demixing_matrix.as_mut_ptr(),
649                matrix_len,
650                &raw mut err,
651            )
652        };
653        if err != 0 {
654            return Err(Error::from_code(err));
655        }
656        let dec = NonNull::new(dec).ok_or(Error::AllocFail)?;
657        Ok(Self::from_raw(
658            dec,
659            sample_rate,
660            channels,
661            streams,
662            coupled_streams,
663            Ownership::Owned,
664        ))
665    }
666
667    fn validate_frame_size(&self, frame_size_per_ch: usize) -> Result<i32> {
668        let frame_size = NonZeroUsize::new(frame_size_per_ch).ok_or(Error::BadArg)?;
669        if frame_size.get() > max_frame_samples_for(self.sample_rate) {
670            return Err(Error::BadArg);
671        }
672        i32::try_from(frame_size.get()).map_err(|_| Error::BadArg)
673    }
674
675    fn ensure_output_layout(&self, len: usize, frame_size_per_ch: usize) -> Result<()> {
676        let expected = frame_size_per_ch
677            .checked_mul(self.channels as usize)
678            .ok_or(Error::BadArg)?;
679        if len != expected {
680            return Err(Error::BadArg);
681        }
682        Ok(())
683    }
684
685    /// Decode into interleaved `i16` PCM.
686    ///
687    /// # Errors
688    /// Returns [`Error::InvalidState`] if the decoder handle was freed, [`Error::BadArg`] for
689    /// buffer/layout issues, a mapped libopus error, or [`Error::InternalError`] if libopus
690    /// reports an impossible decoded sample count.
691    pub fn decode(
692        &mut self,
693        packet: &[u8],
694        out: &mut [i16],
695        frame_size_per_ch: usize,
696        fec: bool,
697    ) -> Result<usize> {
698        self.ensure_output_layout(out.len(), frame_size_per_ch)?;
699        let frame_size = self.validate_frame_size(frame_size_per_ch)?;
700        // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms.
701        if (packet.is_empty() || fec)
702            && !is_frame_size_2_5ms_aligned(frame_size_per_ch, self.sample_rate)
703        {
704            return Err(Error::BadArg);
705        }
706        let packet_len = if packet.is_empty() {
707            0
708        } else {
709            i32::try_from(packet.len()).map_err(|_| Error::BadArg)?
710        };
711        let n = unsafe {
712            opus_projection_decode(
713                self.raw.as_ptr(),
714                if packet.is_empty() {
715                    std::ptr::null()
716                } else {
717                    packet.as_ptr()
718                },
719                packet_len,
720                out.as_mut_ptr(),
721                frame_size,
722                i32::from(fec),
723            )
724        };
725        if n < 0 {
726            return Err(Error::from_code(n));
727        }
728        usize::try_from(n).map_err(|_| Error::InternalError)
729    }
730
731    /// Decode into interleaved `f32` PCM.
732    ///
733    /// # Errors
734    /// Returns [`Error::InvalidState`] if the decoder handle was freed, [`Error::BadArg`] for
735    /// buffer/layout issues, a mapped libopus error, or [`Error::InternalError`] if libopus
736    /// reports an impossible decoded sample count.
737    pub fn decode_float(
738        &mut self,
739        packet: &[u8],
740        out: &mut [f32],
741        frame_size_per_ch: usize,
742        fec: bool,
743    ) -> Result<usize> {
744        self.ensure_output_layout(out.len(), frame_size_per_ch)?;
745        let frame_size = self.validate_frame_size(frame_size_per_ch)?;
746        // libopus requires PLC/FEC frame sizes to be multiples of 2.5 ms.
747        if (packet.is_empty() || fec)
748            && !is_frame_size_2_5ms_aligned(frame_size_per_ch, self.sample_rate)
749        {
750            return Err(Error::BadArg);
751        }
752        let packet_len = if packet.is_empty() {
753            0
754        } else {
755            i32::try_from(packet.len()).map_err(|_| Error::BadArg)?
756        };
757        let n = unsafe {
758            opus_projection_decode_float(
759                self.raw.as_ptr(),
760                if packet.is_empty() {
761                    std::ptr::null()
762                } else {
763                    packet.as_ptr()
764                },
765                packet_len,
766                out.as_mut_ptr(),
767                frame_size,
768                i32::from(fec),
769            )
770        };
771        if n < 0 {
772            return Err(Error::from_code(n));
773        }
774        usize::try_from(n).map_err(|_| Error::InternalError)
775    }
776
777    /// Output channel count.
778    #[must_use]
779    pub const fn channels(&self) -> u8 {
780        self.channels
781    }
782
783    /// Number of coded streams expected in the input bitstream.
784    #[must_use]
785    pub const fn streams(&self) -> u8 {
786        self.streams
787    }
788
789    /// Number of coupled coded streams expected in the input bitstream.
790    #[must_use]
791    pub const fn coupled_streams(&self) -> u8 {
792        self.coupled_streams
793    }
794
795    /// Decoder sample rate.
796    #[must_use]
797    pub const fn sample_rate(&self) -> SampleRate {
798        self.sample_rate
799    }
800}
801
802impl<'a> ProjectionDecoderRef<'a> {
803    /// Wrap an externally-initialized projection decoder without taking ownership.
804    ///
805    /// # Safety
806    /// - `ptr` must point to valid, initialized memory of at least [`ProjectionDecoder::size()`] bytes
807    /// - `ptr` must be aligned to at least `align_of::<usize>()` (malloc-style alignment)
808    /// - `sample_rate`, `channels`, `streams`, and `coupled_streams` must exactly match
809    ///   the decoder state already stored at `ptr`
810    /// - The memory must remain valid for the lifetime `'a`
811    /// - Caller is responsible for freeing the memory after this wrapper is dropped
812    ///
813    /// Passing mismatched metadata is undefined behavior: later safe methods may validate buffer
814    /// sizes with the wrong layout and then call libopus with out-of-bounds buffers.
815    ///
816    /// # Panics
817    /// Panics if `ptr` is null or not pointer-aligned, or if the channel/stream
818    /// counts are invalid.
819    ///
820    /// Use [`ProjectionDecoder::init_in_place`] to initialize the memory before calling this.
821    #[must_use]
822    pub unsafe fn from_raw(
823        ptr: *mut OpusProjectionDecoder,
824        sample_rate: SampleRate,
825        channels: u8,
826        streams: u8,
827        coupled_streams: u8,
828    ) -> Self {
829        assert!(
830            validate_channels(channels).is_ok(),
831            "ProjectionDecoderRef::from_raw called with zero channels"
832        );
833        assert!(
834            validate_stream_counts(streams, coupled_streams).is_ok(),
835            "ProjectionDecoderRef::from_raw called with invalid stream counts"
836        );
837        let decoder = ProjectionDecoder::from_raw(
838            crate::checked_non_null(ptr, "ProjectionDecoderRef::from_raw"),
839            sample_rate,
840            channels,
841            streams,
842            coupled_streams,
843            Ownership::Borrowed,
844        );
845        Self {
846            inner: decoder,
847            _marker: PhantomData,
848        }
849    }
850
851    /// Initialize and wrap an externally allocated buffer.
852    ///
853    /// # Errors
854    /// Returns [`Error::BadArg`] if the buffer is too small, or a mapped libopus error.
855    pub fn init_in(
856        buf: &'a mut AlignedBuffer,
857        sample_rate: SampleRate,
858        channels: u8,
859        streams: u8,
860        coupled_streams: u8,
861        demixing_matrix: &[u8],
862    ) -> Result<Self> {
863        let required = ProjectionDecoder::size(channels, streams, coupled_streams)?;
864        if buf.capacity_bytes() < required {
865            return Err(Error::BadArg);
866        }
867        let ptr = buf.as_mut_ptr::<OpusProjectionDecoder>();
868        unsafe {
869            ProjectionDecoder::init_in_place(
870                ptr,
871                sample_rate,
872                channels,
873                streams,
874                coupled_streams,
875                demixing_matrix,
876            )?;
877        }
878        Ok(unsafe { Self::from_raw(ptr, sample_rate, channels, streams, coupled_streams) })
879    }
880
881    delegate_ref_mut_methods! {
882        fn decode(packet: &[u8], out: &mut [i16], frame_size_per_ch: usize, fec: bool) -> Result<usize>;
883        fn decode_float(packet: &[u8], out: &mut [f32], frame_size_per_ch: usize, fec: bool) -> Result<usize>;
884    }
885}
886
887impl Deref for ProjectionDecoderRef<'_> {
888    type Target = ProjectionDecoder;
889
890    fn deref(&self) -> &Self::Target {
891        &self.inner
892    }
893}