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
69 lines
2.1 KiB
Rust
69 lines
2.1 KiB
Rust
//! 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)
|
|
);
|
|
}
|