audio-opus-bsd 0.1.0

Opus codec (RFC 6716) encoder/decoder for the sonicbrew audio-toolkit — libopus binding, worker-thread encode/decode emitting planar audio-core-bsd AudioFrames
//! Opus decoder: the public [`AudioDecoder`] trait and [`OpusDecoder`]
//! implementation backed by libopus.
//!
//! Like [`AudioEncoder`](crate::AudioEncoder), this trait is defined locally
//! because `audio-core-bsd` 0.1.0 is frozen and exports no codec traits.

use crate::error;
use crate::layout;
use audio_core_bsd::AudioFrame;

/// Largest frame block libopus can decode in one call: 60 ms @ 48 kHz =
/// 2880 samples/channel. Pre-allocating to this bound lets
/// [`OpusDecoder::decode`] accept any conformant Opus packet without
/// per-call reallocation.
const MAX_DECODE_FRAMES: usize = 2880;

/// The decoder contract: turn an Opus packet back into interleaved f32 PCM.
///
/// Runs on a **worker thread** (not the RT audio thread): implementations are
/// expected to perform FFI and heap allocation freely.
///
/// The returned `Vec<f32>` is **interleaved** (`[L0, R0, L1, R1, …]`); use
/// [`OpusDecoder::decode_frame`] to obtain a planar [`AudioFrame`] directly.
pub trait AudioDecoder: Send {
    /// Decode one Opus packet into interleaved f32 PCM.
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Decode`] if the packet is corrupt or the
    /// decoder is misconfigured.
    fn decode(&mut self, packet: &[u8]) -> error::Result<Vec<f32>>;
}

/// Opus (RFC 6716) decoder backed by libopus.
///
/// Wraps [`opus::Decoder`] and stores the configured `sample_rate` and
/// `channels`. A reusable decode-output buffer is pre-allocated large enough
/// for the largest possible Opus frame (60 ms), so the common 20 ms case never
/// reallocates.
#[derive(Debug)]
pub struct OpusDecoder {
    /// The underlying libopus decoder.
    decoder: opus::Decoder,
    /// Configured sample rate in Hz.
    sample_rate: u32,
    /// Channel count (`1` = mono, `2` = stereo).
    channels: u16,
    /// Reusable decode-output buffer
    /// (`len == MAX_DECODE_FRAMES * channels`); only the first
    /// `decoded_frames * channels` samples are meaningful after each decode.
    out_buf: Vec<f32>,
}

impl OpusDecoder {
    /// Construct a new decoder.
    ///
    /// `channels` must be `1` (mono) or `2` (stereo). `sample_rate` must be a
    /// positive value that libopus accepts.
    ///
    /// # Errors
    ///
    /// - [`error::OpusError::InvalidSampleRate`] if `sample_rate == 0`.
    /// - [`error::OpusError::UnsupportedChannels`] if `channels` is not 1 or 2.
    /// - [`error::OpusError::Decode`] if libopus rejects the configuration.
    pub fn new(sample_rate: u32, channels: u16) -> error::Result<Self> {
        if sample_rate == 0 {
            return Err(error::OpusError::InvalidSampleRate(sample_rate));
        }
        let opus_channels = match channels {
            1 => opus::Channels::Mono,
            2 => opus::Channels::Stereo,
            other => return Err(error::OpusError::UnsupportedChannels(other)),
        };
        let decoder = opus::Decoder::new(sample_rate, opus_channels)
            .map_err(|e| error::OpusError::Decode(e.to_string()))?;
        Ok(Self {
            decoder,
            sample_rate,
            channels,
            out_buf: vec![0.0f32; MAX_DECODE_FRAMES * usize::from(channels)],
        })
    }

    /// The configured sample rate, in Hz.
    #[must_use]
    pub fn sample_rate(&self) -> u32 {
        self.sample_rate
    }

    /// The configured channel count.
    #[must_use]
    pub fn channels(&self) -> u16 {
        self.channels
    }

    /// Decode one Opus packet into the reusable interleaved output buffer.
    ///
    /// Returns the number of **per-channel frames** decoded. The interleaved
    /// samples live in `self.out_buf[..returned * channels]` until the next
    /// call. Forward Error Correction (FEC) is disabled (the common case for
    /// in-order packet streams).
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Decode`] if libopus rejects the packet.
    fn decode_interleaved(&mut self, packet: &[u8]) -> error::Result<usize> {
        let decoded = self
            .decoder
            .decode_float(packet, &mut self.out_buf, false)
            .map_err(|e| error::OpusError::Decode(e.to_string()))?;
        Ok(decoded)
    }

    /// Decode one Opus packet into a fresh interleaved f32 `Vec`.
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Decode`] if libopus rejects the packet.
    pub fn decode_interleaved_vec(&mut self, packet: &[u8]) -> error::Result<Vec<f32>> {
        let frames = self.decode_interleaved(packet)?;
        let total = frames * usize::from(self.channels);
        Ok(self.out_buf[..total].to_vec())
    }

    /// Decode one Opus packet into a planar [`AudioFrame`].
    ///
    /// This is the [`audio_core_bsd`]-friendly entry point: it decodes the
    /// packet, converts the interleaved output to planar layout, and wraps it
    /// in an [`AudioFrame`] carrying this decoder's `channels` and
    /// `sample_rate`.
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Decode`] if libopus rejects the packet.
    pub fn decode_frame(&mut self, packet: &[u8]) -> error::Result<AudioFrame> {
        let frames = self.decode_interleaved(packet)?;
        let ch = self.channels;
        // interleaved → planar via the canonical `layout` module (validates
        // length against `channels`).
        let planar = layout::interleaved_to_planar(ch, &self.out_buf[..frames * usize::from(ch)])?;
        Ok(AudioFrame::from_planar(ch, self.sample_rate, planar))
    }
}

