#!/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)