Its line 81 headed 'an individual SE's audio is not extractable yet' while line 189 of the same page says the waves are located and the port can have the audio. The surviving content is the reason Static.slb resists STATIC scanning -- no RIFF/seek/ WAVE delimiters -- which is why the extraction had to be dynamic. The heading generalised that into a claim about the audio. Marked rather than deleted, since it is the evidence for why the route had to be dynamic. INDEX was still quoting the wrong form of it days later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
281 lines
14 KiB
Markdown
281 lines
14 KiB
Markdown
# 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.
|