impl AudioDecoder for OpusDecoder {
    fn decode(&mut self, packet: &[u8]) -> error::Result<Vec<f32>> {
        self.decode_interleaved_vec(packet)
    }
}

#[cfg(test)]
#[allow(clippy::cast_precision_loss)] // test-only: small-range loop indices cast to f32
mod tests {
    use super::super::encoder::{AudioEncoder, OpusEncoder};
    use super::*;

    /// Canonical block size: 960 samples/channel = 20 ms @ 48 kHz.
    const FRAME_SIZE: usize = 960;
    const SAMPLE_RATE: u32 = 48_000;

    #[test]
    fn mono_decoder_constructs() {
        let dec = OpusDecoder::new(SAMPLE_RATE, 1);
        assert!(dec.is_ok());
        let dec = dec.unwrap();
        assert_eq!(dec.sample_rate(), SAMPLE_RATE);
        assert_eq!(dec.channels(), 1);
    }

    #[test]
    fn stereo_decoder_constructs() {
        let dec = OpusDecoder::new(SAMPLE_RATE, 2);
        assert!(dec.is_ok());
    }

    #[test]
    fn zero_sample_rate_is_rejected() {
        let err = OpusDecoder::new(0, 1).unwrap_err();
        assert_eq!(err, error::OpusError::InvalidSampleRate(0));
    }

    #[test]
    fn unsupported_channels_are_rejected() {
        for bad in [0u16, 3, 6] {
            let err = OpusDecoder::new(SAMPLE_RATE, bad).unwrap_err();
            assert_eq!(err, error::OpusError::UnsupportedChannels(bad));
        }
    }

    #[test]
    fn decode_a_known_good_mono_packet() {
        // Encode-then-decode in-test: the only way to obtain a valid Opus
        // packet without a fixture file.
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let pcm: Vec<f32> = (0..FRAME_SIZE)
            .map(|i| {
                0.5 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin()
            })
            .collect();
        let packet = enc.encode(&pcm).unwrap();
        assert!(!packet.is_empty());

        let mut dec = OpusDecoder::new(SAMPLE_RATE, 1).unwrap();
        let out = dec.decode(&packet).unwrap();
        // 960 mono frames → 960 interleaved samples.
        assert_eq!(out.len(), FRAME_SIZE);
    }

    #[test]
    fn decode_a_known_good_stereo_packet() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
        let pcm: Vec<f32> = (0..FRAME_SIZE * 2)
            .map(|i| 0.3 * (i as f32 * 0.01).sin())
            .collect();
        let packet = enc.encode(&pcm).unwrap();
        assert!(!packet.is_empty());

