Files
Sylpheed/tools/re-capture/xma_readoff_trace.py
sylph-decoder 07f0231ff8 re: the menu loop WATCHED -- three wraps, 61.81 s, and my placement is refuted
Settles the conflict by timing the loop instead of converting it. A tailing probe
stamps read_offset with the wall clock as each log line arrives, so the period
needs no bits-to-time step -- the step already shown to be invalid.

Three wraps, each exactly loop_end -> loop_start, and BOTH CONTEXTS WRAP AT THE
SAME INSTANT all three times. That is the property two stems of one performance
must have and the one the linear conversion could not deliver (62.34 vs 63.29 s
would drift a second per cycle).

Cycle 61.56 and 62.06 s, mean 61.81, against the audio autocorrelation's 61.93 --
0.2 % apart from instruments sharing nothing.

Linearity refuted a second time and internally: the fitted rate over 10..60 s is
341 394 bits/s while the cycle covers 22 034 741 bits in 61.81 s = 356 491
bits/s, 4.4 % apart inside one stream.

My own audio locator's PLACEMENT is refuted. loop_start at 3.6 M bits is 11.6 %
of the stream by any reading, ~10.1 s at the cycle's own mean rate, against the
0.25 s that page reported -- for the reason already suspected, that its control
matched slices cut from the wave itself and never tested the aliasing the real
problem has. The length was right and the span was wrong.

Still not measured: loop_start in seconds. Offsets below it play exactly once and
this trace stamped that whole stretch at t=0.002, swallowing the log backlog in
one read, because it started after the music. The fix is to start the trace
before tapping into the menu -- one line, not done.

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

63 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Timestamp the XMA read offset, so a loop can be TIMED instead of converted.
`menu-bgm-loop-fields-conflict.md` leaves a conflict: the context's
`loop_start`/`loop_end` imply a cycle starting ~11.6 % into the wave, while an
audio locator put it at 0.25 s. Converting the bit offsets needs an XMA frame walk
— but the conflict can be settled without one.
`UpdateLoopStatus` logs `input_buffer_read_offset` on every decoded frame. Xenia's
log lines carry no timestamp, so this tails the log and stamps each sample with the
wall clock as it arrives. That gives two things a bit offset alone cannot:
* **the loop period in seconds**, from the wall time between wraps — no
conversion, no assumption about bits per second;
* **an empirical bits→time curve**, which is exactly what the linear assumption
got wrong (it produced 62.34 s and 63.29 s for two stems that must agree).
⚠️ Stamping on arrival dates a sample by when its line was *read*, not emitted, so
absolute times carry the log's buffering. Differences between wraps — which is what
this is for — are unaffected as long as the buffering is stationary.
xma_readoff_trace.py LOG OUT.tsv [seconds]
"""
import re
import sys
import time
LOG, OUT = sys.argv[1], sys.argv[2]
DUR = float(sys.argv[3]) if len(sys.argv) > 3 else 260
PAT = re.compile(rb"XmaContext (\d+): Looped Data: (\d+) < (\d+) \(Start: (\d+)\)")
t0 = time.time()
off = 0
out = open(OUT, "w")
out.write("# t_s\tctx\tread_offset\tloop_end\tloop_start\n")
last = {}
wraps = []
while time.time() - t0 < DUR:
try:
with open(LOG, "rb") as f:
f.seek(off)
chunk = f.read()
off += len(chunk)
except FileNotFoundError:
time.sleep(0.5); continue
now = time.time() - t0
for m in PAT.finditer(chunk):
c = int(m.group(1)); ro = int(m.group(2))
out.write(f"{now:.3f}\t{c}\t{ro}\t{int(m.group(3))}\t{int(m.group(4))}\n")
if c in last and ro < last[c] - 1_000_000:
wraps.append((now, c, last[c], ro))
print(f"[{now:7.1f}s] ctx{c} WRAP {last[c]:,} -> {ro:,}", flush=True)
last[c] = ro
out.flush()
time.sleep(0.5)
out.close()
print(f"wraps seen: {len(wraps)}", flush=True)
for c in sorted({w[1] for w in wraps}):
ts = [w[0] for w in wraps if w[1] == c]
if len(ts) > 1:
gaps = [round(ts[i+1]-ts[i], 3) for i in range(len(ts)-1)]
print(f" ctx{c}: wrap times {[round(t,2) for t in ts]} gaps {gaps}", flush=True)