The trailer opens with two little-endian u32 words. w1 is the leading wave's total sample count -- 282/282 exact, the same number as the last entry of that wave's own seek table. And w0 < w1 with w1-w0 always a whole multiple of 512 (282/282), i.e. a whole number of XMA1 frames, 9 to 91 with a median of 14. What w0 means is left at yellow: trailing the total by a whole number of frames is the shape of a usable-length or loop-end field, but that is a reading of the shape rather than a measurement, and nothing here separates the candidates. Also corrects my own description: I called the region after the seek chunk 'zero padding'. It is not padding -- about 48 non-zero bytes are scattered thinly across the 12 KiB, roughly 17 per KiB in blocks 1, 3 and 7. That is a sparse table, still unidentified, and the distinction matters to anyone skipping it.
604 lines
30 KiB
Markdown
604 lines
30 KiB
Markdown
> ✅✅ **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.p00` | 267 930 992 | 0 | **0** |
|
||
> | `sound.p01` | 268 404 812 | 267 930 992 | **1392** |
|
||
> | `sound.p02` | 268 404 868 | 536 335 804 | **1468** |
|
||
> | `sound.p03` | 268 384 384 | 804 740 672 | **1600** |
|
||
> | `sound.p04` | 14 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 `.pNN` file*; 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 % `seek` residue, 99.95 %
|
||
> combined) are all superseded by an exact rule, verified 8 783/8 783 on the
|
||
> other branch. `slb.rs` still uses the heuristics; the exact fix needs
|
||
> `PakArchive` to 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.
|
||
|
||
## 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`](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:
|
||
|
||
1. it used the constant offset, with no `RIFF` to derive from; and
|
||
2. it built a **stereo** `fmt` chunk.
|
||
|
||
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 `RIFF` position 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 `data` size exceeds the TOC window for 5 296 of 7 586 banks;
|
||
* ✅ for `VOICE_TCAF_608` the 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_608` is 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 on
|
||
`auto/slb-loader` is 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 `data` size
|
||
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 `data` size 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, and `data_at + declared_size` is.
|
||
* ❌ 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 `seek` to the first `RIFF` is **exactly
|
||
12 288 bytes — the same in every one**;
|
||
* it opens with that wave's `seek` chunk (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**:
|
||
|
||
* ✅ **`w1` is the leading wave's total sample count** — 282 / 282, exact. It is
|
||
the same number as the last entry of that wave's own `seek` table, so the
|
||
block restates the wave's length in a directly readable field.
|
||
* ✅ **`w0 < w1` in every case**, and **`w1 − w0` is 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.
|