dsd_source/lib.rs
1//! A trait abstraction over DSD audio sources, plus the small set of shared
2//! DSD model types (bit endianness, channel layout, rate multiplier) used to
3//! describe them.
4//!
5//! Container/metadata crates (e.g. `dsf-meta`, `dff-meta`, or a WavPack
6//! equivalent) implement [`DsdSource`] to describe the *native* shape of the
7//! DSD audio they expose ([`DsdSourceInfo`]) and to hand back a byte stream
8//! of that native-shape audio data.
9//!
10//! Readers (e.g. `dsd-reader`) depend on this crate instead of on any
11//! specific container format, and are responsible for reshaping the native
12//! byte stream into whatever output shape a caller asks for (planar vs.
13//! interleaved, LSB- vs. MSB-first, output block size). This crate does not
14//! perform that reshaping itself; it only describes the source.
15
16use std::convert::TryFrom;
17use std::io::Read;
18
19/// Error type returned by [`DsdSource`] methods.
20///
21/// A boxed, `Send + Sync` trait object is used so that implementers can
22/// wrap their own error types (`std::io::Error`, or a crate-specific error
23/// enum) without this crate needing to know about them.
24pub type DsdSourceError = Box<dyn std::error::Error + Send + Sync + 'static>;
25
26/// The DSD64 native sample rate, in Hz. All other [`DsdRate`] variants are
27/// integer multiples of this base rate.
28pub const DSD_64_RATE: u32 = 2_822_400;
29
30/// DSD bit endianness.
31#[derive(Copy, Clone, PartialEq, Debug)]
32pub enum Endianness {
33 LsbFirst,
34 MsbFirst,
35}
36
37/// DSD channel layout.
38#[derive(Copy, Clone, PartialEq, Debug)]
39pub enum FmtType {
40 /// Block per channel.
41 Planar,
42 /// Byte per channel.
43 Interleaved,
44}
45
46/// DSD rate multiplier, relative to the DSD64 base rate ([`DSD_64_RATE`]).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum DsdRate {
49 #[default]
50 DSD64 = 1,
51 DSD128 = 2,
52 DSD256 = 4,
53 DSD512 = 8,
54}
55
56impl TryFrom<u32> for DsdRate {
57 type Error = &'static str;
58 fn try_from(v: u32) -> Result<Self, Self::Error> {
59 match v {
60 1 => Ok(DsdRate::DSD64),
61 2 => Ok(DsdRate::DSD128),
62 4 => Ok(DsdRate::DSD256),
63 8 => Ok(DsdRate::DSD512),
64 _ => Err("Invalid DSD rate multiplier (expected 1,2,4,8)"),
65 }
66 }
67}
68
69/// Describes the native shape of a [`DsdSource`]'s audio data.
70///
71/// Fields that only make sense for a known container format (channel count,
72/// bit endianness, layout, block size, sample rate) are `Option`, so that
73/// sources with no container metadata (e.g. a raw DSD file) can report
74/// `None` for what they don't know, rather than needing to invent a value.
75#[derive(Clone)]
76pub struct DsdSourceInfo {
77 pub channels: Option<usize>,
78 pub endianness: Option<Endianness>,
79 pub layout: Option<FmtType>,
80 /// Native (or suggested) block size in bytes per channel used when
81 /// reading frames of audio data.
82 pub block_size: Option<u32>,
83 /// Native sample rate of the audio data, in Hz.
84 pub sample_rate: Option<u32>,
85 /// Total length of the audio data, in bytes, across all channels.
86 pub audio_length: u64,
87 /// Byte offset within the container where [`DsdSource::reader`] begins
88 /// yielding data. Informational only (`reader()` already accounts for
89 /// it); useful for callers that want to sanity-check container-reported
90 /// lengths against the underlying file size.
91 pub data_offset: u64,
92 /// ID3 tag associated with this source, if any.
93 pub tag: Option<id3::Tag>,
94}
95
96impl std::fmt::Debug for DsdSourceInfo {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("DsdSourceInfo")
99 .field("channels", &self.channels)
100 .field("endianness", &self.endianness)
101 .field("layout", &self.layout)
102 .field("block_size", &self.block_size)
103 .field("sample_rate", &self.sample_rate)
104 .field("audio_length", &self.audio_length)
105 .field("data_offset", &self.data_offset)
106 .field("tag", &self.tag.is_some())
107 .finish()
108 }
109}
110
111/// A source of DSD audio data with a known native shape.
112///
113/// Implementers describe how their audio is laid out (see [`DsdSourceInfo`])
114/// and provide a [`reader`](DsdSource::reader) that yields that data as raw
115/// bytes, starting from the first byte of audio data. Implementers are not
116/// responsible for converting between planar/interleaved layouts or bit
117/// endianness; that is the responsibility of the reader consuming this trait.
118pub trait DsdSource: Send {
119 /// Describes the native shape of this source's audio data. Fallible
120 /// since some containers only discover certain properties (e.g. channel
121 /// count, sample rate) while parsing, and may not have them available
122 /// even after a successful open.
123 fn info(&self) -> Result<DsdSourceInfo, DsdSourceError>;
124
125 /// Returns a fresh, independent byte stream of this source's native-shape
126 /// audio data, positioned at the first byte of audio data.
127 fn reader(&self) -> Result<Box<dyn Read + Send>, DsdSourceError>;
128
129 // The getters below are conveniences over `info()`, so callers don't
130 // need to call `info()` and destructure it themselves for every field.
131 // They return `None` both when `info()` errors and when the field
132 // itself is `None`, since callers generally treat those two cases the
133 // same way (fall back to some default).
134
135 fn channels(&self) -> Option<usize> {
136 self.info().ok()?.channels
137 }
138 fn endianness(&self) -> Option<Endianness> {
139 self.info().ok()?.endianness
140 }
141 fn layout(&self) -> Option<FmtType> {
142 self.info().ok()?.layout
143 }
144 fn block_size(&self) -> Option<u32> {
145 self.info().ok()?.block_size
146 }
147 fn sample_rate(&self) -> Option<u32> {
148 self.info().ok()?.sample_rate
149 }
150 fn audio_length(&self) -> Option<u64> {
151 self.info().ok().map(|i| i.audio_length)
152 }
153 fn data_offset(&self) -> Option<u64> {
154 self.info().ok().map(|i| i.data_offset)
155 }
156 fn tag(&self) -> Option<id3::Tag> {
157 self.info().ok()?.tag
158 }
159
160 /// Total size of the underlying file, in bytes. Distinct from
161 /// `audio_length` (which describes only the audio data): callers use
162 /// this to sanity-check a source's reported `audio_length` against what
163 /// the file can actually contain, without needing to re-stat the path
164 /// themselves.
165 fn file_len(&self) -> Result<u64, DsdSourceError>;
166}
167
168/// File extensions (lowercase, no leading dot) recognized as this source's
169/// container format. Kept separate from [`DsdSource`] since associated
170/// consts aren't dyn-compatible: implementers declare this alongside
171/// `DsdSource`, and callers reference it on the concrete type (e.g.
172/// `DsfFile::EXTENSIONS`) to build a format dispatch table without a reader
173/// needing to hardcode each format's extensions itself.
174pub trait DsdSourceExtensions {
175 const EXTENSIONS: &'static [&'static str];
176}