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 codec errors and the crate [`Result`] alias.

use thiserror::Error;

/// Errors that can arise when encoding or decoding Opus (RFC 6716) audio.
///
/// All variants carry enough information to build a useful diagnostic without
/// exposing backend-specific error types (`opus::Error` is `Copy + Eq` but its
/// stringified context is far more informative, so it is converted to a
/// [`String`] at the boundary).
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum OpusError {
    /// An encode failure (libopus rejected the input or the encoder was
    /// misconfigured).
    #[error("opus encode error: {0}")]
    Encode(String),

    /// A decode failure (corrupt packet, misconfigured decoder, or a
    /// libopus internal error).
    #[error("opus decode error: {0}")]
    Decode(String),

    /// The sample rate is zero or otherwise unsupported by libopus.
    #[error("invalid sample rate: {0} Hz")]
    InvalidSampleRate(u32),

    /// The channel count is zero.
    #[error("invalid channel count: {0}")]
    InvalidChannelCount(u16),

    /// The channel count is non-zero but unsupported by Opus, which only
    /// permits mono (`1`) or stereo (`2`).
    #[error("unsupported channel count for opus: {0} (only 1 or 2 allowed)")]
    UnsupportedChannels(u16),

    /// A destination buffer was smaller than required.
    #[error("buffer too small: needed {needed} samples, have {have}")]
    BufferTooSmall {
        /// Required number of samples.
        needed: usize,
        /// Samples actually available.
        have: usize,
    },

    /// A sample-buffer length did not match the declared channel count (e.g. a
    /// planar/interleaved buffer whose length is not an exact multiple of
    /// `channels`). The single hottest silent SNR-killer in any Opus binding.
    #[error("layout mismatch: {0}")]
    LayoutMismatch(String),

    /// A worker-thread spawn or join failure (e.g. OS resource exhaustion
    /// preventing `std::thread::Builder::spawn`).
    #[error("opus worker thread error: {0}")]
    WorkerThread(String),
}

/// Convenience alias used by consumers of this crate.
pub type Result<T> = core::result::Result<T, OpusError>;

#[cfg(test)]
mod tests {
    use super::{OpusError, Result};

    #[test]
    fn encode_display_includes_message() {
        let msg = OpusError::Encode("bad arg".into()).to_string();
        assert_eq!(msg, "opus encode error: bad arg");
    }

    #[test]
    fn decode_display_includes_message() {
        let msg = OpusError::Decode("corrupt packet".into()).to_string();
        assert_eq!(msg, "opus decode error: corrupt packet");
    }

    #[test]
    fn invalid_sample_rate_display_includes_value() {
        let msg = OpusError::InvalidSampleRate(0).to_string();
        assert_eq!(msg, "invalid sample rate: 0 Hz");
    }

    #[test]
    fn invalid_channel_count_display_includes_value() {
        let msg = OpusError::InvalidChannelCount(0).to_string();
        assert_eq!(msg, "invalid channel count: 0");
    }

    #[test]
    fn unsupported_channels_display_includes_value() {
        let msg = OpusError::UnsupportedChannels(6).to_string();
        assert_eq!(
            msg,
            "unsupported channel count for opus: 6 (only 1 or 2 allowed)",
        );
    }

    #[test]
    fn buffer_too_small_display_includes_both_counts() {
        let msg = OpusError::BufferTooSmall {
            needed: 960,
            have: 128,
        }
        .to_string();
        assert_eq!(msg, "buffer too small: needed 960 samples, have 128",);
    }

    #[test]
    fn layout_mismatch_display_includes_reason() {
        let msg = OpusError::LayoutMismatch("length 5 is not a multiple of 2 channels".into())
            .to_string();
        assert_eq!(
            msg,
            "layout mismatch: length 5 is not a multiple of 2 channels"
        );
    }

    #[test]
    fn worker_thread_display_includes_reason() {
        let msg = OpusError::WorkerThread("spawn failed: EAGAIN".into()).to_string();
        assert_eq!(msg, "opus worker thread error: spawn failed: EAGAIN");
    }

    #[test]
    fn result_alias_ok_carries_value() {
        let ok: Result<u32> = Ok(42);
        assert_eq!(ok.map(|v| v + 1), Ok(43));
    }

    #[test]
    fn result_alias_err_carries_opus_error() {
        let err: Result<u32> = Err(OpusError::InvalidSampleRate(0));
        assert_eq!(err.err(), Some(OpusError::InvalidSampleRate(0)));
    }

    #[test]
    fn clone_and_partial_eq_hold_across_variants() {
        let a = OpusError::BufferTooSmall { needed: 2, have: 1 };
        assert_eq!(a.clone(), a);
        // Across-variant inequality (both carry the integer `1`).
        assert_ne!(
            OpusError::InvalidSampleRate(1),
            OpusError::InvalidChannelCount(1),
        );
        // `UnsupportedChannels` and `InvalidChannelCount` must not collide
        // even though both wrap a `u16` — the former is the "non-zero but
        // > 2" case, the latter is the "zero" case.
        assert_ne!(
            OpusError::InvalidChannelCount(2),
            OpusError::UnsupportedChannels(2),
        );
        // String variants compare by content.
        assert_eq!(OpusError::Encode("x".into()), OpusError::Encode("x".into()),);
    }
}