Skip to main content

dsdiff/
lib.rs

1//! Parse the header of DSDIFF (`.dff`) files: sample rate, channel count and where the audio is.
2//!
3//! Supported audio is uncompressed DSD, stored interleaved one byte per channel with the
4//! most significant bit first in time. DST-compressed files give [`Error::Compressed`].
5#![forbid(unsafe_code)]
6#![warn(missing_docs, rust_2018_idioms)]
7
8use std::io::{self, Read, Seek, SeekFrom};
9
10const CHUNK_HEADER_LEN: u64 = 12;
11const MAX_CHANNELS: u32 = 64;
12
13/// Errors produced by this crate.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17    /// Underlying I/O failure.
18    #[error("I/O error: {0}")]
19    Io(#[from] io::Error),
20    /// The file does not start with a DSDIFF `FRM8` header.
21    #[error("not a DSDIFF file")]
22    NotDsdiff,
23    /// The file violates the DSDIFF layout.
24    #[error("malformed DSDIFF: {0}")]
25    Malformed(&'static str),
26    /// The audio is DST-compressed, which is not supported.
27    #[error("DST-compressed DSDIFF is not supported")]
28    Compressed,
29    /// A required chunk is missing.
30    #[error("DSDIFF file has no {0}")]
31    Missing(&'static str),
32}
33
34/// Crate-wide result alias.
35pub type Result<T> = std::result::Result<T, Error>;
36
37/// Facts about the audio in a DSDIFF file.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Info {
40    /// Sampling frequency in Hz (for example 2_822_400).
41    pub sample_rate: u32,
42    /// Number of channels.
43    pub channel_count: u32,
44    /// Samples per channel.
45    pub sample_count: u64,
46    /// File offset of the first audio byte.
47    pub audio_offset: u64,
48    /// Audio size in bytes (all channels).
49    pub audio_len: u64,
50}
51
52struct Chunk {
53    id: [u8; 4],
54    start: u64,
55    len: u64,
56}
57
58impl Chunk {
59    fn end(&self) -> u64 {
60        self.start + self.len
61    }
62    /// Chunks are padded to an even length.
63    fn next(&self) -> u64 {
64        self.end() + (self.len & 1)
65    }
66}
67
68fn fill<R: Read, const N: usize>(r: &mut R) -> Result<[u8; N]> {
69    let mut buf = [0u8; N];
70    r.read_exact(&mut buf).map_err(|e| match e.kind() {
71        io::ErrorKind::UnexpectedEof => Error::Malformed("truncated file"),
72        _ => Error::Io(e),
73    })?;
74    Ok(buf)
75}
76
77fn u16_be<R: Read>(r: &mut R) -> Result<u16> {
78    Ok(u16::from_be_bytes(fill(r)?))
79}
80fn u32_be<R: Read>(r: &mut R) -> Result<u32> {
81    Ok(u32::from_be_bytes(fill(r)?))
82}
83fn u64_be<R: Read>(r: &mut R) -> Result<u64> {
84    Ok(u64::from_be_bytes(fill(r)?))
85}
86
87/// Reads the chunk header at the current position, or `None` if no room is left before `limit`.
88fn chunk_header<R: Read + Seek>(r: &mut R, limit: u64) -> Result<Option<Chunk>> {
89    let pos = r.stream_position()?;
90    if limit.saturating_sub(pos) < CHUNK_HEADER_LEN {
91        return Ok(None);
92    }
93    let id = fill::<_, 4>(r)?;
94    let len = u64_be(r)?;
95    let start = pos + CHUNK_HEADER_LEN;
96    let end = start
97        .checked_add(len)
98        .ok_or(Error::Malformed("chunk size overflows"))?;
99    if end > limit {
100        return Err(Error::Malformed(
101            "chunk extends past the end of its container",
102        ));
103    }
104    Ok(Some(Chunk { id, start, len }))
105}
106
107fn need(chunk: &Chunk, bytes: u64) -> Result<()> {
108    if chunk.len < bytes {
109        Err(Error::Malformed("chunk is too short"))
110    } else {
111        Ok(())
112    }
113}
114
115fn parse_prop<R: Read + Seek>(
116    r: &mut R,
117    prop: &Chunk,
118    rate: &mut Option<u32>,
119    channels: &mut Option<u32>,
120) -> Result<()> {
121    r.seek(SeekFrom::Start(prop.start))?;
122    need(prop, 4)?;
123    if fill::<_, 4>(r)? != *b"SND " {
124        return Ok(());
125    }
126    while let Some(chunk) = chunk_header(r, prop.end())? {
127        match &chunk.id {
128            b"FS  " => {
129                need(&chunk, 4)?;
130                *rate = Some(u32_be(r)?);
131            }
132            b"CHNL" => {
133                need(&chunk, 2)?;
134                *channels = Some(u32::from(u16_be(r)?));
135            }
136            b"CMPR" => {
137                need(&chunk, 4)?;
138                if fill::<_, 4>(r)? != *b"DSD " {
139                    return Err(Error::Compressed);
140                }
141            }
142            _ => {}
143        }
144        r.seek(SeekFrom::Start(chunk.next()))?;
145    }
146    Ok(())
147}
148
149/// Parses the header of a DSDIFF file.
150pub fn read_info<R: Read + Seek>(r: &mut R) -> Result<Info> {
151    let file_len = r.seek(SeekFrom::End(0))?;
152    r.seek(SeekFrom::Start(0))?;
153    if fill::<_, 4>(r).map_err(|_| Error::NotDsdiff)? != *b"FRM8" {
154        return Err(Error::NotDsdiff);
155    }
156    let _form_len = u64_be(r)?; // often unreliable, so the real file length is used instead
157    if fill::<_, 4>(r)? != *b"DSD " {
158        return Err(Error::NotDsdiff);
159    }
160
161    let (mut rate, mut channels, mut audio) = (None, None, None);
162    while let Some(chunk) = chunk_header(r, file_len)? {
163        match &chunk.id {
164            b"PROP" => parse_prop(r, &chunk, &mut rate, &mut channels)?,
165            b"DSD " => {
166                audio = Some((chunk.start, chunk.len));
167                break;
168            }
169            b"DST " => return Err(Error::Compressed),
170            _ => {}
171        }
172        r.seek(SeekFrom::Start(chunk.next()))?;
173    }
174
175    let sample_rate = rate.ok_or(Error::Missing("sampling frequency (FS chunk)"))?;
176    let channel_count = channels.ok_or(Error::Missing("channel layout (CHNL chunk)"))?;
177    let (audio_offset, audio_len) = audio.ok_or(Error::Missing("DSD sound data chunk"))?;
178    if sample_rate == 0 || channel_count == 0 || channel_count > MAX_CHANNELS {
179        return Err(Error::Malformed("invalid sample rate or channel count"));
180    }
181    let per_channel = audio_len / u64::from(channel_count);
182    if per_channel * u64::from(channel_count) != audio_len {
183        return Err(Error::Malformed(
184            "audio size is not a multiple of the channel count",
185        ));
186    }
187    Ok(Info {
188        sample_rate,
189        channel_count,
190        sample_count: per_channel.saturating_mul(8),
191        audio_offset,
192        audio_len,
193    })
194}
195
196#[cfg(test)]
197mod tests {
198    use std::io::Cursor;
199
200    use super::*;
201
202    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
203
204    fn chunk(id: &[u8; 4], data: &[u8]) -> Vec<u8> {
205        let mut v = id.to_vec();
206        v.extend((data.len() as u64).to_be_bytes());
207        v.extend(data);
208        if data.len() % 2 == 1 {
209            v.push(0);
210        }
211        v
212    }
213
214    fn file(cmpr: &[u8; 4], audio: &[u8]) -> Vec<u8> {
215        let mut chnl = 2u16.to_be_bytes().to_vec();
216        chnl.extend(b"SLFTSRGT");
217        let mut cmpr_data = cmpr.to_vec();
218        cmpr_data.push(14);
219        cmpr_data.extend(b"not compressed");
220
221        let mut prop = b"SND ".to_vec();
222        prop.extend(chunk(b"FS  ", &2_822_400u32.to_be_bytes()));
223        prop.extend(chunk(b"CHNL", &chnl));
224        prop.extend(chunk(b"CMPR", &cmpr_data));
225
226        let mut body = b"DSD ".to_vec();
227        body.extend(chunk(b"FVER", &[1, 5, 0, 0]));
228        body.extend(chunk(b"PROP", &prop));
229        body.extend(chunk(b"DSD ", audio));
230
231        let mut out = b"FRM8".to_vec();
232        out.extend((body.len() as u64).to_be_bytes());
233        out.extend(body);
234        out
235    }
236
237    #[test]
238    fn reads_format_and_audio_location() -> TestResult {
239        let audio = [1u8, 2, 3, 4, 5, 6, 7, 8];
240        let bytes = file(b"DSD ", &audio);
241        let info = read_info(&mut Cursor::new(&bytes))?;
242        assert_eq!(info.sample_rate, 2_822_400);
243        assert_eq!(info.channel_count, 2);
244        assert_eq!(info.sample_count, 32);
245        let from = usize::try_from(info.audio_offset)?;
246        assert_eq!(bytes.get(from..from + 8), Some(&audio[..]));
247        Ok(())
248    }
249
250    #[test]
251    fn rejects_compressed_foreign_and_truncated() {
252        let dst = file(b"DST ", &[0; 8]);
253        assert!(matches!(
254            read_info(&mut Cursor::new(dst)),
255            Err(Error::Compressed)
256        ));
257        assert!(matches!(
258            read_info(&mut Cursor::new(b"RIFF....".to_vec())),
259            Err(Error::NotDsdiff)
260        ));
261        let mut cut = file(b"DSD ", &[0; 8]);
262        cut.truncate(cut.len() - 3);
263        assert!(matches!(
264            read_info(&mut Cursor::new(cut)),
265            Err(Error::Malformed(_))
266        ));
267    }
268
269    #[test]
270    fn every_prefix_and_byte_flip_returns_without_panicking() {
271        let bytes = file(b"DSD ", &[0x55; 16]);
272        for len in 0..bytes.len() {
273            let prefix = bytes.get(..len).unwrap_or_default().to_vec();
274            let _ = read_info(&mut Cursor::new(prefix));
275        }
276        for index in 0..bytes.len() {
277            for flip in [0x01u8, 0x80, 0xFF] {
278                let mut copy = bytes.clone();
279                if let Some(byte) = copy.get_mut(index) {
280                    *byte ^= flip;
281                }
282                if let Ok(info) = read_info(&mut Cursor::new(copy.clone())) {
283                    assert!(info.channel_count > 0);
284                    assert!(info.audio_offset + info.audio_len <= copy.len() as u64);
285                }
286            }
287        }
288    }
289}