From e1f2a7f7142864d11c36eaf628190c9a94378475 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sun, 30 Aug 2026 10:13:14 +0000 Subject: [PATCH] re: the menu loop starts at 9.44 s -- measured, and the fix was scheduling 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) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- docs/port/HANDOFF.md | 28 ++++++- docs/re/data/menu-bgm-loop-start.txt | 30 +++++++ .../menu-bgm-loop-fields-conflict.md | 30 ++++++- tools/re-capture/menu_loop_firstpass.py | 84 +++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 docs/re/data/menu-bgm-loop-start.txt create mode 100755 tools/re-capture/menu_loop_firstpass.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 93fd12a8..7796140e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2529,7 +2529,33 @@ whatever it returns β€” the same reason a truncated log and a t=0 render both lo fine from inside. Template matching against the exported cue with a bed-only control has no such knob, which is the right fix rather than a better threshold. -## πŸ”΄ 2026-08-30 (latest) β€” I WATCHED the wrap. Your 61.93 is right; your **span** is wrong. +## βœ… 2026-08-30 (final) β€” the window is **`-ss 9.44 -t 61.87`** + +`loop_start` is **9.44 s**, measured. Your `loop_start_s` field β€” authored as 0.0 +and flagged wrong β€” now has its value, and flagging it is what makes this a +one-line change rather than an archaeology problem. + +| | | +|---|---| +| loop region | **[9.44 s, 71.31 s]** of an 87.744 s wave | +| cycle | **61.87 s** (wraps at 96.46 / 158.33 / 220.21, gaps 61.87 / 61.87) | +| played once | the first **9.44 s** β€” an intro | +| never played | the last **16.4 s** β€” the fade-out | + +**Two derivations, both contexts, four numbers, one value**, and neither converts +bits to seconds: (a) time to `read_offset` crossing `loop_start` plus a 1.33 s head +correction at a **locally measured** rate, (b) first pass minus cycle. Both give +9.44 s on both stems. + +βœ… **Your 61.93 stands** β€” my wrap timing says 61.87, 0.1 % apart. Either is fine; +mine is not more precise than yours. + +⚠️ One boot, one bank. The decoder reads ahead of playback, but both endpoints are +`read_offset` events so the lead cancels. +[`menu-bgm-loop-fields-conflict.md`](../re/structures/menu-bgm-loop-fields-conflict.md) Β· +[numbers](../re/data/menu-bgm-loop-start.txt) + +## πŸ”΄ 2026-08-30 (earlier) β€” I WATCHED the wrap. Your 61.93 is right; your **span** is wrong. **Three wraps observed**, each exactly `loop_end` β†’ `loop_start`, and **both contexts wrap at the same instant** all three times β€” the sample-synchrony the diff --git a/docs/re/data/menu-bgm-loop-start.txt b/docs/re/data/menu-bgm-loop-start.txt new file mode 100644 index 00000000..75a08ffd --- /dev/null +++ b/docs/re/data/menu-bgm-loop-start.txt @@ -0,0 +1,30 @@ +# WHERE the menu loop starts -- measured, no bits-to-seconds conversion. +# +# 2026-08-30. tools/re-capture/menu_loop_firstpass.py. The fix over the +# previous run was 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. Offsets below loop_start are played exactly ONCE. +# +# Backlog compression, previous run vs this one: +# before: 616 samples at t=0.002 spanning offsets 32..2,559,033 +# now: 125 samples at t=26.479 spanning offsets 32..515,239 +# +# WRAPS: t = 96.46, 158.33, 220.21 gaps 61.87, 61.87 (both contexts, same +# instant, as they must be to stay sample-synchronous) +# +# TWO DERIVATIONS, both contexts: +# (a) time from offset 32 to read_offset crossing loop_start, +# with the unsampled head corrected at the LOCAL measured rate +# (387,823 / 377,440 bits/s over 748 timestamped samples): +# 8.11 s sampled + 1.33 s head = 9.44 s +# (b) first pass (offset 32 -> loop_end) minus the cycle: +# 70.0 + 1.33 - 61.87 = 9.44 s +# +# ctx0 -> 9.44 s ctx1 -> 9.44 s agreeing to the digit +# +# => LOOP REGION [9.44 s, 71.31 s] of an 87.744 s wave; cycle 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. +# +# The decoder reads ahead of playback, but both endpoints are read_offset +# events, so the lead cancels in the difference. diff --git a/docs/re/structures/menu-bgm-loop-fields-conflict.md b/docs/re/structures/menu-bgm-loop-fields-conflict.md index 220c1d9f..74608e17 100644 --- a/docs/re/structures/menu-bgm-loop-fields-conflict.md +++ b/docs/re/structures/menu-bgm-loop-fields-conflict.md @@ -109,7 +109,35 @@ the aliasing the real problem has. **So the length was right and the placement was wrong** β€” which is why the port's shipped loop sounds correct: it has the right *duration*, over the wrong *span*. -### 🟑 Where `loop_start` sits in seconds is still not measured +### βœ… MEASURED (2026-08-30, latest): `loop_start` is at **9.44 s** + +The fix was scheduling. Tailing the log from *before* the music starts cut the +unsampled backlog from 616 samples spanning offsets 32…2 559 033 down to **125 +spanning 32…515 239**, and the first pass is then sampled like any other cycle +([`../data/menu-bgm-loop-start.txt`](../data/menu-bgm-loop-start.txt)). + +Wraps at **96.46 / 158.33 / 220.21 s**, gaps **61.87 / 61.87**, both contexts +together. + +Two derivations, neither converting bits to seconds: + +| | ctx0 | ctx1 | +|---|---|---| +| (a) time to read_offset crossing `loop_start`, + the head at the **local measured** rate | **9.44 s** | **9.44 s** | +| (b) first pass (offset 32 β†’ `loop_end`) βˆ’ cycle | **9.44 s** | **9.44 s** | + +Four numbers, one value. The head correction is **1.33 s** and uses a rate measured +on 748 timestamped samples of that same stretch β€” not the cycle mean, and not an +assumption of linearity across the stream. + +**So the loop region is `[9.44 s, 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. + +### The superseded position Offsets below `loop_start` play **exactly once**, before the first wrap, and this trace stamped that whole stretch at `t=0.002` β€” 616 samples spanning offsets diff --git a/tools/re-capture/menu_loop_firstpass.py b/tools/re-capture/menu_loop_firstpass.py new file mode 100755 index 00000000..866ad2af --- /dev/null +++ b/tools/re-capture/menu_loop_firstpass.py @@ -0,0 +1,84 @@ +#!/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)