# 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](../port/MISSION.md). ## βœ… 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` | 81–83 | `SE_UI_MISSION_START` / `_UPDATE` / `_END` | Full 322-cue list in [`data/se-ui-cues.txt`](data/se-ui-cues.txt). The remaining blocks are gameplay: `SE_HUD_*` (21–83), `SE_BR_*` briefing (50–72), `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 β€” the TABLE cannot say, but the CODE can All 32 BGM cues are named `BGM_001`…`BGM_109`, and nothing in `SOUNDS`, `FILES` or the bank headers names a screen. That negative stands **for the tables**. It does not stand for the executable. `GamePart_Title`'s phase handler `sub_821C5580` contains `li r5, 1103` feeding a sound call β€” **cue 1103 = `BGM_103`** β€” and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 bytes) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Three routes, one answer; see [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md). So the per-screen BGM binding is recoverable **per screen, from the code that plays it** β€” not from any table. ## ~~❔ And a new negative: an individual SE's audio is not extractable yet~~ > πŸ”΄ **SUPERSEDED by this page's own later section β€” read that instead.** The > heading's claim is **false**: the waves were located in `Static.slb` by *playing* > them and reading the offsets out of the running game β€” move **`0x1ec0`**, confirm > **`0x5d6c0`**, back **`0x0ec0`** β€” and this page states it further down: *"both > waves are located in `Static.slb`. The port can have the audio."* > > What survives below is the **reason the file resists static scanning**, which is > still correct and still worth reading: `Static.slb` carries no `RIFF`/`seek`/ > `WAVE` delimiters at all, so a wave in it is defined only by `(offset, packet > count)` and those come from the oracle, not from the bytes. The negative was > about the *method*, and it was written as though it were about the *audio*. > > Kept rather than deleted: it is the evidence for why the extraction route had to > be dynamic. ⚠️ It was also still being quoted by `INDEX.md` days later > ([audit](data/index-vs-pages-audit.txt)). `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`](structures/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`](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 697–31 000 B/s) that is **269–325 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`** | | **β’Ά confirm** (title β†’ main menu) | 6 | 12 288 | mono 48 kHz | **`0x5d6c0`** | Each head matched at **exactly one** offset. Reference data in [`data/se-cue-runtime-offsets.txt`](data/se-cue-runtime-offsets.txt). **Structurally this also cracks the bank's layout.** The first two are *contiguous* β€” `0x0ec0 + 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. * βœ… **decoded to PCM** β€” see below. The cues play. ### βœ… The waves decode β€” artifact, not assertion [`tools/re-capture/slb_extract_wave.py`](../../tools/re-capture/slb_extract_wave.py) wraps a `(bank, offset, packet count, channels, rate)` slice in a synthesized XMA1 `RIFF` for ffmpeg's `xma1` decoder: ``` slb_extract_wave.py Static.slb 0x1ec0 4 # -> Static_0x1ec0.riff -> 0.533 s ``` | event | offset | packets | decodes to | RMS | peak | envelope | |---|---|---|---|---|---|---| | d-pad move | `0x1ec0` | 4 | **0.533 s** | 2 085 | 29 813 | sharp attack, monotonic decay | | β’· back | `0x0ec0` | 2 | **0.344 s** | 2 985 | 16 973 | attack, peak in frame 2 | | **β’Ά confirm** | `0x5d6c0` | 6 | **1.016 s** | 4 327 | 32 767 | peaks in frame 3, long decay | All three are mono 48 kHz, audible from sample 0, and have the percussive attack-and-decay shape of UI blips β€” not silence, not noise. **Control, run first:** the *same* synthesized wrapper applied to `BGM_001.slb`'s first wave decodes to **173.808875 s** β€” identical to the duration obtained from that bank's own on-disc `RIFF` header in [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md). The wrapper is not approximately right; it reproduces a known-good decode exactly. The bitrates are consistent too: 8 192 B / 0.533 s β‰ˆ 15.4 kB/s, 4 096 / 0.344 β‰ˆ 11.9 kB/s, 12 288 / 1.016 β‰ˆ 12.1 kB/s β€” mono at roughly half the stereo BGM rate. 🟑 **The decoded audio is deliberately not committed.** Three commands regenerate it from the disc, and the corpus keeps measurements and tooling rather than extracted game audio. The offsets, packet counts and this tool are the deliverable. ## The three cues the milestone needs are bound β€” 2026-08-28, second run A second, independent boot repeated the probe and pinned the one that was still unattributed. | event | wave | packets / bytes | runs agreeing | |---|---|---|---| | **d-pad move** | `Static.slb+0x1ec0` | 4 / 8 192 | **2 of 2** β€” identical head bytes | | **β’· back** | `Static.slb+0x0ec0` | 2 / 4 096 | **2 of 2** β€” identical head bytes | | **β’Ά confirm** | `Static.slb+0x5d6c0` | 6 / 12 288 | newly bound this run | The `0x5d6c0` wave was previously logged as "played before any input", because the first run only started counting at the main menu. Counting from the **title** instead shows it appears exactly on the β’Ά that advances title β†’ main menu, along with the two stereo BGM stems (the menu's music starting). So move / confirm / back β€” everything the five-screen milestone needs β€” are located and decodable. 🟑 **What that β’Ά measurement does not separate.** The same press both *confirms* and *opens a screen*, so this wave could be the cue the vocabulary calls `SE_UI_DECIDE` or the one it calls `SE_UI_SUB_WIN_OPN`. The port needs *a* sound on confirm and this is the one the game plays there; which name it carries is not established. ## ❔ β¬… and ➑ play nothing distinct [`menu-navigation-semantics.md`](menu-navigation-semantics.md) measured that left/right do not move the cursor. They also **fire no new XMA stream** β€” so there is no distinct "invalid" cue on the main menu, and `SE_UI_IMPOSI` was not observed. The port should leave left/right silent. ⚠️ **Reach of that negative:** the probe dedups on head bytes, so a press that replayed an *already-heard* wave would log nothing. At that point in the run the cursor, confirm and BGM waves had all been heard. So this excludes a **distinct** invalid cue; it cannot exclude left/right quietly replaying one of those three.