        let mut dec = OpusDecoder::new(SAMPLE_RATE, 2).unwrap();
        let out = dec.decode(&packet).unwrap();
        // 960 stereo frames → 1920 interleaved samples.
        assert_eq!(out.len(), FRAME_SIZE * 2);
    }

    #[test]
    fn decode_frame_produces_correct_channels_and_rate() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
        let pcm = vec![0.0f32; FRAME_SIZE * 2];
        let packet = enc.encode(&pcm).unwrap();

        let mut dec = OpusDecoder::new(SAMPLE_RATE, 2).unwrap();
        let frame = dec.decode_frame(&packet).unwrap();
        assert_eq!(frame.channels, 2);
        assert_eq!(frame.sample_rate, SAMPLE_RATE);
        assert_eq!(frame.num_frames(), FRAME_SIZE);
        // Planar length invariant.
        assert_eq!(frame.samples.len(), FRAME_SIZE * 2);
    }

    #[test]
    fn decode_frame_mono_matches_canonical_block_size() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let pcm = vec![0.0f32; FRAME_SIZE];
        let packet = enc.encode(&pcm).unwrap();

        let mut dec = OpusDecoder::new(SAMPLE_RATE, 1).unwrap();
        let frame = dec.decode_frame(&packet).unwrap();
        assert_eq!(frame.num_frames(), FRAME_SIZE);
    }

    #[test]
    fn decode_corrupt_packet_returns_decode_error() {
        let mut dec = OpusDecoder::new(SAMPLE_RATE, 1).unwrap();
        // A single zero byte is not a valid Opus packet. (libopus's TOC byte
        // parser rejects it.)
        let res = dec.decode(&[0u8]);
        // Some libopus builds tolerate a 1-byte packet as a silence frame; if
        // so, we instead feed a clearly malformed longer payload.
        if res.is_ok() {
            let res2 = dec.decode(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
            assert!(res2.is_err(), "a corrupt packet must yield a Decode error");
        }
    }

    #[test]
    fn out_buffer_is_reused_across_calls() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let mut dec = OpusDecoder::new(SAMPLE_RATE, 1).unwrap();
        let pcm = vec![0.0f32; FRAME_SIZE];
        for _ in 0..3 {
            let packet = enc.encode(&pcm).unwrap();
            let out = dec.decode(&packet).unwrap();
            assert_eq!(out.len(), FRAME_SIZE);
        }
    }

    // ===== interleaved→planar conversion correctness =====
    // Re-pointed at the canonical `layout` module after the private duplicate
    // was removed; `layout`'s proptests independently prove bit-exact
    // round-trip, these lock the stride for the canonical small cases.

    #[test]
    fn interleaved_to_planar_mono_is_identity() {
        let inter = vec![0.1, 0.2, 0.3, 0.4];
        let out = layout::interleaved_to_planar(1, &inter).unwrap();
        assert_eq!(out, inter);
    }

    #[test]
    fn interleaved_to_planar_stereo_stride_is_correct() {
        // Interle: [L0, R0, L1, R1, L2, R2]  (nf=3, ch=2)
        // Planar:  [L0, L1, L2, R0, R1, R2]
        let inter = vec![10.0, 20.0, 11.0, 21.0, 12.0, 22.0];
        let out = layout::interleaved_to_planar(2, &inter).unwrap();
        assert_eq!(out, vec![10.0, 11.0, 12.0, 20.0, 21.0, 22.0]);
    }

    #[test]
    fn interleaved_to_planar_preserves_all_samples() {
        for channels in 1u16..=2 {
            let nf = 960;
            let inter: Vec<f32> = (0..nf * usize::from(channels)).map(|i| i as f32).collect();
            let out = layout::interleaved_to_planar(channels, &inter).unwrap();
            assert_eq!(out.len(), inter.len());
            let mut a = out.clone();
            a.sort_by(|x, y| x.partial_cmp(y).unwrap());
            let mut b = inter.clone();
            b.sort_by(|x, y| x.partial_cmp(y).unwrap());
            assert_eq!(a, b);
        }
    }
}