feat: add qbz-dsd crate (DSF/DFF demux, DSD to PCM, DoP, native DSD)

New frontend-agnostic crate for DSD support. In-house DSF and DFF
demuxers (up to 5.1, DST rejected with a typed error), embedded ID3
tags and artwork via the id3 crate, a verbatim BSD port of Sebastian
Gesemann's dsd2pcm table engine plus a half-band decimation chain
(uniform 88.2 kHz output, ITU stereo downmix for multichannel), DoP
frame packing per the DoP 1.1 standard (marker alternation, 0x69
silence), and native DSD_U32 packing (BE/LE) for kernel-granted
devices. 20 tests over synthesized fixtures.

cc
This commit is contained in:
vicrodh committed 2026-07-03 17:56:26 -06:00
1 parent a7f538b3de
commit 1cc1b3d922
11 files changed
+1670

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
[package]
name = "qbz-dsd"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "DSD (DSF/DFF) demuxing and DSD-to-PCM conversion"
[dependencies]
thiserror = { workspace = true }
log = { workspace = true }
id3 = "1"
+282
View File
@@ -0,0 +1,282 @@
//! Streaming DSD → 88.2 kHz PCM conversion chain.
//!
//! dsd2pcm decimates 8:1 (one float per DSD byte), leaving DSD64 at
//! 352.8 kHz, DSD128 at 705.6 kHz, … A chain of half-band ÷2 stages then
//! brings every rate down to a uniform 88.2 kHz.
//!
//! Why 88.2 kHz (v1 policy, revised after the first smoke): the original
//! 176.4 kHz target is NOT universally supported — the owner's DacMagic
//! Plus USB interface exposes 44.1/48/88.2/96/192 only, so the player fell
//! back to rodio's linear resampler, which has no anti-alias filter: the
//! DSD ultrasonic noise shelf (huge between 44k–88k at a 176.4k container
//! rate) folded straight into the audible band as loud hiss. At 88.2 kHz
//! every DAC that matters accepts the rate natively (no resampler in the
//! path), it is an exact 44.1k-family division, and the half-band chain —
//! which DOES anti-alias properly — removes all DSD noise above 44.1 kHz.
//! A per-device higher-rate policy can come with the Phase-2 capability
//! model (see qbz-nix-docs/dsd-support/).
use crate::demux::{DsdDemuxer, DsdError};
use crate::dsd2pcm::Dsd2Pcm;
use std::sync::OnceLock;
/// Uniform PCM output rate for converted DSD.
pub const OUTPUT_RATE: u32 = 88_200;
/// Default conversion gain. DSD program material can exceed 0 dBFS when
/// low-passed to PCM; the customary −6 dB trim prevents clipping.
pub const DEFAULT_GAIN_DB: f32 = -6.0;
/// DSD bytes requested from the demuxer per conversion block, per channel.
/// 64 KiB ≈ 0.19 s of DSD64 — small enough to stream, big enough to be cheap.
const BLOCK_BYTES_PER_CH: usize = 64 * 1024;
const HALFBAND_TAPS: usize = 63;
/// Symmetric half-band low-pass (cutoff fs/4) for ÷2 decimation, generated
/// once: windowed sinc (Blackman), odd length, even taps (except center)
/// exactly zero by construction of sinc(n/2).
fn halfband_taps() -> &'static [f32; HALFBAND_TAPS] {
static TAPS: OnceLock<[f32; HALFBAND_TAPS]> = OnceLock::new();
TAPS.get_or_init(|| {
let m = (HALFBAND_TAPS - 1) as f64 / 2.0; // 31
let mut taps = [0.0f32; HALFBAND_TAPS];
let mut sum = 0.0f64;
for (n, t) in taps.iter_mut().enumerate() {
let x = n as f64 - m;
let sinc = if x == 0.0 {
0.5
} else {
(std::f64::consts::PI * 0.5 * x).sin() / (std::f64::consts::PI * x)
};
let w = 0.42 - 0.5 * (2.0 * std::f64::consts::PI * n as f64 / (HALFBAND_TAPS - 1) as f64).cos()
+ 0.08 * (4.0 * std::f64::consts::PI * n as f64 / (HALFBAND_TAPS - 1) as f64).cos();
let v = sinc * w;
*t = v as f32;
sum += v;
}
// Normalize to unity DC gain.
let scale = (1.0 / sum) as f32;
for t in taps.iter_mut() {
*t *= scale;
}
taps
})
}
/// Streaming FIR decimate-by-2 stage with history carry-over.
struct HalfBand {
/// Pending input: last TAPS-1 samples of the previous call + new input.
carry: Vec<f32>,
/// Read cursor parity is preserved across calls via drain bookkeeping.
next_center: usize,
}
impl HalfBand {
fn new() -> Self {
Self {
// Prime with zeros so the first outputs are filter warm-up, not
// garbage; keeps output counting deterministic (len/2 per input).
carry: vec![0.0; HALFBAND_TAPS - 1],
next_center: HALFBAND_TAPS - 1,
}
}
/// Feed `input`, append decimated output to `out`.
fn process(&mut self, input: &[f32], out: &mut Vec<f32>) {
let taps = halfband_taps();
self.carry.extend_from_slice(input);
let mut i = self.next_center;
while i < self.carry.len() {
let window = &self.carry[i + 1 - HALFBAND_TAPS..=i];
let mut acc = 0.0f32;
// Half-band: even-indexed taps are zero except the center.
let mut j = 0;
while j < HALFBAND_TAPS {
acc += taps[j] * window[HALFBAND_TAPS - 1 - j];
j += 2;
}
acc += taps[HALFBAND_TAPS / 2] * window[HALFBAND_TAPS / 2];
out.push(acc);
i += 2;
}
// Keep the last TAPS-1 samples; remember cursor parity.
let keep_from = self.carry.len().saturating_sub(HALFBAND_TAPS - 1);
let overshoot = i - self.carry.len(); // 0 or 1
self.carry.drain(..keep_from);
self.next_center = (HALFBAND_TAPS - 1) + overshoot;
}
}
/// Whole-file streaming converter: demuxer → per-channel dsd2pcm →
/// half-band chain → interleaved f32 blocks at [`OUTPUT_RATE`].
pub struct DsdPcmConverter {
demux: Box<dyn DsdDemuxer>,
channels: usize,
lsb_first: bool,
dsd2pcm: Vec<Dsd2Pcm>,
stages: Vec<Vec<HalfBand>>, // stages[stage][channel]
gain: f32,
total_frames: u64,
frames_emitted: u64,
finished: bool,
}
impl DsdPcmConverter {
pub fn new(demux: Box<dyn DsdDemuxer>, gain_db: f32) -> Result<Self, DsdError> {
let info = demux.info().clone();
let ratio = info.dsd_rate / OUTPUT_RATE; // 32 / 64 / 128 / 256
if info.dsd_rate % OUTPUT_RATE != 0 || !(ratio / 8).is_power_of_two() || ratio < 16 {
return Err(DsdError::UnsupportedRate(info.dsd_rate));
}
let n_stages = (ratio / 8).trailing_zeros() as usize; // 2..=5
let channels = info.channels as usize;
let stages = (0..n_stages)
.map(|_| (0..channels).map(|_| HalfBand::new()).collect())
.collect();
Ok(Self {
demux,
channels,
lsb_first: info.lsb_first,
dsd2pcm: (0..channels).map(|_| Dsd2Pcm::new()).collect(),
stages,
gain: 10f32.powf(gain_db / 20.0),
total_frames: info.sample_count / ratio as u64,
frames_emitted: 0,
finished: false,
})
}
pub fn output_rate(&self) -> u32 {
OUTPUT_RATE
}
/// PCM output channels: ALWAYS stereo. Mono sources are duplicated;
/// multichannel (up to 5.1) sources are downmixed (ITU-R BS.775
/// coefficients, LFE discarded, normalized against clipping).
pub fn channels(&self) -> u16 {
2
}
/// Exact number of interleaved PCM frames this converter will emit in
/// total (used to size the WAV header up front).
pub fn total_frames(&self) -> u64 {
self.total_frames
}
/// Produce the next interleaved f32 block, `None` when done. The overall
/// emitted frame count is exactly [`Self::total_frames`]: the final block
/// is silence-padded or truncated as needed so the container size always
/// matches the header.
pub fn next_block(&mut self) -> Result<Option<Vec<f32>>, DsdError> {
if self.finished {
return Ok(None);
}
let mut planar: Vec<Vec<u8>> = (0..self.channels).map(|_| Vec::new()).collect();
let got = self.demux.read_planar(&mut planar, BLOCK_BYTES_PER_CH)?;
if got == 0 {
// EOF: pad with silence if the filter latency left us short.
self.finished = true;
let missing = self.total_frames - self.frames_emitted;
if missing == 0 {
return Ok(None);
}
self.frames_emitted = self.total_frames;
return Ok(Some(vec![0.0; (missing as usize) * 2]));
}
let mut per_ch: Vec<Vec<f32>> = Vec::with_capacity(self.channels);
for ch in 0..self.channels {
let mut buf = Vec::new();
self.dsd2pcm[ch].translate(&planar[ch], self.lsb_first, &mut buf);
for stage in self.stages.iter_mut() {
let mut down = Vec::with_capacity(buf.len() / 2 + 1);
stage[ch].process(&buf, &mut down);
buf = down;
}
per_ch.push(buf);
}
let frames = per_ch.iter().map(|c| c.len()).min().unwrap_or(0) as u64;
let frames = frames.min(self.total_frames - self.frames_emitted) as usize;
if frames == 0 {
// Nothing usable this round (filters still priming) — recurse to
// pull more input; bounded by file size.
return self.next_block();
}
// Fold to stereo. DSF/DFF positional channel order:
// 3ch = FL FR C · 4ch = FL FR BL BR · 5ch = FL FR C BL BR ·
// 6ch = FL FR C LFE BL BR.
// ITU-R BS.775 downmix (center/surrounds at −3 dB, LFE discarded),
// normalized by the worst-case coefficient sum against clipping.
const K: f32 = std::f32::consts::FRAC_1_SQRT_2;
let (ci, sli, sri) = match self.channels {
3 => (Some(2), None, None),
4 => (None, Some(2), Some(3)),
5 => (Some(2), Some(3), Some(4)),
6 => (Some(2), Some(4), Some(5)),
_ => (None, None, None),
};
let norm = 1.0
/ (1.0
+ if ci.is_some() { K } else { 0.0 }
+ if sli.is_some() { K } else { 0.0 });
let mut out = Vec::with_capacity(frames * 2);
for f in 0..frames {
let (l, r) = match self.channels {
1 => (per_ch[0][f], per_ch[0][f]),
2 => (per_ch[0][f], per_ch[1][f]),
_ => {
let mut l = per_ch[0][f];
let mut r = per_ch[1][f];
if let Some(i) = ci {
l += K * per_ch[i][f];
r += K * per_ch[i][f];
}
if let Some(i) = sli {
l += K * per_ch[i][f];
}
if let Some(i) = sri {
r += K * per_ch[i][f];
}
(l * norm, r * norm)
}
};
out.push(l * self.gain);
out.push(r * self.gain);
}
self.frames_emitted += frames as u64;
if self.frames_emitted >= self.total_frames {
self.finished = true;
}
Ok(Some(out))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn halfband_dc_gain_is_unity() {
let mut hb = HalfBand::new();
let mut out = Vec::new();
hb.process(&vec![0.5f32; 8192], &mut out);
assert_eq!(out.len(), 8192 / 2);
let tail = &out[256..];
for &s in tail {
assert!((s - 0.5).abs() < 1e-3, "DC not preserved: {s}");
}
}
#[test]
fn halfband_output_count_is_half_input_across_calls() {
let mut hb = HalfBand::new();
let mut out = Vec::new();
// Odd-sized chunks exercise the parity carry-over.
for chunk in [333usize, 1000, 77, 4096, 1] {
hb.process(&vec![0.1f32; chunk], &mut out);
}
let total: usize = 333 + 1000 + 77 + 4096 + 1;
// Centers sit on fixed parity from the zero prefix → ceil(total/2).
assert_eq!(out.len(), total.div_ceil(2));
}
}
+463
View File
@@ -0,0 +1,463 @@
//! DSF and DFF (DSDIFF) demuxers.
//!
//! Both containers carry raw 1-bit DSD grouped in bytes; they differ in
//! layout and bit order:
//! - DSF (Sony spec): per-channel BLOCKS of `block_size` (4096) bytes,
//! `[ch0 block][ch1 block][ch0 block]…`; bit order declared in the header
//! (`bits_per_sample`: 1 = LSB-first, 8 = MSB-first; LSB-first in practice).
//! Standard ID3v2 tag at the header-declared `metadata_ptr` offset.
//! - DFF (Philips DSDIFF 1.5): frame-interleaved, ONE byte per channel round
//! robin, always MSB-first. Big-endian chunk sizes, chunks padded to even
//! offsets. No standard tagging; a trailing nonstandard "ID3 " chunk is
//! honored when present. DST-compressed DFF is detected and rejected.
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
/// Errors from DSD demuxing/conversion. `UnsupportedDst` / `UnsupportedChannels`
/// are expected user-facing cases (toast + skip), not bugs.
#[derive(Debug, thiserror::Error)]
pub enum DsdError {
#[error("DST-compressed DFF is not supported")]
UnsupportedDst,
#[error("unsupported channel count: {0} (mono, stereo and up to 5.1 supported)")]
UnsupportedChannels(u16),
#[error("unsupported DSD rate: {0} Hz")]
UnsupportedRate(u32),
#[error("corrupt or invalid DSD file: {0}")]
Corrupt(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
/// Tag subset read from the container (DSF: embedded ID3v2; DFF: trailing
/// "ID3 " chunk when present, otherwise empty — callers fall back to
/// filename-derived metadata).
#[derive(Debug, Clone, Default)]
pub struct DsdTags {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub album_artist: Option<String>,
pub genre: Option<String>,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub year: Option<i32>,
/// First embedded picture (front cover preferred), raw bytes.
pub artwork: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct DsdStreamInfo {
/// DSD bit rate per channel (2 822 400 = DSD64, …).
pub dsd_rate: u32,
pub channels: u16,
/// Total DSD bits per channel.
pub sample_count: u64,
/// Bit order inside each byte: true = LSB is temporally first (DSF
/// default), false = MSB first (DFF, and DSF with bits_per_sample = 8).
pub lsb_first: bool,
pub tags: DsdTags,
}
impl DsdStreamInfo {
pub fn duration_secs(&self) -> u64 {
if self.dsd_rate == 0 {
0
} else {
self.sample_count / self.dsd_rate as u64
}
}
}
pub trait DsdDemuxer: Send {
fn info(&self) -> &DsdStreamInfo;
/// Append up to `max_bytes_per_ch` DSD bytes per channel to `out[ch]`
/// (planar). Returns the byte count appended to EACH channel (always
/// equal across channels); 0 means end of stream.
fn read_planar(
&mut self,
out: &mut [Vec<u8>],
max_bytes_per_ch: usize,
) -> Result<usize, DsdError>;
}
const VALID_RATES: [u32; 4] = [2_822_400, 5_644_800, 11_289_600, 22_579_200];
/// Open a DSD file, sniffing DSF vs DFF from the leading magic.
pub fn open_dsd(path: &Path) -> Result<Box<dyn DsdDemuxer>, DsdError> {
let mut file = File::open(path)?;
let mut magic = [0u8; 4];
file.read_exact(&mut magic)?;
file.seek(SeekFrom::Start(0))?;
match &magic {
b"DSD " => Ok(Box::new(DsfReader::open(file)?)),
b"FRM8" => Ok(Box::new(DffReader::open(file)?)),
_ => Err(DsdError::Corrupt("not a DSF or DFF file".into())),
}
}
fn read_u32_le(f: &mut File) -> Result<u32, DsdError> {
let mut b = [0u8; 4];
f.read_exact(&mut b)?;
Ok(u32::from_le_bytes(b))
}
fn read_u64_le(f: &mut File) -> Result<u64, DsdError> {
let mut b = [0u8; 8];
f.read_exact(&mut b)?;
Ok(u64::from_le_bytes(b))
}
fn read_u64_be(f: &mut File) -> Result<u64, DsdError> {
let mut b = [0u8; 8];
f.read_exact(&mut b)?;
Ok(u64::from_be_bytes(b))
}
fn read_id(f: &mut File) -> Result<[u8; 4], DsdError> {
let mut b = [0u8; 4];
f.read_exact(&mut b)?;
Ok(b)
}
fn validate_rate(rate: u32) -> Result<(), DsdError> {
if VALID_RATES.contains(&rate) {
Ok(())
} else {
Err(DsdError::UnsupportedRate(rate))
}
}
fn read_id3_tags(file: &mut File, offset: u64) -> DsdTags {
let mut tags = DsdTags::default();
if file.seek(SeekFrom::Start(offset)).is_err() {
return tags;
}
match id3::Tag::read_from2(&mut *file) {
Ok(tag) => {
use id3::TagLike;
tags.title = tag.title().map(str::to_string);
tags.artist = tag.artist().map(str::to_string);
tags.album = tag.album().map(str::to_string);
tags.album_artist = tag.album_artist().map(str::to_string);
tags.genre = tag.genre_parsed().map(|g| g.into_owned());
tags.track_number = tag.track();
tags.disc_number = tag.disc();
tags.year = tag.year().or_else(|| tag.date_recorded().map(|d| d.year));
tags.artwork = tag
.pictures()
.find(|p| p.picture_type == id3::frame::PictureType::CoverFront)
.or_else(|| tag.pictures().next())
.map(|p| p.data.clone());
}
Err(e) => log::debug!("[qbz-dsd] ID3 read failed (non-fatal): {e}"),
}
tags
}
// ---------------------------------------------------------------------------
// DSF
// ---------------------------------------------------------------------------
struct DsfReader {
file: File,
info: DsdStreamInfo,
block_size: usize,
/// Valid (non-padding) DSD bytes remaining per channel.
remaining_per_ch: u64,
}
impl DsfReader {
fn open(mut file: File) -> Result<Self, DsdError> {
// "DSD " chunk: magic + size(28) + total file size + metadata ptr.
let id = read_id(&mut file)?;
if &id != b"DSD " {
return Err(DsdError::Corrupt("missing DSD chunk".into()));
}
let dsd_chunk_size = read_u64_le(&mut file)?;
if dsd_chunk_size != 28 {
return Err(DsdError::Corrupt(format!(
"bad DSD chunk size {dsd_chunk_size}"
)));
}
let _total_size = read_u64_le(&mut file)?;
let metadata_ptr = read_u64_le(&mut file)?;
// "fmt " chunk.
let id = read_id(&mut file)?;
if &id != b"fmt " {
return Err(DsdError::Corrupt("missing fmt chunk".into()));
}
let fmt_size = read_u64_le(&mut file)?;
if fmt_size < 52 {
return Err(DsdError::Corrupt(format!("bad fmt chunk size {fmt_size}")));
}
let format_version = read_u32_le(&mut file)?;
let format_id = read_u32_le(&mut file)?;
let _channel_type = read_u32_le(&mut file)?;
let channel_num = read_u32_le(&mut file)?;
let sampling_frequency = read_u32_le(&mut file)?;
let bits_per_sample = read_u32_le(&mut file)?;
let sample_count = read_u64_le(&mut file)?;
let block_size = read_u32_le(&mut file)?;
let _reserved = read_u32_le(&mut file)?;
if format_version != 1 || format_id != 0 {
return Err(DsdError::Corrupt(format!(
"unsupported DSF format version {format_version} / id {format_id}"
)));
}
if !(1..=6).contains(&channel_num) {
return Err(DsdError::UnsupportedChannels(channel_num as u16));
}
validate_rate(sampling_frequency)?;
let lsb_first = match bits_per_sample {
1 => true,
8 => false,
other => {
return Err(DsdError::Corrupt(format!(
"bad DSF bits_per_sample {other}"
)))
}
};
if block_size == 0 || block_size > (1 << 20) {
return Err(DsdError::Corrupt(format!("bad DSF block size {block_size}")));
}
// "data" chunk header; sample data starts right after.
let id = read_id(&mut file)?;
if &id != b"data" {
return Err(DsdError::Corrupt("missing data chunk".into()));
}
let _data_chunk_size = read_u64_le(&mut file)?;
let data_start = file.stream_position()?;
let tags = if metadata_ptr != 0 {
let t = read_id3_tags(&mut file, metadata_ptr);
file.seek(SeekFrom::Start(data_start))?;
t
} else {
DsdTags::default()
};
Ok(Self {
file,
block_size: block_size as usize,
remaining_per_ch: sample_count.div_ceil(8),
info: DsdStreamInfo {
dsd_rate: sampling_frequency,
channels: channel_num as u16,
sample_count,
lsb_first,
tags,
},
})
}
}
impl DsdDemuxer for DsfReader {
fn info(&self) -> &DsdStreamInfo {
&self.info
}
fn read_planar(
&mut self,
out: &mut [Vec<u8>],
max_bytes_per_ch: usize,
) -> Result<usize, DsdError> {
debug_assert_eq!(out.len(), self.info.channels as usize);
if self.remaining_per_ch == 0 {
return Ok(0);
}
let mut appended = 0usize;
let mut block = vec![0u8; self.block_size];
while appended < max_bytes_per_ch && self.remaining_per_ch > 0 {
// One block group: block_size bytes for each channel in order.
let valid = (self.remaining_per_ch as usize).min(self.block_size);
for ch in 0..self.info.channels as usize {
match self.file.read_exact(&mut block) {
Ok(()) => out[ch].extend_from_slice(&block[..valid]),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
// Truncated file: stop at what we got.
self.remaining_per_ch = 0;
return Ok(appended);
}
Err(e) => return Err(e.into()),
}
}
self.remaining_per_ch -= valid as u64;
appended += valid;
}
Ok(appended)
}
}
// ---------------------------------------------------------------------------
// DFF (DSDIFF)
// ---------------------------------------------------------------------------
struct DffReader {
file: File,
info: DsdStreamInfo,
/// Bytes (all channels interleaved) remaining in the DSD data chunk.
remaining_total: u64,
}
impl DffReader {
fn open(mut file: File) -> Result<Self, DsdError> {
let id = read_id(&mut file)?;
if &id != b"FRM8" {
return Err(DsdError::Corrupt("missing FRM8 chunk".into()));
}
let _form_size = read_u64_be(&mut file)?;
let form_type = read_id(&mut file)?;
if &form_type != b"DSD " {
return Err(DsdError::Corrupt("FRM8 form type is not DSD".into()));
}
let mut dsd_rate: Option<u32> = None;
let mut channels: Option<u16> = None;
let mut data: Option<(u64, u64)> = None; // (offset, size)
let mut id3_offset: Option<u64> = None;
// Top-level chunk scan (seek past payloads; chunks are even-padded).
loop {
let mut idbuf = [0u8; 4];
match file.read_exact(&mut idbuf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
let size = read_u64_be(&mut file)?;
let payload_start = file.stream_position()?;
match &idbuf {
b"PROP" => {
// Property container: "SND " + subchunks.
let prop_type = read_id(&mut file)?;
if &prop_type != b"SND " {
return Err(DsdError::Corrupt("PROP type is not SND".into()));
}
let prop_end = payload_start + size;
while file.stream_position()? + 12 <= prop_end {
let sub_id = read_id(&mut file)?;
let sub_size = read_u64_be(&mut file)?;
let sub_start = file.stream_position()?;
match &sub_id {
b"FS " => {
let mut b = [0u8; 4];
file.read_exact(&mut b)?;
dsd_rate = Some(u32::from_be_bytes(b));
}
b"CHNL" => {
let mut b = [0u8; 2];
file.read_exact(&mut b)?;
channels = Some(u16::from_be_bytes(b));
}
b"CMPR" => {
let cmpr = read_id(&mut file)?;
if &cmpr == b"DST " {
return Err(DsdError::UnsupportedDst);
}
if &cmpr != b"DSD " {
return Err(DsdError::Corrupt(format!(
"unknown DFF compression {:?}",
String::from_utf8_lossy(&cmpr)
)));
}
}
_ => {}
}
// Subchunks are even-padded too.
let padded = sub_size + (sub_size & 1);
file.seek(SeekFrom::Start(sub_start + padded))?;
}
}
b"DSD " => {
data = Some((payload_start, size));
}
b"DST " => return Err(DsdError::UnsupportedDst),
b"ID3 " => {
id3_offset = Some(payload_start);
}
_ => {}
}
let padded = size + (size & 1);
file.seek(SeekFrom::Start(payload_start + padded))?;
}
let dsd_rate = dsd_rate.ok_or_else(|| DsdError::Corrupt("DFF missing FS".into()))?;
let channels = channels.ok_or_else(|| DsdError::Corrupt("DFF missing CHNL".into()))?;
let (data_offset, data_size) =
data.ok_or_else(|| DsdError::Corrupt("DFF missing DSD data".into()))?;
validate_rate(dsd_rate)?;
if !(1..=6).contains(&channels) {
return Err(DsdError::UnsupportedChannels(channels));
}
let tags = match id3_offset {
Some(off) => read_id3_tags(&mut file, off),
None => DsdTags::default(),
};
file.seek(SeekFrom::Start(data_offset))?;
let sample_count = data_size / channels as u64 * 8;
Ok(Self {
file,
remaining_total: data_size,
info: DsdStreamInfo {
dsd_rate,
channels,
sample_count,
lsb_first: false,
tags,
},
})
}
}
impl DsdDemuxer for DffReader {
fn info(&self) -> &DsdStreamInfo {
&self.info
}
fn read_planar(
&mut self,
out: &mut [Vec<u8>],
max_bytes_per_ch: usize,
) -> Result<usize, DsdError> {
let ch = self.info.channels as usize;
debug_assert_eq!(out.len(), ch);
if self.remaining_total == 0 {
return Ok(0);
}
// Whole frames only (one byte per channel).
let want_total = (max_bytes_per_ch * ch).min(self.remaining_total as usize);
let want_total = want_total - (want_total % ch);
if want_total == 0 {
self.remaining_total = 0;
return Ok(0);
}
let mut buf = vec![0u8; want_total];
let mut filled = 0usize;
while filled < want_total {
match self.file.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
let frames = filled / ch;
for f in 0..frames {
for c in 0..ch {
out[c].push(buf[f * ch + c]);
}
}
self.remaining_total = if filled < want_total {
0
} else {
self.remaining_total - filled as u64
};
Ok(frames)
}
}
+190
View File
@@ -0,0 +1,190 @@
//! DoP (DSD over PCM) framing per the DoP Open Standard v1.1 (dCS).
//!
//! Each 24-bit PCM sample carries 16 DSD bits (MSB-first, temporally
//! first byte in the high bits) under a marker byte that alternates
//! 0x05 / 0xFA on successive frames. Both channels of one frame carry the
//! SAME marker. A DoP-aware DAC detects the alternation and switches to
//! DSD mode; anything that alters even one sample breaks the sequence and
//! the DAC falls back to interpreting the stream as PCM (loud noise) —
//! which is why the packed words must travel a bit-exact integer path
//! (no f32, no gain, no resampling).
//!
//! Output words are FINAL S32 samples: the 24-bit DoP word left-justified
//! (`<< 8`), ready for `snd_pcm_writei` on an S32_LE stream.
use crate::demux::{DsdDemuxer, DsdError};
use crate::dsd2pcm::bit_reverse;
/// PCM carrier rate for a DSD bit rate: 16 DSD bits per frame per channel.
/// DSD64 → 176 400 Hz, DSD128 → 352 800 Hz.
pub const fn dop_carrier_rate(dsd_rate: u32) -> u32 {
dsd_rate / 16
}
/// Stateful DoP frame packer (keeps marker phase across calls).
pub struct DopPacker {
marker_fa: bool,
}
impl DopPacker {
pub fn new() -> Self {
Self { marker_fa: false }
}
/// Pack planar MSB-first DSD bytes (2 bytes per channel per frame) into
/// interleaved S32 DoP samples, appending to `out`. Consumes
/// `min(len)/2` frames worth of every channel.
pub fn pack(&mut self, planar: &[Vec<u8>], out: &mut Vec<i32>) {
let ch = planar.len();
let frames = planar.iter().map(|c| c.len()).min().unwrap_or(0) / 2;
out.reserve(frames * ch);
for f in 0..frames {
let marker: i32 = if self.marker_fa { 0xFA } else { 0x05 };
for c in planar.iter() {
let b0 = c[f * 2] as i32;
let b1 = c[f * 2 + 1] as i32;
out.push(((marker << 16) | (b0 << 8) | b1) << 8);
}
self.marker_fa = !self.marker_fa;
}
}
/// DSD silence (0x69 payload) with valid alternating markers — REQUIRED
/// for pause/stop/tail padding: PCM zeros would break the marker
/// sequence and pop the DAC out of DSD mode.
pub fn silence(&mut self, n_frames: usize, channels: u16, out: &mut Vec<i32>) {
out.reserve(n_frames * channels as usize);
for _ in 0..n_frames {
let marker: i32 = if self.marker_fa { 0xFA } else { 0x05 };
for _ in 0..channels {
out.push(((marker << 16) | 0x6969) << 8);
}
self.marker_fa = !self.marker_fa;
}
}
}
impl Default for DopPacker {
fn default() -> Self {
Self::new()
}
}
/// Whole-file streaming DoP word source: demuxer → (bit reversal when the
/// container is LSB-first) → packer. Yields interleaved S32 samples at the
/// carrier rate. Stereo only — DoP receivers are 2-channel devices.
pub struct DopStream {
demux: Box<dyn DsdDemuxer>,
packer: DopPacker,
lsb_first: bool,
dsd_rate: u32,
total_frames: u64,
buf: Vec<i32>,
idx: usize,
done: bool,
}
/// DSD bytes pulled from the demuxer per refill, per channel.
const REFILL_BYTES_PER_CH: usize = 32 * 1024;
impl DopStream {
pub fn new(demux: Box<dyn DsdDemuxer>) -> Result<Self, DsdError> {
let info = demux.info().clone();
if info.channels != 2 {
return Err(DsdError::UnsupportedChannels(info.channels));
}
Ok(Self {
demux,
packer: DopPacker::new(),
lsb_first: info.lsb_first,
dsd_rate: info.dsd_rate,
total_frames: info.sample_count / 16,
buf: Vec::new(),
idx: 0,
done: false,
})
}
pub fn carrier_rate(&self) -> u32 {
dop_carrier_rate(self.dsd_rate)
}
pub fn dsd_rate(&self) -> u32 {
self.dsd_rate
}
/// Total DoP frames (per channel) this stream will yield.
pub fn total_frames(&self) -> u64 {
self.total_frames
}
fn refill(&mut self) -> bool {
let mut planar: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
match self.demux.read_planar(&mut planar, REFILL_BYTES_PER_CH) {
Ok(0) | Err(_) => {
self.done = true;
false
}
Ok(_) => {
if self.lsb_first {
for chan in planar.iter_mut() {
for b in chan.iter_mut() {
*b = bit_reverse(*b);
}
}
}
self.buf.clear();
self.idx = 0;
self.packer.pack(&planar, &mut self.buf);
!self.buf.is_empty()
}
}
}
}
impl Iterator for DopStream {
type Item = i32;
fn next(&mut self) -> Option<i32> {
loop {
if self.idx < self.buf.len() {
let v = self.buf[self.idx];
self.idx += 1;
return Some(v);
}
if self.done || !self.refill() {
return None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn packer_layout_and_marker_alternation() {
let mut p = DopPacker::new();
let mut out = Vec::new();
// One channel pair, two frames.
let l = vec![0xAB, 0xCD, 0x12, 0x34];
let r = vec![0x55, 0xAA, 0x9C, 0x63];
p.pack(&[l, r], &mut out);
assert_eq!(out.len(), 4);
assert_eq!(out[0], ((0x05 << 16) | 0xABCD) << 8);
assert_eq!(out[1], ((0x05 << 16) | 0x55AA) << 8);
assert_eq!(out[2], ((0xFA << 16) | 0x1234) << 8);
assert_eq!(out[3], ((0xFA << 16) | 0x9C63) << 8);
// Phase continues across calls.
let mut out2 = Vec::new();
p.pack(&[vec![0, 0], vec![0, 0]], &mut out2);
assert_eq!(out2[0], (0x05 << 16) << 8);
}
#[test]
fn silence_is_0x69_payload_with_markers() {
let mut p = DopPacker::new();
let mut out = Vec::new();
p.silence(2, 2, &mut out);
assert_eq!(out[0], ((0x05 << 16) | 0x6969) << 8);
assert_eq!(out[2], ((0xFA << 16) | 0x6969) << 8);
}
}
+167
View File
@@ -0,0 +1,167 @@
//! Faithful Rust port of Sebastian Gesemann's dsd2pcm (BSD-2-Clause).
//!
//! Original: "DSD2PCM conversion engine", (c) 2009 Sebastian Gesemann,
//! distributed under the BSD 2-Clause license; the `HTAPS` coefficient table
//! and the FIFO/table-fold algorithm below are ported verbatim from
//! dsd2pcm.c. A symmetric 96-tap FIR low-pass runs at the DSD rate,
//! evaluated bytewise through 6×256 precomputed partial-sum tables, emitting
//! one float sample per input byte (8:1 decimation).
use std::sync::OnceLock;
const HTAPS_LEN: usize = 48;
const FIFO_SIZE: usize = 16;
const FIFO_MASK: usize = FIFO_SIZE - 1;
const CTABLES: usize = 6;
/// Half of the symmetric 96-tap filter (dsd2pcm.c `htaps`, verbatim).
#[rustfmt::skip]
const HTAPS: [f64; HTAPS_LEN] = [
0.09950731974056658, 0.09562845727714668, 0.08819647126516944,
0.07782552527068175, 0.06534876523171299, 0.05172629311427257,
0.0379429484910187, 0.02490921351762261, 0.0133774746265897,
0.003883043418804416, -0.003284703416210726, -0.008080250212687497,
-0.01067241812471033, -0.01139427235000863, -0.0106813877974587,
-0.009007905078766049, -0.006828859761015335, -0.004535184322001496,
-0.002425035959059578, -0.0006922187080790708, 0.0005700762133516592,
0.001353838005269448, 0.001713709169690937, 0.001742046839472948,
0.001545601648013235, 0.001226696225277855, 0.0008704322683580222,
0.0005381636200535649, 0.000266446345425276, 7.002968738383528e-05,
-5.279407053811266e-05, -0.0001140625650874684, -0.0001304796361231895,
-0.0001189970287491285, -9.396247155265073e-05, -6.577634378272832e-05,
-4.07492895872535e-05, -2.17407957554587e-05, -9.163058931391722e-06,
-2.017460145032201e-06, 1.249721855219005e-06, 2.166655190537392e-06,
1.930520892991082e-06, 1.319400334374195e-06, 7.410039764949091e-07,
3.423230509967409e-07, 1.244182214744588e-07, 3.130441005359396e-08,
];
struct Tables {
bitreverse: [u8; 256],
ctables: [[f32; 256]; CTABLES],
}
fn tables() -> &'static Tables {
static TABLES: OnceLock<Tables> = OnceLock::new();
TABLES.get_or_init(|| {
// Bit-reverse table (dsd2pcm.c precalc, verbatim logic).
let mut bitreverse = [0u8; 256];
let mut e: u32 = 0;
for t in 0..256usize {
bitreverse[t] = e as u8;
let mut m: u32 = 128;
while m != 0 {
e ^= m;
if e & m != 0 {
break;
}
m >>= 1;
}
}
// Partial-sum tables: ctables[CTABLES-1-t][e] = Σ ±htaps[t*8+m].
let mut ctables = [[0.0f32; 256]; CTABLES];
for t in 0..CTABLES {
let k = (HTAPS_LEN - t * 8).min(8);
for e in 0..256usize {
let mut acc = 0.0f64;
for m in 0..k {
let sign = (((e >> (7 - m)) & 1) as i32 * 2 - 1) as f64;
acc += sign * HTAPS[t * 8 + m];
}
ctables[CTABLES - 1 - t][e] = acc as f32;
}
}
Tables { bitreverse, ctables }
})
}
/// Reverse the bit order of a DSD byte (LSB-first ↔ MSB-first).
pub(crate) fn bit_reverse(b: u8) -> u8 {
tables().bitreverse[b as usize]
}
/// Streaming DSD→PCM decimator, one instance per channel. Emits one f32
/// sample (at dsd_rate/8) per input DSD byte.
pub struct Dsd2Pcm {
fifo: [u8; FIFO_SIZE],
pos: usize,
}
impl Dsd2Pcm {
pub fn new() -> Self {
// 0x69 is DSD silence; priming the FIFO with it avoids a start click.
Self { fifo: [0x69; FIFO_SIZE], pos: 0 }
}
/// Translate DSD bytes to float samples, appending to `dst`.
/// `lsb_first` = bit order within each source byte.
pub fn translate(&mut self, src: &[u8], lsb_first: bool, dst: &mut Vec<f32>) {
let tb = tables();
let mut ffp = self.pos;
dst.reserve(src.len());
for &byte in src {
let bite1 = if lsb_first { tb.bitreverse[byte as usize] } else { byte };
self.fifo[ffp] = bite1;
// Pre-reverse the byte that just crossed the filter midpoint so
// the mirrored half reads straight from the table (dsd2pcm.c).
let p = (ffp.wrapping_sub(CTABLES)) & FIFO_MASK;
self.fifo[p] = tb.bitreverse[self.fifo[p] as usize];
let mut acc = 0.0f64;
for i in 0..CTABLES {
let a = self.fifo[(ffp.wrapping_sub(i)) & FIFO_MASK] as usize;
let b = self.fifo[(ffp.wrapping_sub(CTABLES * 2 - 1).wrapping_add(i)) & FIFO_MASK]
as usize;
acc += (tb.ctables[i][a] + tb.ctables[i][b]) as f64;
}
dst.push(acc as f32);
ffp = (ffp + 1) & FIFO_MASK;
}
self.pos = ffp;
}
}
impl Default for Dsd2Pcm {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn silence_in_near_zero_out() {
let mut d = Dsd2Pcm::new();
let mut out = Vec::new();
// 0x69 = 01101001: the classic DSD silence pattern (DC-balanced).
d.translate(&[0x69; 4096], false, &mut out);
assert_eq!(out.len(), 4096);
// Skip the filter warm-up, then expect (near) silence.
for &s in &out[64..] {
assert!(s.abs() < 1e-3, "sample {s} not near zero");
}
}
#[test]
fn all_ones_approaches_full_scale_positive() {
let mut d = Dsd2Pcm::new();
let mut out = Vec::new();
d.translate(&[0xFF; 4096], false, &mut out);
let tail = &out[512..];
let avg: f32 = tail.iter().sum::<f32>() / tail.len() as f32;
// DC gain of the filter is ~1.0 for the all-ones (+1) stream.
assert!(avg > 0.8, "avg {avg} too low for all-ones input");
}
#[test]
fn lsb_msb_orders_differ_only_by_bit_reversal() {
let mut a = Dsd2Pcm::new();
let mut b = Dsd2Pcm::new();
let (mut oa, mut ob) = (Vec::new(), Vec::new());
let pattern: Vec<u8> = (0..=255u8).cycle().take(2048).collect();
let reversed: Vec<u8> = pattern.iter().map(|&x| tables().bitreverse[x as usize]).collect();
a.translate(&pattern, false, &mut oa);
b.translate(&reversed, true, &mut ob);
assert_eq!(oa, ob);
}
}
+47
View File
@@ -0,0 +1,47 @@
//! DSD (Direct Stream Digital) support: DSF/DFF demuxing and streaming
//! DSD-to-PCM conversion.
//!
//! Phase 1 scope (see qbz-nix-docs/dsd-support/): local .dsf/.dff files are
//! demuxed here and converted on the fly to 176.4 kHz / 24-bit PCM, which the
//! player delivers through its existing (bit-perfect-capable) PCM pipeline.
//! DoP and native-DSD delivery are later phases and intentionally absent.
//!
//! Frontend-agnostic (ADR-006): no UI, no player, no Tauri types.
mod convert;
mod demux;
mod dop;
mod dsd2pcm;
mod native;
mod wav;
pub use convert::{DsdPcmConverter, DEFAULT_GAIN_DB, OUTPUT_RATE};
pub use demux::{open_dsd, DsdDemuxer, DsdError, DsdStreamInfo, DsdTags};
pub use dop::{dop_carrier_rate, DopPacker, DopStream};
pub use native::{native_u32_rate, NativeDsdStream, NATIVE_DSD_SILENCE_U32};
pub use wav::{frames_to_pcm24, wav_header, wav_total_size};
/// DSD64 base rate (bits per second per channel). Multiples of this are the
/// only valid DSD rates: DSD64/128/256/512.
pub const DSD64_RATE: u32 = 2_822_400;
/// True when the path has a DSD container extension (.dsf / .dff).
pub fn is_dsd_path(path: &std::path::Path) -> bool {
matches!(
path.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase())
.as_deref(),
Some("dsf") | Some("dff")
)
}
/// "DSD64" / "DSD128" / … label for a DSD bit rate. Falls back to "DSD" for
/// non-standard rates.
pub fn dsd_label(dsd_rate: u32) -> String {
if dsd_rate >= DSD64_RATE && dsd_rate % DSD64_RATE == 0 {
format!("DSD{}", 64 * (dsd_rate / DSD64_RATE))
} else {
"DSD".to_string()
}
}
+178
View File
@@ -0,0 +1,178 @@
//! Native DSD packing for ALSA's DSD_U32 formats (DSD plan Phase 3).
//!
//! One 32-bit sample per channel per frame carries 4 consecutive DSD bytes
//! (MSB-first bit order within each byte — same normalization the DoP path
//! uses). ALSA's frame rate is therefore dsd_rate / 32 (DSD64 → 88 200).
//!
//! Endianness (values below are what an i32 must hold on a little-endian
//! machine so the bytes land in the right memory order):
//! - `DSD_U32_BE` (what the kernel's generic DSD quirk grants USB DACs):
//! temporally-first byte first in memory → `i32::from_le_bytes([b0..b3])`.
//! - `DSD_U32_LE`: first byte in the value's MSB, little-endian storage →
//! `i32::from_be_bytes([b0..b3])`.
//!
//! Native DSD silence is the byte 0x69 in every lane — conveniently the
//! same i32 (0x69696969) in both layouts.
use crate::demux::{DsdDemuxer, DsdError};
use crate::dsd2pcm::bit_reverse;
/// ALSA frame rate for a DSD bit rate in U32 containers.
pub const fn native_u32_rate(dsd_rate: u32) -> u32 {
dsd_rate / 32
}
/// Native DSD silence sample (0x69 in all four byte lanes).
pub const NATIVE_DSD_SILENCE_U32: i32 = 0x6969_6969;
/// Whole-file streaming native-DSD word source: demuxer → (bit reversal for
/// LSB-first containers) → 4-byte packing. Yields interleaved i32 samples at
/// [`native_u32_rate`]. Stereo only, mirroring the DoP path.
pub struct NativeDsdStream {
demux: Box<dyn DsdDemuxer>,
lsb_first: bool,
little_endian: bool,
dsd_rate: u32,
total_frames: u64,
buf: Vec<i32>,
idx: usize,
/// Carry-over bytes per channel when a demux read isn't a multiple of 4.
carry: [Vec<u8>; 2],
done: bool,
}
const REFILL_BYTES_PER_CH: usize = 32 * 1024;
impl NativeDsdStream {
/// `little_endian` selects DSD_U32_LE packing; false = DSD_U32_BE.
pub fn new(demux: Box<dyn DsdDemuxer>, little_endian: bool) -> Result<Self, DsdError> {
let info = demux.info().clone();
if info.channels != 2 {
return Err(DsdError::UnsupportedChannels(info.channels));
}
Ok(Self {
demux,
lsb_first: info.lsb_first,
little_endian,
dsd_rate: info.dsd_rate,
total_frames: info.sample_count / 32,
buf: Vec::new(),
idx: 0,
carry: [Vec::new(), Vec::new()],
done: false,
})
}
pub fn rate(&self) -> u32 {
native_u32_rate(self.dsd_rate)
}
pub fn dsd_rate(&self) -> u32 {
self.dsd_rate
}
pub fn total_frames(&self) -> u64 {
self.total_frames
}
fn pack_word(&self, b: [u8; 4]) -> i32 {
if self.little_endian {
i32::from_be_bytes(b)
} else {
i32::from_le_bytes(b)
}
}
fn refill(&mut self) -> bool {
// Carry bytes are ALREADY bit-normalized (reversed on the refill that
// produced them) — only the newly-read span gets reversed below.
let mut planar: Vec<Vec<u8>> = vec![
std::mem::take(&mut self.carry[0]),
std::mem::take(&mut self.carry[1]),
];
let pre = [planar[0].len(), planar[1].len()];
let got = match self.demux.read_planar(&mut planar, REFILL_BYTES_PER_CH) {
Ok(n) => n,
Err(_) => 0,
};
if self.lsb_first {
for (i, chan) in planar.iter_mut().enumerate() {
for b in chan[pre[i]..].iter_mut() {
*b = bit_reverse(*b);
}
}
}
if got == 0 {
// EOF: pad the final partial word (if any) with DSD silence
// (0x69 — planar is MSB-first-normalized at this point).
let leftover = planar[0].len().min(planar[1].len());
if leftover == 0 {
self.done = true;
return false;
}
for chan in planar.iter_mut() {
while chan.len() % 4 != 0 {
chan.push(0x69);
}
}
self.done = true;
}
let words = planar.iter().map(|c| c.len()).min().unwrap_or(0) / 4;
self.buf.clear();
self.idx = 0;
self.buf.reserve(words * 2);
for w in 0..words {
for chan in planar.iter() {
let b = [
chan[w * 4],
chan[w * 4 + 1],
chan[w * 4 + 2],
chan[w * 4 + 3],
];
self.buf.push(self.pack_word(b));
}
}
// Keep whatever didn't fill a whole word for the next refill.
if !self.done {
for (i, chan) in planar.iter().enumerate() {
self.carry[i] = chan[words * 4..].to_vec();
}
}
!self.buf.is_empty()
}
}
impl Iterator for NativeDsdStream {
type Item = i32;
fn next(&mut self) -> Option<i32> {
loop {
if self.idx < self.buf.len() {
let v = self.buf[self.idx];
self.idx += 1;
return Some(v);
}
if self.done || !self.refill() {
return None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_packing_endianness() {
// Direct math checks (pack_word needs an instance; test the formulas).
let b = [0xAAu8, 0xBB, 0xCC, 0xDD];
assert_eq!(i32::from_le_bytes(b), 0xDDCCBBAAu32 as i32); // BE layout
assert_eq!(i32::from_be_bytes(b), 0xAABBCCDDu32 as i32); // LE layout
assert_eq!(NATIVE_DSD_SILENCE_U32, i32::from_le_bytes([0x69; 4]));
assert_eq!(NATIVE_DSD_SILENCE_U32, i32::from_be_bytes([0x69; 4]));
}
#[test]
fn native_rate_math() {
assert_eq!(native_u32_rate(2_822_400), 88_200);
assert_eq!(native_u32_rate(11_289_600), 352_800);
}
}
+77
View File
@@ -0,0 +1,77 @@
//! Minimal 24-bit PCM WAV encoding for the converted-DSD stream.
//!
//! The converter knows its exact final frame count up front (DSF/DFF headers
//! carry the sample count), so the WAV header sizes are exact — the player's
//! streaming decoder sees an ordinary finite WAV file arriving over a
//! buffered source.
const HEADER_LEN: u64 = 44;
const BYTES_PER_SAMPLE: u64 = 3; // 24-bit
pub fn wav_total_size(total_frames: u64, channels: u16) -> u64 {
HEADER_LEN + total_frames * channels as u64 * BYTES_PER_SAMPLE
}
/// Standard 44-byte RIFF/WAVE header for 24-bit integer PCM.
pub fn wav_header(total_frames: u64, channels: u16, sample_rate: u32) -> Vec<u8> {
let data_len = (total_frames * channels as u64 * BYTES_PER_SAMPLE) as u32;
let byte_rate = sample_rate * channels as u32 * BYTES_PER_SAMPLE as u32;
let block_align = channels * BYTES_PER_SAMPLE as u16;
let mut h = Vec::with_capacity(HEADER_LEN as usize);
h.extend_from_slice(b"RIFF");
h.extend_from_slice(&(36 + data_len).to_le_bytes());
h.extend_from_slice(b"WAVE");
h.extend_from_slice(b"fmt ");
h.extend_from_slice(&16u32.to_le_bytes());
h.extend_from_slice(&1u16.to_le_bytes()); // PCM
h.extend_from_slice(&channels.to_le_bytes());
h.extend_from_slice(&sample_rate.to_le_bytes());
h.extend_from_slice(&byte_rate.to_le_bytes());
h.extend_from_slice(&block_align.to_le_bytes());
h.extend_from_slice(&24u16.to_le_bytes()); // bits per sample
h.extend_from_slice(b"data");
h.extend_from_slice(&data_len.to_le_bytes());
debug_assert_eq!(h.len() as u64, HEADER_LEN);
h
}
/// Encode interleaved f32 frames to little-endian 24-bit PCM, appending to
/// `out`. Values are hard-clamped; no dither (inaudible at 24-bit).
pub fn frames_to_pcm24(frames: &[f32], out: &mut Vec<u8>) {
out.reserve(frames.len() * 3);
for &s in frames {
let v = (s.clamp(-1.0, 1.0) * 8_388_607.0).round() as i32;
let b = v.to_le_bytes();
out.extend_from_slice(&b[0..3]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_and_size_math_agree() {
let frames = 176_400u64 * 3; // 3 seconds
let h = wav_header(frames, 2, 176_400);
assert_eq!(h.len(), 44);
assert_eq!(wav_total_size(frames, 2), 44 + frames * 2 * 3);
// RIFF size field = total - 8.
let riff = u32::from_le_bytes([h[4], h[5], h[6], h[7]]) as u64;
assert_eq!(riff, wav_total_size(frames, 2) - 8);
}
#[test]
fn pcm24_encoding_clamps_and_scales() {
let mut out = Vec::new();
frames_to_pcm24(&[0.0, 1.0, -1.0, 2.0], &mut out);
assert_eq!(out.len(), 12);
assert_eq!(&out[0..3], &[0, 0, 0]);
let max = i32::from_le_bytes([out[3], out[4], out[5], 0]);
assert_eq!(max, 8_388_607);
// -1.0 → -8388607 in 24-bit two's complement (low 3 bytes).
let neg = i32::from_le_bytes([out[6], out[7], out[8], 0xFF]);
assert_eq!(neg, -8_388_607);
assert_eq!(&out[9..12], &out[3..6]); // 2.0 clamps to full scale
}
}
+231
View File
@@ -0,0 +1,231 @@
//! Integration tests over synthesized DSF/DFF files.
use qbz_dsd::{open_dsd, DsdError, DsdPcmConverter};
use std::io::Write;
use std::path::PathBuf;
fn tmp(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name)
}
/// Minimal valid DSF: `groups` block-groups of 0x69 silence.
fn write_dsf(name: &str, channels: u32, rate: u32, groups: usize, metadata: Option<&[u8]>) -> PathBuf {
let block_size = 4096u32;
let bytes_per_ch = groups * block_size as usize;
let sample_count = (bytes_per_ch as u64) * 8;
let data_len = (bytes_per_ch * channels as usize) as u64;
// DSD chunk (28) + fmt chunk (52, header included) + data header (12).
let file_len_without_meta = 28 + 52 + 12 + data_len;
let metadata_ptr = if metadata.is_some() { file_len_without_meta } else { 0 };
let mut f = Vec::new();
f.extend_from_slice(b"DSD ");
f.extend_from_slice(&28u64.to_le_bytes());
f.extend_from_slice(&(file_len_without_meta + metadata.map_or(0, |m| m.len() as u64)).to_le_bytes());
f.extend_from_slice(&metadata_ptr.to_le_bytes());
f.extend_from_slice(b"fmt ");
f.extend_from_slice(&52u64.to_le_bytes());
f.extend_from_slice(&1u32.to_le_bytes()); // version
f.extend_from_slice(&0u32.to_le_bytes()); // format id = DSD raw
f.extend_from_slice(&2u32.to_le_bytes()); // channel type (stereo)
f.extend_from_slice(&channels.to_le_bytes());
f.extend_from_slice(&rate.to_le_bytes());
f.extend_from_slice(&1u32.to_le_bytes()); // bits per sample: 1 = LSB first
f.extend_from_slice(&sample_count.to_le_bytes());
f.extend_from_slice(&block_size.to_le_bytes());
f.extend_from_slice(&0u32.to_le_bytes()); // reserved
f.extend_from_slice(b"data");
f.extend_from_slice(&(12 + data_len).to_le_bytes());
for _ in 0..groups {
for _ in 0..channels {
f.extend_from_slice(&vec![0x69u8; block_size as usize]);
}
}
if let Some(m) = metadata {
f.extend_from_slice(m);
}
let path = tmp(name);
std::fs::File::create(&path).unwrap().write_all(&f).unwrap();
path
}
/// Minimal DFF: FRM8 { FVER, PROP(SND){ FS, CHNL, CMPR }, DSD data }.
fn write_dff(name: &str, channels: u16, rate: u32, data_bytes_total: usize, cmpr: &[u8; 4]) -> PathBuf {
let mut prop = Vec::new();
prop.extend_from_slice(b"SND ");
prop.extend_from_slice(b"FS ");
prop.extend_from_slice(&4u64.to_be_bytes());
prop.extend_from_slice(&rate.to_be_bytes());
prop.extend_from_slice(b"CHNL");
let chnl_len = 2 + 4 * channels as u64;
prop.extend_from_slice(&chnl_len.to_be_bytes());
prop.extend_from_slice(&channels.to_be_bytes());
for _ in 0..channels {
prop.extend_from_slice(b"SLFT");
}
if chnl_len % 2 == 1 {
prop.push(0);
}
prop.extend_from_slice(b"CMPR");
// Compression subchunk: 4-byte ID + pascal-ish name (we write just the ID
// + a 1-byte count 0, padded).
prop.extend_from_slice(&5u64.to_be_bytes());
prop.extend_from_slice(cmpr);
prop.push(0);
prop.push(0); // even padding
let mut f = Vec::new();
f.extend_from_slice(b"FRM8");
f.extend_from_slice(&0u64.to_be_bytes()); // form size (unused by reader)
f.extend_from_slice(b"DSD ");
f.extend_from_slice(b"FVER");
f.extend_from_slice(&4u64.to_be_bytes());
f.extend_from_slice(&[1, 5, 0, 0]);
f.extend_from_slice(b"PROP");
f.extend_from_slice(&(prop.len() as u64).to_be_bytes());
f.extend_from_slice(&prop);
f.extend_from_slice(b"DSD ");
f.extend_from_slice(&(data_bytes_total as u64).to_be_bytes());
f.extend_from_slice(&vec![0x69u8; data_bytes_total]);
let path = tmp(name);
std::fs::File::create(&path).unwrap().write_all(&f).unwrap();
path
}
#[test]
fn dsf_parses_and_converts_to_exact_frame_count() {
let path = write_dsf("silence64.dsf", 2, 2_822_400, 2, None);
let demux = open_dsd(&path).unwrap();
let info = demux.info().clone();
assert_eq!(info.dsd_rate, 2_822_400);
assert_eq!(info.channels, 2);
assert_eq!(info.sample_count, 2 * 4096 * 8);
assert!(info.lsb_first);
let mut conv = DsdPcmConverter::new(demux, -6.0).unwrap();
let expected_frames = info.sample_count / 32; // DSD64 → 88.2k
assert_eq!(conv.total_frames(), expected_frames);
let mut frames = 0u64;
let mut peak = 0f32;
while let Some(block) = conv.next_block().unwrap() {
frames += (block.len() / 2) as u64;
for s in block {
peak = peak.max(s.abs());
}
}
assert_eq!(frames, expected_frames);
assert!(peak < 0.01, "silence converted loud: peak {peak}");
}
#[test]
fn dsf_reads_embedded_id3() {
let mut tag = id3::Tag::new();
use id3::TagLike;
tag.set_title("Karma Police");
tag.set_artist("Radiohead");
tag.set_album("OK Computer");
tag.set_track(6);
let mut blob = Vec::new();
tag.write_to(&mut blob, id3::Version::Id3v24).unwrap();
let path = write_dsf("tagged.dsf", 2, 2_822_400, 1, Some(&blob));
let demux = open_dsd(&path).unwrap();
let tags = &demux.info().tags;
assert_eq!(tags.title.as_deref(), Some("Karma Police"));
assert_eq!(tags.artist.as_deref(), Some("Radiohead"));
assert_eq!(tags.album.as_deref(), Some("OK Computer"));
assert_eq!(tags.track_number, Some(6));
}
#[test]
fn dsf_dsd128_uses_three_stages() {
let path = write_dsf("silence128.dsf", 2, 5_644_800, 2, None);
let demux = open_dsd(&path).unwrap();
let mut conv = DsdPcmConverter::new(demux, -6.0).unwrap();
assert_eq!(conv.total_frames(), (2 * 4096 * 8) / 64);
let mut frames = 0u64;
while let Some(block) = conv.next_block().unwrap() {
frames += (block.len() / 2) as u64;
}
assert_eq!(frames, (2 * 4096 * 8) / 64);
}
#[test]
fn dsf_eight_channels_rejected() {
let path = write_dsf("multi8.dsf", 8, 2_822_400, 1, None);
match open_dsd(&path) {
Err(DsdError::UnsupportedChannels(8)) => {}
Err(other) => panic!("expected UnsupportedChannels, got {other:?}"),
Ok(_) => panic!("expected UnsupportedChannels, got Ok"),
}
}
#[test]
fn dsf_5_1_downmixes_to_stereo() {
let path = write_dsf("multi51.dsf", 6, 2_822_400, 1, None);
let demux = open_dsd(&path).unwrap();
assert_eq!(demux.info().channels, 6);
let mut conv = DsdPcmConverter::new(demux, -6.0).unwrap();
assert_eq!(conv.channels(), 2);
let expected_frames = demux_total_frames(4096 * 8);
assert_eq!(conv.total_frames(), expected_frames);
let mut frames = 0u64;
let mut peak = 0f32;
while let Some(block) = conv.next_block().unwrap() {
frames += (block.len() / 2) as u64;
for s in block {
peak = peak.max(s.abs());
}
}
assert_eq!(frames, expected_frames);
assert!(peak < 0.01, "5.1 silence downmix not silent: peak {peak}");
}
fn demux_total_frames(sample_count: u64) -> u64 {
sample_count / 32 // DSD64 → 88.2 kHz
}
#[test]
fn dop_stream_frames_and_markers() {
use qbz_dsd::DopStream;
let path = write_dsf("dop64.dsf", 2, 2_822_400, 1, None);
let demux = open_dsd(&path).unwrap();
let mut dop = DopStream::new(demux).unwrap();
assert_eq!(dop.carrier_rate(), 176_400);
assert_eq!(dop.total_frames(), 4096 * 8 / 16);
let words: Vec<i32> = dop.by_ref().collect();
assert_eq!(words.len() as u64, dop.total_frames() * 2);
// DSF silence is 0x69 LSB-first → bit-reversed payload is 0x96, and the
// markers alternate per frame across both channels.
assert_eq!(words[0], ((0x05 << 16) | 0x9696) << 8);
assert_eq!(words[1], ((0x05 << 16) | 0x9696) << 8);
assert_eq!(words[2], ((0xFA << 16) | 0x9696) << 8);
}
#[test]
fn dff_parses_stereo() {
let path = write_dff("plain.dff", 2, 2_822_400, 8192, b"DSD ");
let demux = open_dsd(&path).unwrap();
let info = demux.info();
assert_eq!(info.dsd_rate, 2_822_400);
assert_eq!(info.channels, 2);
assert_eq!(info.sample_count, 8192 / 2 * 8);
assert!(!info.lsb_first);
}
#[test]
fn dff_dst_rejected() {
let path = write_dff("dst.dff", 2, 2_822_400, 128, b"DST ");
match open_dsd(&path) {
Err(DsdError::UnsupportedDst) => {}
Err(other) => panic!("expected UnsupportedDst, got {other:?}"),
Ok(_) => panic!("expected UnsupportedDst, got Ok"),
}
}
#[test]
fn garbage_rejected() {
let path = tmp("garbage.bin");
std::fs::write(&path, b"definitely not dsd").unwrap();
assert!(matches!(open_dsd(&path), Err(DsdError::Corrupt(_))));
}