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 encoder: the public [`AudioEncoder`] trait and [`OpusEncoder`]
//! implementation backed by libopus.
//!
//! `audio-core-bsd` 0.1.0 is frozen and does not export an `AudioEncoder`
//! trait, so — following the precedent set by `audio-codec-bsd`'s
//! `ContainerDecoder` — this crate defines [`AudioEncoder`] locally.

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

/// Largest Opus packet this implementation will ever produce for a single
/// 20 ms / 960-sample stereo block at common bitrates.
///
/// 4000 bytes comfortably exceeds libopus's documented maximum packet size
/// (~1276 bytes per frame) and is reused across calls to avoid per-encode
/// heap allocation.
const MAX_PACKET_BYTES: usize = 4000;

/// The encoder contract: turn interleaved f32 PCM into an Opus packet.
///
/// Runs on a **worker thread** (not the RT audio thread): implementations are
/// expected to perform FFI and heap allocation freely. Encoded packets should
/// be handed to a lock-free channel so the RT thread never touches this trait.
///
/// The `pcm` slice is **interleaved** f32 (`[L0, R0, L1, R1, …]`); the
/// concrete encoder knows its channel count from construction.
pub trait AudioEncoder: Send {
    /// Encode one block of interleaved f32 PCM into an Opus packet.
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Encode`] if libopus rejects the input or
    /// the encoder is misconfigured.
    fn encode(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>>;

    /// Set the target bitrate in bits per second.
    ///
    /// Errors are deliberately swallowed (the trait returns `()`); use
    /// [`OpusEncoder::set_bitrate_checked`] when the caller must observe
    /// failures.
    fn set_bitrate(&mut self, bps: u32);
}

/// Opus (RFC 6716) encoder backed by libopus.
///
/// Wraps [`opus::Encoder`] and stores the configured `sample_rate` and
/// `channels`. A reusable output packet buffer is pre-allocated to avoid
/// per-encode heap churn on the worker thread.
#[derive(Debug)]
pub struct OpusEncoder {
    /// The underlying libopus encoder.
    encoder: opus::Encoder,
    /// Configured sample rate in Hz.
    sample_rate: u32,
    /// Channel count (`1` = mono, `2` = stereo).
    channels: u16,
    /// Reusable output packet buffer (`len == MAX_PACKET_BYTES`); only the
    /// first `n` bytes are meaningful after each [`Self::encode_interleaved`]
    /// call, where `n` is the value returned by `opus::Encoder::encode_float`.
    packet_buf: Vec<u8>,
}

impl OpusEncoder {
    /// Construct a new encoder.
    ///
    /// `channels` must be `1` (mono) or `2` (stereo) — Opus supports no other
    /// channel counts. `sample_rate` must be a positive value that libopus
    /// accepts (8/12/16/24/48 kHz are standard).
    ///
    /// # Errors
    ///
    /// - [`error::OpusError::InvalidSampleRate`] if `sample_rate == 0`.
    /// - [`error::OpusError::UnsupportedChannels`] if `channels` is not 1 or 2.
    /// - [`error::OpusError::Encode`] if libopus rejects the configuration.
    pub fn new(
        sample_rate: u32,
        channels: u16,
        application: opus::Application,
    ) -> 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 encoder = opus::Encoder::new(sample_rate, opus_channels, application)
            .map_err(|e| error::OpusError::Encode(e.to_string()))?;
        Ok(Self {
            encoder,
            sample_rate,
            channels,
            packet_buf: vec![0u8; MAX_PACKET_BYTES],
        })
    }

    /// 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
    }

    /// Encode one block of **interleaved** f32 PCM into an Opus packet.
    ///
    /// Reuses the internal packet buffer; the returned [`Vec`] is a fresh
    /// allocation of exactly the encoded byte count (the only unavoidable
    /// allocation on this path, since the trait returns an owned `Vec`).
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Encode`] if libopus rejects the input.
    pub fn encode_interleaved(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>> {
        let n = self
            .encoder
            .encode_float(pcm, &mut self.packet_buf)
            .map_err(|e| error::OpusError::Encode(e.to_string()))?;
        Ok(self.packet_buf[..n].to_vec())
    }

    /// Encode a planar [`AudioFrame`] into an Opus packet.
    ///
    /// This is the [`audio_core_bsd`]-friendly entry point: it validates the
    /// channel count, converts the frame from planar to interleaved layout,
    /// and delegates to [`Self::encode_interleaved`].
    ///
    /// # Errors
    ///
    /// - [`error::OpusError::InvalidChannelCount`] if `frame.channels` is zero
    ///   or disagrees with the encoder's configured channel count.
    /// - [`error::OpusError::Encode`] if libopus rejects the converted input.
    pub fn encode_frame(&mut self, frame: &AudioFrame) -> error::Result<Vec<u8>> {
        if frame.channels == 0 {
            return Err(error::OpusError::InvalidChannelCount(frame.channels));
        }
        if frame.channels != self.channels {
            return Err(error::OpusError::InvalidChannelCount(frame.channels));
        }
        // planar → interleaved via the canonical `layout` module (validates
        // length; the single unavoidable allocation on this path). The opus-
        // facing packet buffer is reused by `encode_interleaved`.
        let interleaved = layout::planar_to_interleaved(self.channels, &frame.samples)?;
        self.encode_interleaved(&interleaved)
    }

    /// Set the target bitrate in bits per second, returning any libopus error.
    ///
    /// `bps` is clamped to `i32::MAX` before forwarding to libopus (Opus
    /// itself rejects anything above ~510 kbps/channel with `OPUS_BAD_ARG`).
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Encode`] if libopus rejects the bitrate.
    pub fn set_bitrate_checked(&mut self, bps: u32) -> error::Result<()> {
        let bps_i32 = i32::try_from(bps).unwrap_or(i32::MAX);
        self.encoder
            .set_bitrate(opus::Bitrate::Bits(bps_i32))
            .map_err(|e| error::OpusError::Encode(e.to_string()))
    }

    /// Set the encoder's computational complexity (`0..=10`).
    ///
    /// Higher values trade CPU for quality. Errors are surfaced because a bad
    /// complexity silently degrades output.
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Encode`] if libopus rejects the value.
    pub fn set_complexity(&mut self, complexity: i32) -> error::Result<()> {
        self.encoder
            .set_complexity(complexity)
            .map_err(|e| error::OpusError::Encode(e.to_string()))
    }

    /// Enable or disable Discontinuous Transmission (DTX).
    ///
    /// # Errors
    ///
    /// Returns [`error::OpusError::Encode`] if libopus rejects the setting.
    pub fn set_dtx(&mut self, enabled: bool) -> error::Result<()> {
        self.encoder
            .set_dtx(enabled)
            .map_err(|e| error::OpusError::Encode(e.to_string()))
    }
}

