audio-resample-bsd 0.2.1

RT-safe audio resampling crate (rubato-based) — the only processing interface callable directly from the real-time audio thread
Documentation
//! Resampling error types and the [`Result`] alias.
//!
//! [`ResampleError`] is the set of classifiable errors that a [`Resampler`]
//! implementation reports during processing. rubato runtime errors are wrapped
//! as a string in [`ResampleError::ResampleFailed`] to avoid the burden of
//! matching rubato's `#[non_exhaustive]` error enum.

use thiserror::Error;

/// Errors that can arise while validating or resampling audio data.
#[derive(Debug, Clone, PartialEq, Error)]
pub enum ResampleError {
    /// The sample rate is not a positive, finite, supported value.
    #[error("invalid sample rate: {rate} Hz")]
    InvalidSampleRate {
        /// The rejected sample rate (Hz).
        rate: f64,
    },

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

    /// The fixed chunk size is zero or otherwise unsupported.
    #[error("invalid chunk size: {0}")]
    InvalidChunkSize(usize),

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

    /// The input length did not match the implementation's fixed chunk ×
    /// channels.
    #[error("input length mismatch: expected {expected} samples, got {actual}")]
    InputLengthMismatch {
        /// Expected input sample count (`chunk × channels`).
        expected: usize,
        /// Actual input sample count.
        actual: usize,
    },

    /// A rubato runtime error. The original message is wrapped as a string to
    /// avoid matching rubato's non-exhaustive error variants.
    #[error("resample failed: {0}")]
    ResampleFailed(String),

    /// Forwarded `audio-core-bsd` error.
    #[error(transparent)]
    Core(#[from] audio_core_bsd::AudioError),
}

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

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

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

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

    #[test]
    fn invalid_chunk_size_display_includes_value() {
        let msg = ResampleError::InvalidChunkSize(0).to_string();
        assert_eq!(msg, "invalid chunk size: 0");
    }

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

    #[test]
    fn input_length_mismatch_display_includes_both_counts() {
        let msg = ResampleError::InputLengthMismatch {
            expected: 1024,
            actual: 512,
        }
        .to_string();
        assert_eq!(msg, "input length mismatch: expected 1024 samples, got 512",);
    }

    #[test]
    fn resample_failed_display_wraps_message() {
        let msg = ResampleError::ResampleFailed("rubato internal".to_string()).to_string();
        assert_eq!(msg, "resample failed: rubato internal");
    }

    #[test]
    fn core_variant_derives_from_audio_error() {
        let core_err = audio_core_bsd::AudioError::InvalidChannelCount(0);
        let resampled: ResampleError = core_err.into();
        let msg = resampled.to_string();
        // audio-core-bsd's InvalidChannelCount(0) message is forwarded via
        // `#[error(transparent)]`.
        assert_eq!(msg, "invalid channel count: 0");
    }

    #[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_resample_error() {
        let err: Result<u32> = Err(ResampleError::InvalidChannelCount(0));
        assert_eq!(err.err(), Some(ResampleError::InvalidChannelCount(0)));
    }

    #[test]
    fn clone_and_partial_eq_hold_across_variants() {
        let a = ResampleError::BufferTooSmall { needed: 2, have: 1 };
        assert_eq!(a.clone(), a);
        // Inequality across variants (both wrap the integer `1`).
        assert_ne!(
            ResampleError::InvalidChannelCount(1),
            ResampleError::InvalidChunkSize(1),
        );
    }

    #[test]
    fn resample_failed_clone_preserves_message() {
        let a = ResampleError::ResampleFailed("boom".to_string());
        let b = a.clone();
        assert_eq!(a, b);
    }
}