The page is 724 lines of chronological record across 16 sections, several of them superseded readings kept for their reasoning. A reader arriving fresh had to read all of it to know what is currently true. Adds a summary table at the top covering the twelve questions the page answers, each with its confidence, plus the wave-enumeration recipe and an explicit note that everything below is history. Also names the four mistakes recorded below so a reader knows they are deliberate rather than stale.
38 KiB
✅✅ SUPERSEDED IN ITS CAUSE (2026-08-26) — see branch
auto/slb-loader. The four offsets are not a header size. They are a packing phase:X = (cumulative start of the .pNN segment holding the bank) mod 2048
segment size cumulative start start mod 2048 sound.p00267 930 992 0 0 sound.p01268 404 812 267 930 992 1392 sound.p02268 404 868 536 335 804 1468 sound.p03268 384 384 804 740 672 1600 sound.p0414 903 296 1 073 125 056 1728 Independently reproduced here: the four values I measured are exactly the running sums of the five segment file sizes, mod 2048. The XMA grid is 2048-aligned inside each
.pNNfile; the segments are not multiples of 2048 long; so every join shifts the phase, and the flat concatenation the TOC addresses inherits the shift.Two things I wrote on this page are therefore wrong in their explanation, even though the measurements stand:
- "It varies by language and subdirectory" — that was a correlation, not a cause. Directories cluster into segments, so the per-directory table is real but explains nothing.
- "The header is high-entropy content of a size the loader must know a priori" — there is no header. Those bytes are the previous bank's audio, which is why they looked like data and had no length field: they are data.
The heuristics below (99.62 % packet scan, 99.97 %
seekresidue, 99.95 % combined) are all superseded by an exact rule, verified 8 783/8 783 on the other branch.slb.rsstill uses the heuristics; the exact fix needsPakArchiveto expose the segment phase, which is an API change.
.slb leading-stream data offset — 1392 was never a constant
✅ 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.
Read this first — the settled state of the .slb container
This page grew as a chronological record, and several of its sections are superseded readings kept for their reasoning. Everything below the summary is history; this table is what is currently true.
| question | answer | confidence |
|---|---|---|
| where a bank's headerless stream starts | first_riff % 2048, or scan_data_offset when there is no RIFF |
✅ |
| why that takes four values (1392/1468/1600/1728) | it is a packing phase: (cumulative .pNN segment start) mod 2048. Not a header. |
✅ |
| what the bytes before it are | the previous bank's audio | ✅ |
| where a wave ends | data_at + declared_size, confirmed by seek magic there — 7 620/7 620 |
✅ |
are the declared data sizes honest |
yes. The TOC window is not the wave boundary. | ✅ |
| a wave's length in seconds | data_bytes / PsuedoBytesPerSec. Not the seek table's sample total, which understates by a median 9.7 %. |
✅ |
| channel count | the byte at RIFF + 49. 2.12 % are stereo; forcing mono yields one frame. |
✅ |
seek chunk layout |
+4 size LE (8 + 4·packets), +8 stream count, +12 packet count LE, +16 cumulative sample table |
✅ |
| which wave an entry names | the first wave starting at/after its offset, or the second when a leading wave + 12 288-byte block precedes — 7 620/7 620 | ✅ |
| the 12 288-byte block | seek chunk + a cue-shaped record + sparse zeros; its first two words are table[-2], table[-1] |
✅ / 🟡 |
| what the block's id field refers to | a real cue, but not this entry's and not a chain | ❔ |
| headerless bank durations | unrecoverable — no seek table to read, and the first seek in the entry usually belongs to the previous bank |
❔ |
Enumerating every wave without the TOC: scan the flat stream for seek,
keep chunks where size == 8 + 4·count and seek_pos − count·2048 ≥ 0. That
yields 9 661 waves with zero false positives across 1.01 GB.
Four mistakes of mine are recorded below rather than deleted, because each one's reasoning is the useful part: reading the packet count big-endian; believing a one-frame decode meant missing audio (it meant stereo); using the seek table's sample total as a duration; and refuting the leading-segment hypothesis with a predicate that was wrong by a constant I had not yet found.
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.
⚠️ Disagreement on the declared-data question — not resolved
auto/slb-loader withdraws the 🟡 finding above that 69.8 % of banks declare
more data than they store, reporting instead that declared sizes are exact
(260/260 checked) and that the extra bytes live outside the TOC window but still
in the .pNN stream — and specifically that VOICE_TCAF_608 is not truncated.
I could not reproduce that, and the arithmetic is against it. Walking that bank's chunks gives a clean, internally consistent structure:
window = [662 403 072, 662 456 412) size 53 340
RIFF at +40 380, its size field 761 360
fmt 32
Dmmy 4 028
data 759 808 <-- declared
The next TOC entry begins at 662 458 368, i.e. 55 296 bytes after this one starts. 759 808 bytes of audio cannot fit there. They would have to span roughly fourteen further TOC windows.
Both readings agree on the underlying fact — the declared size exceeds the TOC window — and differ on what follows from it. Mine said "truncated", which was an over-claim I withdraw: the 1 928 non-zero bytes in the 1 956-byte gap after the window, and the audio-looking bytes at the next entry, are consistent with a bank's data simply continuing past its window. But "declared sizes are exact" requires a wave to span many named entries, which is a much stronger claim than "the bytes are outside the window".
✅ The experiment was run — the data does not span windows
Read VOICE_TCAF_608's declared 759 808 bytes straight out of the flat stream,
ignoring the window boundary, and decoded through FFmpeg's xma1:
| read | bytes in | decoded |
|---|---|---|
data → window end |
8 864 | 896 bytes = 0.01 s |
data → full declared size |
759 808 | 896 bytes = 0.01 s |
control VOICE_D_452, declared fits |
26 624 | 208 970 bytes = 2.18 s |
Reading 86× more bytes yields not one extra sample. The bytes past the
window are not this bank's audio, so "the bytes are outside the TOC window but
still in the .pNN stream" does not hold here — and this is the very bank the
other branch named as not truncated. (No segment join is crossed: the read
sits inside sound.p02, so the packing phase is not a confound.)
So for VOICE_TCAF_608 the audio really is not present. I still withdraw the
word "truncated" as an over-claim about the other 5 295 banks — I measured
that their declared size exceeds their window, not what is in the bytes beyond
it, and I have now tested exactly one of them. What is established is narrower
and worth stating exactly:
- ✅ the declared
datasize exceeds the TOC window for 5 296 of 7 586 banks; - ✅ for
VOICE_TCAF_608the missing bytes cannot be recovered from the stream; - ❔ whether that generalises is untested — the same decode would have to be run across the population, which is the obvious next step and was not done.
❌ Decoded length is not a valid test of where a bank ends — my own test withdrawn
I set out to generalise the VOICE_TCAF_608 result across the 5 296
over-declaring banks, and the first pass looked like a clean reversal: on a
random 60, reading the full declared size instead of stopping at the TOC
window gained audio in 59, median 2.10×, up to 59×. That reads as
"the data really does continue past the window", i.e. the other branch is right
and my truncation reading was wrong.
Then I checked whether the declared size is an honest boundary at all, by reading twice it. If the stream ends where the header says, doubling the input should add little:
| reading 2× the declared size yields >1.5× the audio | 33 of 40 |
| ratio of decoded bytes, 2× input vs 1× | median 1.64, p90 1.75 |
It keeps producing audio indefinitely. XMA1 packets are self-contained, so feeding the decoder the next bank's packets yields perfectly good audio that simply is not this bank's. The decoder cannot tell the difference, and neither can a byte count.
So the 59-of-60 result is withdrawn as evidence. It does not show that the
declared size is honest; it shows only that something decodes after the window,
which was never in doubt — the bytes there are audio, just possibly someone
else's. This is the same error in a new costume as the unanchored data search
earlier on this page: a measurement that returns a plausible number for a
question it cannot actually answer.
What survives:
- ✅
VOICE_TCAF_608is still special, and now more clearly so: for it, reading 86× more bytes gained nothing, where the typical over-declaring bank keeps yielding audio without limit. Whatever is at its offset does not decode at all. - ❔ Whether the declared sizes are honest is unresolved by this method and
cannot be resolved by it. A valid test has to identify the bank boundary
independently of the decoder — the
seek-chunk packet-count chain used onauto/slb-loaderis exactly such a signal, and is the right next step. - ❌ Both my "69.8 % are truncated" and my attempted reversal of it are off the
table. The measured fact is unchanged and narrow: the declared
datasize exceeds the TOC window for 5 296 of 7 586 banks.
❔ The seek chunk's layout — identified, but it does not yield a packet count
The decoder-independent boundary signal this page called for is the seek
chunk. Its shape is now readable; its arithmetic is not.
Immediately after the seek tag sits a little-endian size, then a short header,
then a strictly ascending table:
eng\etc\VOICE_D_452 seek at +3 440 size 64 -> 16 words
eng\Voice\VOICE_TCAF_592 seek at +9 660 size 108 -> 27 words
eng\Voice\VOICE_TCAF_608 seek at +28 092 size 348 -> 87 words
words after seek+8 (big-endian):
0x01000000, <varies>, 0, then ascending: 0, 1572864, 3932160, 6160384, ...
Word 0 is 0x01000000 in every bank examined — a version or entry-size marker.
Word 1 varies and its top byte is 14 / 25 / 85 for the three above. Word 2 is 0,
and the ascending run begins after it.
Two readings tried, both fail:
| reading | D_452 | TCAF_592 | TCAF_608 |
|---|---|---|---|
declared data size ÷ 2048 (packets) |
13 | 17 | 371 |
seek size ÷ 4 (entries) |
16 | 27 | 87 |
| entries − 3 header words | 13 ✅ | 24 ✗ | 84 ✗ |
The third row is the near-miss that would be easy to adopt: it is exactly right
for VOICE_D_452 and wrong for the other two. That is a one-of-three fit, and
this page has already recorded two measurements today that returned plausible
numbers for questions they could not answer — so it is recorded as failed,
not as a rule with exceptions.
The ascending values are not packet indices: the steps (≈1.5–2.2 million) are far too large for a bank of a few hundred packets, so they are sample or fixed-point offsets on some other scale that has not been identified.
What the next attempt should know: the chunk is real, its size field is
little-endian, three header words precede the table, and the entry count is
not the packet count. auto/slb-loader reports chaining seek packet counts
successfully across consecutive entries — whatever field it used is not one of
the two tried here, and reconciling the two readings is the cheapest way in.
❌❌ VOICE_TCAF_608 is NOT truncated — I was decoding it as mono
2026-08-26, resolving the disagreement above in the other branch's favour.
Everything I concluded about this bank was an artefact of a wrong fmt chunk,
and the declared sizes are honest after all.
The seek chunk, read correctly. I read the packet count big-endian. It is
little-endian, and the layout is:
+0 'seek'
+4 u32 LE chunk size (always 8 + 4*packets)
+8 u32 LE stream count (always 1) <-- my "0x01000000" was LE 1 here
+12 u32 LE PACKET COUNT <-- my "<varies>" read big-endian
+16 packets x u32 LE cumulative decoded sample totals
Verified: size == 8 + 4*count on every bank checked. My "entries − 3" reading
matched VOICE_D_452 by coincidence; the real relation is size/4 − 2.
And a seek sits immediately after its own data, so the first seek in
an entry usually belongs to the previous bank — its implied start is negative
(−25 232 for D_452, −145 988 for TCAF_608). I was comparing an entry's first
seek against its first data, which are different waves by construction. That
is why no reading could line up.
The declared sizes are honest — 7 620 / 7 620. For every RIFF-bearing entry
on the disc there is seek magic at exactly data_at + declared_size, and its
packet count × 2048 equals the declared size. Zero failures. For TCAF_608:
probe at 663 207 356 → seek, count 371, 371 × 2048 = 759 808 = declared.
Why it decoded to 896 bytes: its Channels is 2. I decoded it as mono.
Reading it as stereo gives 6 520 176 bytes = 33.96 s — and two independent
length signals in the bank agree: the last cumulative sample 1 626 112 / 48 000 =
33.88 s, and 759 808 / PsuedoBytesPerSec = 33.97 s. The audio was there the
whole time.
170 of 8 021 banks (2.12 %) are stereo — Channels is the byte at
RIFF + 49. That is exactly the 1-in-60 rate of my "gains nothing" outlier.
What this retracts
- ❌ "
VOICE_TCAF_608's missing bytes cannot be recovered from the stream" — wrong, nothing was missing. - ❌ "The declared
datasize exceeds the TOC window for 5 296 banks" stands as a fact about the window, but my framing of it as a problem is withdrawn: the window is simply not the wave boundary, anddata_at + declared_sizeis. - ❌ My
seek-layout write-up above (entry count, "failed readings") was wrong in its endianness and in its pairing assumption. Left in place as a record.
This is the mono/stereo trap already documented on this page — "at two channels every bank yields exactly 1792 bytes, one frame" — met from the other direction. Having written that down, I then spent several passes attributing a one-frame decode to missing data instead of checking the channel count.
Code fixed
to_xma_riffs built the leading segment with a hard-wired mono fmt . It now
reads Channels from the bank's own first RIFF (riff_channels), falling
back to mono only when there is no RIFF to read. 7 disc tests pass.
The decoder-independent boundary, for the record
bytes = u32 LE at seek+12 x 2048 (== the `data` chunk size)
validate = 'seek' magic at data_at + declared_size (7 620/7 620)
samples = the LAST u32 LE entry in the seek table
channels = byte at RIFF + 49 <-- read it, never assume
🟡 Enumerating every wave on the disc, and a bank→wave assignment rule
2026-08-26. auto/slb-loader leaves open "which bank in a window belongs to
the entry's name". This is a measured attempt at it, short of settled.
Every wave can be enumerated without reference to the TOC. Scanning the
1.01 GB flat stream for seek and keeping only chunks satisfying
size == 8 + 4·count with a non-negative implied start gives 9 661 waves and
rejects nothing — not one false positive in a gigabyte of audio. The identity
is that strong. Each wave's extent is then [seek_pos − count·2048, seek_pos).
The assignment rule. Take an entry to name the first wave starting at or
after its offset. Against the 7 620 entries where the answer is independently
known (they carry a RIFF, so the wave is data_at for declared_size):
correct 7 338 / 7 620 = 96.30 %
otherwise 282
The 282 are one class, not a scatter. In every one, the first wave ends
earlier than the RIFF, and the gap between that wave's seek and the first
RIFF is exactly 12 288 bytes — the same value in all 282. A constant that
sharp is structural, not noise. 12 288 is 6 packets, and also 3 × the 4 096-byte
RIFF+Dmmy block the other branch identified.
I first guessed these were leading segments, which would put the earlier wave's
seek at the first RIFF. That is refuted: it happens 0 times out of
282. Whatever occupies those 12 288 bytes is something else.
✅ The 12 288-byte region is a padded metadata block — and the rule is 100 %
I dumped it, as the previous paragraph said to. Across all 282:
- the region from the earlier wave's
seekto the firstRIFFis exactly 12 288 bytes — the same in every one; - it opens with that wave's
seekchunk (240–260 bytes); - the tail after the chunk carries 47–57 non-zero bytes (median 52) and is otherwise zero padding, filling the block out to 12 288.
So an entry may hold a leading wave, then a 12 288-byte padded block, then its
RIFF wave. With that, the assignment rule completes:
| the entry's wave is the first wave at/after its offset | 7 338 (96.30 %) |
| the second — a leading wave + 12 288 block precedes it | 282 |
| neither | 0 |
| first-or-second | 7 620 / 7 620 = 100.00 % |
❌ And this overturns my own refutation from one iteration ago. I proposed
the earlier waves were leading segments, tested it as "the earlier wave's seek
should sit at the first RIFF", got 0 of 282, and recorded the
hypothesis as refuted. The hypothesis was right; my test was wrong by
exactly the padded block — the seek sits at RIFF − 12 288, not at RIFF. A
negative result is only as good as the predicate it tests, and mine was too
strict by a constant I had not yet discovered.
⚠️ Consequence for the open duration question: the rule is now exceptionless on
the checkable set, which is much stronger than the 96.30 % it replaced. It
still cannot be verified on headerless entries — there is no RIFF there to
check against — but "100 % wherever it can be checked, with a named structural
reason for every case" is a materially better basis than before. The trailer's first two words are now identified — see below.
✅🟡 Inside the 12 288-byte block's trailer
2026-08-26. The block is seek chunk, then a trailer of 47–57 non-zero
bytes, then zeros. Two of those bytes-groups are now named.
The trailer opens with two little-endian u32 words, and across all 282:
- ✅
w1is the leading wave's total sample count — 282 / 282, exact. It is the same number as the last entry of that wave's ownseektable, so the block restates the wave's length in a directly readable field. - ✅
w0 < w1in every case, andw1 − w0is always a whole multiple of 512 — 282 / 282. 512 samples is the XMA1 frame, so the difference is a whole number of frames: 9 to 91, median 14.
Examples:
packets w0 w1 leading wave's samples
63 409 600 445 440 445 440
61 375 808 410 624 410 624
12 68 608 75 264 75 264
🟡 What w0 means is not settled. A value that trails the total by a whole
number of frames is the shape of a usable-length or loop-end field — decoder
priming and the final partial frame are both counted in frames — but that is a
reading of the shape, not a measurement, and nothing here distinguishes the
candidates. It is recorded as 🟡 for that reason.
❔ The remaining ~48 non-zero bytes are scattered thinly across the 12 KiB rather than clustered — roughly 17 per KiB in blocks 1, 3 and 7, the rest empty. That is the shape of a sparse table, and it is unidentified.
A correction to how I described this block: I called the region "zero padding" after the seek chunk. It is not padding — it is a sparse structure that is mostly zero. The distinction matters for anyone who tries to skip it.
❌ The seek table's sample total is NOT the wave's length — my durations were low
2026-08-26. I published two artifacts using last cumulative sample ÷ sample rate as the duration. That is wrong, and this is the measurement that shows it.
Trying to pin down w0, I decoded leading waves and found the output matched
neither trailer word. Following that into the RIFF waves, where the extent
is certain, the decoded sample count exceeds the seek table's last cumulative
sample by a median 9.7 % — far too much to be rounding.
The tiebreak is the bank's own PsuedoBytesPerSec field, which gives a
duration as data_bytes / PBPS and is read, not decoded. Over 14 banks:
| comparison | mean absolute difference |
|---|---|
PBPS vs the seek-table duration |
0.287 s |
PBPS vs the decoded duration |
0.007 s |
Two independent quantities — the bank's declared byte rate and an actual FFmpeg
decode — agree with each other to seven milliseconds and both disagree with the
seek total. The seek table's last entry is therefore not the wave's
sample count; it is the last seek point, which stops short of the end.
Corrected, and both artifacts regenerated with data_bytes / PsuedoBytesPerSec:
../data/voice-bank-manifest.txt— total audio 408.3 minutes, not the 390.9 I reported. A 4.3 % understatement in aggregate; the per-bank median error was 9.7 %, but long banks dominate the total and are proportionally more accurate.../data/stage-dialogue-with-durations.txt— every line's timing rises, e.g. "They got Leader!" from 1.78/2.46 s to 2.25/2.93 s.
⚠️ What made this hard to catch: the seek total looked right. It gave
BGM tracks of 2.4 minutes, radio chatter of 2.8 seconds, cutscenes of 11
minutes — a set of numbers that sorted themselves into exactly the right shapes,
which I cited as the chain validating itself. It was validating the structure
and not the scale: a uniform 10 % error preserves every ratio I checked. A
self-consistency check cannot catch a systematic factor, and I should not have
read it as confirmation of the value.
✅ w0 and w1 identified — they are the last two seek-table entries
Settled. Both trailer words are a verbatim copy of the tail of the leading
wave's own seek table:
w0 == table[-2] 282 / 282
w1 == table[-1] 282 / 282
packets w0 w1 table[-2] table[-1]
63 409 600 445 440 409 600 445 440
61 375 808 410 624 375 808 410 624
12 68 608 75 264 68 608 75 264
That closes the question and disposes of the "usable length or loop end"
reading of w0 — it is simply the previous seek point. It also explains,
without needing a separate fact, why w1 − w0 is always a whole multiple of
512: consecutive seek points advance by whole 512-sample XMA1 frames, so any two
adjacent entries differ by a whole number of them. What looked like a meaningful
constraint was a property of the table it was copied from.
Consistent with the section above, neither word is the wave's true length: for
the same banks, data_bytes / PsuedoBytesPerSec gives 481 321 / 410 525 /
81 383 samples against w1 of 445 440 / 410 624 / 75 264. w1 lands within 512
of the true length in only 9 of 282.
🟡 The scattered bytes are an XACT-shaped cue record
2026-08-26. The ~40 remaining non-zero bytes are not noise; they are a small structured record in three runs at roughly +1790, +3840 and +7940 within the block. Two entries side by side:
+1790 2138 00000011 00000020 00000001 00000048
+1814 08000001a0c80000 2138 00000005 0010 0004 3f800000 3f80...
+3838 2138 00000011 00000001 ... 2138 00000011 ... 2138 ffffffff ... 01
+7935 00000001 00000002 00000002 00000064
+1798 214b 00000011 00000020 00000001 00000048 (the next such entry)
Measured across all 282:
the leading 16-bit value names a real SOUNDS cue |
282 / 282 (100 %) |
the block contains 3f800000 3f80… — two IEEE 1.0 floats |
281 |
the block contains the tail 01 / 02 / 02 / 0x64 |
281 |
Two unit floats and a 100 are the shape of volume, pitch and priority —
this reads as an XACT cue record. That is a reading of the shape and is marked
🟡; the values are all defaults, so nothing here varies enough to prove it.
❌ The id is not this entry's own cue. It is a real id, but consistently a different, higher one:
entry BR01_02 id 8501 -> field 8504 = BR02_01
entry BR03_06 id 8518 -> field 8523 = BR04_04
entry BR09_04 id 8553 -> field 8558 = BR10_03
entry VOICE_A_027 id 5027 -> field 5036 = VOICE_A_036
The offsets vary, so it is not a fixed stride.
❌ The chain reading is refuted. I noticed BR09_04's field names BR10_03,
which is itself the next exception entry, and flagged it as suggestive but
untested. Tested: sorting all 282 by offset, entry[i].field == entry[i+1]'s own
id in 8 of 281 (2.8 %) — indistinguishable from coincidence, and BR09_04
was one of the eight. Recording it because it is exactly the kind of pattern
that reads as a discovery when spotted in a sample of one.
The offset from the entry's own id is broadly spread — 7 to 14 and beyond, peaking at 9–11 — so the field is near the entry's id without standing in any fixed relation to it:
BR01_02 own 8501 field 8504 = BR02_01
VOICE_A_027 own 5027 field 5036 = VOICE_A_036
VOICE_A_152 own 5152 field 5166 = VOICE_A_166
❔ Not settled: which cue the id refers to and why, and what the 0x11,
0x20, 0x48, 0x0010, 0x0004 constants are. What is settled is that the
block is a populated metadata record naming a real cue, not padding — which is
the practical point for anyone parsing past it.