feat(disc): read CD-DA off a real drive, measured before it was written

New crate `qbz-disc`: table of contents and raw audio sectors through the
kernel's own cdrom ioctls. No libcdio — not because libcdio is worse (it is
better at RIPPING than this will ever be) but because this path PLAYS a clean
disc, and for that the kernel interface is enough while a native library would
have to be built into Flatpak, Snap and AppImage. Both need the same --device
permission in a sandbox; only one needs the library shipped.

Every claim below was measured on the owner's USB drive with Tool — Fear
Inoculum BEFORE a line of this crate existed, so the design answers evidence
rather than the other way round:

  - the TOC reads back 7 audio tracks, lead-out at LSN 356568, and the track
    lengths match the record exactly (10:21, 11:52, 12:44, 13:37, 10:05, 4:48,
    15:44 — 79:11 total);
  - CDROMREADAUDIO over 20 frames from LSN 4000 returned 47 040 bytes, 99 %
    non-zero samples, peak 14559: real audio, not silence;
  - the drive hands back LITTLE-endian samples. Neighbouring-sample deltas
    average 73.5 read as LE against 20 971 read as BE, a 285x margin, so the
    bytes go straight into a little-endian WAV with no swap. That is one
    drive, so `looks_byte_swapped` keeps the test available as a diagnostic
    instead of an article of faith;
  - the disc's MCN is all zeros, which is why identity is a digest over the
    track table and not the catalogue number.

What this is NOT is accurate extraction: no overlap synchronisation, no cache
modelling, no jitter verification. On a clean disc that is fine for playback,
and an unreadable sector surfaces as an ERROR rather than a hole full of
zeros — silence that pretends to be audio is the one outcome a bit-perfect
player must never produce.

`/dev/sr0` is nowhere in the code: drives are enumerated, because a box can
have several and /dev/cdrom is a symlink that may point at any of them. Data
tracks of a mixed-mode disc are skipped by their control bits.

`CdRef` is the typed reference (`cdda:/dev/sr0#285735+70833`) with ONE parser,
not prefix tests copied around — the scattered version is a known failure mode
here and cost a whole session on 2026-08-20.

11 tests, several asserting against that real table of contents.

cc
This commit is contained in:
vicrodh committed 2026-08-20 23:28:28 -06:00
1 parent 0e9721d24b
commit db0999bf6f
6 files changed
+801

No files matched your search

