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
//! Planar ↔ interleaved PCM layout conversion.
//!
//! [`audio_core_bsd::AudioFrame`] stores samples in **planar** layout:
//! `[ch0_frames…, ch1_frames…]`. The Opus codec (RFC 6716), like most C audio
//! libraries, speaks **interleaved** PCM: `[ch0_f0, ch1_f0, ch0_f1, ch1_f1…]`.
//!
//! A wrong channel stride here yields output that still "decodes" but scores
//! badly on SNR — it is the single hottest silent bug in any Opus binding.
//! The functions in this module are pure, allocation-bounded, and **independent
//! of libopus**, so the stride math can be tested to bit-exactness without an
//! Opus round-trip.
//!
//! ## Layout diagrams
//!
//! Planar (2 channels, 3 frames): `[L0 L1 L2 R0 R1 R2]`
//! Interleaved (same data):       `[L0 R0 L1 R1 L2 R2]`
//!
//! ## When to use which
//!
//! - The allocating variants ([`planar_to_interleaved`] /
//!   [`interleaved_to_planar`]) are for one-shot conversions on a worker
//!   thread where allocation is allowed.
//! - The `_into` variants ([`planar_to_interleaved_into`] /
//!   [`interleaved_to_planar_into`]) write into a caller-provided buffer and
//!   are the zero-allocation path the encoder/decoder hot loops reuse.

use crate::error::{OpusError, Result};

/// Converts a planar sample buffer to interleaved layout, allocating the result.
///
/// `planar.len()` must be an exact multiple of `channels`; otherwise
/// [`OpusError::LayoutMismatch`] is returned. With `channels == 0` only an
/// empty `planar` is accepted (and an empty [`Vec`] is returned).
///
/// # Errors
///
/// Returns [`OpusError::LayoutMismatch`] when `planar.len()` is not an exact
/// multiple of `channels` (or when `channels == 0` and `planar` is non-empty).
///
/// # Examples
///
/// ```
/// use audio_opus_bsd::layout::planar_to_interleaved;
/// // 2 channels × 2 frames, planar: [L0 L1 R0 R1]
/// let planar = [1.0_f32, 2.0, 3.0, 4.0];
/// let inter = planar_to_interleaved(2, &planar)?;
/// assert_eq!(inter, vec![1.0, 3.0, 2.0, 4.0]); // [L0 R0 L1 R1]
/// # Ok::<(), audio_opus_bsd::OpusError>(())
/// ```
pub fn planar_to_interleaved(channels: u16, planar: &[f32]) -> Result<Vec<f32>> {
    let mut out = vec![0.0_f32; planar.len()];
    planar_to_interleaved_into(channels, planar, &mut out)?;
    Ok(out)
}

/// Converts an interleaved sample buffer to planar layout, allocating the result.
///
/// `interleaved.len()` must be an exact multiple of `channels`; otherwise
/// [`OpusError::LayoutMismatch`] is returned. With `channels == 0` only an
/// empty input is accepted.
///
/// # Errors
///
/// Returns [`OpusError::LayoutMismatch`] when `interleaved.len()` is not an
/// exact multiple of `channels` (or when `channels == 0` and the input is
/// non-empty).
///
/// # Examples
///
/// ```
/// use audio_opus_bsd::layout::interleaved_to_planar;
/// // interleaved [L0 R0 L1 R1] → planar [L0 L1 R0 R1]
/// let inter = [1.0_f32, 3.0, 2.0, 4.0];
/// let planar = interleaved_to_planar(2, &inter)?;
/// assert_eq!(planar, vec![1.0, 2.0, 3.0, 4.0]);
/// # Ok::<(), audio_opus_bsd::OpusError>(())
/// ```
pub fn interleaved_to_planar(channels: u16, interleaved: &[f32]) -> Result<Vec<f32>> {
    let mut out = vec![0.0_f32; interleaved.len()];
    interleaved_to_planar_into(channels, interleaved, &mut out)?;
    Ok(out)
}

/// Writes the interleaved form of `planar` into the caller-provided `out` buffer.
///
/// This is the zero-allocation variant the encoder hot loop reuses. Both
/// `planar.len() % channels == 0` (else [`OpusError::LayoutMismatch`]) and
/// `out.len() == planar.len()` (else [`OpusError::BufferTooSmall`]) must hold.
///
/// # Errors
///
/// Returns [`OpusError::LayoutMismatch`] when `planar.len()` is not an exact
/// multiple of `channels`, and [`OpusError::BufferTooSmall`] when
/// `out.len() != planar.len()`. The layout check runs first.
pub fn planar_to_interleaved_into(channels: u16, planar: &[f32], out: &mut [f32]) -> Result<()> {
    validate_layout(channels, planar.len())?;
    if out.len() != planar.len() {
        return Err(OpusError::BufferTooSmall {
            needed: planar.len(),
            have: out.len(),
        });
    }
    let chans = usize::from(channels);
    // chans == 0 ⇒ planar was empty (validated above) ⇒ nothing to write.
    if chans == 0 {
        return Ok(());
    }
    let frames = planar.len() / chans;
    // inter[i*chans + c] = planar[c*frames + i]
    for (frame_idx, chunk) in out.chunks_mut(chans).enumerate() {
        for (chan_idx, slot) in chunk.iter_mut().enumerate() {
            *slot = planar[chan_idx * frames + frame_idx];
        }
    }
    Ok(())
}

