aifc/
lib.rs

1//! # AIFF and AIFF-C Audio Format Reader and Writer
2//!
3//! This crate contains [`AifcReader`] and [`AifcWriter`] for Audio Interchange File Formats
4//! AIFF and AIFF-C. This crate supports uncompressed sample data,
5//! μ-law, A-law and IMA ADPCM ("ima4") compressed sample data.
6//!
7//! These audio formats are made of chunks, which contain header data (the COMM chunk),
8//! audio sample data (the SSND chunk) and other data, such as marker data (the MARK chunk).
9//!
10//! # Examples
11//!
12//! Reading audio info and samples:
13//!
14//! ```no_run
15//! # fn example() -> aifc::AifcResult<()> {
16//! let mut stream = std::io::BufReader::new(std::fs::File::open("test.aiff")?);
17//! let mut reader = aifc::AifcReader::new(&mut stream)?;
18//! let info = reader.info();
19//! for sample in reader.samples()? {
20//!     println!("Got sample {:?}", sample.expect("Sample read error"));
21//! }
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! Writing AIFF-C with the default 2 channels, sample rate 44100 and signed 16-bit integer samples:
27//!
28//! ```no_run
29//! # fn example() -> aifc::AifcResult<()> {
30//! let mut stream = std::io::BufWriter::new(std::fs::File::create("test.aiff")?);
31//! let info = aifc::AifcWriteInfo::default();
32//! let mut writer = aifc::AifcWriter::new(&mut stream, &info)?;
33//! writer.write_samples_i16(&[ 0, 10, -10, 0 ])?;
34//! writer.finalize()?;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! # Text decoding
40//!
41//! AIFF and AIFF-C originally support ASCII only text in their metadata chunks (NAME, ANNO, etc.).
42//! In addition to ASCII, older apps may have used ISO-8859-1 and newer apps may have used UTF-8.
43//!
44//! A text decoder could try to decode UTF-8 first with
45//! [`String::from_utf8()`](String::from_utf8) and if it fails, try to decode ISO-8859-1,
46//! and if it fails, decode ASCII. Or it could just assume that everything is UTF-8 and
47//! call [`String::from_utf8_lossy()`](String::from_utf8_lossy).
48//!
49//! When writing new files, the ID3 chunk has proper support for UTF-8 text and can be used
50//! as a replacement for most metadata chunks.
51
52#![forbid(
53    unsafe_code,
54    clippy::panic,
55    clippy::exit,
56    clippy::unwrap_used,
57    clippy::expect_used,
58    clippy::unimplemented,
59    clippy::todo,
60    clippy::unreachable,
61)]
62#![deny(
63    clippy::cast_ptr_alignment,
64    clippy::char_lit_as_u8,
65    clippy::unnecessary_cast,
66    clippy::cast_lossless,
67    clippy::cast_possible_truncation,
68    clippy::cast_possible_wrap,
69    clippy::cast_sign_loss,
70    clippy::checked_conversions,
71)]
72
73// silly way to test rust code blocks in README.md
74// https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html
75#[doc = include_str!("../README.md")]
76#[cfg(doctest)]
77pub struct ReadmeDoctests;
78
79use std::io::{Read, Write, Seek, SeekFrom};
80
81mod aifcreader;
82mod aifcwriter;
83mod chunks;
84mod aifcresult;
85mod cast;
86mod f80;
87
88pub use aifcreader::{AifcReader, AifcReadInfo, Samples, Sample, Chunks};
89pub use aifcwriter::{AifcWriter, AifcWriteInfo};
90pub use chunks::{Markers, Marker, Comments, Comment, Instrument, Loop};
91pub use aifcresult::{AifcResult, AifcError};
92
93/// A chunk id is a four byte identifier.
94///
95/// Valid chunk ids are made of ASCII characters in the range 0x20-0x7e.
96/// Chunk ids should not start with a space character (0x20).
97/// Chunk ids are case-sensitive.
98pub type ChunkId = [u8; 4];
99
100/// Marker id, which should be a positive number.
101pub type MarkerId = i16;
102
103fn buffer_size_error() -> std::io::Error { std::io::Error::from(std::io::ErrorKind::InvalidInput) }
104fn read_error() -> std::io::Error { std::io::Error::from(std::io::ErrorKind::Other) }
105fn unexpectedeof() -> std::io::Error { std::io::Error::from(std::io::ErrorKind::UnexpectedEof) }
106
107/// Offset between comment timestamp and Unix timestamp.
108const UNIX_TIMESTAMP_OFFSET: i64 = 2082844800;
109
110const CHUNKID_FORM: [u8; 4] = *b"FORM";
111const CHUNKID_AIFF: [u8; 4] = *b"AIFF";
112const CHUNKID_AIFC: [u8; 4] = *b"AIFC";
113
114/// The common (header) "COMM" chunk id.
115pub const CHUNKID_COMM: [u8; 4] = *b"COMM";
116/// The format version "FVER" chunk id.
117pub const CHUNKID_FVER: [u8; 4] = *b"FVER";
118/// The sound data "SSND" chunk id.
119pub const CHUNKID_SSND: [u8; 4] = *b"SSND";
120/// The marker "MARK" chunk id.
121pub const CHUNKID_MARK: [u8; 4] = *b"MARK";
122/// The comments "COMT" chunk id.
123pub const CHUNKID_COMT: [u8; 4] = *b"COMT";
124/// The instrument "INST" chunk id.
125pub const CHUNKID_INST: [u8; 4] = *b"INST";
126/// The MIDI "MIDI" data chunk id. A stream may contain multiple MIDI chunks.
127pub const CHUNKID_MIDI: [u8; 4] = *b"MIDI";
128/// The audio recording "AESD" chunk id.
129pub const CHUNKID_AESD: [u8; 4] = *b"AESD";
130/// The application specific "APPL" chunk id. A stream may contain multiple APPL chunks.
131pub const CHUNKID_APPL: [u8; 4] = *b"APPL";
132/// The name "NAME" chunk id.
133pub const CHUNKID_NAME: [u8; 4] = *b"NAME";
134/// The author "AUTH" chunk id.
135pub const CHUNKID_AUTH: [u8; 4] = *b"AUTH";
136/// The copyright "(c) " chunk id.
137pub const CHUNKID_COPY: [u8; 4] = *b"(c) ";
138/// The annotation "ANNO" chunk id. A stream may contain multiple ANNO chunks.
139///
140/// It is recommended to write comments "COMT" chunks instead of annotation "ANNO" chunks in AIFF-C.
141pub const CHUNKID_ANNO: [u8; 4] = *b"ANNO";
142/// The ID3 "ID3 " data chunk id.
143pub const CHUNKID_ID3: [u8; 4] = *b"ID3 ";
144
145const COMPRESSIONTYPE_NONE: [u8; 4] = *b"NONE";
146const COMPRESSIONTYPE_TWOS: [u8; 4] = *b"twos";
147const COMPRESSIONTYPE_IN24: [u8; 4] = *b"in24";
148const COMPRESSIONTYPE_IN32: [u8; 4] = *b"in32";
149const COMPRESSIONTYPE_RAW: [u8; 4] = *b"raw ";
150const COMPRESSIONTYPE_SOWT: [u8; 4] = *b"sowt";
151const COMPRESSIONTYPE_23NI: [u8; 4] = *b"23ni";
152const COMPRESSIONTYPE_FL32_UPPER: [u8; 4] = *b"FL32";
153const COMPRESSIONTYPE_FL32: [u8; 4] = *b"fl32";
154const COMPRESSIONTYPE_FL64_UPPER: [u8; 4] = *b"FL64";
155const COMPRESSIONTYPE_FL64: [u8; 4] = *b"fl64";
156const COMPRESSIONTYPE_ULAW: [u8; 4] = *b"ulaw";
157const COMPRESSIONTYPE_ULAW_UPPER: [u8; 4] = *b"ULAW";
158const COMPRESSIONTYPE_ALAW: [u8; 4] = *b"alaw";
159const COMPRESSIONTYPE_ALAW_UPPER: [u8; 4] = *b"ALAW";
160const COMPRESSIONTYPE_IMA4: [u8; 4] = *b"ima4";
161
162/// ChunkRef contains a chunk id, chunk start position and its size.
163#[derive(Debug, Clone, PartialEq)]
164pub struct ChunkRef {
165    /// Chunk id.
166    pub id: ChunkId,
167    /// Chunk start position (the start of the chunk id) in the stream relative
168    /// to the start of the FORM chunk.
169    pub pos: u64,
170    /// Chunk byte size without the chunk id and size fields.
171    pub size: u32
172}
173
174/// File format: AIFF or AIFF-C.
175#[derive(Debug, Clone, Copy, PartialEq)]
176pub enum FileFormat {
177    /// AIFF.
178    Aiff,
179    /// AIFF-C.
180    Aifc
181}
182
183/// Sample format.
184///
185/// Unsupported sample formats are represented as `Custom` values.
186#[derive(Debug, Clone, Copy, PartialEq)]
187pub enum SampleFormat {
188    /// Unsigned 8-bit integer sample format.
189    U8,
190    /// Signed 8-bit integer sample format.
191    I8,
192    /// Signed big-endian 16-bit integer sample format.
193    I16,
194    /// Signed little-endian 16-bit integer sample format.
195    I16LE,
196    /// Signed big-endian 24-bit integer sample format.
197    I24,
198    /// Signed big-endian 32-bit integer sample format.
199    I32,
200    /// Signed little-endian 32-bit integer sample format.
201    I32LE,
202    /// Signed 32-bit floating point sample format.
203    F32,
204    /// Signed 64-bit floating point sample format.
205    F64,
206    /// Compressed μ-law sample format. Reading and writing samples should happen
207    /// as signed 16-bit integers.
208    CompressedUlaw,
209    /// Compressed A-law sample format. Reading and writing samples should happen
210    /// as signed 16-bit integers.
211    CompressedAlaw,
212    /// Compressed IMA ADPCM "ima4" sample format. Reading and writing samples should happen
213    /// as signed 16-bit integers.
214    CompressedIma4,
215    /// Custom: unsupported compression type. The inner value is the the four byte name of
216    /// the compression type. Samples can be read and written only as raw data.
217    Custom([u8; 4])
218}
219
220impl SampleFormat {
221    /// Returns the size of the decoded sample in bytes.
222    /// Custom formats always return 0.
223    pub fn decoded_size(&self) -> usize {
224        match &self {
225            SampleFormat::U8 => 1,
226            SampleFormat::I8 => 1,
227            SampleFormat::I16 | SampleFormat::I16LE => 2,
228            SampleFormat::I24 => 3,
229            SampleFormat::I32 | SampleFormat::I32LE => 4,
230            SampleFormat::F32 => 4,
231            SampleFormat::F64 => 8,
232            SampleFormat::CompressedUlaw => 2,
233            SampleFormat::CompressedAlaw => 2,
234            SampleFormat::CompressedIma4 => 2,
235            SampleFormat::Custom(_) => 0,
236        }
237    }
238
239    /// Returns the size of the sample in the stream in bytes.
240    /// CompressedIma4 returns 0. Custom formats return 1 (they are assumed to write single bytes).
241    #[inline(always)]
242    fn encoded_size(&self) -> u64 {
243        match &self {
244            SampleFormat::U8 => 1,
245            SampleFormat::I8 => 1,
246            SampleFormat::I16 | SampleFormat::I16LE => 2,
247            SampleFormat::I24 => 3,
248            SampleFormat::I32 | SampleFormat::I32LE => 4,
249            SampleFormat::F32 => 4,
250            SampleFormat::F64 => 8,
251            SampleFormat::CompressedUlaw => 1,
252            SampleFormat::CompressedAlaw => 1,
253            SampleFormat::CompressedIma4 => 0,
254            SampleFormat::Custom(_) => 1,
255        }
256    }
257
258    /// Calculates the sample count based on byte length and sample format.
259    fn calculate_sample_len(&self, sample_byte_len: u32) -> Option<u64> {
260        match self {
261            SampleFormat::CompressedIma4 => {
262                // floor down to match macOS Audio Toolbox behavior
263                Some(u64::from(sample_byte_len / 34) * 64)
264            },
265            SampleFormat::Custom(_) => None,
266            _ => {
267                // floor down to match macOS Audio Toolbox behavior
268                Some(u64::from(sample_byte_len) / self.encoded_size())
269            }
270        }
271    }
272
273    /// The maximum channel count for the sample format.
274    fn maximum_channel_count(&self) -> i16 {
275        match self {
276            SampleFormat::CompressedIma4 => 2,
277            _ => i16::MAX
278        }
279    }
280
281    /// Returns the COMM chunk's bits per sample value for the writer.
282    /// Compressed formats and custom formats return 0.
283    fn bits_per_sample(&self) -> u8 {
284        match &self {
285            SampleFormat::U8 => 8,
286            SampleFormat::I8 => 8,
287            SampleFormat::I16 => 16,
288            SampleFormat::I16LE => 16,
289            SampleFormat::I24 => 24,
290            SampleFormat::I32 => 32,
291            SampleFormat::I32LE => 32,
292            SampleFormat::F32 => 32,
293            SampleFormat::F64 => 64,
294            SampleFormat::CompressedUlaw => 0,
295            SampleFormat::CompressedAlaw => 0,
296            SampleFormat::CompressedIma4 => 0,
297            SampleFormat::Custom(_) => 0,
298        }
299    }
300}
301
302/// Checks if the given data is the start of AIFF or AIFF-C.
303///
304/// Only the first 12 bytes are checked. If the data length is less than 12 bytes,
305/// then the result is always None.
306///
307/// # Examples
308///
309/// ```
310/// match aifc::recognize(b"This is not an AIFF or AIFF-C") {
311///     Some(aifc::FileFormat::Aiff) => { println!("It's AIFF"); },
312///     Some(aifc::FileFormat::Aifc) => { println!("It's AIFF-C"); },
313///     None => { println!("Not AIFF or AIFF-C"); },
314/// }
315/// ```
316pub fn recognize(data: &[u8]) -> Option<FileFormat> {
317    if data.len() < 12 ||
318        data[0] != b'F' || data[1] != b'O' || data[2] != b'R' || data[3] != b'M' ||
319        data[8] != b'A' || data[9] != b'I' || data[10] != b'F' {
320        return None;
321    }
322    match data[11] {
323        b'F' => Some(FileFormat::Aiff),
324        b'C' => Some(FileFormat::Aifc),
325        _ => None
326    }
327}
328
329/// CountingWrite counts the bytes written to the underlying Write object.
330struct CountingWrite<W> where W: Write {
331    pub handle: W,
332    pub bytes_written: u64
333}
334
335impl<W: Write> CountingWrite<W> {
336    pub fn new(handle: W) -> CountingWrite<W> {
337        CountingWrite {
338            handle,
339            bytes_written: 0
340        }
341    }
342
343    /// Writes buf to the stream without counting them in bytes_written.
344    fn write_not_counted(&mut self, buf: &[u8]) -> Result<usize, crate::aifcresult::AifcError> {
345        self.handle.write_all(buf)?;
346        Ok(buf.len())
347    }
348}
349
350impl<W: Write> Write for CountingWrite<W> {
351    fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
352        self.handle.write_all(buf)?;
353        self.bytes_written += u64::try_from(buf.len()).map_err(|_| crate::buffer_size_error())?;
354        Ok(buf.len())
355    }
356    fn flush(&mut self) -> Result<(), std::io::Error> {
357        self.handle.flush()
358    }
359}
360
361fn is_even_u32(value: u32) -> bool {
362    value & 1 == 0
363}
364
365fn is_even_u64(value: u64) -> bool {
366    value & 1 == 0
367}
368
369fn is_even_usize(value: usize) -> bool {
370    value & 1 == 0
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_recognize() {
379        assert_eq!(recognize(&[]), None);
380        assert_eq!(recognize(b"FORM"), None);
381        assert_eq!(recognize(b"FORM....AIFX"), None);
382        assert_eq!(recognize(b"form....AIFF"), None);
383        assert_eq!(recognize(b"FORM....aiff"), None);
384        assert_eq!(recognize(b"FORM....AIFF"), Some(FileFormat::Aiff));
385        assert_eq!(recognize(b"FORM....AIFC"), Some(FileFormat::Aifc));
386        assert_eq!(recognize(b"FORM....AIFFCOMM....blahblah.."), Some(FileFormat::Aiff));
387    }
388}