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
33 lines
1.4 KiB
Rust
33 lines
1.4 KiB
Rust
//! Walk a real ISO without mounting it.
|
|
//! `cargo run -p qbz-disc --example iso_probe -- <path.iso>`
|
|
fn main() {
|
|
let path = std::env::args().nth(1).expect("usage: iso_probe <file.iso>");
|
|
let p = std::path::PathBuf::from(&path);
|
|
let mut iso = match qbz_disc::iso9660::IsoImage::open(&p) {
|
|
Ok(i) => i,
|
|
Err(e) => { println!("open failed: {e}"); return; }
|
|
};
|
|
println!("root: {:?}", iso.root());
|
|
let root = iso.root().clone();
|
|
for e in iso.read_dir(&root).unwrap_or_default() {
|
|
println!(" /{:<16} lsn {:>8} {:>12} bytes {}", e.name, e.lsn, e.size,
|
|
if e.is_dir { "DIR" } else { "" });
|
|
}
|
|
if let Ok(Some(audio)) = iso.find("/2C_AUDIO") {
|
|
println!("/2C_AUDIO:");
|
|
let kids = iso.read_dir(&audio).unwrap_or_default();
|
|
for e in kids.iter().take(6) {
|
|
println!(" {:<16} lsn {:>8} {:>12} bytes", e.name, e.lsn, e.size);
|
|
}
|
|
println!(" … {} entries total", kids.len());
|
|
}
|
|
// The structure the SACD reader will actually ask for.
|
|
for want in ["/MASTER1.TOC", "/2C_AUDIO/2C_AREA1.TOC", "/2C_AUDIO/TRACK001.2CH"] {
|
|
match iso.find(want) {
|
|
Ok(Some(e)) => println!("find {want} -> lsn {} size {}", e.lsn, e.size),
|
|
Ok(None) => println!("find {want} -> NOT FOUND"),
|
|
Err(e) => println!("find {want} -> error {e}"),
|
|
}
|
|
}
|
|
}
|