Closes the open question at the bottom of slb-data-offset.md. X = (cumulative start of the .pNN segment holding the wave) mod 2048 The XMA packet grid is 2048-aligned inside each individual segment file, but the .pak TOC addresses entries in the flat concatenation at offsets that are themselves multiples of 2048. The segment files are not multiples of 2048 long, so each join shifts the grid by size % 2048 — and the four disc-wide values are exactly the running sums: 1392 = |p00| % 2048; 1468 = +76; 1600 = +132; 1728 = +128 Exact for 7620/7620 banks with a RIFF and 1163/1163 RIFF-less ones via their seek chunk, 0 mismatches. Supersedes both heuristics (the 99.62 % packet scan and the 99.97 % seek-residue rule) and dissolves the 28 ties. The refutation test — an entry straddling a segment join must show two phases in one file — passes on all 3 straddlers. The leading bytes are the previous bank's audio, not a header: byte diversity per offset is indistinguishable from a known packet (101.06 vs 101.90, no fixed field anywhere), the seek packet counts chain exactly across consecutive entries, and the inter-entry bytes no TOC entry claims are 1903/1928 non-zero. Also recorded: the real bank header layout (id, block size 0x800, header size in blocks, XMAWAVEFORMAT), and the loader search — a null result. None of the four values exists as an immediate, a table or a float anywhere in default.xex, which is what a pack-time artifact predicts. Sound subsystem addresses mapped for the next pass. Withdraws the 🟡 "most banks declare more data than they store" finding: declared data sizes are exact (260/260), the bytes are just outside the TOC window. Also withdraws my own "the header is unique, so nothing is shared" inference — the windows tile, they do not overlap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
29 KiB
.slb leading-stream data offset — 1392 was never a constant
⚠️ Read the last section first. A second pass on 2026-08-26 closed the ❔ at the bottom of the original writeup: the offset is
(cumulative start of the.pNNsegment holding the wave) mod 2048, exact for 8 783 of 8 783 banks. Thefirst_riff % 2048rule below is a consequence of that and stays correct where aRIFFexists; the packet-plausibility scan and theseek-residue heuristic are superseded, and the 🟡 "most banks declare moredatathan they store" finding is withdrawn. Jump to X is the.pNNsegment's grid phase.
✅ Settled 2026-08-26, verified by decoding. A bank's leading headerless
packet stream does not start at a fixed offset. It starts at
first_riff % 2048. HEADERLESS_DATA_OFFSET = 1392 is the value that
offset happens to take in <lang>\etc\, and assuming it everywhere starts the
decode mid-packet and throws away almost all of the audio.
The rule
XMA1 packets are 2048 bytes and the leading stream is a whole number of them
ending at the first RIFF. So its start is forced:
start = first_riff % 2048
Disc-wide that lands on exactly four values — 1392, 1468, 1600, 1728 — all
of the form 1392 + 4k. Across the 3 965 Japanese and 3 393 English banks with
a non-empty leading region, no other value occurs:
| 1392 | 1468 | 1600 | 1728 | |
|---|---|---|---|---|
eng\etc, eng\Movie, eng\Briefing |
1 520 | — | — | — |
eng\Voice |
8 | 1 873 | — | — |
jpn\etc |
— | 1 402 | 303 | — |
jpn\Briefing |
— | 71 | — | — |
jpn\Movie |
— | — | 61 | — |
jpn\Voice |
— | — | 2 033 | 95 |
It varies by language and subdirectory, which is why a constant derived
from eng\etc\ looked right for years' worth of the banks anyone had reason to
open.
Verified by decoding, not by arithmetic
The alignment argument alone proves nothing — any offset can be made to "align"
by definition. The test is whether more audio comes out. Decoded through
FFmpeg's xma1 at mono/48 kHz, on a random sample of 140 banks that have a
non-empty leading region:
| outcome | banks |
|---|---|
more audio at ri % 2048 |
85 |
| byte-identical | 54 |
| less audio | 1 |
Median gain among the improved: 70×. The 54 identical ones are the control —
they are the eng\etc-style banks where ri % 2048 is 1392, so the rule
must and does reproduce the old behaviour exactly. Individual cases:
eng\Voice\VOICE_TCAF_592.slb 1 506 -> 97 152 bytes (65x)
jpn\Voice\VOICE_TCAF_592.slb 2 910 -> 127 178 bytes (44x)
eng\etc\VOICE_D_452.slb 30 154 -> 30 154 bytes (unchanged, control)
The one counterexample — ✅ explained
eng\Voice\VOICE_TCAF_608.slb decodes 2 840 bytes at 1392 and 896 at 1468.
It is not a bank where the old constant works and the derived offset fails: both offsets yield well under a tenth of a second from a 38 988-byte region, i.e. both fail, and 1392 merely produces marginally more garbage.
The reason is that the bank is truncated. Its data chunk declares 759 808
bytes and the pak entry holds 8 864 — 99 % short. There is almost nothing
there to decode at any offset. See the section below.
❌ This withdraws my own claim from earlier the same day
sound-pak-contents.md reported that the leading
region rule holds for "0 of 5 100 Japanese banks" and filed a backlog item
saying the Japanese banks were a different, undecoded layout. That was wrong.
The Japanese banks are the same format; only the offset differs. The measurement
behind it was correct — zero of them satisfy (riff − 1392) % 2048 == 0 — but
the conclusion drawn from it was not, and the reason is instructive: I treated
HEADERLESS_DATA_OFFSET as a property of the format when it was a property of
the sample the format was derived from.
The same error was hiding a defect in the English set too: 1 873 eng\Voice
banks sit at 1468 and were being decoded mid-packet just as badly.
The RIFF-less banks had the same bug, plus a worse one
✅ Settled 2026-08-26. 1 495 banks (799 jpn, 696 eng) carry no RIFF at
all and take a separate code path. That path was wrong twice over:
- it used the constant offset, with no
RIFFto derive from; and - it built a stereo
fmtchunk.
Decoded across a random 48-bank sample:
| banks where the old stereo-at-1392 beat the best mono offset | 0 of 48 |
| median gain | 184× |
| range | 25× – 489 344× |
Stereo is the same failure signature recorded for the leading segment: it stops after one frame. Individual banks went from 0–4 816 bytes to 180 000–380 000.
The winning offsets fall out by directory, and they reproduce the
distribution measured independently from the RIFF-bearing banks — which is the
cross-check that makes this more than curve-fitting:
eng\etc 1392 (11/11) eng\Voice 1468 (9/9) eng\Briefing 1392 (2/2)
jpn\Voice 1600 (12/13) jpn\etc 1468 (8/12), 1600 (4)
Note jpn\etc splits, so the path alone is not enough to pick the offset.
Picking the offset without a decoder
An XMA1 packet opens with a big-endian header — 6 bits frame count, 15 bits frame-offset-in-bits, 3 bits metadata, 8 bits packet-skip. At the true offset those fields stay in range packet after packet; one byte off and they do not. Scoring the first 24 packets and taking the best candidate:
7 330 of 7 358 (99.62 %) on the labelled set — every bank that has a
RIFF, where the answer is forced and therefore known. All 28 misses are
ties on the top score; there is not a single case where the scan picks wrongly
with a unique winner. scan_data_offset therefore falls back to 1392 on a tie.
This is used only for the RIFF-less banks. Where a RIFF exists the offset is
derived from it exactly, never scanned.
✅ A second, independent signal — and it breaks the ties
Settled 2026-08-26. The 28 ties needed a different signal, not more of the
same one, and the banks carry one: a seek chunk sitting on a packet
boundary. Its position modulo 2048 therefore is the data offset.
seek at 3 516 / 5 564 / 7 612 / 9 660 / 13 756 / 19 900 — all ≡ 1468 (mod 2048)
On the 6 033 labelled banks that have a seek before their first RIFF,
6 031 agree (99.97 %) and 2 disagree. That is better than the packet scan
and, more importantly, structural rather than statistical — which is why it is
now tried first.
Applied to the packet scan's 28 ties: 26 resolved correctly, 0 wrongly, and
2 with no usable seek. The combined rule — seek residue, else packet
plausibility, else 1392 — scores 7 354 / 7 358 = 99.95 % on the labelled set,
up from 99.62 %.
762 of the 1 495 RIFF-less banks carry a seek, and its residue lands on the
four known offsets there too (1468 ×343, 1600 ×255, 1392 ×148, 1728 ×16), so the
signal is available in the population that needs it.
❌ The header is not audio being discarded
Worth ruling out, since a wrong data offset was the whole subject of this page: if the bytes before the offset were audio, we would be throwing away the start of every clip. Adding 0 to the candidate set and re-running the scan, it wins 6 of 7 358 — noise. The header is genuinely not part of the packet stream. (1 482 banks have an all-zero header; 5 876 have content in it, which is what prompted the check.)
Is that 99.62 % transferable? — checked, and it is conservative
The labelled set has a RIFF; the population the scan actually serves does not.
Since the scan is unbounded it reads past the RIFF on labelled banks, so the
99.62 % could have been borrowing discriminating power that a RIFF-less bank
cannot offer. That would make the headline number optimistic for the only case
it is used in — worth checking before trusting it.
Confining the scan to the leading region drops it to 69.98 % with 1 910 ties, which at first looks like exactly that problem. It is not. Splitting by how much leading audio there is separates the two explanations:
| correct | ties | |
|---|---|---|
| unbounded, all 7 358 labelled banks | 99.62 % | 28 |
| confined to the leading region, all 7 358 | 69.98 % | 1 910 |
| ≥24 packets of leading audio (989 banks), unbounded | 100 % | 0 |
| ≥24 packets of leading audio (989 banks), confined | 100 % | 0 |
The last two rows settle it. Where there is enough audio to score, the
discriminator is perfect whether or not the RIFF is in range — so it is
not leaning on the RIFF. The 69.98 % is an artifact of short leading
regions: with only two or three packets to judge, candidates tie and the
tie-break decides. Unboundedness helps those banks by giving the scan more bytes,
which is why the two columns differ at all.
A RIFF-less bank is a whole pak entry, tens of kilobytes, so 24 packets are
always available — it is always in the 100 % regime. The 99.62 % figure is
therefore conservative for the population the scan is used on, not optimistic.
What this does not settle
-
❔ Why the offset takes those four values, and what the bytes before it are. This was probed and remains open; what is now ruled out is recorded below.
-
The 28 ties. The scan cannot separate them and falls back to 1392, which is right for roughly a third of that population and wrong for the rest.
-
Why the offset takes exactly these four values by directory is still unexplained — see above.
-
Nothing here was run in the game — this is a decoder-side result measured with FFmpeg as the oracle.
🟡 Most banks declare more data than they store
Measured 2026-08-26. Of the 7 586 banks that carry both a RIFF and a
data chunk after it, 5 296 (69.8 %) declare a data size larger than the
bytes actually present in the pak entry. The remaining 2 290 declare less,
which is the ordinary multi-sub-wave case. Not one declares exactly what it
holds.
Worst cases run to 99 % short:
eng\Movie\VOICE_RT16C.slb declared 1 810 432 available 489 392 -73 %
jpn\etc\VOICE_D_589.slb declared 1 177 600 available 6 708 -99 %
eng\Voice\VOICE_TCAF_608.slb declared 759 808 available 8 864 -99 %
This contradicts a claim in the decoder's own comment, which says the
declared size "is honest per sub-wave". It is not, for about seven banks in ten.
The code is nonetheless safe — it clamps the range with .min(slb.len()) — so
this is a documentation defect and an integrity observation, not a crash.
⚠️ Method note on this measurement. My first pass searched for data from
offset 0, which can hit those four bytes by chance inside the leading audio
region and read a garbage length. Re-running it anchored after the first
RIFF changed the count from 5 038 to 5 296 — the flaw was slightly
under-counting, but it could as easily have gone the other way, and an
unanchored chunk search over binary audio is not a safe way to ask this
question.
❔ Why the declared sizes are too large is not settled. Plausible readings — an authoring-time allocation that was never trimmed, or deliberate truncation of unused tails — are guesses; nothing here distinguishes them, and the game has not been observed reading one of these banks.
❔ What the header is — four things it is not
The bytes before the data offset are still unexplained, but the field has been narrowed. Probing the header of banks at each of the four offsets:
- Not a length field. There is no word in the first 64 bytes equal to the
offset, the offset minus 1392, the
RIFFposition or the entry size, in either endianness. The offset has to be derived; it is not read. - Not a seek table or any ascending index. Treated as big-endian words, only about half of consecutive pairs are non-decreasing — which is what random data gives. Every word is distinct and none is zero, across all four offsets.
- Not zero padding, at least not usually: 1 482 of 7 358 banks have an all-zero header, but 5 876 have content in it.
- Not audio being discarded. Adding 0 to the offset candidates, it wins 6 of 7 358 — noise. (Recorded above.)
So it is high-entropy content of a size that is constant per language and subdirectory, carrying no field that names its own length. That combination suggests something the loader knows the size of a priori rather than something self-describing.
First step if this is picked up again: find the loader. SETTINGS.PATH is
game:\dat\sound.pak+ and SETTINGS.PARAM is Pj_Silph.xgs, so there is code
that opens a bank by name and seeks to its data; the constant, or the table it
indexes, should be visible there. That is static PE work
(/work/*.pe, offset = VA − 0x82000000), not another pass over the archive —
this page has taken the byte-level evidence about as far as it goes.
✅ SETTLED 2026-08-26 (second pass) — X is the .pNN segment's grid phase
The open ❔ above ("why the offset takes those four values, and what is in those bytes") is now closed, exactly, with no residual heuristic:
X = (cumulative start of the.pNNsegment file that holds the wave) mod 2048.
segment size size % 2048 cum_start X
sound.p00 267 930 992 1392 0 0
sound.p01 268 404 812 76 267 930 992 1392
sound.p02 268 404 868 132 536 335 804 1468
sound.p03 268 384 384 128 804 740 672 1600
sound.p04 14 903 296 0 1 073 125 056 1728
The four values are the running sums of the segment sizes mod 2048.
1392 = |p00| mod 2048; 1468 = 1392 + 76; 1600 = 1468 + 132;
1728 = 1600 + 128. That identity is not a fit — it is arithmetic, and it comes
out of five file sizes that nothing in this analysis chose.
Reproduce with tools/re-capture/slb_segment_phase.py phases.
Why
The XMA1 packet grid is 2048-aligned inside each individual .pNN file: a
segment file starts at its own offset 0 with a bank header and everything after
is on that file's own 2048 grid. But the .pak TOC addresses entries in the
flat concatenation of p00..p04, and every TOC offset is itself a multiple
of 2048 (checked: 0 of 9 519 entries are misaligned). The segment files are
not multiples of 2048 long, so each join shifts the grid by
size % 2048, and an entry inside segment k sees the accumulated shift.
Nothing in the game computes 1392. It is a build-time artifact of where the packer chose to cut a ~1.07 GB stream into five ~256 MB files.
Verification — 100 %, no exceptions
tools/re-capture/slb_segment_phase.py verify:
| banks | agree | disagree | |
|---|---|---|---|
has a RIFF/WAVE — X read off the wave header |
7 620 | 7 620 | 0 |
RIFF-less — X read off the leading seek chunk |
1 163 | 1 163 | 0 |
This replaces both heuristics in the section above. The packet-plausibility
scan (99.62 %) and the seek-residue rule (99.97 %, combined 99.95 %) are no
longer needed for anything: the offset is now derivable for every bank,
including the 1 495 RIFF-less ones and including the 28 ties the scan could
not break. Those sections stand as an honest record of how the number was
narrowed, not as the recommended method.
The prediction that could have refuted it, and did not
If X really is a per-segment property rather than a per-bank one, then an entry that straddles a segment boundary must show two different phases inside one file. Exactly 3 of 9 518 entries straddle, and they do:
eng\Voice\VOICE_ACRO_010.slb window starts 11 708 B before the p01→p02 join
seek chunk @11 632 ≡ 1392 (mod 2048) <- p01 phase
---- sound.p01 / sound.p02 join at 11 708 ----
RIFF @21 948 ≡ 1468 (mod 2048) <- p02 phase
seek @52 668 ≡ 1468 (mod 2048)
jpn\etc\VOICE_D_149.slb (1468 -> 1600) jpn\Voice\VOICE_TCAF_577.slb (1600 -> 1728)
A single-offset-per-bank model cannot produce that. ⚠️ It also means "the data offset of a bank" is not well defined for those three entries — any decoder that stores one offset per file will decode part of them mid-packet.
✅ What is in those X bytes: audio, from the previous bank
Not a header. They are the tail of the preceding bank's XMA1 packet stream, carried into this window because the TOC window boundary and the bank boundary are different things.
Three independent lines:
-
Statistics are identical to known audio. Over 167
eng\etcbanks, the number of distinct byte values seen at each fixed offset averages 101.06 across[0, 1392)and 101.90 across[1392, 3440)— a region that is certainly one XMA packet. Not one position in[0, 3600)takes ≤8 distinct values, i.e. there is no fixed field anywhere in the header. A real header would show constants. -
The tiling arithmetic is exact. A wave's
seek(XMAdpds) chunk sits immediately after its data and holds oneu32 LEper packet, so it names its own packet count and therefore where its data started. Entry K's last wave overruns the entry; entry K+1 opens with aseekwhose packet count matches, at exactly the overrun distance measured past the TOC window rounded up to 2048:VOICE_D_451 window 67 704 -> padded 69 632 last wave ends at 73 072 VOICE_D_452 leading seek @ 3 440 (14 packets) 73 072 - 69 632 = 3 440 ✅ VOICE_D_452 window 67 704 -> padded 69 632 last wave ends at 103 792 VOICE_D_453 leading seek @34 160 (20 packets) 103 792 - 69 632 = 34 160 ✅tools/re-capture/slb_segment_phase.py chain 'eng\etc\VOICE_D_450.slb' 4. -
The bytes the TOC skips are audio too. Between the end of
VOICE_D_451's stored 67 704 bytes and the start ofVOICE_D_452sit 1 928 bytes of.pNNthat no entry claims. 1 903 of the 1 928 are non-zero — dense audio, not padding. The stream is continuous through them.
❌ Withdrawn: "the header is unique per bank, so it is not shared data"
An earlier step here searched all 1 088 MB of p00..p04 for one bank's exact
1 392-byte header and found one occurrence, its own — and I briefly read
that as ruling out any shared-stream model. It does not. The windows tile,
they do not overlap: the bytes appear once because they are stored once. The
measurement was right, the inference from it was wrong.
✅ The real .slb bank layout (this is what the entries contain)
Each bank is self-contained and starts on its segment's 2048 grid. All fields big-endian.
+0x00 u32 bank / cue id (BGM_001 -> 1001, BGM_105 -> 1105; voice ids run
consecutively in stream order, e.g. 7228, 7229, …)
+0x04 u32 0x11 (17) constant on every bank seen
+0x08 u32 0x20 (32) constant
+0x0C u32 1 constant
+0x10 u32 0x48 (72) constant
+0x14 u32 0
+0x18 u32 0x800 BLOCK SIZE — the 2048 alignment unit
+0x1C u32 data size in bytes ❔ does not equal the sum of the waves; see below
+0x20 u32 bank id again
+0x24 u32 header size in BLOCKS always 5 -> the first wave is at +10240
+0x28 u16 bits per sample (16) u16 channels (2 for BGM, 4 for voice banks)
+0x2C f32 } three floats, 1.0 / 0.5 / 0.1 on BGM_001, 1.0 / 0.4 on BGM_105
+0x30 f32 } ❔ volume / mix, not identified
+0x34 f32 }
... zero fill to 0x2800 (10240)
then, repeated per wave:
RIFF/WAVE exactly 4096 bytes = 2 blocks:
'fmt ' 32 bytes standard little-endian XMAWAVEFORMAT
'Dmmy' 4028 bytes of zero pad, so that…
'data' <size> …audio begins at riff+4096, on the grid
<data> size is EXACTLY packets * 2048
'seek' 8 + 4*packets u32 LE cumulative decoded-sample counts
+0x18 == 0x800 plus bank[0x00] == bank[0x20] is a reliable signature: scanned
at the correct phase it finds every bank and nothing else.
+0x24 is always 5 on this disc, so a bank's own audio starts 10 240 bytes
after its header — but that header is generally not at offset 0 of the pak
entry (see the next section), which is why the value never showed up as a
constant.
The fmt chunk decodes cleanly as XMA1 XMAWAVEFORMAT
eng\etc\VOICE_D_452.slb first wave: tag 0x0165, 16 bits, NumStreams 1,
LoopCount 0, Version 2, PsuedoBytesPerSec 12 212, SampleRate 48 000,
loop 0..0, Channels 1, ChannelMask 1 — mono. BGM_105.slb:
LoopCount 0xFF, PsuedoBytesPerSec 35 879, 48 000 Hz, LoopStart 0x003D72B1,
LoopEnd 0x0168BDBA, Channels 2. So the format was never in doubt; only the
framing was.
❌ Withdrawn: "🟡 Most banks declare more data than they store"
The section above reports that 5 296 of 7 586 banks (69.8 %) declare a data
size larger than the entry holds, and calls the decoder's "honest per sub-wave"
comment a documentation defect. The measurement was right and the reading was
wrong — and the comment it accused was correct.
Declared data sizes are exact. The bytes are simply outside the TOC window,
still present in the .pNN stream. Taking 400 random eng\/jpn\ banks and,
for every last wave that overruns its window, reading the declared range
straight from the segment stream: the wave's seek chunk lands at exactly the
declared end in 260 of 260 cases, 0 failures.
So eng\Voice\VOICE_TCAF_608.slb is not "truncated, 99 % short", and that is
not why it decodes badly — its wave continues into the next window. The
counterexample section above needs revisiting on that basis.
data size is always an exact multiple of 2048, which is the packet count.
❔ Still open — the TOC window is not the bank
The .pak TOC entry for a cue is a window that contains the cue's bank but
is not aligned to it, and the offset drifts entry to entry:
eng\etc\VOICE_D_450.slb bank ids 7212 @ 7 536, 7226 @ 60 784
eng\etc\VOICE_D_451.slb bank id 7228 @ 30 064
eng\etc\VOICE_D_452.slb bank ids 7229 @ 5 488, 7230 @ 48 496
eng\etc\VOICE_D_453.slb bank id 7231 @ 36 208
eng\etc\VOICE_D_454.slb bank id 7232 @ 50 544
Bank ids run consecutively in pak-offset order, so a name→bank mapping exists,
but which bank in a window belongs to the entry's name is not established —
some windows hold two. A window can also cut a bank in half. Since a bank header
can only sit at ≡ X (mod 2048) while a TOC offset is ≡ 0, a bank header can
never be at entry offset 0; the leading region is structural, not accidental.
❔ +0x1C (data size) does not match the sum of the wave data chunks —
VOICE_D_452's bank 7229 declares 38 980 for a 26 624-byte wave. Unexplained.
🟡 The loader — found, mapped, and it does not contain X (null result)
Static search of default.xex (/work/*.pe, /work/xenia-rs/sylpheed.db),
done as the deliberate refutation attempt: if some code computes X, it should be
visible. It is not, and under the segment-phase explanation it should not be.
What was searched, all negative:
- 1392 / 1468 / 1600 / 1728 as immediates (
li/addi/subi/cmpwi/cmplwi/ori/lis, and the negations) over all 25 481 functions: 15 / 1 / 7 / 10 hits, every one accounted for and none audio-related. Only two functions hold ≥2 of the values and both aresubi rN,r1,K/addi r1,rN,Kstack-frame pairs (sub_82766DB0,sub_821DB270, both D3D/shader-compiler code). The most promising-looking hit —cmplwi cr6,r31,0x570at 0x8217FC68, next door to the sound manager — isERROR_FILE_CORRUPT, in a run of0x7B/0x5/0xB7/0x48F=ERROR_INVALID_NAME/ACCESS_DENIED/ALREADY_EXISTS/DEVICE_NOT_CONNECTED: storage-device retry code, not sizes.li r7,1468at 0x827D3BC4 is a__LINE__for an HLSL-compiler assert. - A table of the four values anywhere in the 9 568 256-byte image: 32-bit BE
aligned and unaligned, 32-bit LE, 16-bit both endians, and
float32. Zero clusters holding ≥2 of the four. The only 4-byte-aligned BE occurrence of any of them in the whole image is one1600at 0x820489A0, in a zlib-adjacent globals blob.0x8202D668looks like a hit (… 0570 05BC …) but is a compiler message-offset table — it continues0x664,0x6B8, not0x640,0x6C0— and is immediately followed by"internal error: unknown ". .slb/slb/XACT/XWB/xma/wavebankas strings — absent from the image in ASCII and UTF-16. No 4CC-shaped immediates in the audio code.
The sound subsystem, for whoever picks this up next (all located by residual
__FILE__/debug-printf strings — the image has no user symbols at all:
25 310 of 25 481 functions are sub_XXXXXXXX, RTTI stripped, demangled_names
empty):
| range / address | what |
|---|---|
| 0x82175E00–0x8217DC00 | silph::SoundManager game layer |
| 0x821774A0 | BGM bank-slot manager — holds both the BankSlots::get_bgm_data_area (0x820A18B0) and BankSlots::setup_bgm (0x820A1938) failure strings |
| 0x82179988 | SoundManager::Impl::SetMovieMode, called from the movie handler 0x821B4968 |
| 0x82178F60 | sound config loader — refs SOUNDS/SETTINGS/game:\ at 0x820A1878 |
| 0x82604A00–0x8260E900 | gsfw sound_framework middleware, 383 functions; public entries 0x826069E8 (play/register, r3=15), 0x82606A38 (stop/release), 0x826062D8, 0x82605028; assert sites 0x82608120, 0x8260DF28, 0x8260D8C0, 0x8260D9D0 → the gsfw_object.h / gsfw_objectcore.cpp strings |
| 0x824D1000–0x824DC400 | XMA/XAudio driver — the only kernel audio imports (XMACreateContext wrapper 0x824D3BA8, XAudioRegisterRenderDriverClient 0x824DC280, …) |
The one size-related constant on the bank path is in sub_821774A0: a
128 KB (0x20000) threshold on the loaded resource size, then
addi r4,r29,2047 / addi r11,r3,2047 / clrrwi r11,r11,11 — allocate
size + 2047 and round the pointer up to 2048 — stored at +76 of the slot
struct and handed to a gsfw vtable slot. So the runtime does honour the 2048
grid; it just never needs to name 1392, because the 2048-alignment it applies is
to its own buffer, and the phase is baked into the file at pack time.
🟡 rather than ✅ because the walk from "a bank is opened" to "the first packet
is submitted" was not completed — sub_821774A0 is the BGM path, and no code
was traced that consumes a voice .slb. The claim proven here is the narrower
one: none of the four values exists as a constant or a table anywhere in the
executable. That is consistent with, but does not by itself prove, "the game
never needs X".
Consequences for our decoder
crates/sylpheed-formats/src/slb.rs currently takes the offset from
first_riff % 2048 (right, where a RIFF exists) and from scan_data_offset
otherwise (99.6 % right). Both can be replaced by the exact rule, which needs
one new input: the pak offset of the entry, plus the segment start table —
PakArchive already knows both. Suggested shape:
PakArchiveexposessegment_phase(concat_offset) -> usize.slb::to_xma_rifftakes the phase instead of deriving it.- the 3 straddling entries need the phase looked up per wave, not per file.
⚠️ Also worth revisiting: because the declared data sizes are honest and the
window is not the bank, the reader currently clamps away audio that is really
there. Reading a cue's full audio means reading past the TOC entry into the
.pNN stream.
Not implemented here — this pass was static RE only, and the API change reaches every caller.
Evidence log
- 2026-08-26 —
X = segment_cum_start % 2048.CONFIRMED. 7 620/7 620 banks with aRIFFand 1 163/1 163RIFF-less banks viaseek, 0 mismatches; independently, the four values are the running sums of the five.pNNsizes mod 2048; independently, the 3 straddling entries show the phase step inside one file at the segment join.tools/re-capture/slb_segment_phase.py. - 2026-08-26 — leading X bytes are the previous bank's audio.
CONFIRMED. Byte-diversity identical to a known packet (101.06 vs 101.90, no fixed field);seekpacket counts chain exactly across three consecutive entries; the unclaimed inter-entry.pNNbytes are 1 903/1 928 non-zero. - 2026-08-26 — declared
datasizes are exact, 260/260 checked. Demotes the 🟡 "most banks declare more than they store" reading above. - 2026-08-26 — no
.slbdata offset exists indefault.xex.PROBABLE(exhaustive constant/table/string search; the loader walk itself is unfinished).