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}