/// Writes the planar form of `interleaved` into the caller-provided `out` buffer.
///
/// This is the zero-allocation variant the decoder hot loop reuses. Both
/// `interleaved.len() % channels == 0` (else [`OpusError::LayoutMismatch`]) and
/// `out.len() == interleaved.len()` (else [`OpusError::BufferTooSmall`]) must
/// hold.
///
/// # Errors
///
/// Returns [`OpusError::LayoutMismatch`] when `interleaved.len()` is not an
/// exact multiple of `channels`, and [`OpusError::BufferTooSmall`] when
/// `out.len() != interleaved.len()`. The layout check runs first.
pub fn interleaved_to_planar_into(
    channels: u16,
    interleaved: &[f32],
    out: &mut [f32],
) -> Result<()> {
    validate_layout(channels, interleaved.len())?;
    if out.len() != interleaved.len() {
        return Err(OpusError::BufferTooSmall {
            needed: interleaved.len(),
            have: out.len(),
        });
    }
    let chans = usize::from(channels);
    if chans == 0 {
        return Ok(());
    }
    let frames = interleaved.len() / chans;
    // planar[c*frames + i] = inter[i*chans + c]
    for (frame_idx, chunk) in interleaved.chunks(chans).enumerate() {
        for (chan_idx, &sample) in chunk.iter().enumerate() {
            out[chan_idx * frames + frame_idx] = sample;
        }
    }
    Ok(())
}

/// Checks that `len` is a valid sample-buffer length for `channels`:
/// `channels == 0` requires `len == 0`, otherwise `len % channels == 0`.
fn validate_layout(channels: u16, len: usize) -> Result<()> {
    let chans = usize::from(channels);
    if chans == 0 {
        if len != 0 {
            return Err(OpusError::LayoutMismatch(format!(
                "{channels} channels cannot own {len} non-empty samples"
            )));
        }
        return Ok(());
    }
    if len % chans != 0 {
        return Err(OpusError::LayoutMismatch(format!(
            "length {len} is not a multiple of {channels} channels"
        )));
    }
    Ok(())
}

#[cfg(test)]
#[allow(clippy::cast_precision_loss)] // test-only: small-range ramp cast to f32
mod tests {
    use super::*;
    use proptest::prelude::*;

    #[test]
    fn stereo_planar_to_interleaved_is_exact() {
        // 2 channels × 2 frames. Planar: [L0 L1 | R0 R1] = [10 20 30 40].
        let planar: &[f32] = &[10.0, 20.0, 30.0, 40.0];
        let inter = planar_to_interleaved(2, planar).expect("valid stereo planar");
        // Interleaved: [L0 R0 L1 R1] = [10 30 20 40].
        assert_eq!(inter, vec![10.0, 30.0, 20.0, 40.0]);
    }

    #[test]
    fn stereo_interleaved_to_planar_is_exact() {
        let inter: &[f32] = &[10.0, 30.0, 20.0, 40.0];
        let planar = interleaved_to_planar(2, inter).expect("valid stereo interleaved");
        assert_eq!(planar, vec![10.0, 20.0, 30.0, 40.0]);
    }

    #[test]
    fn round_trip_planar_interleaved_planar_is_identity_mono() {
        let original: Vec<f32> = (0..960).map(|i| i as f32 * 0.001).collect();
        let inter = planar_to_interleaved(1, &original).expect("mono planar");
        let back = interleaved_to_planar(1, &inter).expect("mono inter");
        assert_eq!(back, original);
    }

    #[test]
    fn round_trip_planar_interleaved_planar_is_identity_stereo() {
        let original: Vec<f32> = (0..1920).map(|i| i as f32 * 0.001).collect();
        let inter = planar_to_interleaved(2, &original).expect("stereo planar");
        assert_eq!(inter.len(), original.len());
        let back = interleaved_to_planar(2, &inter).expect("stereo inter");
        assert_eq!(back, original);
    }

    #[test]
    fn round_trip_interleaved_planar_interleaved_is_identity() {
        let original: Vec<f32> = (0..960).map(|i| i as f32 * 0.001).collect();
        let planar = interleaved_to_planar(2, &original).expect("stereo inter");
        let back = planar_to_interleaved(2, &planar).expect("stereo planar");
        assert_eq!(back, original);
    }

