audio-codec-bsd 0.1.1

FLAC/PCM/WAV multi-format audio container decoder (symphonia + hound, pure Rust) emitting planar audio-core-bsd AudioFrames
Documentation
//! The container-decoder public API contract.
//!
//! This module defines the types every concrete decoder (FLAC, WAV, PCM)
//! exchanges with its callers.

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

/// Supported container formats.
///
/// Each variant maps to a concrete decoder implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FormatKind {
    /// Free Lossless Audio Codec — lossless compressed container.
    Flac,
    /// RIFF Waveform — PCM or IEEE-float, uncompressed.
    Wav,
    /// Raw headerless PCM — little-endian samples with no container.
    Pcm,
}

/// Metadata describing a decoded stream, populated at
/// [`ContainerDecoder::open`] time.
///
/// All fields are known once the container header has been parsed; they do
/// not change across subsequent [`ContainerDecoder::next_frame`] calls.
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct StreamInfo {
    /// The sniffed container format (FLAC / WAV / PCM).
    pub format: FormatKind,
    /// Sample rate in Hz (`48000` is the common baseline).
    pub sample_rate: u32,
    /// Number of channels (`1` = mono, `2` = stereo, …).
    pub channels: u16,
    /// Bit depth of the *encoded* samples (e.g. `16`, `24`).
    pub bits_per_sample: u32,
    /// Total per-channel frame count, when known from the container header.
    ///
    /// `None` indicates an unbounded or streaming source (e.g. raw PCM with no
    /// length prefix). Callers must not assume a finite length in that case.
    pub total_frames: Option<u64>,
}

/// Multi-format container decoder.
///
/// Runs on a **worker thread** (not the RT audio thread): file I/O and heap
/// allocation are expected and permitted here. Decoded frames should be handed
/// to a lock-free ring buffer so the RT thread never blocks on this trait.
///
/// ## Design notes
///
/// - [`ContainerDecoder::open`] returns a populated [`StreamInfo`] rather than
///   a bare stream handle, so callers can validate format/channel/rate before
///   the first decode.
/// - [`ContainerDecoder::next_frame`] returns `Option<AudioFrame>`:
///   `Ok(None)` signals normal end-of-stream; `Err(CodecError::Eof)` is
///   reserved for callers that treat EOF as a hard error.
/// - Samples are stored in **planar** layout (`[ch0…, ch1…]`) per
///   [`AudioFrame`]; symphonia's interleaved output is de-interleaved by the
///   concrete decoder before construction.
///
pub trait ContainerDecoder: Send {
    /// Open the container at `path`, sniff/probe the format, and return the
    /// stream metadata.
    ///
    /// # Errors
    ///
    /// - [`CodecError::Io`] if the file cannot be opened or read.
    /// - [`CodecError::Format`] if the container format is unrecognised.
    ///
    /// [`CodecError::Io`]: crate::CodecError
    /// [`CodecError::Format`]: crate::CodecError
    fn open(&mut self, path: &std::path::Path) -> error::Result<StreamInfo>;

    /// Decode the next chunk as a planar [`AudioFrame`].
    ///
    /// Returns `Ok(None)` at end-of-stream (never panics on EOF). Samples are
    /// stored in **planar** layout (`[ch0…, ch1…]`) per [`AudioFrame`].
    ///
    /// # Errors
    ///
    /// - [`CodecError::Decode`] if the stream is corrupt or a packet is
    ///   malformed.
    /// - [`CodecError::Io`] if an underlying read fails mid-stream.
    ///
    /// [`CodecError::Decode`]: crate::CodecError
    /// [`CodecError::Io`]: crate::CodecError
    fn next_frame(&mut self) -> error::Result<Option<AudioFrame>>;
}

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

    #[test]
    fn format_kind_variants_are_distinct() {
        assert_ne!(FormatKind::Flac, FormatKind::Wav);
        assert_ne!(FormatKind::Wav, FormatKind::Pcm);
        assert_ne!(FormatKind::Flac, FormatKind::Pcm);
    }

    #[test]
    fn format_kind_copy_and_clone_round_trip() {
        let a = FormatKind::Flac;
        let b = a;
        assert_eq!(a, b);
        assert_eq!(a.clone(), b);
    }

    #[test]
    fn format_kind_hash_is_consistent() {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        fn hash_of<T: Hash>(v: &T) -> u64 {
            let mut h = DefaultHasher::new();
            v.hash(&mut h);
            h.finish()
        }
        // Equal values hash equally.
        assert_eq!(hash_of(&FormatKind::Wav), hash_of(&FormatKind::Wav));
        // The three variants are distinct (not a collision guarantee, but
        // catches accidental aliasing).
        assert_ne!(hash_of(&FormatKind::Flac), hash_of(&FormatKind::Wav));
    }

    #[test]
    fn stream_info_round_trips_all_fields() {
        let info = StreamInfo {
            format: FormatKind::Flac,
            sample_rate: 96_000,
            channels: 2,
            bits_per_sample: 24,
            total_frames: Some(4_800_000),
        };
        assert_eq!(info.format, FormatKind::Flac);
        assert_eq!(info.sample_rate, 96_000);
        assert_eq!(info.channels, 2);
        assert_eq!(info.bits_per_sample, 24);
        assert_eq!(info.total_frames, Some(4_800_000));
    }

    #[test]
    fn stream_info_with_unknown_total_frames() {
        let info = StreamInfo {
            format: FormatKind::Pcm,
            sample_rate: 48_000,
            channels: 1,
            bits_per_sample: 16,
            total_frames: None,
        };
        assert!(info.total_frames.is_none());
    }

    #[test]
    fn stream_info_clone_and_eq_hold() {
        let a = StreamInfo {
            format: FormatKind::Wav,
            sample_rate: 44_100,
            channels: 2,
            bits_per_sample: 16,
            total_frames: Some(100),
        };
        assert_eq!(a.clone(), a);
        // A differing field breaks equality.
        let mut b = a.clone();
        b.bits_per_sample = 24;
        assert_ne!(a, b);
    }
}