+11
View File
@@ -6369,6 +6369,15 @@ dependencies = [
"qbz-models", "qbz-models",
] ]
[[package]]
name = "qbz-disc"
version = "2.0.2"
dependencies = [
"libc",
"log",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "qbz-dsd" name = "qbz-dsd"
version = "2.0.2" version = "2.0.2"
@@ -6670,6 +6679,7 @@ dependencies = [
"qbz-core", "qbz-core",
"qbz-credentials", "qbz-credentials",
"qbz-dac-wizard-core", "qbz-dac-wizard-core",
"qbz-disc",
"qbz-external-reco", "qbz-external-reco",
"qbz-i18n", "qbz-i18n",
"qbz-integrations", "qbz-integrations",
@@ -6760,6 +6770,7 @@ version = "2.0.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"log", "log",
"qbz-disc",
"qbz-jellyfin", "qbz-jellyfin",
"qbz-library", "qbz-library",
"qbz-media-cache", "qbz-media-cache",
+4
View File
@@ -16,6 +16,10 @@ members = [
"qbz-cast", "qbz-cast",
"qbz-cmaf", "qbz-cmaf",
"qbz-dsd", "qbz-dsd",
# Optical media (CD-DA now; SACD/ISO next). Frontend-agnostic, and NOT a
# dependency of qbz-dsd's reverse: qbz-dsd will depend on THIS, never the
# other way, or the two close a cycle.
"qbz-disc",
"qbz-secrets", "qbz-secrets",
"qbz-offline-cache", "qbz-offline-cache",
"qbz-plex", "qbz-plex",
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "qbz-disc"
version.workspace = true
edition.workspace = true
[dependencies]
log = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
+68
View File
@@ -0,0 +1,68 @@
//! Read the disc in the drive and prove the crate agrees with reality.
//! `cargo run -p qbz-disc --example cd_probe`
fn main() {
let devs = qbz_disc::list_devices();
println!("devices: {devs:?}");
let Some(dev) = devs.first() else {
println!("no drive");
return;
};
let toc = match qbz_disc::read_toc(dev) {
Ok(t) => t,
Err(e) => {
println!("toc failed: {e}");
return;
}
};
println!("fingerprint: {}", toc.fingerprint());
println!("leadout LSN: {}", toc.leadout_lsn);
let mut total = 0u64;
for t in &toc.tracks {
let d = t.duration_secs();
total += d;
println!(
" track {:2} lsn {:>7} {:>6} sectors {:>2}:{:02} {}",
t.number,
t.start_lsn,
t.sectors,
d / 60,
d % 60,
if t.is_audio { "audio" } else { "DATA (skipped)" }
);
}
println!("total {}:{:02}", total / 60, total % 60);
// Read the first audio track's opening chunk and prove it is music.
let Some(first) = toc.audio_tracks().next() else {
return;
};
let mut r = match qbz_disc::cdda::TrackReader::open(dev, first) {
Ok(r) => r,
Err(e) => {
println!("open failed: {e}");
return;
}
};
let mut buf = Vec::new();
// Skip the first second — track 1 often opens on silence.
for _ in 0..2 {
if let Err(e) = r.next_chunk(&mut buf) {
println!("read failed: {e}");
return;
}
}
let samples: Vec<i16> = buf
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
let peak = samples.iter().map(|s| s.unsigned_abs()).max().unwrap_or(0);
let nz = samples.iter().filter(|s| **s != 0).count();
println!(
"read {} bytes | {} samples | non-zero {}% | peak {} | byte-swapped? {}",
buf.len(),
samples.len(),
100 * nz / samples.len().max(1),
peak,
qbz_disc::cdda::looks_byte_swapped(&buf)
);
}
+504
View File
@@ -0,0 +1,504 @@
//! CD-DA off a real drive, through the kernel's own ioctls.
//!
//! No libcdio. Not because libcdio is bad — it is better at RIPPING than this
//! will ever be — but because this path PLAYS a clean disc, and for that the
//! kernel interface is enough while a native library would have to be built
//! into Flatpak, Snap and AppImage. Both approaches need the same `--device`
//! permission in a sandbox; only one needs the library shipped.
//!
//! WHAT THIS IS NOT: accurate extraction. There is no overlap synchronisation,
//! no cache modelling, no jitter verification, no scratch reconstruction. On a
//! clean disc that is fine for playback; on a marginal one it can click, and
//! an unreadable sector surfaces as an ERROR rather than a hole full of zeros.
//! Silence that pretends to be audio is the one outcome bit-perfect playback
//! must never produce.
//!
//! MEASURED on the owner's USB drive with Tool — *Fear Inoculum* (2026-08-20),
//! before any of this existed:
//! - `cd-info` reports access mode IOCTL; the TOC reads back 7 audio tracks,
//! lead-out at LSN 356568;
//! - `CDROMREADAUDIO` over 20 frames from LSN 4000 returned 47 040 bytes,
//! 99 % non-zero samples, peak 14559 — real audio, not silence;
//! - the drive hands back LITTLE-endian samples. Neighbouring samples差
//! average 73.5 read as LE against 20 971 read as BE, a 285x margin. So
//! the bytes go straight into a little-endian WAV with no swap. That is
//! measured for THIS drive; `looks_byte_swapped` below keeps the check
//! available as a diagnostic rather than as an article of faith.
use std::os::fd::{AsRawFd, OwnedFd};
use std::path::{Path, PathBuf};
// --- ioctl numbers and layouts (uapi/linux/cdrom.h) -------------------------
const CDROMREADTOCHDR: libc::c_ulong = 0x5305;
const CDROMREADTOCENTRY: libc::c_ulong = 0x5306;
const CDROMREADAUDIO: libc::c_ulong = 0x530e;
const CDROM_DRIVE_STATUS: libc::c_ulong = 0x5326;
const CDROM_LBA: u8 = 0x01;
/// The lead-out's pseudo track number: its start is the end of the last track.
const CDROM_LEADOUT: u8 = 0xAA;
const CDS_DISC_OK: libc::c_int = 4;
/// `cdte_ctrl` bit 2 marks a DATA track. A mixed-mode disc carries one and it
/// must be skipped: handing its bytes to an audio path produces noise.
const CTRL_DATA: u8 = 0x04;
/// The kernel accepts 1..=75 frames per `CDROMREADAUDIO` call.
pub const MAX_FRAMES_PER_READ: u32 = 75;
#[repr(C)]
#[derive(Default)]
struct CdromTocHdr {
trk0: u8,
trk1: u8,
}
#[repr(C)]
#[derive(Default)]
struct CdromTocEntry {
track: u8,
/// adr in the low nibble, ctrl in the high one — a C bitfield, read whole.
adr_ctrl: u8,
format: u8,
_pad: u8,
addr_lba: i32,
datamode: u8,
_pad2: [u8; 3],
}
#[repr(C)]
struct CdromReadAudio {
addr_lba: i32,
addr_format: u8,
_pad: [u8; 3],
nframes: libc::c_int,
buf: *mut u8,
}
#[derive(Debug, thiserror::Error)]
pub enum CdError {
#[error("no optical drive found")]
NoDrive,
#[error("cannot open {0}: {1}")]
Open(PathBuf, std::io::Error),
#[error("no disc in the drive")]
NoDisc,
#[error("the disc has no audio tracks")]
NotAudio,
#[error("the drive refused to read audio at sector {lsn}: {source}")]
Read {
lsn: u32,
#[source]
source: std::io::Error,
},
#[error("reading the table of contents failed: {0}")]
Toc(std::io::Error),
}
/// One entry of the disc's table of contents.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TocTrack {
pub number: u8,
pub start_lsn: u32,
/// Sectors up to the next track (or the lead-out for the last one).
pub sectors: u32,
/// False for the data track of a mixed-mode disc.
pub is_audio: bool,
}
impl TocTrack {
pub fn duration_secs(&self) -> u64 {
crate::sectors_to_secs(self.sectors)
}
/// PCM frames (sample pairs) this track decodes to.
pub fn frames(&self) -> u64 {
self.sectors as u64 * 588
}
}
#[derive(Debug, Clone)]
pub struct Toc {
pub device: PathBuf,
pub tracks: Vec<TocTrack>,
pub leadout_lsn: u32,
}
impl Toc {
/// The AUDIO tracks, in disc order. A caller that wants to play something
/// should use this and never `tracks` — the data track of an enhanced CD
/// lives in the latter.
pub fn audio_tracks(&self) -> impl Iterator<Item = &TocTrack> {
self.tracks.iter().filter(|t| t.is_audio)
}
/// A stable identity for the MEDIUM, not the drive.
///
/// `/dev/sr0` names a piece of hardware; two different discs in it are two
/// different sessions, and a persisted queue that resolves against the
/// drive would happily play track 3 of whatever is loaded now. The digest
/// is over every track's start and length plus the lead-out, which is what
/// a disc actually is. (The disc's own MCN would be tidier — the owner's
/// Fear Inoculum reports all zeros, so it is not dependable.)
pub fn fingerprint(&self) -> String {
// FNV-1a: no dependency, and this only has to be stable and
// collision-resistant enough to tell two albums apart.
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
let mut eat = |v: u32| {
for b in v.to_le_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x1000_0000_01b3);
}
};
for t in &self.tracks {
eat(t.start_lsn);
eat(t.sectors);
eat(t.number as u32);
}
eat(self.leadout_lsn);
format!("{h:016x}")
}
}
/// Optical devices the kernel is showing, most conventional first.
///
/// `/dev/sr0` is NOT hardcoded anywhere in this crate: a box can have several
/// drives, and `/dev/cdrom` is a udev symlink that may point at any of them.
pub fn list_devices() -> Vec<PathBuf> {
let mut out = Vec::new();
for n in 0..8 {
let p = PathBuf::from(format!("/dev/sr{n}"));
if p.exists() {
out.push(p);
}
}
if out.is_empty() {
let alt = PathBuf::from("/dev/cdrom");
if alt.exists() {
out.push(alt);
}
}
out
}
fn open_device(dev: &Path) -> Result<OwnedFd, CdError> {
// O_NONBLOCK is required, not optional: without it `open` on an optical
// device BLOCKS until a disc is loaded and spun up, which on an empty tray
// means hanging forever.
let c = std::ffi::CString::new(dev.as_os_str().as_encoded_bytes())
.map_err(|_| CdError::Open(dev.to_path_buf(), std::io::Error::other("bad path")))?;
let fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_NONBLOCK) };
if fd < 0 {
return Err(CdError::Open(
dev.to_path_buf(),
std::io::Error::last_os_error(),
));
}
Ok(unsafe { <OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd) })
}
/// Read the table of contents, skipping data tracks by their control bits.
pub fn read_toc(dev: &Path) -> Result<Toc, CdError> {
let fd = open_device(dev)?;
let raw = fd.as_raw_fd();
let status = unsafe { libc::ioctl(raw, CDROM_DRIVE_STATUS, 0) };
if status != CDS_DISC_OK {
return Err(CdError::NoDisc);
}
let mut hdr = CdromTocHdr::default();
if unsafe { libc::ioctl(raw, CDROMREADTOCHDR, &mut hdr) } < 0 {
return Err(CdError::Toc(std::io::Error::last_os_error()));
}
let entry_lba = |n: u8| -> Result<(u32, u8), CdError> {
let mut e = CdromTocEntry {
track: n,
format: CDROM_LBA,
..Default::default()
};
if unsafe { libc::ioctl(raw, CDROMREADTOCENTRY, &mut e) } < 0 {
return Err(CdError::Toc(std::io::Error::last_os_error()));
}
Ok((e.addr_lba.max(0) as u32, e.adr_ctrl >> 4))
};
let (leadout_lsn, _) = entry_lba(CDROM_LEADOUT)?;
let mut raw_tracks: Vec<(u8, u32, bool)> = Vec::new();
for n in hdr.trk0..=hdr.trk1 {
let (lba, ctrl) = entry_lba(n)?;
raw_tracks.push((n, lba, ctrl & CTRL_DATA == 0));
}
// A track's length is the gap to whatever starts next — the following
// track, or the lead-out for the last one. The TOC does not carry lengths.
let mut tracks = Vec::with_capacity(raw_tracks.len());
for (i, &(number, start_lsn, is_audio)) in raw_tracks.iter().enumerate() {
let next = raw_tracks
.get(i + 1)
.map(|t| t.1)
.unwrap_or(leadout_lsn);
tracks.push(TocTrack {
number,
start_lsn,
sectors: next.saturating_sub(start_lsn),
is_audio,
});
}
if !tracks.iter().any(|t| t.is_audio) {
return Err(CdError::NotAudio);
}
Ok(Toc {
device: dev.to_path_buf(),
tracks,
leadout_lsn,
})
}
/// Read `frames` sectors starting at `lsn` into `out`, which is resized to fit.
///
/// Retries a few times before giving up: a marginal sector often reads on a
/// second attempt, and the alternative to a retry here is a click. What it
/// will NOT do is return zeros — an unrecoverable sector is an error, and the
/// caller stops the track rather than playing silence over it.
pub fn read_audio(dev: &Path, lsn: u32, frames: u32, out: &mut Vec<u8>) -> Result<(), CdError> {
let frames = frames.clamp(1, MAX_FRAMES_PER_READ);
out.resize(frames as usize * crate::CDDA_SECTOR_BYTES, 0);
let fd = open_device(dev)?;
read_audio_fd(fd.as_raw_fd(), dev, lsn, frames, out)
}
fn read_audio_fd(
raw: libc::c_int,
dev: &Path,
lsn: u32,
frames: u32,
out: &mut [u8],
) -> Result<(), CdError> {
let mut last = std::io::Error::other("no attempt made");
for attempt in 0..3 {
let mut arg = CdromReadAudio {
addr_lba: lsn as i32,
addr_format: CDROM_LBA,
_pad: [0; 3],
nframes: frames as libc::c_int,
buf: out.as_mut_ptr(),
};
if unsafe { libc::ioctl(raw, CDROMREADAUDIO, &mut arg) } >= 0 {
if attempt > 0 {
log::info!("[cdda] sector {lsn} read on attempt {}", attempt + 1);
}
return Ok(());
}
last = std::io::Error::last_os_error();
}
log::warn!(
"[cdda] {} refused sector {lsn} after 3 attempts: {last}",
dev.display()
);
Err(CdError::Read { lsn, source: last })
}
/// An open reader positioned on one track, so a whole track is read through a
/// SINGLE file descriptor instead of re-opening the device 4000 times.
pub struct TrackReader {
fd: OwnedFd,
device: PathBuf,
next_lsn: u32,
end_lsn: u32,
}
impl TrackReader {
pub fn open(dev: &Path, track: &TocTrack) -> Result<Self, CdError> {
Ok(Self {
fd: open_device(dev)?,
device: dev.to_path_buf(),
next_lsn: track.start_lsn,
end_lsn: track.start_lsn + track.sectors,
})
}
pub fn finished(&self) -> bool {
self.next_lsn >= self.end_lsn
}
/// Next chunk of raw CD-DA bytes, up to `MAX_FRAMES_PER_READ` sectors.
/// `Ok(0)` means the track is done.
pub fn next_chunk(&mut self, out: &mut Vec<u8>) -> Result<usize, CdError> {
if self.finished() {
out.clear();
return Ok(0);
}
let frames = (self.end_lsn - self.next_lsn).min(MAX_FRAMES_PER_READ);
out.resize(frames as usize * crate::CDDA_SECTOR_BYTES, 0);
read_audio_fd(
self.fd.as_raw_fd(),
&self.device,
self.next_lsn,
frames,
out,
)?;
self.next_lsn += frames;
Ok(out.len())
}
}
/// Diagnostic: does this buffer look byte-swapped?
///
/// Audio is continuous, so neighbouring samples differ by little; a wrong
/// endianness turns that into noise. Measured on the owner's drive the ratio
/// was 285x in favour of little-endian, so this returns false there. It exists
/// because "every drive is little-endian" is a claim about ONE drive, and the
/// honest way to hold it is as something checkable rather than assumed.
pub fn looks_byte_swapped(pcm: &[u8]) -> bool {
let step = |swap: bool| -> f64 {
let mut total = 0f64;
let mut n = 0u32;
let mut prev: Option<i16> = None;
// Left channel only: 4 bytes per frame, take the first pair.
for f in pcm.chunks_exact(4) {
let s = if swap {
i16::from_be_bytes([f[0], f[1]])
} else {
i16::from_le_bytes([f[0], f[1]])
};
if let Some(p) = prev {
total += (s as i32 - p as i32).unsigned_abs() as f64;
n += 1;
}
prev = Some(s);
}
if n == 0 {
0.0
} else {
total / n as f64
}
};
let le = step(false);
let be = step(true);
be > 0.0 && le > be
}
#[cfg(test)]
mod tests {
use super::*;
fn toc(entries: &[(u8, u32, bool)], leadout: u32) -> Toc {
let mut tracks = Vec::new();
for (i, &(number, start_lsn, is_audio)) in entries.iter().enumerate() {
let next = entries.get(i + 1).map(|e| e.1).unwrap_or(leadout);
tracks.push(TocTrack {
number,
start_lsn,
sectors: next - start_lsn,
is_audio,
});
}
Toc {
device: PathBuf::from("/dev/null"),
tracks,
leadout_lsn: leadout,
}
}
/// The REAL table of contents of the owner's disc, read 2026-08-20.
fn fear_inoculum() -> Toc {
toc(
&[
(1, 0, true),
(2, 46_577, true),
(3, 100_047, true),
(4, 157_373, true),
(5, 218_718, true),
(6, 264_120, true),
(7, 285_735, true),
],
356_568,
)
}
#[test]
fn track_lengths_come_from_the_gaps_and_the_leadout() {
let t = fear_inoculum();
// Track 1 = 10:21, and the LAST track has to reach the lead-out or the
// longest song on the disc would be cut short.
assert_eq!(t.tracks[0].sectors, 46_577);
assert_eq!(t.tracks[0].duration_secs(), 621);
assert_eq!(t.tracks[6].sectors, 70_833);
assert_eq!(t.tracks[6].duration_secs(), 944);
assert_eq!(t.tracks.len(), 7);
}
#[test]
fn a_data_track_is_never_offered_as_audio() {
// Enhanced CD: three songs then a data track.
let t = toc(&[(1, 0, true), (2, 20_000, true), (3, 40_000, false)], 60_000);
assert_eq!(t.tracks.len(), 3);
assert_eq!(t.audio_tracks().count(), 2);
assert!(t.audio_tracks().all(|x| x.number != 3));
}
#[test]
fn the_fingerprint_identifies_the_MEDIUM_not_the_drive() {
let a = fear_inoculum();
let mut b = fear_inoculum();
b.device = PathBuf::from("/dev/sr3");
// Same disc in a different drive: same identity.
assert_eq!(a.fingerprint(), b.fingerprint());
// A different disc with the same track COUNT must not collide — this
// is the case that would resolve a persisted queue onto the wrong
// album.
let other = toc(
&[
(1, 0, true),
(2, 46_500, true),
(3, 100_047, true),
(4, 157_373, true),
(5, 218_718, true),
(6, 264_120, true),
(7, 285_735, true),
],
356_568,
);
assert_ne!(a.fingerprint(), other.fingerprint());
}
#[test]
fn frames_per_track_match_the_wav_the_player_will_be_promised() {
let t = fear_inoculum();
let first = &t.tracks[0];
assert_eq!(first.frames(), 46_577 * 588);
// 10:21 of 44.1k stereo 16-bit — the size the streaming path declares.
assert_eq!(
crate::wav_total_size_16(first.frames(), 2),
44 + 46_577 * 588 * 4
);
}
#[test]
fn a_read_is_clamped_to_what_the_kernel_accepts() {
// The ioctl takes 1..=75 frames; asking for more is not a silent
// truncation somewhere deep, it is clamped here where it is visible.
assert_eq!(MAX_FRAMES_PER_READ, 75);
}
#[test]
fn the_endianness_probe_agrees_with_the_measured_drive() {
// A smooth ramp is what real audio looks like read the RIGHT way.
let mut pcm = Vec::new();
for i in 0..2000i16 {
let s = (i.wrapping_mul(7)) % 3000;
pcm.extend_from_slice(&s.to_le_bytes()); // L
pcm.extend_from_slice(&s.to_le_bytes()); // R
}
assert!(!looks_byte_swapped(&pcm), "little-endian audio flagged as swapped");
let swapped: Vec<u8> = pcm
.chunks_exact(2)
.flat_map(|p| [p[1], p[0]])
.collect();
assert!(looks_byte_swapped(&swapped), "byte-swapped audio not detected");
}
}
+203
View File
@@ -0,0 +1,203 @@
//! Optical media QBZ can PLAY without indexing any of it.
//!
//! Frontend-agnostic (ADR-006): no UI, no player, no Qt. The crate hands back
//! a track list and raw bytes; who plays them, and how, is the frontend's
//! problem.
//!
//! Deliberately does NOT depend on `qbz-dsd`. The dependency runs the other
//! way — `qbz-dsd` adapts this crate's SACD reader to its `DsdDemuxer` trait —
//! because the reverse closes a cycle.
//!
//! Phase 1 covers CD-DA from a real drive. What is not supported is DETECTED
//! and REPORTED, never silently degraded: a data track in a mixed-mode disc is
//! skipped by name, an unreadable sector is an error rather than a hole full
//! of zeros. Silence that pretends to be audio is the one outcome a
//! bit-perfect player must never produce.
#[cfg(target_os = "linux")]
pub mod cdda;
#[cfg(target_os = "linux")]
pub use cdda::{list_devices, read_audio, read_toc, CdError, Toc, TocTrack};
/// Bytes per CD-DA sector (a "frame" in the kernel's vocabulary): 588 stereo
/// 16-bit sample pairs.
pub const CDDA_SECTOR_BYTES: usize = 2352;
/// Sectors per second of CD-DA audio.
pub const CDDA_SECTORS_PER_SEC: u32 = 75;
/// CD-DA is 44 100 Hz, 16-bit, stereo. These are not configurable and any code
/// that treats them as such has misunderstood the format.
pub const CDDA_SAMPLE_RATE: u32 = 44_100;
pub const CDDA_BITS: u16 = 16;
pub const CDDA_CHANNELS: u16 = 2;
/// Seconds of audio in `sectors`, rounded down.
pub fn sectors_to_secs(sectors: u32) -> u64 {
(sectors / CDDA_SECTORS_PER_SEC) as u64
}
/// A 44-byte RIFF/WAVE header for 16-BIT PCM.
///
/// `qbz-dsd` has a `wav_header` already and it is hardcoded to 24-bit — the
/// depth its converter emits. Reusing it for a CD would mean either lying in
/// the header or promoting 16-bit samples to 24, and while that shift is
/// arithmetically lossless it makes the DAC open at 24 bits for a 16-bit
/// source. That is exactly the silent conversion a bit-perfect path must not
/// contain, so a CD gets its own header.
pub fn wav_header_16(total_frames: u64, channels: u16, sample_rate: u32) -> Vec<u8> {
const HEADER_LEN: usize = 44;
let bytes_per_sample = 2u64;
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);
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(&CDDA_BITS.to_le_bytes());
h.extend_from_slice(b"data");
h.extend_from_slice(&data_len.to_le_bytes());
debug_assert_eq!(h.len(), HEADER_LEN);
h
}
/// Total size of the WAV a track becomes — what the streaming player needs as
/// its `content_length`.
pub fn wav_total_size_16(total_frames: u64, channels: u16) -> u64 {
44 + total_frames * channels as u64 * 2
}
/// A CD-DA track, encoded as ONE string so it can ride in `LocalTrack.file_path`
/// and through every store and queue that carries a path today.
///
/// `cdda:/dev/sr0#0+46577` — device, start sector, sector count.
///
/// It is a real type with a single `parse`/`to_path_string` pair rather than
/// ad-hoc `starts_with` tests scattered around, because the scattered version
/// is a known failure mode in this codebase: a second list of prefixes always
/// ends up missing one site, and the fold is silent (see the six "a place that
/// enumerates sources by hand" bugs of 2026-08-20). One parser, one shape.
///
/// It is NOT a filesystem path and nothing may stat, open or canonicalise it.
/// Callers test with [`CdRef::is_cd_path`] BEFORE reaching for the filesystem.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CdRef {
pub device: std::path::PathBuf,
pub start_lsn: u32,
pub sectors: u32,
}
impl CdRef {
pub const SCHEME: &'static str = "cdda:";
/// Cheap test callers use to route BEFORE touching the filesystem.
pub fn is_cd_path(s: &str) -> bool {
s.starts_with(Self::SCHEME)
}
pub fn to_path_string(&self) -> String {
format!(
"{}{}#{}+{}",
Self::SCHEME,
self.device.display(),
self.start_lsn,
self.sectors
)
}
pub fn parse(s: &str) -> Option<Self> {
let rest = s.strip_prefix(Self::SCHEME)?;
let (dev, range) = rest.rsplit_once('#')?;
let (lsn, sectors) = range.split_once('+')?;
if dev.is_empty() {
return None;
}
Some(Self {
device: std::path::PathBuf::from(dev),
start_lsn: lsn.parse().ok()?,
sectors: sectors.parse().ok()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_header_describes_a_cd_and_not_a_dsd_conversion() {
// 10 seconds of CD-DA.
let frames = 44_100u64 * 10;
let h = wav_header_16(frames, 2, 44_100);
assert_eq!(&h[0..4], b"RIFF");
assert_eq!(&h[8..12], b"WAVE");
// bits per sample at offset 34 — 16, never 24.
assert_eq!(u16::from_le_bytes([h[34], h[35]]), 16);
assert_eq!(u32::from_le_bytes([h[24], h[25], h[26], h[27]]), 44_100);
// byte rate = 44100 * 2ch * 2 bytes
assert_eq!(u32::from_le_bytes([h[28], h[29], h[30], h[31]]), 176_400);
// and the sizes agree with the standalone helper
assert_eq!(
wav_total_size_16(frames, 2),
44 + u32::from_le_bytes([h[40], h[41], h[42], h[43]]) as u64
);
}
#[test]
fn a_measured_track_length_converts_to_its_real_duration() {
// Tool — Fear Inoculum, measured off the owner's disc 2026-08-20:
// track 1 spans 46577 sectors and track 7 spans 70833.
assert_eq!(sectors_to_secs(46_577), 621); // 10:21
assert_eq!(sectors_to_secs(70_833), 944); // 15:44
}
#[test]
fn a_cd_reference_round_trips() {
let r = CdRef {
device: std::path::PathBuf::from("/dev/sr0"),
start_lsn: 285_735,
sectors: 70_833,
};
let s = r.to_path_string();
assert_eq!(s, "cdda:/dev/sr0#285735+70833");
assert_eq!(CdRef::parse(&s), Some(r));
assert!(CdRef::is_cd_path(&s));
}
#[test]
fn a_real_path_is_never_mistaken_for_a_disc() {
// The whole point of the scheme: no filesystem path can collide with
// it, so a routing test cannot send a real file down the disc path.
for p in [
"/home/u/Music/a.flac",
"/home/u/cdda:weird/x.dsf",
"plex:1234",
"",
] {
assert!(!CdRef::is_cd_path(p), "{p} was taken for a disc");
assert_eq!(CdRef::parse(p), None);
}
}
#[test]
fn a_malformed_reference_is_rejected_rather_than_guessed() {
for bad in [
"cdda:",
"cdda:/dev/sr0",
"cdda:/dev/sr0#",
"cdda:/dev/sr0#12",
"cdda:/dev/sr0#12+",
"cdda:/dev/sr0#a+b",
"cdda:#0+1",
] {
assert_eq!(CdRef::parse(bad), None, "{bad} parsed");
}
}
}