    #[test]
    fn uneven_length_returns_layout_mismatch() {
        // 5 samples, 2 channels → 5 % 2 == 1.
        let bad: &[f32] = &[1.0, 2.0, 3.0, 4.0, 5.0];
        let err = planar_to_interleaved(2, bad).unwrap_err();
        assert!(matches!(err, OpusError::LayoutMismatch(_)), "got {err:?}");
        let err = interleaved_to_planar(2, bad).unwrap_err();
        assert!(matches!(err, OpusError::LayoutMismatch(_)), "got {err:?}");
    }

    #[test]
    fn into_variants_validate_buffer_size() {
        // Even-length planar (4 samples, 2 channels → valid layout) but an
        // undersized output buffer of length 2.
        let planar: &[f32] = &[1.0, 2.0, 3.0, 4.0];
        let mut out = vec![0.0_f32; 2];
        let err = planar_to_interleaved_into(2, planar, &mut out).unwrap_err();
        assert!(
            matches!(err, OpusError::BufferTooSmall { .. }),
            "expected BufferTooSmall, got {err:?}"
        );
        let mut out2 = vec![0.0_f32; 2];
        let err = interleaved_to_planar_into(2, planar, &mut out2).unwrap_err();
        assert!(
            matches!(err, OpusError::BufferTooSmall { .. }),
            "expected BufferTooSmall, got {err:?}"
        );
    }

    #[test]
    fn into_variants_reject_uneven_length_before_buffer_check() {
        // 5 samples, 2 channels: layout error wins over buffer-size error even
        // when the output buffer is also the wrong size.
        let uneven: &[f32] = &[1.0, 2.0, 3.0, 4.0, 5.0];
        let mut out = vec![0.0_f32; 5];
        let err = planar_to_interleaved_into(2, uneven, &mut out).unwrap_err();
        assert!(
            matches!(err, OpusError::LayoutMismatch(_)),
            "expected LayoutMismatch, got {err:?}"
        );
    }

    #[test]
    fn zero_channels_empty_buffer_round_trips() {
        assert!(planar_to_interleaved(0, &[]).unwrap().is_empty());
        assert!(interleaved_to_planar(0, &[]).unwrap().is_empty());
    }

    #[test]
    fn zero_channels_non_empty_is_layout_mismatch() {
        let err = planar_to_interleaved(0, &[1.0]).unwrap_err();
        assert!(matches!(err, OpusError::LayoutMismatch(_)));
    }

    #[test]
    fn empty_buffer_round_trips_for_any_channel_count() {
        let inter = planar_to_interleaved(2, &[]).unwrap();
        assert!(inter.is_empty());
        let back = interleaved_to_planar(2, &inter).unwrap();
        assert!(back.is_empty());
    }

    #[test]
    fn into_variant_matches_allocating_variant() {
        // The zero-alloc path must produce byte-identical output to the
        // allocating path for a non-trivial stereo input.
        let planar: Vec<f32> = (0..8).map(|i| i as f32).collect();
        let alloc = planar_to_interleaved(2, &planar).unwrap();
        let mut into = vec![0.0_f32; planar.len()];
        planar_to_interleaved_into(2, &planar, &mut into).unwrap();
        assert_eq!(alloc, into);
    }

    proptest! {
        /// planar → interleaved → planar is the identity for any valid mono or
        /// stereo buffer. Sample values are arbitrary distinct floats (a ramp);
        /// because conversion is a pure copy, exact `==` is valid here (no
        /// arithmetic is performed on the floats).
        #[test]
        fn prop_planar_inter_planar_identity(
            channels in 1u16..=2u16,
            frames in 0usize..256,
        ) {
            let chans = usize::from(channels);
            let planar: Vec<f32> = (0..chans * frames).map(|i| i as f32).collect();
            let inter = planar_to_interleaved(channels, &planar).unwrap();
            let back = interleaved_to_planar(channels, &inter).unwrap();
            prop_assert_eq!(back, planar);
        }

        /// interleaved → planar → interleaved is the identity (the inverse
        /// direction), confirming neither conversion loses or duplicates a
        /// sample regardless of stride.
        #[test]
        fn prop_inter_planar_inter_identity(
            channels in 1u16..=2u16,
            frames in 0usize..256,
        ) {
            let chans = usize::from(channels);
            let inter: Vec<f32> = (0..chans * frames).map(|i| i as f32).collect();
            let planar = interleaved_to_planar(channels, &inter).unwrap();
            let back = planar_to_interleaved(channels, &planar).unwrap();
            prop_assert_eq!(back, inter);
        }
    }
}