Nothing here changes what a tool computes; it changes where tools look. - tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has existed nowhere since /work became a clone, so they matched nothing and printed empty results. They now resolve the disc through a new disc.py from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised). Nine scripts that imported siblings from the retired Reborn checkout or an old session scratchpad now import from their own directory. unitgroup.py only needs the variable when --pak is not given. - sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead devkit key and a doc comment claiming a devkit fallback that does not exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either. - sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so always skipped. It reads $SYLPHEED_DISC now, and passes against the disc. - Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe as places to look now name sylpheed.db, Canary's ppc_context.h and the flat .pe; docs/re/README.md no longer says the native Canary build does not run. Historical records keep their original paths: findings that were measured against /work/xenia-rs/sylpheed.db still say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 lines
2.4 KiB
Python
Executable File
60 lines
2.4 KiB
Python
Executable File
#!/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
|
|
from disc import disc_root
|
|
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 = disc_root()
|
|
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())
|