Skip to main content

audiobox/
codec.rs

1use crate::{Audio, AudioFormat, DecodeOptions, EncodeOptions, Error, ErrorCode, Result};
2use std::io::Read;
3use std::path::Path;
4
5/// Limits for the whole-file convenience reader. Encoded bytes and decoded PCM
6/// have separate budgets; codec-specific limits still apply during decoding.
7#[derive(Clone, Debug)]
8pub struct ReadAudioOptions {
9    /// Maximum encoded file bytes; exact equality is accepted. Defaults to 512 MiB.
10    pub max_encoded_bytes: usize,
11    /// Decoder format override and decoded PCM/shape limits.
12    pub decode: DecodeOptions,
13}
14impl Default for ReadAudioOptions {
15    fn default() -> Self {
16        Self {
17            max_encoded_bytes: 512 * 1024 * 1024,
18            decode: DecodeOptions::default(),
19        }
20    }
21}
22
23/// Detect a supported format from its bytes, independent of the extension.
24pub fn sniff(bytes: &[u8]) -> Option<AudioFormat> {
25    if bytes.len() < 4 {
26        return None;
27    }
28    match &bytes[..4] {
29        b"RIFF" | b"RF64" | b"BW64" if bytes.get(8..12) == Some(b"WAVE") => Some(AudioFormat::Wav),
30        b"FORM" if bytes.get(8..12) == Some(b"AIFF") || bytes.get(8..12) == Some(b"AIFC") => {
31            Some(AudioFormat::Aiff)
32        }
33        b"caff" => Some(AudioFormat::Caf),
34        b".snd" => Some(AudioFormat::Au),
35        b"fLaC" => Some(AudioFormat::Flac),
36        b"OggS" => crate::ogg::sniff(bytes),
37        _ if bytes.starts_with(b"ID3") || crate::mp3::has_frame(bytes) => Some(AudioFormat::Mp3),
38        _ => None,
39    }
40}
41pub fn decode(bytes: &[u8]) -> Result<Audio> {
42    decode_with_options(bytes, &DecodeOptions::default())
43}
44pub fn decode_with_options(bytes: &[u8], options: &DecodeOptions) -> Result<Audio> {
45    let format = options.format.or_else(|| sniff(bytes)).ok_or_else(|| {
46        Error::new(
47            ErrorCode::UnsupportedFormat,
48            "Unknown or unsupported audio format",
49        )
50    })?;
51    match format {
52        AudioFormat::Wav => crate::formats::decode_wav(bytes, options),
53        AudioFormat::Aiff => crate::formats::decode_aiff(bytes, options),
54        AudioFormat::Caf => crate::formats::decode_caf(bytes, options),
55        AudioFormat::Au => crate::formats::decode_au(bytes, options),
56        AudioFormat::Flac => crate::flac::decode(bytes, options),
57        AudioFormat::Mp3 => crate::mp3::decode(bytes, options),
58        AudioFormat::Vorbis | AudioFormat::Opus => crate::ogg::decode(bytes, format, options),
59        AudioFormat::Raw => Err(Error::invalid(
60            "Raw PCM requires pcm::decode_pcm with an explicit shape",
61        )),
62    }
63}
64pub fn encode(audio: &Audio, format: AudioFormat) -> Result<Vec<u8>> {
65    encode_with_options(audio, format, &EncodeOptions::default())
66}
67pub fn encode_with_options(
68    audio: &Audio,
69    format: AudioFormat,
70    options: &EncodeOptions,
71) -> Result<Vec<u8>> {
72    if options.dither != crate::Dither::None {
73        if format == AudioFormat::Mp3 {
74            return Err(Error::invalid("Dither is not supported for MP3 output"));
75        }
76        crate::pcm::validate_dither(options.sample_format, options.dither)?;
77    }
78    crate::metadata::validate_options(options, format)?;
79    match format {
80        AudioFormat::Wav => crate::formats::encode_wav(audio, options),
81        AudioFormat::Aiff => crate::formats::encode_aiff(audio, options),
82        AudioFormat::Caf => crate::formats::encode_caf(audio, options),
83        AudioFormat::Au => crate::formats::encode_au(audio, options),
84        AudioFormat::Flac => crate::flac::encode(audio, options),
85        AudioFormat::Mp3 => crate::mp3::encode(audio, options),
86        AudioFormat::Vorbis | AudioFormat::Opus => Err(Error::new(
87            ErrorCode::UnsupportedFormat,
88            "Ogg encoding is not supported",
89        )),
90        AudioFormat::Raw => crate::pcm::encode_pcm_with_dither(
91            audio,
92            options.sample_format,
93            options.endian,
94            options.dither,
95        ),
96    }
97}
98/// Decode a regular file using a 512 MiB encoded-byte cap and default decoded
99/// [`crate::Limits`]. See [`read_audio_file_with_options`] for path and error policy.
100pub fn read_audio_file(path: impl AsRef<Path>) -> Result<Audio> {
101    read_audio_file_with_options(path, &ReadAudioOptions::default())
102}
103/// Read a bounded regular file, then decode the same buffer without copying it.
104///
105/// Follows symlinks to regular files, including outside any project directory;
106/// containment is the caller's responsibility. Static directories, FIFOs, devices
107/// and other nonregular targets are rejected **before opening** with
108/// `InvalidArgument`. The opened handle is checked again. This assumes a stable
109/// filesystem path: portable safe std I/O does not atomically protect the open
110/// against hostile replacement by a FIFO or another target between checks.
111///
112/// Both metadata size and actual bytes are bounded. Reads at most cap+1 bytes
113/// (the extra byte detects growth), stores at most cap, and accepts exact-cap
114/// files. Oversize or failed buffer reservation returns `LimitExceeded`; I/O
115/// errors return `Io`. Invalid encoded data/decoded limits retain decoder errors.
116/// There is no unrestricted `fs::read` or second encoded-buffer allocation.
117pub fn read_audio_file_with_options(
118    path: impl AsRef<Path>,
119    options: &ReadAudioOptions,
120) -> Result<Audio> {
121    let path = path.as_ref();
122    let check = |metadata: std::fs::Metadata| {
123        if !metadata.is_file() {
124            return Err(Error::invalid("Audio input must be a regular file"));
125        }
126        if metadata.len() as u128 > options.max_encoded_bytes as u128 {
127            return Err(Error::limit("Encoded file exceeds byte limit"));
128        }
129        Ok(())
130    };
131    check(std::fs::metadata(path)?)?;
132    let file = std::fs::File::open(path)?;
133    check(file.metadata()?)?;
134    decode_with_options(
135        &read_bounded(file, options.max_encoded_bytes)?,
136        &options.decode,
137    )
138}
139
140fn read_bounded(mut reader: impl Read, limit: usize) -> Result<Vec<u8>> {
141    let mut bytes = Vec::new();
142    let mut chunk = [0u8; 8192];
143    loop {
144        let remaining = limit - bytes.len();
145        let count = remaining.min(chunk.len()).max(1);
146        let n = match reader.read(&mut chunk[..count]) {
147            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
148            value => value?,
149        };
150        if n == 0 {
151            return Ok(bytes);
152        }
153        if n > remaining {
154            return Err(Error::limit("Encoded file exceeds byte limit"));
155        }
156        let next = bytes.len() + n;
157        if next > bytes.capacity() {
158            let target = next.max(
159                bytes
160                    .capacity()
161                    .saturating_add(bytes.capacity() / 2)
162                    .min(limit),
163            );
164            bytes
165                .try_reserve_exact(target - bytes.len())
166                .map_err(|_| Error::limit("Cannot reserve encoded file buffer"))?;
167        }
168        bytes.extend_from_slice(&chunk[..n]);
169    }
170}
171pub fn write_audio_file(
172    path: impl AsRef<Path>,
173    audio: &Audio,
174    options: &EncodeOptions,
175) -> Result<()> {
176    let path = path.as_ref();
177    let format = path
178        .extension()
179        .and_then(|s| s.to_str())
180        .ok_or_else(|| Error::invalid("Output path requires an audio extension"))?
181        .parse()?;
182    std::fs::write(path, encode_with_options(audio, format, options)?)?;
183    Ok(())
184}
185
186#[cfg(test)]
187mod bounded_read_tests {
188    use super::*;
189
190    #[test]
191    fn actual_bytes_are_bounded_even_when_the_source_keeps_growing() {
192        struct Growing {
193            read: usize,
194        }
195        impl Read for Growing {
196            fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
197                bytes.fill(7);
198                self.read += bytes.len();
199                Ok(bytes.len())
200            }
201        }
202        for limit in [0, 1, 8191, 8192, 8193, 20000] {
203            let mut source = Growing { read: 0 };
204            assert_eq!(
205                read_bounded(&mut source, limit).unwrap_err().code,
206                ErrorCode::LimitExceeded
207            );
208            assert_eq!(source.read, limit + 1);
209            let bytes = vec![7; limit];
210            assert_eq!(
211                read_bounded(std::io::Cursor::new(&bytes), limit).unwrap(),
212                bytes
213            );
214        }
215    }
216
217    #[test]
218    fn interrupted_reads_are_retried_and_other_errors_are_io() {
219        struct Interrupted(bool);
220        impl Read for Interrupted {
221            fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
222                if !self.0 {
223                    self.0 = true;
224                    Err(std::io::ErrorKind::Interrupted.into())
225                } else {
226                    Ok(0)
227                }
228            }
229        }
230        assert!(read_bounded(Interrupted(false), 0).unwrap().is_empty());
231        struct Failed;
232        impl Read for Failed {
233            fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
234                Err(std::io::ErrorKind::PermissionDenied.into())
235            }
236        }
237        assert_eq!(read_bounded(Failed, 10).unwrap_err().code, ErrorCode::Io);
238    }
239}