diff --git a/crates/sylpheed-formats/examples/slb_hybrid_scan.rs b/crates/sylpheed-formats/examples/slb_hybrid_scan.rs new file mode 100644 index 0000000..7c82825 --- /dev/null +++ b/crates/sylpheed-formats/examples/slb_hybrid_scan.rs @@ -0,0 +1,31 @@ +//! How many `.slb` banks would a "leading headerless stream" rule affect? +//! +//! The rule fires when the first `RIFF` sits at exactly +//! `HEADERLESS_DATA_OFFSET + n*XMA1_PACKET` with a non-zero leading region. +//! Before trusting it, count how many banks it would change โ€” including the +//! `RT*` movie banks that already decode correctly today. +use sylpheed_formats::{slb, PakArchive}; + +fn main() { + let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC"); + let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak"); + let (mut total, mut has_riff, mut hybrid, mut hybrid_nonzero) = (0, 0, 0, 0); + for e in snd.entries() { + let Ok(b) = snd.read(e) else { continue }; + total += 1; + let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { continue }; + has_riff += 1; + if ri > slb::HEADERLESS_DATA_OFFSET + && (ri - slb::HEADERLESS_DATA_OFFSET) % slb::XMA1_PACKET == 0 + { + hybrid += 1; + if b[slb::HEADERLESS_DATA_OFFSET..ri].iter().any(|x| *x != 0) { + hybrid_nonzero += 1; + } + } + } + println!( + "sound.pak entries {total}; with a RIFF {has_riff}; \ + first RIFF at 1392+n*2048 {hybrid}; of those with a NON-ZERO leading region {hybrid_nonzero}" + ); +} diff --git a/crates/sylpheed-formats/examples/voice_bank_dump.rs b/crates/sylpheed-formats/examples/voice_bank_dump.rs new file mode 100644 index 0000000..87dd11e --- /dev/null +++ b/crates/sylpheed-formats/examples/voice_bank_dump.rs @@ -0,0 +1,19 @@ +//! Dump each resupply voice bank's sub-waves to `.xma` RIFFs for decoding. +use sylpheed_formats::{slb, PakArchive}; + +fn main() { + let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC"); + let out = std::env::var("OUT_DIR_X").unwrap_or_else(|_| "/tmp/voicebanks".into()); + std::fs::create_dir_all(&out).unwrap(); + let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak"); + for n in 450..=454 { + let path = format!("eng\\etc\\VOICE_D_{n}.slb"); + let Some(entry) = snd.find_by_name(&path) else { continue }; + let bytes = snd.read(entry).expect("read"); + for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() { + let f = format!("{out}/VOICE_D_{n}_{i}.xma"); + std::fs::write(&f, r).unwrap(); + println!("{f} {} bytes", r.len()); + } + } +} diff --git a/crates/sylpheed-formats/examples/voice_bank_shape.rs b/crates/sylpheed-formats/examples/voice_bank_shape.rs index f97833b..585de42 100644 --- a/crates/sylpheed-formats/examples/voice_bank_shape.rs +++ b/crates/sylpheed-formats/examples/voice_bank_shape.rs @@ -55,6 +55,15 @@ fn main() { }; let tail = &bytes[last..]; let zeros = tail.iter().filter(|b| **b == 0).count(); + // Dump the first bytes of the leading region so its structure is + // visible rather than guessed at. + let hex: String = head + .iter() + .take(48) + .map(|b| format!("{b:02x}")) + .collect::>() + .join(" "); + println!(" head[0..48] {hex}"); println!( "{:<14} {:>7} {:>6} {:>6} {:>6.1}% 1st RIFF @{:>6} data chunks {} head {:>5.1}% zero/{:>3} distinct tail {:>5} B ({:>5.1}% zero) {:?}", format!("VOICE_D_{n}"), diff --git a/crates/sylpheed-formats/src/slb.rs b/crates/sylpheed-formats/src/slb.rs index addd12a..71a2552 100644 --- a/crates/sylpheed-formats/src/slb.rs +++ b/crates/sylpheed-formats/src/slb.rs @@ -23,6 +23,10 @@ /// Fixed offset of the raw XMA1 stream in a headerless `.slb` (no `RIFF`). pub const HEADERLESS_DATA_OFFSET: usize = 1392; +/// XMA1 packet size. A headerless stream is always a whole number of these, which +/// is how a leading stream is told apart from arbitrary bytes before a `RIFF`. +pub const XMA1_PACKET: usize = 2048; + /// Voice language for cutscene audio. Only English and Japanese voice exist on /// the disc (subtitles cover more languages, voice does not). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -138,6 +142,22 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec> { } return out; } + // โŒ A "leading headerless stream" rule was tried here and WITHDRAWN. + // + // The structure is real: in all five resupply banks the first `RIFF` sits at + // exactly `HEADERLESS_DATA_OFFSET + n*XMA1_PACKET` (n = 8, 1, 7, 22, 29), and + // 87 % of `VOICE_D_453` lies in front of it. Emitting that region as a + // sub-wave raised byte coverage from 5.4 % to 89.9 %. + // + // But byte coverage was the wrong success metric. The emitted streams decode + // to **1792 PCM bytes** โ€” silence โ€” through the same FFmpeg path that decodes + // the RIFF sub-waves fine, so the region is not XMA1 under the synthesised + // format. And the rule is not narrow: it matches **1524 of the 8021** + // RIFF-bearing entries in `sound.pak`, including `RT*` banks that decode + // correctly today. Landing it would have risked a large regression to fix + // five banks it does not actually fix. + // + // See docs/re/voice-bank-leading-region.md. let mut pos = 0usize; while let Some(ri) = find(slb, b"RIFF", pos) { // Parse this sub-wave's fmt + data (declared size is honest per sub-wave). diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c708843..d6adc7e 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1047,9 +1047,19 @@ premise was wrong.** its leading region being 100 % zero / 1 distinct value. ๐ŸŸก So the in-game verdict tested a decode that had thrown away most of the bank and is **not** evidence against the binding โ€” though it does not confirm it either. - โ–ถ๏ธ First step: decode the leading region (it is not padding and not a RIFF โ€” - directory? seek table? raw stream?). Then a human has to listen; audio - judgement cannot be done in this container. See + โœ… **(same day) The region's SIZE is now exact**: the first `RIFF` sits at + `1392 + n*2048` in all five banks (n = 8, 1, 7, 22, 29) โ€” 1392 being the + crate's own `HEADERLESS_DATA_OFFSET` and 2048 the XMA1 packet size. No free + parameter. + โŒ **But my fix for it is WITHDRAWN.** Emitting that region as a sub-wave took + `VOICE_D_453` from 5.4 % to 89.9 % byte coverage โ€” and the stream decodes to + **1792 PCM bytes**, silence, while the RIFF sub-waves decode to 150โ€“270 KB. + Byte coverage was the wrong success metric. The rule also matches **1524 of + 8021** RIFF-bearing `sound.pak` entries, including `RT*` banks that work today, + so it risked a wide regression to not-fix five banks. + โ–ถ๏ธ Next cheap probe: vary the synthesised `fmt` (channels / streams / sample + rate) instead of assuming the container. Then a human has to listen; audio + judgement cannot be done here. See [`voice-bank-leading-region.md`](voice-bank-leading-region.md). * โŒ **(2026-08-25) My own boot-nav diagnosis, MEASURED AND WITHDRAWN.** I said the run died because `skip_intro.sh` gates the title test at `rmse <= 1500` diff --git a/docs/re/voice-bank-leading-region.md b/docs/re/voice-bank-leading-region.md index e10671c..7b76a2c 100644 --- a/docs/re/voice-bank-leading-region.md +++ b/docs/re/voice-bank-leading-region.md @@ -84,11 +84,49 @@ tested. โš ๏ธ This does **not** establish that the binding is right. It removes the only recorded evidence against it. +## โœ… The region's structure โ€” and โŒ my own fix for it, withdrawn + +The leading region is not shapeless. In **all five** banks the first `RIFF` sits +at exactly `HEADERLESS_DATA_OFFSET + n * 2048` โ€” where `HEADERLESS_DATA_OFFSET` +(1392) is a constant this crate already had, and 2048 is the XMA1 packet size: + +| bank | first RIFF | โˆ’ 1392 | รท 2048 | +|---|---|---|---| +| `VOICE_D_450` | 17 776 | 16 384 | **8** | +| `VOICE_D_451` | 3 440 | 2 048 | **1** | +| `VOICE_D_452` | 15 728 | 14 336 | **7** | +| `VOICE_D_453` | 46 448 | 45 056 | **22** | +| `VOICE_D_454` | 60 784 | 59 392 | **29** | + +Exact on 5/5, with no free parameter to tune. The byte layout is a 1392-byte +header, a whole number of 2048-byte packets, then the RIFF section โ€” and the +raw bytes agree: high-entropy from offset 0, then a zero run immediately before +the `RIFF`. `VOICE_D_451` is again the control: its one packet is all zeros. + +**So I made the obvious fix โ€” emit that region as a sub-wave โ€” and then withdrew +it.** Two measurements killed it: + +* **It does not recover audio.** Coverage rose from 5.4 % to 89.9 % for + `VOICE_D_453`, but the emitted stream decodes through FFmpeg to **1792 PCM + bytes** โ€” silence โ€” while the RIFF sub-waves from the same banks decode to + 150โ€“270 KB. Byte coverage was the wrong success metric, and it looked like + progress. The region is shaped like a packet stream but is not XMA1 under the + synthesised format. +* **It is not narrow.** The rule matches **1524 of the 8021** RIFF-bearing + entries in `sound.pak` (1524 with a non-zero leading region), including `RT*` + movie banks that decode correctly today. Landing it would have risked a wide + regression in order to not-fix five banks. + +The refuted attempt is recorded in `slb.rs` beside the code, so the next person +does not re-derive the arithmetic and re-make the same change. + ## What this does not settle -* โ” **What the leading region is.** It is not padding and not a RIFF. Whether it - is a directory, a seek table, an alternate codec stream, or the audio itself is - unestablished โ€” I did not decode it. +* โ” **What the leading region holds.** Its *size* is now exact (1392 + nยท2048) + and it is neither padding nor a RIFF, but it does not decode as XMA1. A seek + table, a different codec, or a different channel/rate configuration all remain + open. The next cheap probe is to vary the synthesised `fmt` (channels, streams, + sample rate) rather than to assume the container. * โ” Whether `hokyu_DS_s13A` really plays `VOICE_D_452`. That needs the leading region decoded *and* a human listening; audio judgement cannot be done here. * โ” Whether the same leading region exists across the other ~9 500 `sound.pak`