This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/voice-bank-leading-region.md
Sylpheed RE agent a32c00057e re: recover the .slb leading segment — mono, and scoped by measurement
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
2026-08-26 00:37:06 +00:00

20 KiB
Raw Blame History

The resupply voice banks — the decoder discards up to 87 % of them

Status: FIXEDto_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 150270 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), including RT* 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

23.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_*_H tankers on VOICE_D_453/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 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/452 the 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/454 the 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 35 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.104.96" as evidence of truncation. It is not: XMA1 is variable-bitrate, frames per packet genuinely range 4.219.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 / cue is an upper bound on the rate;
  • the audio cannot outlast its movie → samples / movie is 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 35 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_s13A really plays VOICE_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.pak entries, or is peculiar to these banks. Only five were measured.