to_xma_riffs now emits the leading headerless segment when it sits at a whole number of XMA1 packets and carries a non-zero byte. VOICE_D_453 goes from a 0.14 s trailing fragment to a 45116-byte leading sub-wave that dominates it. I withdrew this exact change earlier for two reasons. Both are now answered rather than argued away: * "It recovers no audio" -- it used the STEREO format. At two channels every bank yields exactly 1792 bytes, one frame, whatever its size. Mono yields up to 113x more. * "It matches 1524 of 8021 RIFF-bearing entries" -- the byte-level reach is still 1524, but the audible reach is not. Across the 84 movie-bound banks the segment adds >1 s to exactly 7, the hokyu_*_H tankers on D_453/D_454 -- precisely the broken ones -- and <=0.25 s to 66 of the rest. The largest non-resupply addition is S04A at +0.66 s on a 256 s movie. The safety oracle is recorded with its limits: 8 of the 84 banks ALREADY exceed their movie's duration before the change, by hundredths of a second, so it cannot resolve differences at that scale. It establishes scoping, not correctness. Callers clamp to the movie length regardless. VOICE_D_451's all-zero leading region is skipped by the non-zero guard, so the rule cannot prepend silence to a bank that does not need it. Pinned, as is the packet arithmetic (n = 8, 1, 7, 22, 29) which has no tunable. slb_disc, movie_subtitle_disc and movie_manifest_disc all still pass. NOT verified by ear -- that needs a human. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
20 KiB
The resupply voice banks — the decoder discards up to 87 % of them
Status: ✅ FIXED — to_xma_riffs now emits the leading segment as mono, and
VOICE_D_453/454 recover their line (tests/slb_leading_segment_disc.rs).
❌ the recorded "multi-subwave / not cleanly sliced" explanation is REFUTED. ✅ each shared bank is one generic line, which explains the
in-game verdict that rejected the hokyu_DS_s13A binding — the line really is
generic. ❌ my own "audio is missing" conclusion is RETRACTED: a subtitle cue
is a START time, not a point inside the clip, and under the correct reading every
bank fits at ordinary 48 kHz.
Artifact: crates/sylpheed-formats/examples/voice_bank_shape.rs.
Why this was worth chasing
The record-table decode gives a direct binding for a movie the corpus records as unbound:
S13_SUPPLY_ACROPOLIS MOVIE = hokyu_DS_s13A.wmv VOICETRACK = VOICE_D_452
crates/sylpheed-formats/tests/movie_manifest_disc.rs asserts the opposite —
voice_token == None — with the note that extending unbound movies by shared
demo line was "verified WRONG against the running game". That inference
predicted the same value the disc actually stores, so the two are in direct
conflict, and it is the only place on the disc where a runtime observation
disagrees with the record table.
First, the shape of the data. The resupply banks are shared: five slots bind
VOICE_D_452 (S04, S07, S08, S12, S13), five bind VOICE_D_451, four bind
VOICE_D_450, four VOICE_D_453, three VOICE_D_454 — 21 hokyu slots over five
banks. The movies repeat too (hokyu_DS_s07A.wmv serves S07 and S12). These are
generic resupply cutscenes, not per-stage recordings.
❌ What was recorded, and why it is wrong
structures/movie-subtitles.md notes that 450=2.8 s, 451=1.6 s, 452=2.2 s
but 453=0.14 s and 454=0.43 s — "far too short for the spoken line, so
these .slb banks are likely multi-subwave / not cleanly sliced".
Measured, that is not it. Every RIFF present in every bank is found and
parsed — the count of RIFF magics equals the number of sub-waves recovered,
in all five banks:
| bank | bytes | RIFF magics | sub-waves recovered | bytes covered |
|---|---|---|---|---|
VOICE_D_450 |
65 652 | 1 | 1 | 56.2 % |
VOICE_D_451 |
67 704 | 2 | 2 | 64.8 % |
VOICE_D_452 |
67 704 | 2 | 2 | 46.7 % |
VOICE_D_453 |
53 340 | 1 | 1 | 5.4 % |
VOICE_D_454 |
71 808 | 1 | 1 | 9.7 % |
Nothing is being missed between sub-waves, and nothing is lost after them:
the last data chunk ends exactly at EOF in four of the five banks.
✅ The real defect: a leading region before the first RIFF
slb::to_xma_riffs locates audio by searching for the RIFF magic. In these
banks a large region precedes it, and the search skips the lot:
| bank | first RIFF at | share of file before it | that region |
|---|---|---|---|
VOICE_D_451 |
3 440 | 5 % | 100.0 % zero, 1 distinct byte — real padding |
VOICE_D_452 |
15 728 | 23 % | 78.3 % zero, 256 distinct |
VOICE_D_450 |
17 776 | 27 % | 69.1 % zero, 256 distinct |
VOICE_D_453 |
46 448 | 87 % | 27.4 % zero, 256 distinct |
VOICE_D_454 |
60 784 | 85 % | 21.1 % zero, 256 distinct |
VOICE_D_451 is the control: its leading region really is padding, and its
coverage is unremarkable. The other four have high-entropy content there — 46 KB
in 453, 60 KB in 454 — which the decoder throws away. That is exactly the
size needed to explain a "line" that decodes to 0.14 s.
🟡 What this does to the conflict
The in-game test that rejected this binding listened to a decode that had discarded most of the bank, for precisely the two-bank class involved. A correct bank played from the wrong region sounds exactly like "the wrong line", so the verdict does not refute the disc's binding — it is evidence about the decoder, not about the mapping.
Note also what was actually rejected: a value derived by inference from a shared demo id. The record table supplies the same value as a stored field. Those are different kinds of evidence for the same claim, and only the first was tested.
⚠️ This does not establish that the binding is right. It removes the only recorded evidence against it.
✅ The region's structure — and ❌ my own fix for it, withdrawn
The leading region is not shapeless. In all five banks the first RIFF sits
at exactly HEADERLESS_DATA_OFFSET + n * 2048 — where HEADERLESS_DATA_OFFSET
(1392) is a constant this crate already had, and 2048 is the XMA1 packet size:
| bank | first RIFF | − 1392 | ÷ 2048 |
|---|---|---|---|
VOICE_D_450 |
17 776 | 16 384 | 8 |
VOICE_D_451 |
3 440 | 2 048 | 1 |
VOICE_D_452 |
15 728 | 14 336 | 7 |
VOICE_D_453 |
46 448 | 45 056 | 22 |
VOICE_D_454 |
60 784 | 59 392 | 29 |
Exact on 5/5, with no free parameter to tune. The byte layout is a 1392-byte
header, a whole number of 2048-byte packets, then the RIFF section — and the
raw bytes agree: high-entropy from offset 0, then a zero run immediately before
the RIFF. VOICE_D_451 is again the control: its one packet is all zeros.
So I made the obvious fix — emit that region as a sub-wave — and then withdrew it. Two measurements killed it:
- It does not recover audio. Coverage rose from 5.4 % to 89.9 % for
VOICE_D_453, but the emitted stream decodes through FFmpeg to 1792 PCM bytes — silence — while the RIFF sub-waves from the same banks decode to 150–270 KB. Byte coverage was the wrong success metric, and it looked like progress. The region is shaped like a packet stream but is not XMA1 under the synthesised format. - It is not narrow. The rule matches 1524 of the 8021 RIFF-bearing
entries in
sound.pak(1524 with a non-zero leading region), includingRT*movie banks that decode correctly today. Landing it would have risked a wide regression in order to not-fix five banks.
The refuted attempt is recorded in slb.rs beside the code, so the next person
does not re-derive the arithmetic and re-make the same change.
❌ RETRACTED — "audio is missing". The cue is a START time.
This is the load-bearing error of this whole file, and it is mine. I treated a subtitle cue as a timestamp that must fall inside the voice clip, and concluded that a 0.07 s clip could not host a cue at 4.70 s.
A cue is when the line starts. The voice plays from the cue, so the clip only has to fit in the window between the cue and the end of the movie. Under that reading every bank fits comfortably — at plain 48 kHz:
| bank | samples | at 48 kHz | cue | window to movie end | fits |
|---|---|---|---|---|---|
VOICE_D_450 |
158 967 | 3.31 s | 4.00 s | 5.30 s | ✅ |
VOICE_D_451 |
76 084 | 1.59 s | 3.70 s | 5.60 s | ✅ |
VOICE_D_452 |
119 562 | 2.49 s | 0.00 s | 8.34 s | ✅ |
VOICE_D_453 |
108 608 | 2.26 s | 4.70 s | 4.60 s | ✅ |
VOICE_D_454 |
167 828 | 3.50 s | 0.00 s | 9.50 s | ✅ |
2–3.5 s is also the right length for the lines themselves ("Rhino 3 has landed. Commencing resupply."). So there is no evidence any audio is missing, and the sample rate is plausibly the ordinary 48 kHz.
❌ The 17 091 – 20 563 Hz window from the previous entry is void with it: its lower bound came from the same misreading.
What survives from that work: the leading region is XMA1 mono, the decode runs to the final frame, and the cue values really are seconds. Those were measured. Only the interpretation of what a cue means was wrong — and it was wrong for three iterations, through two write-ups that each called the result "proven".
The original argument, kept
Below is what I wrote when I believed a cue had to fall inside the clip.
The corpus's original wording was that 0.14 s is "far too short for the spoken line". That is a judgement, and judgements about audio cannot be made in this container. The subtitle tracks settle it instead: each carries cue start times, and a subtitle that appears at t seconds cannot sit inside a clip shorter than t.
Decoded with FFmpeg (measured, not estimated from a compression ratio — the
first version of this used an 8:1 guess and that is not good enough to hang a
conclusion on), artifact examples/voice_len_vs_subs.rs:
| movie | bank | last cue | decoded audio | verdict |
|---|---|---|---|---|
hokyu_LS_s02A |
D_450 |
4.00 s | 1.41 s | audio missing |
hokyu_LS_s09A |
D_451 |
3.70 s | 1.81 s | audio missing |
hokyu_LS_s02H |
D_453 |
4.70 s | 0.07 s | audio missing |
hokyu_DS_s13A |
D_452 |
0.00 s | 1.21 s | no signal |
hokyu_DS_s07H |
D_454 |
0.00 s | 0.21 s | no signal |
Three of the five are decisive; the other two have their only cue at 0.0 s, which tells us nothing in either direction. So something is genuinely missing from these banks — independent of anything above, and now measured rather than felt.
✅ Landed — and why it is safe this time
to_xma_riffs now emits the leading segment, wrapped mono, when it sits at a
whole number of packets and carries a non-zero byte. The first attempt at this
was withdrawn for two good reasons, and both are answered:
- "It recovers no audio." It used the stereo format. Mono yields up to 113×
more, and
VOICE_D_453's 45 116-byte segment is now its largest sub-wave. - "It matches 1524 of 8021 entries." The byte-level reach is still 1524, but
the audible reach is not. Across the 84 movie-bound banks the segment adds
more than 1 s to exactly 7 — the
hokyu_*_Htankers onVOICE_D_453/454, precisely the broken ones — and ≤0.25 s to 66 of the rest. The largest non-resupply addition isS04Aat +0.66 s on a 256 s movie.
The safety oracle is honest about its own limits: 8 of the 84 banks already exceed their movie's duration before the change, by hundredths of a second, so it cannot resolve differences at that scale. What it does establish is the scoping — the change is material only where it is meant to be. Callers clamp to the movie length regardless.
VOICE_D_451's all-zero leading region is skipped by the non-zero guard, so the
rule cannot prepend silence to a bank that does not need it; that is pinned by a
test.
✅ The two parts are SEQUENTIAL SEGMENTS — and that closes the original mystery
The last open question was whether the leading mono region duplicates the RIFF sub-wave (in which case adding them would double-count). It does not. Decoding both parts of each bank to mono PCM and measuring energy:
| bank | leading | RIFF | ||
|---|---|---|---|---|
| secs | RMS | secs | RMS | |
VOICE_D_450 |
0.49 | 158 | 2.82 | 9 898 |
VOICE_D_451 |
0.01 | 0 | 1.58 | 9 128 |
VOICE_D_452 |
0.31 | 301 | 2.18 | 9 061 |
VOICE_D_453 |
2.12 | 9 770 | 0.14 | 14 462 |
VOICE_D_454 |
3.07 | 10 428 | 0.43 | 11 639 |
Two shapes, and no bank has the same content twice:
- in
450/451/452the leading region is silence or near-silence (RMS 0 to 301 against ~9 000 for speech) — a lead-in — and the RIFF holds the line; - in
453/454the leading region holds the line and the RIFF is a short loud tail fragment.
So the parts are sequential segments of one clip. The totals used above do not double-count, and the 48 kHz fit stands.
🎯 Why 453 and 454 looked broken and the others did not
This is exactly the original symptom. The corpus recorded 450 = 2.8 s,
451 = 1.6 s, 452 = 2.2 s — plausible — but 453 = 0.14 s and
454 = 0.43 s. The decoder skips everything before the first RIFF. For the
first three that discards only silence, so they looked fine; for the last two it
discards the line itself, leaving the trailing fragment. One rule, two
outcomes, depending on which segment happens to hold the speech.
✅ Each shared bank is ONE generic line
The banks are bound to 3–5 movie slots each, and the movies sharing a bank have
identical subtitle text — 5 banks out of 5 (examples/shared_bank_takes.rs):
| bank | movies | the line |
|---|---|---|
VOICE_D_450 |
3 | "Rhino 3 has landed. Commencing resupply." |
VOICE_D_451 |
3 (+2 with no cue) | "Rhino Leader has landed. Commencing resupply." |
VOICE_D_452 |
4 | "Resupply complete. You are cleared for take-off!" |
VOICE_D_453 |
4 | "All right, beginning resupply." |
VOICE_D_454 |
2 | "Resupply complete. Switching to take-off sequence." |
So a bank holds one line, reused across stages — not several takes.
🎯 And this explains the historical in-game rejection
hokyu_DS_s13A → VOICE_D_452 was rejected as "the wrong recording". But
VOICE_D_452's line is the generic "Resupply complete. You are cleared for
take-off!", identical for s02A, s07A, s08A and s13A. Someone expecting
a stage-13-specific line would hear the generic one and call it wrong — while the
binding is exactly right. The disc says so, and now the subtitle text says so
independently.
✅ The leading region IS XMA1 — but mono, not stereo
Retried with the fmt chunk built to slb::synth_xma1_fmt's exact byte layout
(and with the bank's own RIFF sub-wave decoded through the same pipe as a
control, so a broken harness cannot masquerade as a result). Artifact
examples/slb_fmt_probe.rs.
The parameter that matters is the channel count, and the difference is not subtle:
| bank | leading bytes | decoded at channels = 2 | decoded at channels = 1 |
|---|---|---|---|
VOICE_D_450 |
16 384 | 1 792 | 46 756 |
VOICE_D_451 |
2 048 | 1 792 | 896 |
VOICE_D_452 |
14 336 | 1 792 | 30 154 |
VOICE_D_453 |
45 056 | 1 792 | 203 648 |
VOICE_D_454 |
59 392 | 1 792 | 294 440 |
channels = 2 yields exactly 1792 bytes for every bank regardless of size —
one frame, then it stops. That constant is the tell: stereo is simply the wrong
shape. At channels = 1 the same data yields up to 113× more audio, and the
control sub-wave decodes to 13 568 bytes, so the harness is sound.
VOICE_D_451 decoding to almost nothing is the expected control the other way:
its leading region is the all-zero one.
The sample rate and channel mask make no difference to the decoded byte count — as expected, since they set playback speed rather than sample count.
✅ Why FFmpeg stops — and a correction to my own claim
I had been discarding FFmpeg's stderr. It says exactly what happens:
Reserved bit is not implemented. …your file has a feature which has not been implemented.
frame[198] would have to skip -2070 bits
And the frame index is always the last one:
| bank | packets | frames decoded | frames/packet | fails at frame |
|---|---|---|---|---|
VOICE_D_450 |
8 | 45.7 | 5.71 | 44 |
VOICE_D_452 |
7 | 29.4 | 4.21 | 28 |
VOICE_D_453 |
22 | 198.9 | 9.04 | 198 |
VOICE_D_454 |
29 | 287.5 | 9.92 | 287 |
❌ So the previous entry's "the decodes are visibly partial" is WRONG, and it was mine. I read "samples per input byte ranges 2.10–4.96" as evidence of truncation. It is not: XMA1 is variable-bitrate, frames per packet genuinely range 4.21–9.92 here, and the only thing actually lost is the final frame of each stream. The decode is essentially complete.
❌ Solving for the sample rate from the subtitle cue — still does NOT converge
decoded samples / last subtitle cue should give the real rate if the decode
were complete. It does not agree with itself:
| bank | samples | cue | implied |
|---|---|---|---|
VOICE_D_453 |
101 824 | 4.70 s | 21 665 Hz |
VOICE_D_450 |
23 378 | 4.00 s | 5 844 Hz |
21 665 Hz is temptingly close to 22 050, and I nearly wrote that down. The second bank refutes it: no single rate explains both.
I then tried the obvious repair — count the whole bank, leading region plus
RIFF sub-waves, since the two split the audio very differently per bank (D_450
is mostly RIFF, D_453 mostly leading region). Two banks then agreed at a
suspiciously tidy ~2.1× ratio, which would put the rate near 22 kHz. The third
refutes that too:
| bank | leading | RIFF | total samples | cue | implied |
|---|---|---|---|---|---|
VOICE_D_450 |
23 378 | 135 589 | 158 967 | 4.00 s | 39 742 Hz |
VOICE_D_451 |
448 | 75 636 | 76 084 | 3.70 s | 20 563 Hz |
VOICE_D_453 |
101 824 | 6 784 | 108 608 | 4.70 s | 23 108 Hz |
So: the container is identified, the decode is essentially complete, and the duration still does not reconcile.
✅ The cue unit is seconds — checked, not assumed
parse_timing computes mm*60 + ss, but only if the token really is mm:ss.cc.
The movies are on the disc, so their duration is an independent oracle: a cue
must land inside its own movie. Over every English movie with a subtitle
track — 66 movies, 0 cues land after the movie ends
(examples/cue_unit_check.rs). Centiseconds would have overflowed essentially
all 66. So the seconds reading stands and the verdicts built on it survive.
❌ "The sample rate does not converge" — that was MY error, twice over
I reported implied rates of 39 742 / 20 563 / 23 108 Hz and called them irreconcilable. They are not, because they are not estimates of the same quantity — each is a one-sided bound:
- the audio must be at least as long as the last cue →
samples / cueis an upper bound on the rate; - the audio cannot outlast its movie →
samples / movieis a lower bound.
| bank | samples | cue | movie | lower (Hz) | upper (Hz) |
|---|---|---|---|---|---|
VOICE_D_450 |
158 967 | 4.00 s | 9.30 s | 17 091 | 39 742 |
VOICE_D_451 |
76 084 | 3.70 s | 9.30 s | 8 180 | 20 563 |
VOICE_D_453 |
108 608 | 4.70 s | 9.30 s | 11 677 | 23 108 |
Intersecting them gives 17 091 – 20 563 Hz, a non-empty window. A single sample rate is consistent with all three banks. I had been comparing the numbers as if they were competing point estimates.
❔ But the window contains no standard rate
Nothing in 17 091 – 20 563 Hz is a normal XMA sample rate (22 050, 24 000, 32 000, 44 100, 48 000 all sit outside). Two things could move it, and neither is settled:
- The lower bound assumes the whole bank plays inside one movie. Each of
these banks is bound to 3–5 different movie slots, so if it holds several
takes rather than one line, its total length has no reason to fit one movie and
the lower bound is void — leaving only
rate ≤ 20 563, which 22 050 nearly satisfies. - The final frame of each stream is lost to the decoder, but that is ~512 samples and cannot move a bound by 10 %.
So the honest state is: container identified, decode essentially complete, cue unit verified, and the rate constrained but not determined.
🟡 Where the tension probably is now
Checked and cleared: the cue unit is seconds (above). What is left is whether a shared bank holds one line or several takes — that decides whether the lower bound above is real, and it is the thing to establish next.
❔ The first attempt at this — inconclusive, and for a boring reason
The recorded next step was to vary the synthesised fmt (channels, streams,
sample rate) rather than assume the container. I tried 36 combinations over
VOICE_D_453's 22-packet leading region and every one produced 0 PCM bytes —
including combinations that should be equivalent to the crate's own
synth_xma1_fmt(2, 2, 48000), which does at least parse (it yields 1792 bytes).
That means the probe tested my hand-built fmt chunk, not the hypothesis.
The specific error is worth recording: I read synth_xma1_fmt(2, 2, 48000)'s
second argument as a stream count when it is a channel mask, and built the
WAVEFORMATEX around that misreading. Replicating the real layout is what turned
0 bytes into 203 648.
What this does not settle
- ❔ What the leading region holds. Its size is now exact (1392 + n·2048)
and it is neither padding nor a RIFF, but it does not decode as XMA1. A seek
table, a different codec, or a different channel/rate configuration all remain
open. The next cheap probe is to vary the synthesised
fmt(channels, streams, sample rate) rather than to assume the container. - ❔ Whether
hokyu_DS_s13Areally playsVOICE_D_452. That needs the leading region decoded and a human listening; audio judgement cannot be done here. - ❔ Whether the same leading region exists across the other ~9 500
sound.pakentries, or is peculiar to these banks. Only five were measured.