re: the UI cues decode -- 0.53 s, 0.34 s and 1.02 s of real audio
Finishing the step I left open last iteration rather than starting something new. Offsets and packet counts were in hand; what was missing was proof they are actually waves. slb_extract_wave.py wraps a (bank, offset, packets, channels, rate) slice in a synthesized XMA1 RIFF, following the layout the Rust decoder already uses. The three located cues decode to 0.533 s, 0.344 s and 1.016 s of mono 48 kHz audio, audible from sample 0, each with the percussive attack-and-decay envelope of a UI blip. Bitrates come out at 12-15 kB/s, about half the stereo BGM rate, which is what mono should be. The control matters more than the results. The SAME wrapper applied to BGM_001's first wave decodes to 173.808875 s -- identical to the duration that bank's own on-disc RIFF header produced back when Q10 was answered. So the header I synthesized is not approximately right, it reproduces a known-good decode exactly, and the cue durations are trustworthy for the same reason. I did not commit the decoded audio. Three commands regenerate it from the disc, and the corpus's job is measurements and tooling rather than extracted game assets. The offsets, the packet counts and the tool are the deliverable.
This commit is contained in:
@@ -175,7 +175,10 @@ authored version can be deleted.
|
||||
(`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.
|
||||
counted. ✅ **The slices decode**: 0.533 s, 0.344 s and 1.016 s of mono 48 kHz
|
||||
audio with the attack-and-decay shape of UI blips, via
|
||||
`tools/re-capture/slb_extract_wave.py` — whose wrapper reproduces a known-good
|
||||
`BGM_001` decode to the same 173.808875 s, so it is verified, not assumed.
|
||||
|
||||
* **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
|
||||
@@ -256,7 +259,6 @@ here until 2026-08-28 and is now settled.)
|
||||
| | what | why it is stuck |
|
||||
|---|---|---|
|
||||
| 🟡 | **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** |
|
||||
|
||||
@@ -17,3 +17,15 @@ 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.
|
||||
|
||||
DECODED 2026-08-28 with tools/re-capture/slb_extract_wave.py + ffmpeg's xma1:
|
||||
|
||||
event offset packets decoded rms peak envelope shape
|
||||
---------------- -------- ------- --------- ----- ----- ------------------------
|
||||
d-pad move 0x1ec0 4 0.533 s 2085 29813 sharp attack, monotonic decay
|
||||
B / back 0x0ec0 2 0.344 s 2985 16973 attack, peak in frame 2
|
||||
(pre-input cue) 0x5d6c0 6 1.016 s 4327 32767 peaks in frame 3, long decay
|
||||
|
||||
CONTROL: the same synthesized RIFF wrapper applied to BGM_001.slb's first wave
|
||||
(offset 14336, stereo) decodes to 173.808875 s -- identical to the duration
|
||||
obtained from that bank's own on-disc RIFF header. The wrapper is correct.
|
||||
|
||||
@@ -182,5 +182,37 @@ So the index cannot be derived by counting; it has to be observed per cue.
|
||||
* 🟡 **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.
|
||||
* ✅ **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 |
|
||||
| pre-input cue | `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.
|
||||
|
||||
58
tools/re-capture/slb_extract_wave.py
Executable file
58
tools/re-capture/slb_extract_wave.py
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract one wave out of a `.slb` bank as a standalone XMA1 RIFF.
|
||||
|
||||
For banks with no internal delimiters -- `Static.slb`, which is a packed run of
|
||||
whole 2048-byte XMA packets -- a wave is defined only by (offset, packet count).
|
||||
Those come from the running game: launch Canary with `--xma_param_probe=true`,
|
||||
trigger the sound, and the log prints the stream's packet count and first 32
|
||||
bytes; search those bytes in the bank to get the offset.
|
||||
See docs/re/menu-audio-cues.md.
|
||||
|
||||
slb_extract_wave.py <bank.slb name> <offset> <packets> [channels] [rate] [out.riff]
|
||||
slb_extract_wave.py Static.slb 0x1ec0 4 # the d-pad cursor cue
|
||||
|
||||
Decode with: ffmpeg -i out.riff out.wav
|
||||
"""
|
||||
import os, struct, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
exec(open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"slb_segment_phase.py")).read().split("def cmd_phases")[0])
|
||||
|
||||
XMA1_PACKET = 2048
|
||||
|
||||
def synth_fmt(channels, mask, rate):
|
||||
"""The 32-byte XMAWAVEFORMAT ffmpeg's `xma1` decoder wants."""
|
||||
f = b"fmt " + struct.pack("<I", 32)
|
||||
f += struct.pack("<HHHH", 0x0165, 16, 0, 0) # tag=XMA1, bits, opts, skip
|
||||
f += struct.pack("<H", 1) + bytes([0, 3]) # streams, loopcount, version
|
||||
f += struct.pack("<IIII", rate * channels * 2, rate, 0, 0)
|
||||
f += bytes([4, channels]) + struct.pack("<H", mask)
|
||||
return f
|
||||
|
||||
def riff(fmt, data):
|
||||
body = b"WAVE" + fmt + b"data" + struct.pack("<I", len(data)) + data
|
||||
return b"RIFF" + struct.pack("<I", len(body)) + body
|
||||
|
||||
def main():
|
||||
a = sys.argv[1:]
|
||||
if len(a) < 3:
|
||||
print(__doc__); return 1
|
||||
name, off, pkts = a[0], int(a[1], 0), int(a[2], 0)
|
||||
ch = int(a[3]) if len(a) > 3 else 1
|
||||
rate = int(a[4]) if len(a) > 4 else 48000
|
||||
out = a[5] if len(a) > 5 else f"{name.split('.')[0]}_{off:#x}.riff"
|
||||
disc = os.environ.get("SYLPHEED_DISC", "/work/sylph_extract")
|
||||
pak = Pak(os.path.join(disc, "dat", "sound.pak"))
|
||||
i = pak.find(name)
|
||||
if i is None:
|
||||
print(f"{name}: not in sound.pak"); return 2
|
||||
b = pak.read(i)
|
||||
data = b[off:off + pkts * XMA1_PACKET]
|
||||
if len(data) < pkts * XMA1_PACKET:
|
||||
print(f"warning: short read -- {len(data)} of {pkts * XMA1_PACKET} bytes")
|
||||
open(out, "wb").write(riff(synth_fmt(ch, 1 if ch == 1 else 2, rate), data))
|
||||
print(f"{out}: {len(data)} bytes, {pkts} packets, {ch}ch {rate}Hz")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user