re: the .slb leading region is 1392+n*2048 — and my fix for it is withdrawn
The structure is exact. In all five resupply banks the first RIFF sits at HEADERLESS_DATA_OFFSET + n*2048, where 1392 is a constant this crate already had and 2048 is the XMA1 packet size: n = 8, 1, 7, 22, 29. No free parameter to tune, and the raw bytes agree -- high entropy from offset 0, then a zero run immediately before the RIFF. VOICE_D_451 is the control, its single packet being all zeros. So I made the obvious fix, emitting that region as a sub-wave, and then withdrew it on two measurements: * It does not recover audio. Coverage went 5.4% -> 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. * It is not narrow. The rule matches 1524 of the 8021 RIFF-bearing entries in sound.pak, including RT* movie banks that decode correctly today. Landing it would have risked a wide regression in order to not-fix five banks. to_xma_riffs is back to its previous behaviour, verified by re-measuring: coverage is 5.4% / 9.7% again. The refuted attempt is recorded in the code beside the branch it would have changed, so the next person does not re-derive the arithmetic and re-make the change. XMA1_PACKET is kept as a named constant because the blast-radius scan uses it. Artifacts: examples/voice_bank_shape.rs (structure), voice_bank_dump.rs (sub-waves for decoding), slb_hybrid_scan.rs (the 1524 count). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
31
crates/sylpheed-formats/examples/slb_hybrid_scan.rs
Normal file
31
crates/sylpheed-formats/examples/slb_hybrid_scan.rs
Normal file
@@ -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}"
|
||||||
|
);
|
||||||
|
}
|
||||||
19
crates/sylpheed-formats/examples/voice_bank_dump.rs
Normal file
19
crates/sylpheed-formats/examples/voice_bank_dump.rs
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,15 @@ fn main() {
|
|||||||
};
|
};
|
||||||
let tail = &bytes[last..];
|
let tail = &bytes[last..];
|
||||||
let zeros = tail.iter().filter(|b| **b == 0).count();
|
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::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
println!(" head[0..48] {hex}");
|
||||||
println!(
|
println!(
|
||||||
"{:<14} {:>7} {:>6} {:>6} {:>6.1}% 1st RIFF @{:>6} data chunks {} head {:>5.1}% zero/{:>3} distinct tail {:>5} B ({:>5.1}% zero) {:?}",
|
"{:<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}"),
|
format!("VOICE_D_{n}"),
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
/// Fixed offset of the raw XMA1 stream in a headerless `.slb` (no `RIFF`).
|
/// Fixed offset of the raw XMA1 stream in a headerless `.slb` (no `RIFF`).
|
||||||
pub const HEADERLESS_DATA_OFFSET: usize = 1392;
|
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
|
/// Voice language for cutscene audio. Only English and Japanese voice exist on
|
||||||
/// the disc (subtitles cover more languages, voice does not).
|
/// the disc (subtitles cover more languages, voice does not).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -138,6 +142,22 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
return out;
|
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;
|
let mut pos = 0usize;
|
||||||
while let Some(ri) = find(slb, b"RIFF", pos) {
|
while let Some(ri) = find(slb, b"RIFF", pos) {
|
||||||
// Parse this sub-wave's fmt + data (declared size is honest per sub-wave).
|
// Parse this sub-wave's fmt + data (declared size is honest per sub-wave).
|
||||||
|
|||||||
@@ -1047,9 +1047,19 @@ premise was wrong.**
|
|||||||
its leading region being 100 % zero / 1 distinct value. 🟡 So the in-game
|
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**
|
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.
|
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 —
|
✅ **(same day) The region's SIZE is now exact**: the first `RIFF` sits at
|
||||||
directory? seek table? raw stream?). Then a human has to listen; audio
|
`1392 + n*2048` in all five banks (n = 8, 1, 7, 22, 29) — 1392 being the
|
||||||
judgement cannot be done in this container. See
|
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).
|
[`voice-bank-leading-region.md`](voice-bank-leading-region.md).
|
||||||
* ❌ **(2026-08-25) My own boot-nav diagnosis, MEASURED AND WITHDRAWN.** I said
|
* ❌ **(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`
|
the run died because `skip_intro.sh` gates the title test at `rmse <= 1500`
|
||||||
|
|||||||
@@ -84,11 +84,49 @@ tested.
|
|||||||
⚠️ This does **not** establish that the binding is right. It removes the only
|
⚠️ This does **not** establish that the binding is right. It removes the only
|
||||||
recorded evidence against it.
|
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 this does not settle
|
||||||
|
|
||||||
* ❔ **What the leading region is.** It is not padding and not a RIFF. Whether it
|
* ❔ **What the leading region holds.** Its *size* is now exact (1392 + n·2048)
|
||||||
is a directory, a seek table, an alternate codec stream, or the audio itself is
|
and it is neither padding nor a RIFF, but it does not decode as XMA1. A seek
|
||||||
unestablished — I did not decode it.
|
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
|
* ❔ 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.
|
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`
|
* ❔ Whether the same leading region exists across the other ~9 500 `sound.pak`
|
||||||
|
|||||||
Reference in New Issue
Block a user