formats: a music bank's third sub-wave was its own header
The port hit `sound_bank_riffs("BGM_103.slb")` returning three against a census
that says two, and refused to guess which to drop. It was our reader.
`to_xma_riffs`'s hybrid branch derives a leading packet stream's start as
`first_riff % XMA1_PACKET`. That is right only when the bank header is smaller
than one 2048-byte packet -- true of the voice banks the branch was written for
(1392/1468/1600/1728), false of a music bank, whose header is exactly five
packets. The modulus returned 0 and the whole 10 240-byte header was emitted as
sub-wave 0.
The header states its own length, so the guard needs no threshold: BE u32 0x800
at +0x18 with the bank id repeated at +0x00 and +0x20, header length in blocks at
+0x24. Disc-wide over sound.pak's 9 519 entries, 28 match at offset 0 -- every
music bank, ids 1001-1023 and 1101-1105 -- and on 28/28 the declared header ends
EXACTLY at the first RIFF. Zero have a gap, so a header and a leading packet
stream never coexist here; zero false positives among the other 9 491.
Controlled rather than argued: decoding the emitted region through the same
chain, on the same bank, in the same run gives 0.009 s of PCM where the bank's
real wave 0 gives 87.744 s against a declared 87.75. The region is also 99.1%
zero bytes. And the oracle had already said two -- the XMA probe at the main menu
saw exactly two streams, at BGM_103's two declared wave sizes.
BGM_106-109 are deliberately NOT in the 28: their entries start mid-bank, so they
have no header at offset 0 and their leading region is real audio. The
VOICE_D_453 recovery is untouched and its tests still pass, 10/10 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
This commit is contained in:
121
docs/re/structures/slb-bank-header-not-a-wave.md
Normal file
121
docs/re/structures/slb-bank-header-not-a-wave.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# ✅ A music bank's "third sub-wave" is its **header**, and the bug was arithmetic
|
||||
|
||||
**Status:** ✅ `CONFIRMED` — **decoded**, with a disc-wide check over all 9 519
|
||||
`sound.pak` entries, a decode control, and independent corroboration from the
|
||||
running game. Fixed in `sylpheed-formats` 2026-08-29.
|
||||
|
||||
**Raised by the port**, on its P6 critical path:
|
||||
`sound_bank_riffs("BGM_103.slb")` returned **three** sub-waves against
|
||||
[`bgm-two-stems.md`](bgm-two-stems.md)'s census, which says a music bank is
|
||||
exactly two. Its exporter was summing all three, so the shipped menu music was
|
||||
the sum of three things where the corpus predicted two. It declined to choose
|
||||
which to drop, which was right — that is a decoding question.
|
||||
|
||||
## The answer
|
||||
|
||||
The third thing is **the bank header**. Not a stem, not an artefact of the disc:
|
||||
our own reader was emitting it.
|
||||
|
||||
`to_xma_riffs` has a hybrid branch for banks that carry a headerless packet
|
||||
stream *before* their first `RIFF` — the fix that recovered `VOICE_D_453`'s line
|
||||
([`slb-data-offset.md`](slb-data-offset.md)). It derives that stream's start as
|
||||
|
||||
```rust
|
||||
first_riff % XMA1_PACKET // XMA1_PACKET = 2048
|
||||
```
|
||||
|
||||
which is correct **only when the bank header is smaller than one packet**. It is,
|
||||
in the voice banks the branch was written for: their headers put the first `RIFF`
|
||||
at 1392, 1468, 1600 or 1728 mod 2048.
|
||||
|
||||
A music bank's header is **exactly five packets — 10 240 bytes** — so the
|
||||
modulus returns **0**, and the branch emitted `slb[0..10240]`: the whole header,
|
||||
as sub-wave 0.
|
||||
|
||||
The header states its own length, so nothing here needs a heuristic:
|
||||
|
||||
```
|
||||
BGM_103.slb
|
||||
+0x00 BE u32 1103 bank id
|
||||
+0x18 BE u32 0x00000800 block size = 2048
|
||||
+0x1c BE u32 7839244 data size
|
||||
+0x20 BE u32 1103 the id again ← signature, with +0x18
|
||||
+0x24 BE u32 5 HEADER LENGTH IN BLOCKS → 5 × 2048 = 10240
|
||||
+0x28 BE u32 0x00100002 16 bit / 2 ch
|
||||
```
|
||||
|
||||
## The disc-wide check
|
||||
|
||||
Over all **9 519** entries of `sound.pak`
|
||||
([`tools/re-capture/slb_segment_phase.py`](../../../tools/re-capture/slb_segment_phase.py)
|
||||
supplies the reader):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| entries matching the header signature at offset 0 | **28** |
|
||||
| ...whose declared header ends **exactly** at the first `RIFF` | **28 / 28** |
|
||||
| ...with a real gap between header and first `RIFF` | **0** |
|
||||
| false positives among the 9 491 others | **0** |
|
||||
|
||||
The 28 are exactly the music banks — ids **1001–1023** and **1101–1105**. So on
|
||||
this disc a bank header at offset 0 and a leading packet stream **never
|
||||
coexist**, and the guard is not a threshold: if a bank states a header, believe
|
||||
it, and there is nothing before the first `RIFF`.
|
||||
|
||||
⚠️ `BGM_106`–`BGM_109` are **not** in the 28 and must not be: their pak entries
|
||||
start mid-bank, so they have no header at offset 0 and their leading region is
|
||||
real audio (the tail of the previous bank). That is the same straddle
|
||||
[`bgm-two-stems.md`](bgm-two-stems.md) already documents.
|
||||
|
||||
## The decode control
|
||||
|
||||
Decoding the emitted region proves it is not audio, and the control is run
|
||||
through **the same chain, on the same bank, in the same invocation**:
|
||||
|
||||
| | bytes | PCM decoded |
|
||||
|---|---|---|
|
||||
| `BGM_103` — what we emitted as "sub-wave 0" | 10 240 | **0.009 s** |
|
||||
| `BGM_103` — its real wave 0 (control) | 3 876 864 | **87.744 s** (declared 87.75) |
|
||||
| `BGM_001` — what we emitted as "sub-wave 0" | 10 240 | **0.009 s** |
|
||||
| `BGM_001` — its real wave 0 (control) | 4 466 688 | **173.809 s** (declared 173.82) |
|
||||
|
||||
FFmpeg `xma1`, mono/stereo taken from the bank's own `fmt `. The region is also
|
||||
**99.1 % zero bytes** (67–93 non-zero of 10 240 across the 28 banks) and its last
|
||||
non-zero byte is at 6431, so its final 1.86 packets are entirely empty.
|
||||
|
||||
## Corroboration from the oracle, which was already in the corpus
|
||||
|
||||
[`bgm-two-stems.md`](bgm-two-stems.md) records that at the **main menu**, with
|
||||
`--xma_param_probe=true`, the decoder was handed **two** stereo 48 kHz streams —
|
||||
of **3 876 864** and **3 930 112** bytes, byte-for-byte `BGM_103`'s two declared
|
||||
waves. A third stem would have been a third stream. The running game was already
|
||||
saying two.
|
||||
|
||||
## The fix
|
||||
|
||||
`slb::bank_header_len` (new, `pub`) reads the signature and returns the declared
|
||||
length; the hybrid branch uses it in preference to the modulus:
|
||||
|
||||
```rust
|
||||
let start = bank_header_len(slb).unwrap_or_else(|| leading_data_offset(ri));
|
||||
if ri > start { /* emit the leading stream */ }
|
||||
```
|
||||
|
||||
Two regression tests in
|
||||
[`tests/slb_leading_segment_disc.rs`](../../../crates/sylpheed-formats/tests/slb_leading_segment_disc.rs):
|
||||
the disc-wide 28/28 identity, and `BGM_103`/`BGM_001` returning exactly two
|
||||
sub-waves at their declared payload sizes. The pre-existing voice-bank tests —
|
||||
`broken_banks_recover_their_line`, `derived_offset_recovers_voice_banks_without_regressing_etc`
|
||||
— still pass, so the `VOICE_D_453` recovery is untouched. 10/10 green with
|
||||
`SYLPHEED_DISC` set.
|
||||
|
||||
## Reach
|
||||
|
||||
* The 28 are the only banks on the disc that state a header at offset 0. A bank
|
||||
format elsewhere with a header ≥ 2048 B that we have not seen would have had
|
||||
the same bug; nothing on this disc does.
|
||||
* This says nothing about **which** of the two remaining waves is which — that is
|
||||
still 🟡 in [`bgm-two-stems.md`](bgm-two-stems.md) (surround-rear pair vs a
|
||||
second intensity layer), and both readings predict playing them together.
|
||||
* It does not change the count for any voice bank: `VOICE_*` entries have no
|
||||
header at offset 0, so their leading region is emitted exactly as before.
|
||||
Reference in New Issue
Block a user