re: the container's audio clock is within 1.5 % of real time -- a uniform slowdown is refuted

BGM_103's loop bounds are bit offsets in the decoder context, and each wave's
duration follows from its declared byte rate, cross-checked against decoded PCM to
0.007 %. Cycle media length 62.34 / 63.29 s against 61.87 s wall: ratio 0.985,
where a uniform 8.5 % slowdown predicts 1.085 -- 10 % away and on the other side
of 1.0.

The method's error bar is its own 1.5 % self-disagreement between two stems that
must have equal duration, which is why the loop page refused this conversion for a
finer question. It is adequate for an 8.5 % effect.

Does NOT settle the frame clock: audio can hold real time on a timer while
rendering lags, and 27.6 fps and the 8.5 % splash excess are both frame-clock
numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 22:45:33 +00:00
parent df08b2fddf
commit a5795d47d1
3 changed files with 246 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
# Is this container's AUDIO clock real time? -- 2026-08-30
#
# WHY: ui-keyframe-time-unit.md reads "the game presents at 27.6 fps" off frame
# counts over wall-clock windows on one container. A guest running ~92 % of real
# time produces the same numbers, and three trials sharing a container cannot
# separate the two. sylpheed-port raised the general form of this after finding
# their own box takes 146.6 s of wall clock for 137.44 s of media (+6.7 %) on a
# 720p Theora decode, and warned that cross-agent timing must go through media
# length rather than wall clock.
#
# THE REFERENCE, and it is media-derived, not wall-clock derived:
# BGM_103's loop bounds are BIT offsets in the XMA decoder context
# (menu-bgm-loop-fields-conflict.md), and each wave's duration follows from its
# own declared byte rate on the disc, cross-checked against the decoded PCM
# (4 211 729 frames / 48 000 Hz = 87.744 s, 0.007 % from the declared 87.75).
#
# ctx bits/s declared cycle bits media s wall s wall/media
# 0 353470 22,034,741 62.34 61.87 0.9925
# 1 358325 22,677,193 63.29 61.87 0.9776
#
# ⚠️ THE METHOD'S OWN ERROR BAR IS 1.5 %. The two stems are sample-synchronous and
# must have equal duration; the linear bits->seconds conversion gives 62.34 and
# 63.29, disagreeing by 1.5 %. That is the precision available here, and it is why
# menu-bgm-loop-fields-conflict.md refused this conversion for the loop question.
# It is good enough for THIS question, which is about a 8.5 % effect.
#
# ✅ RESULT: wall/media = 0.985 +- 0.015.
# A uniform 8.5 % slowdown predicts 1.085. The measurement is 10 % away from
# that AND ON THE OTHER SIDE OF 1.0 -- wall clock is if anything SHORTER than
# media, not longer. A container running the whole guest slow is REFUTED.
#
# 🔴 WHAT THIS DOES **NOT** SETTLE, and it is the part that matters.
# This bounds the AUDIO clock. On a box with no GPU, audio can hold real time on
# a timer while RENDERING lags -- so it does not follow that the frame clock is
# real time, and every number the doubt was raised about (27.6 fps, the 8.5 %
# splash excess) is a FRAME-clock number.
#
# What it does establish is that the two clocks can be COMPARED: if the frame
# clock ran 8.5 % slow while audio did not, a UI animation's wall-clock period
# would exceed its declared period in a run whose audio rate is nominal. That is
# a single-capture measurement, and it is what frame_vs_audio_clock.py does.

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Does this container's FRAME clock run at the same rate as its AUDIO clock?
The corpus reads "the game presents at 27.6 fps" and "the splash dwells run 8.5 %
long" off wall-clock windows on one container. A guest running ~92 % of real time
produces identical numbers, and three trials sharing a container cannot separate
them (ui-keyframe-time-unit.md, corrected 2026-08-30).
The AUDIO clock is already bounded: BGM_103's loop cycle is 22.03 M / 22.68 M bits
of a stream whose declared rate makes it 62.34 / 63.29 media-seconds, against a
measured 61.87 s wall -- ratio 0.985, where a uniform 8.5 % slowdown predicts
1.085. ⚠️ But audio on a GPU-less box can hold real time while RENDERING lags, so
that bounds the audio clock only.
This measures a UI ANIMATION's period in the same run as the audio rate. If the
frame clock ran slow while audio did not, the animation's wall-clock period would
exceed its declared period while the audio rate stayed nominal.
🔴 CONTROL FIRST, and the run is void without it: the same detector must recover
the TITLE plate's period, which this corpus has measured four times at
2.12-2.34 s and twice by mid-crossings at 2.530 / 2.540 s. An estimator that
cannot find a known period cannot be trusted on an unknown one.
Phase 2 also answers a question sylpheed-port has open: whether a FOCUSED MENU
BUTTON glows at all. Eleven focus records declare a 120-unit cycle; only the
plate is authored to animate.
frame_vs_audio_clock.py LOG OUTDIR [title_s] [menu_s]
"""
import os, re, subprocess, sys, time
import numpy as np
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ring_row import ring_row
LOG, OUT = sys.argv[1], sys.argv[2]
TITLE_S = float(sys.argv[3]) if len(sys.argv) > 3 else 60.0
MENU_S = float(sys.argv[4]) if len(sys.argv) > 4 else 90.0
W, H = 1280, 720
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
RO = re.compile(rb"XmaContext(?:Fake)? (\d+): Looped Data: (\d+) < (\d+) \(Start: (\d+)\)")
T0 = time.time()
os.makedirs(OUT, exist_ok=True)
def _open():
return subprocess.Popen(
["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0",
"-video_size", f"{W}x{H}", "-i", ":98", "-r", "10",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE, bufsize=W * H * 3 * 4)
def glyph(a):
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def deliveries(vk):
try:
return len(re.findall(
(r"RE-INPUT\] XamInputGetKeystrokeEx -> user=\d+ vk=%s flags=0001" % vk).encode(),
open(LOG, "rb").read()))
except FileNotFoundError:
return 0
def press(btn, vk):
for _ in range(5):
before = deliveries(vk)
subprocess.run([sys.executable, PAD, "tap", btn, "0.5"], check=False)
for _ in range(20):
time.sleep(0.25)
if deliveries(vk) > before:
return True
return False
def sample(seconds, signal, tag):
"""Sample `signal(frame)` at ~10 Hz for `seconds`. Returns (t[], v[])."""
p = _open(); n = W * H * 3
ts, vs = [], []
t_end = time.time() + seconds
seg = time.time()
while time.time() < t_end:
if time.time() - seg > 25:
p.kill(); p = _open(); seg = time.time()
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = _open(); seg = time.time(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3).astype(int)
ts.append(time.time() - T0); vs.append(signal(a))
p.kill()
np.savetxt(f"{OUT}/{tag}.tsv", np.column_stack([ts, vs]), fmt="%.4f", delimiter="\t")
return np.array(ts), np.array(vs, dtype=float)
def period(ts, vs, lo=0.6, hi=8.0):
"""Dominant period by autocorrelation on a uniform resample."""
if len(ts) < 40:
return None, 0.0
dt = np.median(np.diff(ts))
if not np.isfinite(dt) or dt <= 0:
return None, 0.0
grid = np.arange(ts[0], ts[-1], dt)
y = np.interp(grid, ts, vs)
y = y - y.mean()
if y.std() == 0:
return None, 0.0
ac = np.correlate(y, y, "full")[len(y) - 1:]
ac /= ac[0]
k0, k1 = max(1, int(lo / dt)), min(len(ac) - 1, int(hi / dt))
if k1 <= k0:
return None, 0.0
k = k0 + int(np.argmax(ac[k0:k1]))
return k * dt, float(ac[k])
def audio_rate(window):
"""bits/s of read_offset progress over the last `window` seconds of log."""
try:
data = open(LOG, "rb").read()
except FileNotFoundError:
return {}
per = {}
for m in RO.finditer(data):
per.setdefault(int(m.group(1)), []).append(int(m.group(2)))
return {c: (v[0], v[-1], len(v)) for c, v in per.items()}
# ---- phase 1: the TITLE plate, as the control -------------------------------
print(f"[{time.time()-T0:7.1f}s] waiting for the title", flush=True)
p = _open(); n = W * H * 3
while True:
b = p.stdout.read(n)
if len(b) < n:
p.kill(); p = _open(); continue
a = np.frombuffer(b, np.uint8).reshape(H, W, 3).astype(int)
if 500 <= glyph(a) <= 2500:
break
if time.time() - T0 > 900:
p.kill(); sys.exit("never reached the title")
p.kill()
print(f"[{time.time()-T0:7.1f}s] TITLE — sampling the plate for {TITLE_S:.0f}s", flush=True)
a0 = audio_rate(0)
t1, v1 = sample(TITLE_S, glyph, "title-plate")
per1, ac1 = period(t1, v1)
print(f" plate period = {per1:.3f} s (autocorr {ac1:.2f}) from {len(t1)} samples"
if per1 else " plate period: NOT FOUND", flush=True)
# ---- phase 2: the MENU ------------------------------------------------------
print(f"[{time.time()-T0:7.1f}s] pressing A for the menu", flush=True)
press("A", "5800")
time.sleep(12)
p = _open()
b = p.stdout.read(n); p.kill()
a = np.frombuffer(b, np.uint8).reshape(H, W, 3).astype(int) if len(b) == n else None
y = ring_row(Image.fromarray(a.astype(np.uint8))) if a is not None else None
print(f"[{time.time()-T0:7.1f}s] on the menu, ring y = {y}", flush=True)
if y is None:
sys.exit("no ring row on the menu — cannot place the glow window")
lo, hi = int(y) - 26, int(y) + 26
ab = audio_rate(0)
t2, v2 = sample(MENU_S, lambda f: float(f[lo:hi, 480:900].mean()), "menu-focus")
per2, ac2 = period(t2, v2)
ae = audio_rate(0)
print("\n================ RESULT ================")
print(f"CONTROL title plate period : {per1:.3f} s (autocorr {ac1:.2f})"
if per1 else "CONTROL title plate period : NOT FOUND")
print(f" corpus mid-crossings: 2.530 / 2.540 s")
print(f"MEASURE menu focus period : {per2:.3f} s (autocorr {ac2:.2f})"
if per2 else "MEASURE menu focus period : NOT FOUND — no periodic glow detected")
print("\nAUDIO read_offset progress during the run (bits):")
for c in sorted(ae):
s, e, k = ae[c]
print(f" ctx{c}: {s:,} -> {e:,} ({k} samples)")
print("FRAME VS AUDIO CLOCK RUN DONE", flush=True)

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Boot with APU debug logging on, then run frame_vs_audio_clock.py.
# log_mask=13 leaves Apu ENABLED (it disables Kernel+Cpu+Gpu); log_level=3 is
# what emits XELOGAPU's "Looped Data" lines -- level 2 emits NONE, verified.
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${OUT:-/sylph-home/re/clockcheck}"; mkdir -p "$OUT"
LOG="$OUT/canary.stdout"
bash "$SD/ensure_single_emulator.sh"
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true
nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \
+extension GLX +extension RANDR >/tmp/xvfb98.log 2>&1' "$DISPLAY" </dev/null >/dev/null 2>&1 &
for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; done
nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox </dev/null >/tmp/openbox98.log 2>&1 &
fi
XUID="${SYLPH_XUID:-$(ls "${XENIA_CONTENT:-$HOME/.local/share/Xenia/content}" 2>/dev/null | head -1)}"
[ -n "$XUID" ] || { echo "NO PROFILE"; exit 2; }
echo "── EFFECTIVE CONFIG ──"
echo " out=$OUT profile=$XUID"
echo " log_mask=13 log_level=3 (Apu debug ON -- level 2 emits no Looped Data)"
cd /sylph-home/re
nohup run-canary --apu=sdl --log_mask=13 --log_level=3 \
--logged_profile_slot_0_xuid="$XUID" </dev/null >"$LOG" 2>&1 &
python3 "$SD/frame_vs_audio_clock.py" "$LOG" "$OUT" "${TITLE_S:-60}" "${MENU_S:-90}"