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
//! Container-format auto-detection and decoder factory.
//!
//! [`open`] sniffs the leading magic bytes of a file and returns a boxed
//! concrete decoder for the sniffed container. This is the ergonomic
//! entry point for end-to-end decoding: callers do not need to know whether
//! a file is FLAC, WAV, or PCM ahead of time.
//!
//! ## Magic-byte sniffing
//!
//! | Magic                  | Container | Decoder           |
//! |------------------------|-----------|-------------------|
//! | `b"fLaC"`              | FLAC      | [`SymphoniaDecoder`] |
//! | `b"RIFF"` … `b"WAVE"`  | RIFF/WAVE | [`WavDecoder`]    |
//!
//! Anything else yields [`CodecError::Format`]. Sniffing reads at most 12
//! bytes, so it is cheap and non-destructive.

use std::fs::File;
use std::io::Read;
use std::path::Path;

use crate::decoder::ContainerDecoder;
use crate::error::{CodecError, Result};
use crate::symphonia_impl::SymphoniaDecoder;
use crate::wav::WavDecoder;

/// Sniff the container by magic bytes and open the matching decoder.
///
/// Reads the first 12 bytes of `path`, matches the magic signature, and
/// returns a boxed [`ContainerDecoder`] for the sniffed container. The caller
/// should then call [`ContainerDecoder::open`] on the returned box to obtain
/// a [`StreamInfo`](crate::StreamInfo) before iterating
/// [`ContainerDecoder::next_frame`].
///
/// # Errors
///
/// - [`CodecError::Io`] if the file cannot be opened or read.
/// - [`CodecError::Format`] if the magic bytes match no supported container.
///
/// # Example
///
/// ```rust,no_run
/// use audio_codec_bsd::{open, ContainerDecoder};
/// use std::path::Path;
///
/// let mut dec = open(Path::new("song.flac"))?;
/// let info = dec.open(Path::new("song.flac"))?;
/// while let Some(_frame) = dec.next_frame()? {
///     // push into a lock-free ring for the RT thread.
/// }
/// # Ok::<(), audio_codec_bsd::CodecError>(())
/// ```
pub fn open(path: &Path) -> Result<Box<dyn ContainerDecoder>> {
    let mut file = File::open(path).map_err(|e| CodecError::Io(e.to_string()))?;
    let mut magic = [0u8; 12];
    let n = file
        .read(&mut magic)
        .map_err(|e| CodecError::Io(e.to_string()))?;
    let m = &magic[..n];

    // Native FLAC: starts with "fLaC".
    if m.len() >= 4 && &m[..4] == b"fLaC" {
        let dec = SymphoniaDecoder::open(path)?;
        return Ok(Box::new(dec));
    }
    // RIFF/WAVE: "RIFF" .... "WAVE".
    if m.len() >= 12 && &m[..4] == b"RIFF" && &m[8..12] == b"WAVE" {
        let dec = WavDecoder::open(path)?;
        return Ok(Box::new(dec));
    }

    Err(CodecError::Format(format!(
        "unrecognised magic bytes: {m:?}"
    )))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// An unrecognised magic yields [`CodecError::Format`], never a panic.
    #[test]
    fn unknown_magic_yields_format_error() {
        let mut path = std::env::temp_dir();
        path.push(format!(
            "audio_codec_bsd_format_unknown_{id}.bin",
            id = std::process::id()
        ));
        std::fs::write(&path, b"XYZW this is not audio").expect("write temp");
        let res = open(&path);
        let _ = std::fs::remove_file(&path);
        assert!(
            matches!(res, Err(CodecError::Format(_))),
            "expected Format error"
        );
    }

    /// A missing file yields [`CodecError::Io`], never a panic.
    #[test]
    fn missing_file_yields_io_error() {
        let path = std::path::Path::new("/no/such/audio/file.flac");
        let res = open(path);
        assert!(matches!(res, Err(CodecError::Io(_))), "expected Io error");
    }

    /// A WAV magic routes to a decoder that reports [`FormatKind::Wav`].
    #[test]
    fn riff_magic_routes_to_wav_decoder() {
        let mut path = std::env::temp_dir();
        path.push(format!(
            "audio_codec_bsd_format_wav_{id}.wav",
            id = std::process::id()
        ));
        // Write a *valid* (empty-body) WAV so the routed `WavDecoder` can parse
        // the header. Routing is decided on the RIFF/WAVE magic bytes; the
        // empty data chunk means `next_frame` immediately yields `Ok(None)`,
        // which is fine — this test only asserts routing succeeds.
        let spec = hound::WavSpec {
            channels: 1,
            sample_rate: 8000,
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
        };
        let writer = hound::WavWriter::create(&path, spec).expect("create wav");
        writer.finalize().expect("finalize empty wav");
        let res = open(&path);
        let _ = std::fs::remove_file(&path);
        assert!(res.is_ok(), "expected routing to WavDecoder");
    }
}