Skip to main content

audio_codec_bsd/
format.rs

1//! Container-format auto-detection and decoder factory.
2//!
3//! [`open`] sniffs the leading magic bytes of a file and returns a boxed
4//! concrete decoder for the sniffed container. This is the ergonomic
5//! entry point for end-to-end decoding: callers do not need to know whether
6//! a file is FLAC, WAV, or PCM ahead of time.
7//!
8//! ## Magic-byte sniffing
9//!
10//! | Magic                  | Container | Decoder           |
11//! |------------------------|-----------|-------------------|
12//! | `b"fLaC"`              | FLAC      | [`SymphoniaDecoder`] |
13//! | `b"RIFF"` … `b"WAVE"`  | RIFF/WAVE | [`WavDecoder`]    |
14//!
15//! Anything else yields [`CodecError::Format`]. Sniffing reads at most 12
16//! bytes, so it is cheap and non-destructive.
17
18use std::fs::File;
19use std::io::Read;
20use std::path::Path;
21
22use crate::decoder::ContainerDecoder;
23use crate::error::{CodecError, Result};
24use crate::symphonia_impl::SymphoniaDecoder;
25use crate::wav::WavDecoder;
26
27/// Sniff the container by magic bytes and open the matching decoder.
28///
29/// Reads the first 12 bytes of `path`, matches the magic signature, and
30/// returns a boxed [`ContainerDecoder`] for the sniffed container. The caller
31/// should then call [`ContainerDecoder::open`] on the returned box to obtain
32/// a [`StreamInfo`](crate::StreamInfo) before iterating
33/// [`ContainerDecoder::next_frame`].
34///
35/// # Errors
36///
37/// - [`CodecError::Io`] if the file cannot be opened or read.
38/// - [`CodecError::Format`] if the magic bytes match no supported container.
39///
40/// # Example
41///
42/// ```rust,no_run
43/// use audio_codec_bsd::{open, ContainerDecoder};
44/// use std::path::Path;
45///
46/// let mut dec = open(Path::new("song.flac"))?;
47/// let info = dec.open(Path::new("song.flac"))?;
48/// while let Some(_frame) = dec.next_frame()? {
49///     // push into a lock-free ring for the RT thread.
50/// }
51/// # Ok::<(), audio_codec_bsd::CodecError>(())
52/// ```
53pub fn open(path: &Path) -> Result<Box<dyn ContainerDecoder>> {
54    let mut file = File::open(path).map_err(|e| CodecError::Io(e.to_string()))?;
55    let mut magic = [0u8; 12];
56    let n = file
57        .read(&mut magic)
58        .map_err(|e| CodecError::Io(e.to_string()))?;
59    let m = &magic[..n];
60
61    // Native FLAC: starts with "fLaC".
62    if m.len() >= 4 && &m[..4] == b"fLaC" {
63        let dec = SymphoniaDecoder::open(path)?;
64        return Ok(Box::new(dec));
65    }
66    // RIFF/WAVE: "RIFF" .... "WAVE".
67    if m.len() >= 12 && &m[..4] == b"RIFF" && &m[8..12] == b"WAVE" {
68        let dec = WavDecoder::open(path)?;
69        return Ok(Box::new(dec));
70    }
71
72    Err(CodecError::Format(format!(
73        "unrecognised magic bytes: {m:?}"
74    )))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    /// An unrecognised magic yields [`CodecError::Format`], never a panic.
82    #[test]
83    fn unknown_magic_yields_format_error() {
84        let mut path = std::env::temp_dir();
85        path.push(format!(
86            "audio_codec_bsd_format_unknown_{id}.bin",
87            id = std::process::id()
88        ));
89        std::fs::write(&path, b"XYZW this is not audio").expect("write temp");
90        let res = open(&path);
91        let _ = std::fs::remove_file(&path);
92        assert!(
93            matches!(res, Err(CodecError::Format(_))),
94            "expected Format error"
95        );
96    }
97
98    /// A missing file yields [`CodecError::Io`], never a panic.
99    #[test]
100    fn missing_file_yields_io_error() {
101        let path = std::path::Path::new("/no/such/audio/file.flac");
102        let res = open(path);
103        assert!(matches!(res, Err(CodecError::Io(_))), "expected Io error");
104    }
105
106    /// A WAV magic routes to a decoder that reports [`FormatKind::Wav`].
107    #[test]
108    fn riff_magic_routes_to_wav_decoder() {
109        let mut path = std::env::temp_dir();
110        path.push(format!(
111            "audio_codec_bsd_format_wav_{id}.wav",
112            id = std::process::id()
113        ));
114        // Write a *valid* (empty-body) WAV so the routed `WavDecoder` can parse
115        // the header. Routing is decided on the RIFF/WAVE magic bytes; the
116        // empty data chunk means `next_frame` immediately yields `Ok(None)`,
117        // which is fine — this test only asserts routing succeeds.
118        let spec = hound::WavSpec {
119            channels: 1,
120            sample_rate: 8000,
121            bits_per_sample: 16,
122            sample_format: hound::SampleFormat::Int,
123        };
124        let writer = hound::WavWriter::create(&path, spec).expect("create wav");
125        writer.finalize().expect("finalize empty wav");
126        let res = open(&path);
127        let _ = std::fs::remove_file(&path);
128        assert!(res.is_ok(), "expected routing to WavDecoder");
129    }
130}