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
63 lines
2.5 KiB
Python
Executable File
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)
|