Skip to main content

dsf_meta/
lib.rs

1/*
2 20251014: Changed by clone206 to prevent id3 errors from stopping processing of audio.
3
4*/
5
6//! DSF file utilities.
7//!
8//! A DSF (DSD Stream File) is a high-resolution audio file which
9//! contains uncompressed DSD audio data along with information about
10//! how the audio data is encoded. It can also optionally include an
11//! [`ID3v2`](http://id3.org/) tag which contains metadata about the
12//! music e.g. artist, album, etc.
13//!
14//! # Examples
15//!
16//! This example displays the metadata for the DSF file
17//! `my/music.dsf`.
18//!
19//!```no_run
20//! use dsf_meta::DsfFile;
21//! use std::path::Path;
22//!
23//! let path = Path::new("my/music.dsf");
24//!
25//! match DsfFile::open(path) {
26//!     Ok(dsf_file) => {
27//!         eprintln!("DSF file metadata:\n\n{}", dsf_file);
28//!     }
29//!     Err(error) => {
30//!         eprintln!("Error: {}", error);
31//!     }
32//! }
33//! ```
34//! 
35//! Recovering from ID3 errors example:
36//!```no_run
37//! use dsf_meta::DsfFile;
38//! use std::path::Path;
39//! 
40//! let path = Path::new("my/music.dsf");
41//! 
42//! let dsf_file = DsfFile::open(path).unwrap();
43//! 
44//! if let Some(e) = dsf_file.tag_read_err() {
45//!     eprintln!("[Warning] Full read of ID3 tag failed. Partial read attempted: {}", e);
46//! }
47//! ```
48
49// Get pedantic warnings when linting with `cargo clippy`.
50#![warn(clippy::pedantic)]
51
52mod id3_display;
53
54use crate::id3_display::id3_tag_to_string;
55use id3::Tag;
56use sampled_data_duration::ConstantRateDuration;
57use std::convert::TryFrom;
58use std::fmt;
59use std::fs::File;
60use std::io;
61use std::io::prelude::*;
62use std::io::Read;
63use std::io::SeekFrom;
64use std::path::Path;
65use std::u64;
66
67/// The offset in bytes of the sample data within a DSF file.
68pub const DSF_SAMPLE_DATA_OFFSET: u64 = 92;
69
70/// In memory representation of a DSF file.
71///
72/// The [DSF File Format
73/// Specification](https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf)
74/// divides a DSF file into four chunks:
75///
76/// - [DSD chunk](struct.DsdChunk.html): basic file headers, size and pointers to other chunks.
77/// - [Fmt chunk](struct.FmtChunk.html): information about the audio format e.g. sampling rate, etc.
78/// - [Data chunk](struct.DataChunk.html): the audio samples.
79/// - [Metadata chunk](id3::Tag): an optional `ID3v2` metadata tag.
80///
81/// The fields of the `DsfFile` struct reflect this specification,
82/// with an additional [File] field for the underlying file,
83/// and a [Tag read error](Option<Error>) field to store any error that occurred while reading the ID3 tag.
84pub struct DsfFile {
85    file: File,
86    dsd_chunk: DsdChunk,
87    fmt_chunk: FmtChunk,
88    data_chunk: DataChunk,
89    id3_tag: Option<Tag>,
90    tag_read_err: Option<Error>,
91}
92impl DsfFile {
93    /// Attempt to open and parse the metadata of DSF file in
94    /// read-only mode. Sample data is not read into memory to keep
95    /// the memory footprint small.
96    ///
97    /// # Errors
98    ///
99    /// This function will return an error if `path` does not exist or
100    /// is not a readable and valid DSF file.
101    ///
102    /// # Examples
103    ///
104    ///```no_run
105    /// use dsf_meta::DsfFile;
106    /// use std::path::Path;
107    ///
108    /// let path = Path::new("my/music.dsf");
109    ///
110    /// match DsfFile::open(path) {
111    ///     Ok(dsf_file) => {
112    ///         println!("DSF file metadata:\n\n{}", dsf_file);
113    ///     }
114    ///     Err(error) => {
115    ///         println!("Error: {}", error);
116    ///     }
117    /// }
118    /// ```
119    pub fn open(path: &Path) -> Result<DsfFile, Error> {
120        let mut file = File::open(path)?;
121
122        let mut dsd_chunk_buffer: [u8; 28] = [0; 28];
123        file.read_exact(&mut dsd_chunk_buffer)?;
124        let dsd_chunk = DsdChunk::try_from(dsd_chunk_buffer)?;
125
126        let mut fmt_chunk_buffer: [u8; 52] = [0; 52];
127        file.read_exact(&mut fmt_chunk_buffer)?;
128        let fmt_chunk = FmtChunk::try_from(fmt_chunk_buffer)?;
129
130        let mut data_chunk_buffer: [u8; 12] = [0; 12];
131        file.read_exact(&mut data_chunk_buffer)?;
132        let data_chunk = DataChunk::try_from(data_chunk_buffer)?;
133
134        let mut dsf_file = DsfFile {
135            file: file,
136            dsd_chunk: dsd_chunk,
137            fmt_chunk: fmt_chunk,
138            data_chunk: data_chunk,
139            id3_tag: None,
140            tag_read_err: None
141        };
142
143        dsf_file.id3_tag = if dsf_file.dsd_chunk.metadata_offset == 0 {
144            None
145        } else {
146            match dsf_file.file.seek(SeekFrom::Start(dsf_file.dsd_chunk.metadata_offset)) {
147                Ok(_n) => match Tag::read_from2(&dsf_file.file) {
148                    Ok(tag) => Some(tag),
149                    Err(e) => {
150                        let partial = e.partial_tag.clone();
151                        dsf_file.tag_read_err = Some(Error::from(e));
152                        partial
153                    }
154                },
155                Err(e) => {
156                    dsf_file.tag_read_err = Some(Error::from(e));
157                    None
158                }
159            }
160        };
161
162        Ok(dsf_file)
163    }
164
165    /// Return a reference to the underlying [File]
166    #[must_use]
167    pub fn file(&self) -> &File {
168        &self.file
169    }
170
171    /// Return a reference to the [`DsdChunk`](struct.DsdChunk.html).
172    #[must_use]
173    pub fn dsd_chunk(&self) -> &DsdChunk {
174        &self.dsd_chunk
175    }
176
177    /// Return a reference to the [`FmtChunk`](struct.FmtChunk.html).
178    #[must_use]
179    pub fn fmt_chunk(&self) -> &FmtChunk {
180        &self.fmt_chunk
181    }
182
183    /// Return a reference to the [`DataChunk`](struct.DataChunk.html).
184    #[must_use]
185    pub fn data_chunk(&self) -> &DataChunk {
186        &self.data_chunk
187    }
188
189    /// Return a reference to the optional `ID3v2` [Tag]
190    #[must_use]
191    pub fn id3_tag(&self) -> &Option<Tag> {
192        &self.id3_tag
193    }
194
195    /// Return the error if attempt to read the ID3 tag failed.
196    pub fn tag_read_err(&self) -> Option<&Error> {
197        self.tag_read_err.as_ref() 
198    }
199}
200impl fmt::Display for DsfFile {
201    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
202        let id3_tag_as_string = match &self.id3_tag {
203            Some(tag) => id3_tag_to_string(tag),
204            None => String::from("None"),
205        };
206        write!(
207            f,
208            "DSD chunk:\n{}\n\nFmt chunk:\n{}\n\nData chunk:\n{}\n\nID3Tag:\n{}",
209            self.dsd_chunk, self.fmt_chunk, self.data_chunk, &id3_tag_as_string,
210        )
211    }
212}
213
214/// Return a `u64` which starts from `index` in the specified byte
215/// buffer, interpretting the bytes as little-endian.
216fn u64_from_byte_buffer(buffer: &[u8], index: usize) -> u64 {
217    let mut byte_array: [u8; 8] = [0; 8];
218    byte_array.copy_from_slice(&buffer[index..index + 8]);
219
220    u64::from_le_bytes(byte_array)
221}
222
223/// Return a `u32` which starts from `index` in the specified byte
224/// buffer, interpretting the bytes as little-endian.
225fn u32_from_byte_buffer(buffer: &[u8], index: usize) -> u32 {
226    let mut byte_array: [u8; 4] = [0; 4];
227    byte_array.copy_from_slice(&buffer[index..index + 4]);
228
229    u32::from_le_bytes(byte_array)
230}
231
232/// The first chunk of a DSF file is the
233/// [`DsdChunk`](struct.DsdChunk.html), which must begin with the
234/// following four bytes.
235const DSD_CHUNK_HEADER: [u8; 4] = [b'D', b'S', b'D', b' '];
236
237/// The DSD chunk is the first chunk of a DSF file.
238///
239/// It contains the DSF file size and the offset of the `ID3v2` tag if
240/// one exists.
241pub struct DsdChunk {
242    file_size: u64,
243    metadata_offset: u64,
244}
245impl DsdChunk {
246    /// Make a new `DsdChunk`.
247    fn new(file_size: u64, metadata_offset: u64) -> DsdChunk {
248        DsdChunk {
249            file_size,
250            metadata_offset,
251        }
252    }
253
254    /// Returns the file size in bytes.
255    #[must_use]
256    pub fn file_size(&self) -> u64 {
257        self.file_size
258    }
259}
260impl fmt::Display for DsdChunk {
261    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
262        write!(
263            f,
264            "File size = {} bytes\nMetadata offset = {} bytes",
265            self.file_size, self.metadata_offset
266        )
267    }
268}
269impl TryFrom<[u8; 28]> for DsdChunk {
270    type Error = Error;
271
272    fn try_from(buffer: [u8; 28]) -> Result<Self, Self::Error> {
273        if buffer[0..4] != DSD_CHUNK_HEADER {
274            return Err(Error::DsdChunkHeader);
275        }
276
277        let chunk_size = u64_from_byte_buffer(&buffer, 4);
278        if chunk_size != 28 {
279            return Err(Error::DsdChunkSize);
280        }
281
282        let file_size = u64_from_byte_buffer(&buffer, 12);
283        let metadata_offset = u64_from_byte_buffer(&buffer, 20);
284
285        Ok(DsdChunk::new(file_size, metadata_offset))
286    }
287}
288
289/// The first four bytes of the [`FmtChunk`](struct.FmtChunk.html).
290const FMT_CHUNK_HEADER: [u8; 4] = [b'f', b'm', b't', b' '];
291
292/// The fmt chunk contains information about the audio format.
293///
294/// - Channel type: mono, stereo, 5.1, etc.
295/// - Channel number: 1 for mono, 2 for stereo, etc.
296/// - Sampling frequency: the DSD sampling frequency.
297/// - Bits per sample: whether the samples are big or little endian encoded.
298/// - Block size per channel: this should always be 4096 bytes.
299pub struct FmtChunk {
300    channel_type: ChannelType,
301    channel_num: u32,
302    sampling_frequency: u32,
303    bits_per_sample: u32,
304    sample_count: u64,
305    block_size_per_channel: u32,
306}
307impl FmtChunk {
308    /// Make a new `FmtChunk`.
309    fn new(
310        channel_type: ChannelType,
311        channel_num: u32,
312        sampling_frequency: u32,
313        bits_per_sample: u32,
314        sample_count: u64,
315        block_size_per_channel: u32,
316    ) -> FmtChunk {
317        FmtChunk {
318            channel_type,
319            channel_num,
320            sampling_frequency,
321            bits_per_sample,
322            sample_count,
323            block_size_per_channel,
324        }
325    }
326
327    /// Return a reference to the
328    /// [`ChannelType`](enum.ChannelType.html).
329    #[must_use]
330    pub fn channel_type(&self) -> &ChannelType {
331        &self.channel_type
332    }
333
334    /// Return the number of channels. This should be in the range 1
335    /// to 6.
336    #[must_use]
337    pub fn channel_num(&self) -> u32 {
338        self.channel_num
339    }
340
341    /// Return the sampling freqency. DSD sampling frequencies are
342    /// much higher than PCM because of the 1-bit sampling, so you
343    /// should get values like:
344    ///
345    /// -  2822400 Hz for DSD64
346    /// -  5644800 Hz for DSD128
347    /// - 11289600 Hz for DSD256
348    ///
349    /// and so on.
350    #[must_use]
351    pub fn sampling_frequency(&self) -> u32 {
352        self.sampling_frequency
353    }
354
355    /// Returns the `bits_per_sample` field. This is a bit of a
356    /// misnomer in my opinion, but that’s what’s in the DSF
357    /// specification. If it is equal to 1 then the sample data is
358    /// stored least significant bit first. If it is equal to 8 then
359    /// the sample data is stored most significant bit first.
360    // TODO: Consider creating an endian-ness field instead to replace
361    // this field
362    #[must_use]
363    pub fn bits_per_sample(&self) -> u32 {
364        self.bits_per_sample
365    }
366
367    /// Return the number of DSD samples per channel.
368    #[must_use]
369    pub fn sample_count(&self) -> u64 {
370        self.sample_count
371    }
372
373    /// Return the block size per channel in bytes. This is fixed and
374    /// should always be 4096 bytes.
375    #[must_use]
376    pub fn block_size_per_channel(&self) -> u32 {
377        self.block_size_per_channel
378    }
379
380    /// Return the duration of the audio.
381    fn duration(&self) -> ConstantRateDuration {
382        ConstantRateDuration::new(self.sample_count, u64::from(self.sampling_frequency))
383    }
384}
385impl fmt::Display for FmtChunk {
386    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
387        write!(
388            f,
389            "Channel type = {}
390Channel number = {}
391Sampling frequency = {} Hz
392Bits per sample = {}
393Sample count per channel = {}
394Block size per channel = {} bytes
395Calculated duration = {} h:min:s;samples",
396            self.channel_type,
397            self.channel_num,
398            self.sampling_frequency,
399            self.bits_per_sample,
400            self.sample_count,
401            self.block_size_per_channel,
402            self.duration()
403        )
404    }
405}
406impl TryFrom<[u8; 52]> for FmtChunk {
407    type Error = Error;
408
409    fn try_from(buffer: [u8; 52]) -> Result<Self, Self::Error> {
410        if buffer[0..4] != FMT_CHUNK_HEADER {
411            return Err(Error::FmtChunkHeader);
412        }
413
414        let chunk_size = u64_from_byte_buffer(&buffer, 4);
415        if chunk_size != 52 {
416            return Err(Error::FmtChunkSize);
417        }
418
419        let format_version = u32_from_byte_buffer(&buffer, 12);
420        if format_version != 1 {
421            return Err(Error::FormatVersion);
422        }
423
424        let format_id = u32_from_byte_buffer(&buffer, 16);
425        if format_id != 0 {
426            return Err(Error::FormatId);
427        }
428
429        let channel_type = ChannelType::try_from(u32_from_byte_buffer(&buffer, 20))?;
430
431        let channel_num = u32_from_byte_buffer(&buffer, 24);
432        match channel_num {
433            1 | 2 | 3 | 4 | 5 | 6 => (),
434            _ => return Err(Error::ChannelNum),
435        }
436
437        let sampling_frequency = u32_from_byte_buffer(&buffer, 28);
438        let bits_per_sample = u32_from_byte_buffer(&buffer, 32);
439        let sample_count = u64_from_byte_buffer(&buffer, 36);
440
441        let block_size_per_channel = u32_from_byte_buffer(&buffer, 44);
442        if block_size_per_channel != BLOCK_SIZE_AS_U32 {
443            return Err(Error::BlockSizePerChannelNonStandard);
444        }
445
446        let reserved = u32_from_byte_buffer(&buffer, 48);
447        if reserved != 0 {
448            return Err(Error::ReservedNotZero);
449        }
450
451        Ok(FmtChunk::new(
452            channel_type,
453            channel_num,
454            sampling_frequency,
455            bits_per_sample,
456            sample_count,
457            block_size_per_channel,
458        ))
459    }
460}
461
462/// The different channel formats possible for a DSF file.
463///
464/// The channel specification is as follows:
465///
466/// <table style="empty-cells: hide;">
467/// <tr><td></td><th style="text-align: center;" colspan="6">Channel Index</th></tr>
468/// <tr><th>Channel Type</th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th></tr>
469/// <tr><th>Mono</th>
470///   <td>Center</td>
471///   <td></td><td></td><td></td><td></td><td></td>
472/// </tr>
473/// <tr><th>Stereo</th>
474///   <td>Front Left</td><td>Front Right</td>
475///   <td></td><td></td><td></td><td></td>
476/// </tr>
477/// <tr><th>3-Channels</th>
478///   <td>Front Left</td><td>Front Right</td><td>Center</td>
479///   <td></td><td></td><td></td>
480/// </tr>
481/// <tr><th>Quad</th>
482///   <td>Front Left</td><td>Front Right</td><td>Back Left</td>
483///   <td>Back Right</td><td></td><td></td>
484/// </tr>
485/// <tr><th>4-Channels</th>
486///   <td>Front Left</td><td>Front Right</td><td>Center</td>
487///   <td>Low Frequency</td><td></td><td></td>
488/// </tr>
489/// <tr><th>5-Channels</th>
490///   <td>Front Left</td><td>Front Right</td><td>Center</td>
491///   <td>Back Left</td><td>Back Right</td><td></td>
492/// </tr>
493/// <tr><th>5.1-Channels</th>
494///   <td>Front Left</td><td>Front Right</td><td>Center</td>
495///   <td>Low Frequency</td><td>Back Left</td><td>Back Right</td>
496/// </tr>
497/// </table>
498#[derive(Debug, Eq, PartialEq)]
499pub enum ChannelType {
500    Mono,
501    Stereo,
502    ThreeChannels,
503    Quad,
504    FourChannels,
505    FiveChannels,
506    FivePointOneChannels,
507}
508impl fmt::Display for ChannelType {
509    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
510        let channel_type_as_str = match self {
511            ChannelType::Mono => "Mono",
512            ChannelType::Stereo => "Stereo: FL, FR.",
513            ChannelType::ThreeChannels => "3 channels: FL, FR, C.",
514            ChannelType::Quad => "Quad: FL, FR, BL, BR.",
515            ChannelType::FourChannels => "4 channels: FL, FR, C, LFE.",
516            ChannelType::FiveChannels => "5 channels: FL, FR, C, BL, BR.",
517            ChannelType::FivePointOneChannels => "5.1 channels: FL, FR, C, LFE, BL, BR.",
518        };
519
520        write!(f, "{}", channel_type_as_str)
521    }
522}
523impl TryFrom<u32> for ChannelType {
524    type Error = Error;
525
526    fn try_from(channel_type_as_u32: u32) -> Result<Self, Self::Error> {
527        match channel_type_as_u32 {
528            1 => Ok(ChannelType::Mono),
529            2 => Ok(ChannelType::Stereo),
530            3 => Ok(ChannelType::ThreeChannels),
531            4 => Ok(ChannelType::Quad),
532            5 => Ok(ChannelType::FourChannels),
533            6 => Ok(ChannelType::FiveChannels),
534            7 => Ok(ChannelType::FivePointOneChannels),
535            _ => Err(Error::ChannelType),
536        }
537    }
538}
539
540/// First four bytes of the [`DataChunk`](struct.DataChunk.html).
541const DATA_CHUNK_HEADER: [u8; 4] = [b'd', b'a', b't', b'a'];
542
543/// The data chunk contains the DSD sample data.
544pub struct DataChunk {
545    chunk_size: u64,
546}
547impl DataChunk {
548    /// Make a new `DataChunk`.
549    fn new(chunk_size: u64) -> DataChunk {
550        DataChunk { chunk_size }
551    }
552
553    /// The size of the data chunk in bytes. This is equal to the
554    /// sample data + 12 bytes. The extra 12 bytes are taken up by the
555    /// data chunk header and this size field.
556    #[must_use]
557    pub fn chunk_size(&self) -> u64 {
558        self.chunk_size
559    }
560}
561impl fmt::Display for DataChunk {
562    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
563        write!(f, "Data chunk size = {} bytes", self.chunk_size)
564    }
565}
566impl TryFrom<[u8; 12]> for DataChunk {
567    type Error = Error;
568
569    fn try_from(buffer: [u8; 12]) -> Result<Self, Self::Error> {
570        if buffer[0..4] != DATA_CHUNK_HEADER {
571            return Err(Error::DataChunkHeader);
572        }
573
574        let chunk_size = u64_from_byte_buffer(&buffer, 4);
575
576        Ok(DataChunk::new(chunk_size))
577    }
578}
579
580/// The block size is always 4096 bytes.
581const BLOCK_SIZE: usize = 4096;
582/// We define another version of the block size as a `u32` because
583/// when we try to simply cast a `usize` to a `u32` we get the
584/// following pedantic clippy warning: *casting `usize` to `u32` may
585/// truncate the value on targets with 64-bit wide
586/// pointers*. Obviously, this is a false-positive because we are
587/// dealing with a constant which is well within the range of a `u32`,
588/// see clippy [issue
589/// 7486](https://github.com/rust-lang/rust-clippy/issues/8316#issue-1109140641). However,
590/// in the interests of getting a perfect report from clippy we add
591/// the following constant:
592const BLOCK_SIZE_AS_U32: u32 = 4096;
593
594/// Errors provided by the dsf crate.
595#[derive(Debug)]
596pub enum Error {
597    BlockSizePerChannelNonStandard,
598    ChannelNum,
599    ChannelType,
600    DataChunkHeader,
601    DsdChunkHeader,
602    DsdChunkSize,
603    FmtChunkHeader,
604    FmtChunkSize,
605    FormatId,
606    FormatVersion,
607    Id3Error(id3::Error),
608    IoError(io::Error),
609    ReservedNotZero,
610    ChannelIndexOutOfRange,
611    SampleIndexOutOfRange,
612    FrameIndexOutOfRange,
613}
614impl fmt::Display for Error {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        match self {
617            Error::BlockSizePerChannelNonStandard => write!(
618                f,
619                "A fmt chunk is expected to specify its block size per channel as {}.",
620                BLOCK_SIZE
621            ),
622            Error::ChannelNum => {
623                f.write_str("A fmt chunk’s channel num is expected to be in the range 1–6.")
624            }
625            Error::ChannelType => {
626                f.write_str("A fmt chunk’s channel type is expected to be in the range 1–7.")
627            }
628            Error::DataChunkHeader => f.write_str("A data chunk must start with the bytes 'data'."),
629            Error::DsdChunkHeader => f.write_str("A DSD chunk must start with the bytes 'DSD '."),
630            Error::DsdChunkSize => f.write_str("A DSD chunk must specify its size as 28 bytes."),
631            Error::FmtChunkHeader => f.write_str("A fmt chunk must start with the bytes 'fmt '."),
632            Error::FmtChunkSize => {
633                f.write_str("A fmt chunk is expected to specify its size as 52 bytes.")
634            }
635            Error::FormatId => f.write_str("A fmt chumk must specifiy a format ID of 0."),
636            Error::FormatVersion => f.write_str("A fmt chunk must specify version 1."),
637            Error::Id3Error(id3_error) => write!(f, "Id3 error: {}", id3_error),
638            Error::IoError(io_error) => write!(f, "IO error: {}", io_error),
639            Error::ReservedNotZero => {
640                f.write_str("A fmt chunk’s reserved space is expected to be zero filled.")
641            }
642            Error::ChannelIndexOutOfRange => f.write_str("Channel index is out of range."),
643            Error::SampleIndexOutOfRange => f.write_str("Sample index is out of range."),
644            Error::FrameIndexOutOfRange => f.write_str("Frame index is out of range."),
645        }
646    }
647}
648impl std::error::Error for Error {
649    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
650        match self {
651            Error::Id3Error(id3_error) => Some(id3_error),
652            Error::IoError(io_error) => Some(io_error),
653            _ => None,
654        }
655    }
656}
657impl From<id3::Error> for Error {
658    fn from(error: id3::Error) -> Self {
659        Error::Id3Error(error)
660    }
661}
662impl From<io::Error> for Error {
663    fn from(error: io::Error) -> Self {
664        Error::IoError(error)
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use std::process::Command;
672
673    fn get_sweep_dsf_file() -> Result<DsfFile, Error> {
674        let sweep_filename = "sweep-176400hz-0-22050hz-20s-D64-2.8mhz.dsf";
675        let path = Path::new(sweep_filename);
676
677        if !path.is_file() {
678            let sweep_url =
679                "http://samplerateconverter.com/free-files/samples/dsf/sweep-176400hz-0-22050hz-20s-D64-2.8mhz.zip";
680
681            Command::new("wget")
682                .arg(sweep_url)
683                .status()
684                .unwrap_or_else(|_| panic!("Failed to download {}", sweep_url));
685
686            let sweep_zip_filename = "sweep-176400hz-0-22050hz-20s-D64-2.8mhz.zip";
687            Command::new("unzip")
688                .arg(sweep_zip_filename)
689                .status()
690                .unwrap_or_else(|_| panic!("Failed to unzip {}", sweep_zip_filename));
691        }
692
693        DsfFile::open(path)
694    }
695
696    #[test]
697    fn sweep_file() {
698        let dsf_file = get_sweep_dsf_file().unwrap();
699
700        assert_eq!(dsf_file.dsd_chunk.file_size, 14_114_908);
701        assert_eq!(dsf_file.dsd_chunk.metadata_offset, 0);
702
703        assert_eq!(dsf_file.fmt_chunk.channel_type, ChannelType::Stereo);
704        assert_eq!(dsf_file.fmt_chunk.channel_num, 2);
705        assert_eq!(dsf_file.fmt_chunk.sampling_frequency, 2_822_400);
706        assert_eq!(dsf_file.fmt_chunk.bits_per_sample, 1);
707        assert_eq!(dsf_file.fmt_chunk.sample_count, 56_459_264);
708        assert_eq!(dsf_file.fmt_chunk.block_size_per_channel, 4096);
709        // TODO: dsf_file.fmt_chunk.duration test
710
711        assert_eq!(dsf_file.data_chunk.chunk_size, 14_114_828);
712
713        // TODO: sample data test
714    }
715}