Files
Sylpheed/tools/re-capture/menu_bgm_logdriven.py
sylph-decoder b0fe35f7cd re: the menu BGM loops at 61.93 s, and the game never reaches the fade
240 s parked on the main menu, reached by using the XMA probe log as the screen
oracle instead of video -- the route the previous iteration wrote down. Menu in
26.8 s against never-in-378 s for the video rig, guest at 0.92x, capture at
0.08 % silence against the recipe page's own best of 0.31 %. BGM_103's contexts
verify the screen and no ADV context appears afterwards, so the attract loop
never took over.

Three results, two instruments.

NO SEAM: zero runs >= 0.3 s below median-18 dB in 232 s. The port's 3.4 s
near-silence is a property of its authored loop, not of the game.

NOT THE WAVE LENGTH: autocorrelation r at 87.750 s is -0.009 on four independent
windows; the top lag is 61.909 s with a 2x harmonic. Estimator controls recover
87.750 and 60.000 exactly.

61.93 s, INDEPENDENTLY: locating 30 s slices of the capture inside the decoded
summed waves shows playback advancing exactly +5.00 s per 5 s and wrapping at
61.93, from three wraps. Control: slices cut from the wave itself at 10/45/70 s
are found at 10.00/45.00/70.00. Two points mis-lock where the slice straddles a
wrap and they carry the two lowest scores in the table.

Offsets span 0.25..57.18 s of an 87.744 s wave, so the loop is [~0, 61.93) and
the final ~25.8 s is never played -- exactly where bgm-two-stems.md found the
fade-out and trailing silence. The game loops before the fade, which is why
there is no seam.

Also corrects my own '8 of 10 three-chunk regions start mid-stream'. The port
counts 25 three-chunk regions; it is right that both numbers cannot describe the
same set. My audit run was CUT SHORT -- the committed file ends mid-list with no
summary line -- so that was a ratio over an unknown fraction of the population,
and the claim that the defect is specific to multichannel regions is now
unsupported. The ADV verification and the fix's own sweep are unaffected; that
sweep ran to completion and printed its totals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 09:34:42 +00:00

92 lines
3.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Park on the menu and record it, using the XMA probe log as the screen oracle.
`menu-bgm-loop-not-yet-captured.md` records why the obvious rig fails: an audio
tee plus rendering runs the guest at ~0.20x real time and never reaches the menu.
`--gpu=null` is ~0.96x and gives a clean capture, but has no video — so the screen
oracle has to come from somewhere else.
It comes from the decoder. `bgm-two-stems.md` measured that sitting on the main
menu decodes exactly `BGM_103`'s two declared waves, **3 876 864** and
**3 930 112 B**, concurrently. `--xma_param_probe` logs every stream handed to the
decoder, so those two byte_sizes appearing IS the menu — evidence about what is
being *recorded*, which is better provenance for an audio question than a
screenshot ever was.
Presses are driven off the same log rather than a stopwatch:
* `ADV`'s contexts (1 294 336 / 1 118 208 / 1 171 456) mean the intro is playing
→ one Ⓐ ends it (Q9);
* then Ⓐ again for title → menu, retried at most RETRIES times;
* the moment `BGM_103` appears, **stop pressing** — Ⓐ on the menu activates a
button and leaves it.
⚠️ `ADV` reappearing after that would mean the attract loop took over, i.e. we are
not parked. That is checked and reported, not assumed.
menu_bgm_logdriven.py LOG OUTDIR [hold_s]
"""
import os
import re
import subprocess
import sys
import time
LOG = sys.argv[1]
OUT = sys.argv[2]
HOLD = float(sys.argv[3]) if len(sys.argv) > 3 else 240
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
ADV = {1_294_336, 1_118_208, 1_171_456}
BGM103 = {3_876_864, 3_930_112}
RETRIES = 4
SIZE = re.compile(rb"byte_size=(\d+)")
def seen(path, since=0):
"""All byte_sizes in the log, and the byte offset reached."""
try:
with open(path, "rb") as f:
f.seek(since)
data = f.read()
return {int(m.group(1)) for m in SIZE.finditer(data)}, since + len(data)
except FileNotFoundError:
return set(), since
def tap():
subprocess.run([sys.executable, PAD, "tap", "A", "0.12"], check=False)
print(f"[{time.time()-T0:7.1f}s] tapped A", flush=True)
T0 = time.time()
off, taps, menu_at = 0, 0, None
adv_seen = False
ev = open(f"{OUT}/events.tsv", "w")
ev.write("# t_s\tevent\n")
while time.time() - T0 < 900:
new, off = seen(LOG, off)
if new & ADV and not adv_seen:
adv_seen = True
ev.write(f"{time.time()-T0:.2f}\tADV decoding (intro movie)\n"); ev.flush()
print(f"[{time.time()-T0:7.1f}s] ADV contexts seen — intro is playing", flush=True)
time.sleep(3)
tap(); taps += 1
continue
if new & BGM103 and menu_at is None:
menu_at = time.time()
ev.write(f"{menu_at-T0:.2f}\tBGM_103 decoding (MENU)\n"); ev.flush()
print(f"[{menu_at-T0:7.1f}s] BGM_103 contexts — ON THE MENU. holding {HOLD}s, no more input", flush=True)
if menu_at is None and adv_seen and taps <= RETRIES and (time.time() - T0) % 25 < 0.6:
tap(); taps += 1
time.sleep(1)
if menu_at and time.time() - menu_at > HOLD:
print(f"[{time.time()-T0:7.1f}s] hold complete", flush=True)
break
time.sleep(0.5)
if menu_at is None:
print("NEVER REACHED THE MENU (BGM_103 never decoded)", flush=True)
else:
late, _ = seen(LOG, 0)
ev.write(f"{time.time()-T0:.2f}\tend; taps={taps}\n")
print("taps:", taps, flush=True)
ev.close()