#!/usr/bin/env python3 """Measure where the menu loop STARTS, by sampling the first pass properly. `menu-bgm-loop-fields-conflict.md` settles the loop's *length* (61.81 s, three wraps) but not where in the wave it begins. Offsets below `loop_start` are played **exactly once**, before the first wrap — and the previous trace started after the music and swallowed that whole stretch in one read, stamping 616 samples spanning offsets 32…2 559 033 at `t=0.002`. The fix is scheduling, not analysis: **tail the log from before the music starts**, so the first pass is sampled at the same cadence as every later cycle. Then loop_start_time = (first pass, offset 32 → loop_end) − (cycle, wrap to wrap) with no bits→seconds conversion anywhere — the step that is refuted (the rate varies 4.4 % within one stream). ⚠️ Context ids are reused: `ADV`'s streams are also ctx0/1/2. Everything is timestamped and the BGM start time is recorded, so the analysis can discard anything before it rather than relying on the ids. menu_loop_firstpass.py LOG OUTDIR [hold_s] """ import os import re import subprocess import sys import time LOG, OUT = sys.argv[1], sys.argv[2] HOLD = float(sys.argv[3]) if len(sys.argv) > 3 else 200 PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py") ADV = {1_294_336, 1_118_208, 1_171_456} BGM = {3_876_864, 3_930_112} SIZE = re.compile(rb"byte_size=(\d+)") LOOP = re.compile(rb"XmaContext (\d+): Looped Data: (\d+) < (\d+) \(Start: (\d+)\)") T0 = time.time() off = 0 adv_seen = False bgm_at = None taps = 0 tsv = open(f"{OUT}/readoff.tsv", "w") tsv.write("# t_s\tctx\tread_offset\tloop_end\tloop_start\n") ev = open(f"{OUT}/events.tsv", "w") 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) while time.time() - T0 < 900: try: with open(LOG, "rb") as f: f.seek(off) chunk = f.read() off += len(chunk) except FileNotFoundError: time.sleep(0.3); continue now = time.time() - T0 sizes = {int(m.group(1)) for m in SIZE.finditer(chunk)} # the loop trace runs from the very first poll, so the first pass is sampled for m in LOOP.finditer(chunk): tsv.write(f"{now:.3f}\t{int(m.group(1))}\t{int(m.group(2))}\t" f"{int(m.group(3))}\t{int(m.group(4))}\n") tsv.flush() if sizes & ADV and not adv_seen: adv_seen = True print(f"[{now:7.1f}s] ADV — intro playing", flush=True) ev.write(f"{now:.3f}\tadv\n"); ev.flush() time.sleep(3); tap(); taps += 1 if sizes & BGM and bgm_at is None: bgm_at = now print(f"[{now:7.1f}s] BGM_103 — ON THE MENU, holding {HOLD}s", flush=True) ev.write(f"{now:.3f}\tbgm\n"); ev.flush() if bgm_at is None and adv_seen and taps <= 4 and now % 25 < 0.4: tap(); taps += 1; time.sleep(1) if bgm_at is not None and now - bgm_at > HOLD: print(f"[{now:7.1f}s] hold complete", flush=True) break time.sleep(0.3) tsv.close(); ev.close() print(f"bgm_at={bgm_at} taps={taps}", flush=True)