From 0d7199bc2251cd51aa31130c63572b9a83a12bd2 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Fri, 28 Aug 2026 19:30:03 +0000 Subject: [PATCH] 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. --- docs/port/HANDOFF.md | 31 ++++++++++-------- docs/re/METHOD.md | 6 ++++ docs/re/REFUTED.md | 7 +++++ docs/re/data/se-cue-runtime-offsets.txt | 19 +++++++++++ docs/re/menu-audio-cues.md | 42 +++++++++++++++++++++++++ docs/re/structures/bgm-two-stems.md | 21 +++++++++++++ 6 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 docs/re/data/se-cue-runtime-offsets.txt diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index d6a55ce..70e20af 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -163,15 +163,19 @@ authored version can be deleted. 🟑 **Which event fires which cue is a name match**, not a measurement β€” strong, because these are the authors' own event names, but nobody has watched the game emit cue 2 on a d-pad press. The port is authoring it. - ❔ **The SE audio cannot be exported at all from the disc.** `Static.slb`'s - 8 353 472 readable bytes contain **0 `RIFF`, 0 `seek`, 0 `WAVE`** β€” the boundary - marker that works for all 7 620 other banks is absent β€” and there is **no XACT - container anywhere**: 0 Γ— `XGSF`/`SDBK`/`WBND` across all 1.08 GB of - `sound.pak`, and no `XACT`/`.xgs` string in the executable. The `.xgs`/`.slb` - names come from the authoring tool, not the shipped format. The bank is - certainly the right one β€” 8 353 472 B over 322 cues is 0.84–1.01 s each at the - disc's own bitrates β€” but the index into it exists only at runtime. **The UI - sound effects must be sourced or authored; they cannot be extracted.** + βœ… **The SE audio IS extractable β€” by playing it.** (This corrects an earlier + "cannot be extracted" on this page.) The disc carries no index: `Static.slb` has + **0 `RIFF`/`seek`/`WAVE`** and there is **no XACT container anywhere** (0 Γ— + `XGSF`/`SDBK`/`WBND` in 1.08 GB of `sound.pak`; no `XACT` string in the + executable β€” those extensions are the authoring tool's). But Canary's + `--xma_param_probe=true` logs every stream's head bytes, and searching them in + `Static.slb` locates the wave exactly: + **d-pad move β†’ `0x1ec0`, 4 packets / 8 192 B, mono 48 kHz; β’· back β†’ `0x0ec0`, + 2 packets / 4 096 B.** Each matched at one offset only, and they are contiguous + (`0x0ec0 + 4096 = 0x1ec0`) β€” the bank is a packed run of whole 2 048-byte + packets with no delimiters, which is why nothing could be scanned for. + ⚠️ The order is **not** cue-id order, so the index must be observed per cue, not + counted. ❔ Decoding a located slice to PCM has not been attempted. * **The boot sequence is not data-driven β€” the port authors it.** ❔ Four places were checked and the order is in none: `config.ini`'s `[SYSTEM]` is empty, the @@ -251,15 +255,16 @@ here until 2026-08-28 and is now settled.) | | what | why it is stuck | |---|---|---| -| 🟑 | **cue β†’ event binding as a measurement** (Q8) | nobody has watched the game emit `SE_UI_CURSOR` on a d-pad press. Needs the guest's cue call observed with its argument | -| ❔ | **the wave index into `Static.slb`** (Q8) | same instrumentation. This container runs `--mute=true` against an SDL dummy device, so it cannot watch the audio path at all | +| 🟑 | **cue NAME β†’ event binding** (Q8) | the eventβ†’**wave** binding is now measured; that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still read off the authors' identifiers | +| ❔ | **decoding a located `Static.slb` slice to PCM** (Q8) | offsets and packet counts are in hand; the XMA1 `RIFF` wrapper for a mono slice was not built | | 🟑 | **the paint-order tie-break** (Q3) | eight candidates refuted; costs one element's blend on one screen | | 🟑 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names | | ❔ | **the boot transitions in code** (Q6) | bounded as code-not-data, not proven. `sub_821C6458` β€” the substantial function in `GamePart_Title`'s neighbourhood β€” has **not been read** | | ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes | -The first two share one unblocker β€” an emulator build whose audio path can be -observed β€” and would be the only entries here that change a port decision. +(An earlier version of this table called the audio items blocked on "an emulator +whose audio path can be observed". That was wrong β€” this build already has +`--xma_param_probe`, and using it settled both.) ⚠️ **A container caveat that bounds all of the above:** the emulator has twice been killed mid-run with no crash line in its own log (at ~50 s and ~145 s), on a diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 1c6204e..55315d1 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -203,3 +203,9 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the 0.5 s (`ffmpeg x11grab` at 10 fps) answered at **0.999 with a strictly monotone playhead**. Sparse sampling does not weaken a signal gracefully; it turns it into noise that looks like a different answer. +* **Check what instrumentation the local build already has before declaring a + question blocked on tooling.** Two Q8 residuals were written off as needing "an + emulator whose audio path can be observed". The emulator in this container + already had `xma_param_probe` β€” visible in the startup CONFIG DUMP the whole + time β€” added by this project for exactly that purpose. Read the cvar dump, or + `nm -C | grep cvars::`, before concluding you cannot measure something. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index d618245..74e44db 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -158,6 +158,13 @@ neighbourhood, not just the line. ## Audio +* "an individual SE cue's audio cannot be extracted" β†’ **mine, and wrong.** + `--xma_param_probe=true` logs each stream's head bytes; searching them in + `Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md) +* "`Static.slb` has no wave boundaries, so its layout is unknown" β†’ it is a + **packed run** of whole 2 048-byte XMA packets with no delimiters β€” the two + located cues are contiguous. There is nothing to scan for, by design. + * "`Pj_Silph.xgs` holds the cueβ†’wave index, so parse XACT" β†’ **no XACT container exists on this disc**: 0 Γ— `XGSF`/`SDBK`/`WBND` in all 1.08 GB of `sound.pak`, and no `XACT`/`.xgs` string in the executable. The extensions are the authoring diff --git a/docs/re/data/se-cue-runtime-offsets.txt b/docs/re/data/se-cue-runtime-offsets.txt new file mode 100644 index 0000000..d2bd2ec --- /dev/null +++ b/docs/re/data/se-cue-runtime-offsets.txt @@ -0,0 +1,19 @@ +SE cue waves located inside Static.slb by playing them. + +Method: run Canary with --xma_param_probe=true, drive the main menu, and read the +per-stream XMA-PARAM lines. Each line carries the stream's first 32 bytes; those +bytes are then searched for in Static.slb's 8 353 472 readable bytes. + +event packets bytes channels/rate offset in Static.slb +------------------------- ------- ------ -------------- -------------------- +d-pad move (cursor) 4 8192 mono 48000 Hz 0x1ec0 +B / back (cancel) 2 4096 mono 48000 Hz 0x0ec0 +(played before any input) 6 12288 mono 48000 Hz 0x5d6c0 + +Each head matched at exactly ONE offset. The first two are contiguous: +0x0ec0 + 4096 = 0x1ec0. + +Simultaneously at the main menu, two STEREO 48 kHz streams were decoding: +1893 packets / 3 876 864 B and 1919 packets / 3 930 112 B. See +structures/bgm-two-stems.md -- this is the runtime observation that the two +stems of a music bank play at the same time. diff --git a/docs/re/menu-audio-cues.md b/docs/re/menu-audio-cues.md index 1c6e991..70be63c 100644 --- a/docs/re/menu-audio-cues.md +++ b/docs/re/menu-audio-cues.md @@ -142,3 +142,45 @@ right bank; only the index into it is missing. 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`](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. +* ❔ **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. diff --git a/docs/re/structures/bgm-two-stems.md b/docs/re/structures/bgm-two-stems.md index 2a384e1..1eedfcd 100644 --- a/docs/re/structures/bgm-two-stems.md +++ b/docs/re/structures/bgm-two-stems.md @@ -113,3 +113,24 @@ transcribing one. before silence is at RMS 168 against 4 788 at the head). Looping the wave as stored gives a fade-out and six seconds of nothing every cycle. No loop-point field has been identified in the XMA header, so a menu loop is **authored**. + +## βœ… Confirmed at runtime β€” two stereo streams decode at once + +The page above said the "played together" reading needed "a runtime observation +of whether the game submits both waves to the mixer at once", and that this +container could not do it. It can: `--xma_param_probe=true` logs every XMA stream +the decoder is handed. + +Sitting on the **main menu**, two stereo 48 kHz streams were decoding +simultaneously β€” **1 893 packets / 3 876 864 B** and **1 919 packets / +3 930 112 B**. Two stereo streams alive at the same moment on a screen with one +piece of music playing is the direct observation this page was missing: **a music +bank's two waves are simultaneous, not sequential.** Concatenating them is wrong, +now measured as well as inferred. + +🟑 It still does **not** separate the two readings β€” surround-rear pair versus a +second intensity layer β€” because both predict simultaneity. And the two byte +sizes do not match any bank's declared wave sizes in +[`../data/bgm-wave-census.txt`](../data/bgm-wave-census.txt), so *which* bank the +menu plays is still not established; the game hands the decoder a window, not the +whole declared wave.