Files
Sylpheed/docs/re/menu-audio-cues.md
Sylpheed RE agent 72f4f1bd76 re: the UI sound effects ARE extractable -- retracting "cannot be
extracted", and the tool was in the build all along

Two iterations ago I closed Q8 by declaring the SE audio undecodable:
Static.slb has no wave boundaries, there is no XACT container anywhere,
and I said the index "exists only at runtime" as though that put it out
of reach. The disc half of that stands. The conclusion did not.

This build of Canary carries a cvar called xma_param_probe, added by this
project, whose own comment says it logs each XMA stream's parameters and
head bytes so raw sound.pak entries can be matched to real decode params.
It has been sitting in the startup CONFIG DUMP of every log I have read
this session.

Run with it, driving the main menu: a d-pad move spawns a new mono 48 kHz
stream of 4 packets / 8192 bytes, and B spawns a different one of 2
packets / 4096 bytes. Searching their logged head bytes in Static.slb
finds each at exactly one offset -- 0x1ec0 and 0x0ec0 -- and the two are
contiguous, 0x0ec0 + 4096 = 0x1ec0. So the bank is a packed run of whole
2048-byte packets with no delimiters, which is precisely why the seek
scan found nothing: there is nothing to find. A wave is (offset, packet
count) and nothing else.

That splits Q8's binding cleanly. Event -> WAVE is now measured: the port
can have the audio. Event -> cue NAME is still a name match on the
authors' identifiers, and the page says so.

The same run settled something for Q10 too. Sitting on the main menu, TWO
stereo 48 kHz streams were decoding simultaneously. bgm-two-stems.md said
that observation was what it needed and that this container could not
make it; it can, and a music bank's two waves are now measured as
simultaneous rather than only inferred.

METHOD gets the general lesson, because it cost two iterations: check
what instrumentation the local build already has before declaring a
question blocked on tooling.
2026-08-28 19:30:03 +00:00

9.5 KiB
Raw Blame History

Menu audio — the event vocabulary is on the disc; the binding is not

Status: CONFIRMED and decoded for the cue vocabulary and the bank that holds it. 🟡 the cue↔event binding is a name match, not a measurement. two negatives with their reach: no cue names a screen's music, and the audio for an individual SE cue cannot yet be extracted.

Answers most of MISSION Q8.

The UI cue vocabulary — decoded

dat/tables.pak's sound table has a SOUNDS record of 5 798 cues. 322 are SE_* and the low block is the user-interface vocabulary, named by the game's authors after the events, not after the sounds:

id cue id cue
1 SE_UI_START 9 SE_UI_SUB_WIN_CLS
2 SE_UI_CURSOR 10 SE_UI_ALART_WIN
3 SE_UI_DECIDE 11 SE_UI_PAUSE
4 SE_UI_CANSEL (sic) 12 SE_UI_SPLASH_IN
5 SE_UI_IMPOSI 13 SE_UI_SPLASH_OUT
6 SE_UI_WAIT 14 SE_UI_LOAD_CMP
7 SE_UI_NEXT 17/18 SE_UI_WEAPON_PLAN / _CREATE
8 SE_UI_SUB_WIN_OPN 8183 SE_UI_MISSION_START / _UPDATE / _END

Full 322-cue list in data/se-ui-cues.txt. The remaining blocks are gameplay: SE_HUD_* (2183), SE_BR_* briefing (5072), SE_SW_* / SE_MW_* weapons, SE_DS_* engine, SE_EXP_* explosions, SE_SHIELD_*.

They all live in one bank — decoded

The sound table's BANK_SE record is a single field: Static.slb. And the disc-wide check agrees — 0 of the 322 SE_* cues has an entry in FILES, the 5 135-path list where every voice, briefing and BGM bank is named. SE is the one family that is not one-cue-one-file.

SETTINGS names the XACT project too: PATH = game:\dat\sound.pak+, PARAM = Pj_Silph.xgs.

🟡 The binding: a name match, and I am calling it that

MISSION asks which cue fires on move / confirm / back / error. Read off the names:

event cue
cursor moves SE_UI_CURSOR (2)
Ⓐ confirm SE_UI_DECIDE (3)
Ⓑ back SE_UI_CANSEL (4)
invalid / error SE_UI_IMPOSI (5)
submenu opens / closes SE_UI_SUB_WIN_OPN / _CLS (8/9)
the splash SE_UI_SPLASH_IN / _OUT (12/13)

Nobody has watched the game emit cue 2 on a d-pad press. This is inference from identifiers, and it belongs in the same box as Q4's GamePart ids — with one honest difference worth stating: these are the authors' own event names, chosen to describe when the sound plays, not asset labels I am interpreting. It is a strong name match. It is still a name match, and the port is authoring it.

Measuring it needs the guest's cue-play call observed with its argument — the same instrumentation Q4's residual wants, and not something this container can do today (it runs --mute=true against an SDL dummy device, so there is no audio path to watch either).

Which BGM per screen — undecodable, reach already recorded

All 32 BGM cues are named BGM_001BGM_109. Nothing in SOUNDS, FILES or the bank headers names a screen — see structures/bgm-two-stems.md. Unchanged by this iteration.

And a new negative: an individual SE's audio is not extractable yet

