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
48 lines
2.2 KiB
Rust
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(" "));
|
|
}
|