Tailing the log from BEFORE the music starts cut the unsampled backlog from 616
samples spanning offsets 32..2,559,033 to 125 spanning 32..515,239, so the first
pass is sampled like any later cycle. Offsets below loop_start play exactly once,
which is why the previous run could not measure them.
Wraps at 96.46 / 158.33 / 220.21 s, gaps 61.87 / 61.87, both contexts together.
Two derivations, neither converting bits to seconds:
(a) time to read_offset crossing loop_start, plus a 1.33 s head correction at a
rate measured on 748 timestamped samples of that same stretch
(b) first pass (offset 32 -> loop_end) minus the cycle
Both give 9.44 s on both contexts -- four numbers, one value.
So the loop region is [9.44, 71.31] s of an 87.744 s wave, cycling every 61.87 s.
The first 9.44 s is an intro played once; the last 16.4 s, the fade-out
bgm-two-stems.md documents, is never played at all.
The decoder reads ahead of playback, but both endpoints are read_offset events so
the lead cancels in the difference. One boot, one bank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
85 lines
3.1 KiB
Python
Executable File
85 lines
3.1 KiB
Python
Executable File
#!/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)
|