Files
Sylpheed/docs/re/structures/slb-bank-header-not-a-wave.md
sylph-decoder f34d24941d 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
2026-08-29 12:30:16 +00:00

5.4 KiB
Raw Permalink Blame History

A music bank's "third sub-wave" is its header, and the bug was arithmetic

Status: CONFIRMEDdecoded, 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'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). It derives that stream's start as

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 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 10011023 and 11011105. 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_106BGM_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 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 (6793 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 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:

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: 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 (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.