Static.slb is 8 353 472 readable bytes (declared 8 970 240 — exactly the 616 768 over-declaration the corpus already records), and it contains 0 RIFF, 0 seek, 0 WAVE — scanned over the whole buffer, so this is not an offset problem. Every other bank on the disc is delimited by seek magic at data_at + declared_size (7 620/7 620 in slb-data-offset.md); Static.slb is outside that population. So the route that extracts every voice line and every BGM does not give you SE_UI_CURSOR's audio: the cue is named, the bank is named, and the wave inside the bank is not locatable by any boundary marker.

The named next step is Pj_Silph.xgs — the XACT project file SETTINGS points at, which in XACT is exactly where cue→wave-index lives. It is in sound.pak: name_hash("Pj_Silph.xgs") resolves to TOC index 9454. ⚠️ Its entry is 533 bytes at a 2048-aligned offset and carries no XGSF magic — and its region has phase 1728, so those 533 bytes are very likely the previous bank's tail rather than its own (the leading-region effect, [slb-data-offset.md] (structures/slb-data-offset.md)). Locating its real bytes is the first job, not parsing XACT.

For the port

  • the cue names and ids are disc facts — use them as the event vocabulary;
  • which event fires which cue is authored from the table above;
  • which BGM plays on which screen is authored — nothing on the disc says;
  • and the UI sound effects cannot be exported yet. That is a gap in the assets, not in the naming.

Pj_Silph.xgs is a dead end — retracting the lead, with the reach

The section above named Pj_Silph.xgs as "the named next step", on the reasoning that in XACT the cue→wave index lives in the project file. That route is dead.

check result
the entry's own bytes 533 bytes, high entropy, no XGSF magic
±8 KB around the entry in the flat concatenation no XGSF, SDBK, WBND, RIFF or seek
all of sound.pak — 1.08 GB across .p00.p04 0 × XGSF, 0 × SDBK, 0 × WBND
the executable 0 occurrences of XGSF/XACT; no string matching xact, .xgs, wavebank or soundbank

Control, run first: the same magic scan over BGM_001.slb's neighbourhood finds RIFF exactly where the bank structure says it should be. The scan works; the magic genuinely is not there.

So the .xgs / .slb names are inherited from the authoring tool, not from the shipped format — nothing on this disc is a parseable XACT container. There is no XACT project file to read, and writing an XACT parser would have been wasted work.

Where that leaves the SE audio: undecodable, with reach

Locating an individual SE cue's wave inside Static.slb is not possible from anything found on the disc. Looked in:

  • Static.slb itself — 8 353 472 readable bytes, 0 RIFF / 0 seek / 0 WAVE, where every one of the other 7 620 banks is delimited by seek magic at data_at + declared_size (structures/slb-data-offset.md);
  • the XACT project file — absent, as above;
  • the sound table's five records — SOUNDS gives cue→id, FILES gives bank paths, BANK_SE gives the one bank name, SETTINGS gives a path and the (absent) project file, STAGES is empty. No record carries an offset.

What does corroborate the binding: Static.slb is 8 353 472 bytes, and at the bitrates the disc uses elsewhere (25 69731 000 B/s) that is 269325 seconds of audio — 0.84 to 1.01 s per cue over 322 cues. Exactly the shape of a bank of short UI/HUD/weapon effects, which is what BANK_SE says it is. The bank is the right bank; only the index into it is missing.

If it is ever picked up, the route is the running game, not the disc: watch the guest hand a wave offset/length to the decoder when a cue fires. That is the same instrumentation the cue↔event binding needs, and the same one this container cannot run.

Retracting "cannot be extracted" — the waves are locatable, by playing them

The section above concluded that an individual SE cue's audio "cannot be extracted". That was too strong, and the tool to do it was already in this build. Canary here carries a cvar xma_param_probe, added for exactly this purpose — its own comment says it logs each XMA stream's parameters and head bytes "so raw sound.pak entries can be matched to real decode params".

Run with --xma_param_probe=true, drive the menu, and each newly-decoded stream prints one line with its packet count, channel count, sample rate and first 32 bytes. Searching those bytes in Static.slb finds the wave.

event packets bytes format offset in Static.slb
d-pad move (cursor) 4 8 192 mono 48 kHz 0x1ec0
Ⓑ back (cancel) 2 4 096 mono 48 kHz 0x0ec0
played before any input 6 12 288 mono 48 kHz 0x5d6c0

Each head matched at exactly one offset. Reference data in data/se-cue-runtime-offsets.txt.

Structurally this also cracks the bank's layout. The first two are contiguous0x0ec0 + 4096 = 0x1ec0 — so Static.slb is a packed run of XMA waves, each a whole number of 2 048-byte packets, with no delimiter between them. That is why the seek-magic scan found nothing: there is nothing to find. A wave is defined only by (offset, packet count), and those live in the game's own tables, not in the bank.

⚠️ The order is not cue-id order. 's wave precedes the cursor's in the file, while the name match puts SE_UI_CURSOR at id 2 and SE_UI_CANSEL at 4. So the index cannot be derived by counting; it has to be observed per cue.

What is now measured, and what is still a name match

  • measured — a d-pad move and a Ⓑ press each play a specific, different wave, and both waves are located in Static.slb. The port can have the audio.
  • 🟡 still a name match — that the cursor's wave is the cue called SE_UI_CURSOR. The event→wave binding is measured; the event→name binding is still read off the authors' identifiers.
  • not done — decoding a located slice to PCM. The offsets and packet counts are here; building the XMA1 RIFF around a mono slice was not attempted.