Files
Sylpheed/tools/re-capture/slb_extract_wave.py
sim e909c7c133 chore: retire the last dead paths and names from the consolidation
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>
2026-09-16 22:30:28 +02:00

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())