dsd-source 0.2.0

A trait abstraction over DSD audio sources (DSF, DFF, WavPack, etc.) for use with dsd-reader and its container metadata crates.
Documentation
//! A trait abstraction over DSD audio sources, plus the small set of shared
//! DSD model types (bit endianness, channel layout, rate multiplier) used to
//! describe them.
//!
//! Container/metadata crates (e.g. `dsf-meta`, `dff-meta`, or a WavPack
//! equivalent) implement [`DsdSource`] to describe the *native* shape of the
//! DSD audio they expose ([`DsdSourceInfo`]) and to hand back a byte stream
//! of that native-shape audio data.
//!
//! Readers (e.g. `dsd-reader`) depend on this crate instead of on any
//! specific container format, and are responsible for reshaping the native
//! byte stream into whatever output shape a caller asks for (planar vs.
//! interleaved, LSB- vs. MSB-first, output block size). This crate does not
//! perform that reshaping itself; it only describes the source.

use std::convert::TryFrom;
use std::io::Read;

/// Error type returned by [`DsdSource`] methods.
///
/// A boxed, `Send + Sync` trait object is used so that implementers can
/// wrap their own error types (`std::io::Error`, or a crate-specific error
/// enum) without this crate needing to know about them.
pub type DsdSourceError = Box<dyn std::error::Error + Send + Sync + 'static>;

/// The DSD64 native sample rate, in Hz. All other [`DsdRate`] variants are
/// integer multiples of this base rate.
pub const DSD_64_RATE: u32 = 2_822_400;

/// DSD bit endianness.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Endianness {
    LsbFirst,
    MsbFirst,
}

/// DSD channel layout.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum FmtType {
    /// Block per channel.
    Planar,
    /// Byte per channel.
    Interleaved,
}

/// DSD rate multiplier, relative to the DSD64 base rate ([`DSD_64_RATE`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DsdRate {
    #[default]
    DSD64 = 1,
    DSD128 = 2,
    DSD256 = 4,
    DSD512 = 8,
}

impl TryFrom<u32> for DsdRate {
    type Error = &'static str;
    fn try_from(v: u32) -> Result<Self, Self::Error> {
        match v {
            1 => Ok(DsdRate::DSD64),
            2 => Ok(DsdRate::DSD128),
            4 => Ok(DsdRate::DSD256),
            8 => Ok(DsdRate::DSD512),
            _ => Err("Invalid DSD rate multiplier (expected 1,2,4,8)"),
        }
    }
}

/// Describes the native shape of a [`DsdSource`]'s audio data.
///
/// Fields that only make sense for a known container format (channel count,
/// bit endianness, layout, block size, sample rate) are `Option`, so that
/// sources with no container metadata (e.g. a raw DSD file) can report
/// `None` for what they don't know, rather than needing to invent a value.
#[derive(Clone)]
pub struct DsdSourceInfo {
    pub channels: Option<usize>,
    pub endianness: Option<Endianness>,
    pub layout: Option<FmtType>,
    /// Native (or suggested) block size in bytes per channel used when
    /// reading frames of audio data.
    pub block_size: Option<u32>,
    /// Native sample rate of the audio data, in Hz.
    pub sample_rate: Option<u32>,
    /// Total length of the audio data, in bytes, across all channels.
    pub audio_length: u64,
    /// Byte offset within the container where [`DsdSource::reader`] begins
    /// yielding data. Informational only (`reader()` already accounts for
    /// it); useful for callers that want to sanity-check container-reported
    /// lengths against the underlying file size.
    pub data_offset: u64,
    /// ID3 tag associated with this source, if any.
    pub tag: Option<id3::Tag>,
}

impl std::fmt::Debug for DsdSourceInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DsdSourceInfo")
            .field("channels", &self.channels)
            .field("endianness", &self.endianness)
            .field("layout", &self.layout)
            .field("block_size", &self.block_size)
            .field("sample_rate", &self.sample_rate)
            .field("audio_length", &self.audio_length)
            .field("data_offset", &self.data_offset)
            .field("tag", &self.tag.is_some())
            .finish()
    }
}

/// A source of DSD audio data with a known native shape.
///
/// Implementers describe how their audio is laid out (see [`DsdSourceInfo`])
/// and provide a [`reader`](DsdSource::reader) that yields that data as raw
/// bytes, starting from the first byte of audio data. Implementers are not
/// responsible for converting between planar/interleaved layouts or bit
/// endianness; that is the responsibility of the reader consuming this trait.
pub trait DsdSource: Send {
    /// Describes the native shape of this source's audio data. Fallible
    /// since some containers only discover certain properties (e.g. channel
    /// count, sample rate) while parsing, and may not have them available
    /// even after a successful open.
    fn info(&self) -> Result<DsdSourceInfo, DsdSourceError>;

    /// Returns a fresh, independent byte stream of this source's native-shape
    /// audio data, positioned at the first byte of audio data.
    fn reader(&self) -> Result<Box<dyn Read + Send>, DsdSourceError>;

    // The getters below are conveniences over `info()`, so callers don't
    // need to call `info()` and destructure it themselves for every field.
    // They return `None` both when `info()` errors and when the field
    // itself is `None`, since callers generally treat those two cases the
    // same way (fall back to some default).

    fn channels(&self) -> Option<usize> {
        self.info().ok()?.channels
    }
    fn endianness(&self) -> Option<Endianness> {
        self.info().ok()?.endianness
    }
    fn layout(&self) -> Option<FmtType> {
        self.info().ok()?.layout
    }
    fn block_size(&self) -> Option<u32> {
        self.info().ok()?.block_size
    }
    fn sample_rate(&self) -> Option<u32> {
        self.info().ok()?.sample_rate
    }
    fn audio_length(&self) -> Option<u64> {
        self.info().ok().map(|i| i.audio_length)
    }
    fn data_offset(&self) -> Option<u64> {
        self.info().ok().map(|i| i.data_offset)
    }
    fn tag(&self) -> Option<id3::Tag> {
        self.info().ok()?.tag
    }

    /// Total size of the underlying file, in bytes. Distinct from
    /// `audio_length` (which describes only the audio data): callers use
    /// this to sanity-check a source's reported `audio_length` against what
    /// the file can actually contain, without needing to re-stat the path
    /// themselves.
    fn file_len(&self) -> Result<u64, DsdSourceError>;
}

/// File extensions (lowercase, no leading dot) recognized as this source's
/// container format. Kept separate from [`DsdSource`] since associated
/// consts aren't dyn-compatible: implementers declare this alongside
/// `DsdSource`, and callers reference it on the concrete type (e.g.
/// `DsfFile::EXTENSIONS`) to build a format dispatch table without a reader
/// needing to hardcode each format's extensions itself.
pub trait DsdSourceExtensions {
    const EXTENSIONS: &'static [&'static str];
}