Files
qbz/crates/qbz-disc/examples/sacd_probe.rs
T
vicrodh 06405969e9 feat(disc): read a SACD image's stereo area, measured against two real discs
Adds an ISO 9660 walk (no mounting, no privileges) and a Scarlet Book parser
for the stereo audio area, plus the `DsdDemuxer` adapter that lets every DSD
delivery the player already has — PCM, DoP, native — play a disc image.

The parser exists because the previous account of this format, written in this
repo, was WRONG: it said the per-sector header was a fixed 8 bytes, derived
from looking at one sector. Every structure below was re-derived by a CENSUS —
3 066 893 sectors across both of the owner's Rheingold discs, zero exceptions —
and each claim is falsifiable rather than asserted:

  - byte 0 carries packet_count = (b>>5)&7 and frame_info_count = (b>>2)&7,
    so the header is 1 + pc*2 + fic*3 and takes the values 5, 8 or 10;
  - packet descriptors are BIG-endian, and the length field is ELEVEN bits,
    not the twelve every published layout shows. The first descriptor on both
    discs is 0x181B, whose low twelve bits are 2075 — larger than the sector
    it lives in. With eleven bits it is 27, and 5 + 27 + 2016 = 2048;
  - the proof of the whole decoding is the sum rule
    `header + SUM(packet lengths) == 2048`, which the parser CHECKS on every
    sector rather than trusting;
  - the area is uncompressed, proven from the data and not from a flag: the
    audio-packet bytes divide by 9408 (one uncompressed DSD64 stereo frame)
    exactly — 350 495 frames on Disc 1, 306 696 on Disc 2. DST is
    variable-length; those could not be integers.

Track geometry comes from SACDTRL1/TRL2, tied together by an identity that
holds for 44 of 44 tracks: start_lsn == area_start + start_time * 350. The
ISO 9660 extents mirror SACDTRL1 byte for byte, so the one-sector overlap
between consecutive tracks is the disc's own accounting, not a bug to fix.

The channel layout inside a frame was the last unknown, and three statistical
probes could not settle it — they were measuring the DSD noise floor. What
settled it was physics: converted with the real filter, block-per-channel puts
23 % of its energy above the audible band while byte-interleaving puts 0.2 %
there. A correctly decoded 1-bit stream MUST carry that rising noise floor,
and no filter puts it back once scrambling has folded it away.

Also in this commit, from the same measurements: `2C_TAREA.2CH` on Disc 1 is
recorded in FOUR extents, so `IsoImage::find` REFUSES a multi-extent file
rather than returning its first quarter as if it were the whole thing.

cc
2026-08-21 01:33:59 -06:00

48 lines
2.2 KiB
Rust

//! Read a SACD image's stereo area and pull real DSD out of a track.
//! `cargo run -p qbz-disc --example sacd_probe -- <disc.iso>`
fn main() {
let path = std::env::args().nth(1).expect("usage: sacd_probe <disc.iso>");
let p = std::path::PathBuf::from(&path);
let area = match qbz_disc::sacd::read_area(&p) {
Ok(a) => a,
Err(e) => { println!("area failed: {e}"); return; }
};
println!("album: {:?} | artist: {:?}", area.album, area.artist);
println!("channels {} | area LSN {}..{} | total {:.2}s ({}:{:02})",
area.channels, area.track_start_lsn, area.track_end_lsn,
area.total_playtime_secs,
area.total_playtime_secs as u32 / 60, area.total_playtime_secs as u32 % 60);
println!("{} tracks", area.tracks.len());
for t in area.tracks.iter().take(6) {
println!(" {:2} lsn {:>8} len {:>7} {:>3}:{:02} {}",
t.number, t.start_lsn, t.length_lsn,
t.duration_secs as u32 / 60, t.duration_secs as u32 % 60,
t.title.as_deref().unwrap_or("—"));
}
// Pull audio out of the FIRST track and see whether it looks like DSD.
let t = &area.tracks[0];
let mut r = match qbz_disc::sacd::SacdTrackReader::open(&p, t) {
Ok(r) => r, Err(e) => { println!("reader failed: {e}"); return; }
};
let mut buf = Vec::new();
let mut total = 0usize;
let mut silence = 0usize;
// ~2.8 seconds of audio: enough to see whether it is silence or music.
for _ in 0..10 {
match r.next_chunk(&mut buf, 100) {
Ok(0) => break,
Ok(n) => {
total += n;
silence += buf.iter().filter(|b| **b == 0x99).count();
}
Err(e) => { println!("read failed: {e}"); return; }
}
}
println!("\nextracted {total} bytes of audio payload from track 1");
println!("0x99 (DSD silence) bytes: {silence} ({}%)", 100 * silence / total.max(1));
let frames = total as f64 / qbz_disc::sacd::DSD64_STEREO_FRAME as f64;
println!("that is {frames:.3} DSD64 stereo frames ({:.3}s of audio)", frames / 75.0);
println!("first 32 bytes: {}", buf.iter().take(32)
.map(|b| format!("{b:02x}")).collect::<Vec<_>>().join(" "));
}