impl AudioEncoder for OpusEncoder {
    fn encode(&mut self, pcm: &[f32]) -> error::Result<Vec<u8>> {
        self.encode_interleaved(pcm)
    }

    fn set_bitrate(&mut self, bps: u32) {
        let _ = self.set_bitrate_checked(bps);
    }
}

#[cfg(test)]
#[allow(clippy::cast_precision_loss)] // test-only: small-range loop indices cast to f32
mod tests {
    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_encoder_constructs() {
        let enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio);
        assert!(enc.is_ok());
        let enc = enc.unwrap();
        assert_eq!(enc.sample_rate(), SAMPLE_RATE);
        assert_eq!(enc.channels(), 1);
    }

    #[test]
    fn stereo_encoder_constructs() {
        let enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio);
        assert!(enc.is_ok());
    }

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

    #[test]
    fn unsupported_channels_are_rejected() {
        // Opus only permits mono (1) or stereo (2).
        for bad in [0u16, 3, 6, 8] {
            let err = OpusEncoder::new(SAMPLE_RATE, bad, opus::Application::Audio).unwrap_err();
            assert_eq!(err, error::OpusError::UnsupportedChannels(bad));
        }
    }

    #[test]
    fn mono_encode_produces_non_empty_packet() {
        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();
        assert!(!packet.is_empty());
        // An Opus packet for a 20 ms mono frame is always well under 4 KiB.
        assert!(packet.len() < MAX_PACKET_BYTES);
    }

    #[test]
    fn stereo_encode_produces_non_empty_packet() {
        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();
        assert!(!packet.is_empty());
    }

    #[test]
    fn encode_frame_validates_channel_count() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
        // A mono frame handed to a stereo encoder must be rejected — silently
        // accepting it would feed a half-length interleaved buffer to libopus.
        let mono_frame = AudioFrame::silence(1, FRAME_SIZE, SAMPLE_RATE);
        let err = enc.encode_frame(&mono_frame).unwrap_err();
        assert_eq!(err, error::OpusError::InvalidChannelCount(1));
    }

    #[test]
    fn encode_frame_zero_channels_is_rejected() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let zero_ch = AudioFrame::new(0, SAMPLE_RATE);
        let err = enc.encode_frame(&zero_ch).unwrap_err();
        assert_eq!(err, error::OpusError::InvalidChannelCount(0));
    }

    #[test]
    fn encode_frame_planar_mono_round_trips_to_packet() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let frame = AudioFrame::silence(1, FRAME_SIZE, SAMPLE_RATE);
        let packet = enc.encode_frame(&frame).unwrap();
        assert!(!packet.is_empty());
    }

    #[test]
    fn encode_frame_planar_stereo_round_trips_to_packet() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 2, opus::Application::Audio).unwrap();
        let frame = AudioFrame::silence(2, FRAME_SIZE, SAMPLE_RATE);
        let packet = enc.encode_frame(&frame).unwrap();
        assert!(!packet.is_empty());
    }

    #[test]
    fn set_bitrate_checked_accepts_valid_value() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        // 64 kbps is a sane Opus bitrate.
        assert!(enc.set_bitrate_checked(64_000).is_ok());
    }

    #[test]
    fn set_bitrate_checked_handles_oversized_u32_without_panic() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        // 4 Gbps fits in `u32` but exceeds `i32::MAX`, exercising the
        // `try_from(...).unwrap_or(i32::MAX)` clamp in `set_bitrate_checked`.
        // libopus 1.5.2 itself *clamps* (not rejects) absurd bitrates, so this
        // must neither panic nor return `Err` — proving the u32→i32 boundary
        // is handled gracefully.
        let res = enc.set_bitrate_checked(4_000_000_000);
        assert!(
            res.is_ok(),
            "oversized u32 bitrate must be clamped, not error"
        );
    }

    #[test]
    fn set_bitrate_trait_method_swallows_anything() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        // The trait method returns `()` — no input must cause a panic, even
        // one that overflows `i32`.
        enc.set_bitrate(4_000_000_000);
    }

    #[test]
    fn set_complexity_accepts_valid_range() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        for c in 0..=10 {
            assert!(
                enc.set_complexity(c).is_ok(),
                "complexity {c} should be accepted"
            );
        }
    }

    #[test]
    fn set_dtx_toggles_without_error() {
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        assert!(enc.set_dtx(true).is_ok());
        assert!(enc.set_dtx(false).is_ok());
    }

    // ===== planar↔interleaved conversion correctness (the #1 SNR-killer) =====
    // 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 planar_to_interleaved_mono_is_identity() {
        // Mono: planar and interleaved layouts coincide.
        let planar = vec![0.1, 0.2, 0.3, 0.4];
        let out = layout::planar_to_interleaved(1, &planar).unwrap();
        assert_eq!(out, planar);
    }

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

    #[test]
    fn planar_to_interleaved_preserves_all_samples() {
        // No samples may be dropped or duplicated — length invariant.
        for channels in 1u16..=2 {
            let nf = 960;
            let planar: Vec<f32> = (0..nf * usize::from(channels)).map(|i| i as f32).collect();
            let out = layout::planar_to_interleaved(channels, &planar).unwrap();
            assert_eq!(out.len(), planar.len());
            // Multiset equality: same samples regardless of order.
            let mut a = out.clone();
            a.sort_by(|x, y| x.partial_cmp(y).unwrap());
            let mut b = planar.clone();
            b.sort_by(|x, y| x.partial_cmp(y).unwrap());
            assert_eq!(a, b);
        }
    }

    #[test]
    fn packet_buffer_is_reused_across_calls() {
        // Two consecutive encodes must both succeed and produce packets —
        // guards against the reusable buffer being left in a bad state.
        let mut enc = OpusEncoder::new(SAMPLE_RATE, 1, opus::Application::Audio).unwrap();
        let pcm = vec![0.0f32; FRAME_SIZE];
        let p1 = enc.encode(&pcm).unwrap();
        let p2 = enc.encode(&pcm).unwrap();
        assert!(!p1.is_empty());
        assert!(!p2.is_empty());
    }
}