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
92 lines
3.4 KiB
Python
Executable File
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()
|