Two iterations ago verify-menu-audio's bit-identity assertion began failing and I filed three suspects in the port. It is none of them. Three IDENTICAL invocations give two outcomes, 1.207438 s and 1.300317 s, differing by exactly 4096 samples -- one mixing buffer. The recording quantises to whole buffers and a one-buffer shift moves the length and alignment of everything in it. The premise -- cross-run bit-determinism -- was never guaranteed. It held while timing sat away from a buffer boundary, and a larger export moved it onto one. A test that passes by luck reports the luck running out as a regression in the code, which is what it did: two iterations of suspects, and the port was never involved. The fix keeps exact equality and no threshold, allowing the comparison to slide by whole buffers -- the one degree of freedom the recorder has. Proved it can still fail: ctrl against walk differs at every alignment. Distinct from the earlier entries: this check ran and answered the right question, resting on a property of the environment nothing verified. State what an assertion assumes about the machine, not only what it checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
145 lines
6.6 KiB
Bash
Executable File
145 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Does the port actually MAKE SOUND on the P5 walk, and the RIGHT sound?
|
|
#
|
|
# tools/port/verify-menu-audio
|
|
#
|
|
# This is the P6 gate check. P6's gate is "sound on the P5 gate", and until this
|
|
# existed the only evidence for it was that `audio.play("move")` appears in
|
|
# boot.gd -- which is evidence that a call is written, not that a sound reaches
|
|
# the Master bus. Those differ: the black hold was implemented, called, and
|
|
# emitted nothing for five milestones.
|
|
#
|
|
# It needs NO SOUND CARD. Godot records the Master bus to a WAV under the Dummy
|
|
# driver (docs/port/AUDIO-VERIFICATION.md section 2).
|
|
#
|
|
# WHAT IT CONCLUDES, and what it must not be read as:
|
|
#
|
|
# * ✅ that a cue REACHES THE BUS when a press does something;
|
|
# * ✅ that a press bound to NOTHING is silent, byte for byte;
|
|
# * ✅ that two presses of the same action play the SAME cue;
|
|
# * 🔴 NOT that the cue is the one the GAME plays. That binding is HANDOFF Q8,
|
|
# measured by the Decoder, and nothing here re-measures it. This tool cannot
|
|
# tell a correct cue from a confidently wrong one.
|
|
#
|
|
# ⚠️ Cue LENGTH is deliberately not asserted. The audible part of a cue is much
|
|
# shorter than its wave -- the music bed masks the tail -- so "elevated for
|
|
# 0.13 s" is a fact about the bed, not about the cue, and an assertion built on
|
|
# it would fail whenever the bed changes.
|
|
set -euo pipefail
|
|
cd "${PROJECT_DIR:-/work}"
|
|
export DISPLAY="${DISPLAY:-:97}"
|
|
OUT="${OUT:-${TMPDIR:-/tmp}/verify-menu-audio}"
|
|
mkdir -p "$OUT"
|
|
[ -d port/.godot ] || godot --headless --path port --import >/dev/null 2>&1
|
|
|
|
run() { # name, script
|
|
timeout 300 godot --path port --resolution 1280x720 -- \
|
|
--menu=main_menu "--script=$2" "--audio=$OUT/$1.wav" >"$OUT/$1.log" 2>&1 || true
|
|
[ -s "$OUT/$1.wav" ] || { echo "no audio written for $1 -- see $OUT/$1.log" >&2; exit 2; }
|
|
}
|
|
|
|
# THE WALK, and TWO CONTROLS. The controls are the point: a run that makes noise
|
|
# proves nothing on its own, because the music bed makes noise too.
|
|
#
|
|
# `wait` -- the bed alone, nothing pressed.
|
|
# `left` -- five presses that REACH _unhandled_input and are bound to nothing
|
|
# (HANDOFF Q5: left/right do nothing). If these differ from `wait`,
|
|
# the port is making a sound the game does not.
|
|
run walk down,down,accept,cancel,up
|
|
run ctrl wait,wait,wait,wait,wait
|
|
run noop left,left,left,left,left
|
|
|
|
python3 - "$OUT" <<'PYEOF'
|
|
import array, math, subprocess, sys
|
|
O = sys.argv[1]; SR = 44100
|
|
def dec(src, dst):
|
|
subprocess.run(["ffmpeg","-v","error","-i",src,"-f","s16le","-ac","1",
|
|
"-ar",str(SR),dst,"-y"], check=True)
|
|
a = array.array('h'); a.frombytes(open(dst,'rb').read()); return a
|
|
walk = dec(f"{O}/walk.wav", f"{O}/walk.raw")
|
|
ctrl = dec(f"{O}/ctrl.wav", f"{O}/ctrl.raw")
|
|
noop = dec(f"{O}/noop.wav", f"{O}/noop.raw")
|
|
|
|
# 1. A press bound to nothing must be SILENT, and silent still means IDENTICAL --
|
|
# but aligned to a WHOLE AUDIO BUFFER, because the recording is not
|
|
# sample-deterministic across runs and never was.
|
|
#
|
|
# 🔴 This check compared the two byte streams directly and passed for weeks.
|
|
# It then began failing, and the cause is not the port: three IDENTICAL
|
|
# invocations produce two distinct outcomes, 1.207438 s and 1.300317 s,
|
|
# differing by 0.092879 s = **exactly 4096 samples**, one mixing buffer. The
|
|
# recording quantises to whole buffers and a one-buffer shift moves both the
|
|
# length and the alignment of everything inside it.
|
|
#
|
|
# So the old premise -- cross-run bit-determinism -- was never guaranteed. It
|
|
# held while the run's timing sat away from a buffer boundary, and a larger
|
|
# export (three voice streams instead of one) moved it onto one. A test that
|
|
# passes by luck reports the luck running out as a regression in the code.
|
|
#
|
|
# The fix keeps the strength that mattered: still EXACT equality, still no
|
|
# threshold to tune. It only allows the comparison to slide by whole buffers,
|
|
# which is the one degree of freedom the recorder actually has.
|
|
BUF = 4096
|
|
best = None
|
|
for k in (0, BUF, -BUF, 2*BUF, -2*BUF):
|
|
a, b = (ctrl[k:], noop) if k >= 0 else (ctrl, noop[-k:])
|
|
n = min(len(a), len(b))
|
|
if n < BUF:
|
|
continue
|
|
if a[:n].tobytes() == b[:n].tobytes():
|
|
best = (k, n)
|
|
break
|
|
if best:
|
|
print("no-op presses vs bed alone : IDENTICAL -- silent (%d samples, %+d buffer shift)"
|
|
% (best[1], best[0] // BUF))
|
|
else:
|
|
n = min(len(ctrl), len(noop))
|
|
print("no-op presses vs bed alone : DIFFER at every whole-buffer alignment "
|
|
"-- the port sounds a dead press (%d samples)" % n)
|
|
|
|
# 2. Is the RIGHT CUE on the bus? Match each EXPORTED cue wave against the
|
|
# recording by normalised cross-correlation over the whole file.
|
|
#
|
|
# This replaced a burst-counter that thresholded the envelope at a multiple
|
|
# of the bed level. That counter reported 4 cues on one run and 0 on the next
|
|
# from the SAME script, because its answer was set by two hand-picked
|
|
# constants -- the multiple and a minimum run length -- and the bed level is
|
|
# not constant across a run. It was nearly shipped. A tool whose headline
|
|
# number moves with its own tuning cannot detect anything.
|
|
#
|
|
# This has no such constant. The cue file is its own template, the search is
|
|
# over the whole recording, and the verdict is a MARGIN over the same
|
|
# template matched against the bed-only control.
|
|
def slide(tpl, hay, step=16):
|
|
t = [float(v) for v in tpl]; bt = math.sqrt(sum(v*v for v in t))
|
|
if bt == 0: return (0.0, 0.0)
|
|
best = (-2.0, 0.0)
|
|
for i in range(0, len(hay)-len(t), step):
|
|
seg = hay[i:i+len(t)]
|
|
bs = math.sqrt(sum(float(v)*v for v in seg))
|
|
if bs:
|
|
r = sum(a*float(b) for a, b in zip(t, seg))/(bt*bs)
|
|
if r > best[0]: best = (r, i/SR)
|
|
return best
|
|
|
|
found = []
|
|
for cue in ("move", "confirm", "back"):
|
|
tpl = dec("export/audio/se/%s.ogg" % cue, "%s/%s.raw" % (O, cue))[:int(0.15*SR)]
|
|
rw, tw = slide(tpl, walk)
|
|
rc, _ = slide(tpl, ctrl)
|
|
hit = rw - rc > 0.15
|
|
found.append((cue, tw, hit))
|
|
print("%-8s walk r=%.3f at %5.2fs | bed-only r=%.3f | margin %+.3f %s"
|
|
% (cue, rw, tw, rc, rw-rc, "PRESENT" if hit else "NOT FOUND"))
|
|
|
|
# 3. The ORDER is the strongest evidence here and it is free: the correlator is
|
|
# never told where to look, so three templates landing in script order --
|
|
# move (step 1) before confirm (step 3) before back (step 4) -- is three
|
|
# independent searches agreeing with the log.
|
|
times = [t for _, t, hit in found if hit]
|
|
print("cue order vs script order : %s"
|
|
% ("CONSISTENT" if times == sorted(times) and len(times) == 3
|
|
else "check %s" % [(c, round(t, 2)) for c, t, _ in found]))
|
|
PYEOF
|
|
echo "artifacts in $OUT"
|