port: P6 gate verified with sound on the bus; tighten the backdrop guard to a positive primitive test

verify-menu-audio records the Master bus over the P5 walk under the Dummy
driver. A dead press is bit-identical to the bed alone; all three cues match
their exported wave in the recording with margin over a bed-only control; the
cue order matches the script order, which the correlator was never told.

The first version of this tool counted envelope bursts above a multiple of the
bed and gave 4 cues on one run and 0 on the next from the same script. Replaced
with template matching, which has no tuned constant. Cue LENGTH is deliberately
not asserted -- the bed masks the tail and I nearly filed that as a defect.

Also acts on the Decoder's .tbm self-refutation. No port verdict is affected --
all six forced elements are .prm solid black, and GP_TITLE has no full-screen
.tbm at all -- but the guard was sprite.is_none(), a symptom test of the same
shape as the one they say fixed their symptom not their cause. Now role ==
primitive. Six verdicts identical, 16 screens validate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-29 21:54:14 +00:00
parent 7844bf5681
commit 7ffcf743a2
3 changed files with 222 additions and 1 deletions

View File

@@ -733,7 +733,29 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
.iter()
.enumerate()
.filter(|(_, e)| {
e.sprite.is_none()
// 🔴 UNTEXTURED SOLID QUAD, tested positively -- NOT merely "has no
// sprite". Those coincide in GP_TITLE and the distinction is still
// the whole point, because the negative test guards a SYMPTOM.
//
// The rule needs the element's alpha to BE its pixels' alpha. That
// is true of a `.prm` solid quad and of nothing else. The Decoder
// found this the expensive way twice: first `.t32` sprites (an
// element's alpha says nothing about a texture that is mostly
// transparent), guarded with "no sprite" -- and then `.tbm`, which
// is 38 of their 80 forced-first verdicts and declares fade
// `ffffffff`. A solid WHITE quad painted first at alpha 255 would
// make the screen white; no screen is white, so a `.tbm`'s white is
// a modulation ON a texture and its element alpha proves nothing
// about coverage either.
//
// "No sprite" would keep admitting a `.tbm` that this exporter
// happens not to emit a sprite for. `role == "primitive"` cannot.
// GP_TITLE has no full-screen `.tbm` at all -- every layerless
// full-screen element here is `.prm` and pure black, checked -- so
// this changes no verdict today and is a guard against a corpus
// that grows.
e.role == "primitive"
&& e.sprite.is_none()
&& e.size.is_some_and(|s| s[0] as u32 >= design[0] && s[1] as u32 >= design[1])
})
.filter(|(i, e)| {

View File

@@ -5361,3 +5361,88 @@ framing reaches significance**, which is where both of us landed anyway.
`--screen=NAME` render at t=0 both return a **complete, well-formed answer to a
different question**. That is why neither looks like an error — nothing inside
either view can tell you it is not the view you asked for.
## P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead
`tools/port/verify-menu-audio`. Until now the evidence that P6's gate was met was
that `audio.play("move")` appears in `boot.gd` — evidence that a *call is
written*, not that a sound reaches the bus. Those differ, and this project has
the case to prove it: 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. Three runs on `main_menu`: the walk (`down,down,accept,cancel,up`) and
two controls — `wait`×5 for the bed alone, and `left`×5 for **presses that reach
`_unhandled_input` and are bound to nothing** (Q5: left/right do nothing).
| check | result |
|---|---|
| a dead press is silent | **bit-identical** to the bed alone, 114 688 samples |
| `move` on the bus | r=0.201 at 1.85 s, bed-only 0.016, margin **+0.185** |
| `confirm` | r=0.945 at 2.14 s, bed-only 0.371, margin **+0.574** |
| `back` | r=0.660 at 2.42 s, bed-only 0.195, margin **+0.465** |
| cue order vs script order | **consistent** |
The order is the strongest line and it is free: the correlator is never told
where to look, so three different templates landing in script order is three
independent searches agreeing with the log. `move`'s absolute r is low because it
is the quietest cue under the loudest part of the bed; the margin over the same
template against the bed is what carries it.
🔴 **What it cannot conclude, and must never be read as:** that these are the cues
the *game* plays. That binding is HANDOFF Q8, the Decoder's, and nothing here
re-measures it. This tool cannot tell a correct cue from a confidently wrong one.
### The instrument I nearly shipped
The first version counted envelope bursts above a multiple of the bed level. It
reported **4 cues on one run and 0 on the next, from the same script** — 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. I caught it only because
I ran it twice.
The replacement has no such constant: **the exported cue file is its own
template**, the search covers the whole recording, and the verdict is a margin
over that same template matched against the bed-only control.
⚠️ Cue *length* is deliberately not asserted. The audible part of a cue is far
shorter than its wave — 0.120.38 s against authored 0.3441.016 s — because the
bed masks the tail. "Elevated for 0.13 s" is a fact about the bed, not the cue,
and I came close to filing that gap as a defect.
### A check that could not be made independent
I tried to verify Q8's cue durations against the exported waves. They agree
exactly — 0.533 / 1.016 / 0.344 — **and the agreement is worthless**: the
exporter decodes from Q8's own offsets and packet counts, so the duration is
determined by the claim under test. It confirms the export is faithful to the
authored table, nothing more. Recording it because "I checked and it matched" is
exactly what a correlated instrument feels like from the inside.
## Their `.tbm` self-refutation does not reach this archive — and it fixes my guard anyway
The Decoder downgraded 38 of the forced-backdrop rule's 80 verdicts from decoded
to inferred: those elements are `.tbm`, declaring fade `ffffffff`, and a solid
white quad painted first at alpha 255 would make the screen white. No screen is
white, so a `.tbm`'s white is a modulation **on a texture** and its element alpha
says nothing about coverage — the `.t32` mistake one file extension further out.
**No verdict the port ships is affected.** All six forced elements here are
`palogo_eff0.prm` and `pgloading_eff00.prm`, `role: primitive`, `kind_raw 0x10`,
fade `0xff000000`. They fall in the 42 that stay decoded. And **no layerless
full-screen element anywhere in `GP_TITLE` has a non-black fade** — checked
across all 16 screens, so the downgrade cannot touch this archive.
But their diagnosis applies to my code regardless: *an element's alpha is not its
texture's opacity, and only an untextured primitive makes the two the same fact*.
My guard was `sprite.is_none()` — a **symptom** test, the same shape as the one
they say fixed their symptom and not their cause. It would keep admitting a
`.tbm` that this exporter happens not to emit a sprite for.
The guard is now the positive test, `role == "primitive"`. It changes no verdict
today — the six are identical before and after, 16 screens still validate — and
it is correct by construction if the corpus grows.
⚠️ Not adopted from their message: their reading that the blend question now
narrows to `pbafc.prm`. That is theirs to settle and the port draws no additive
quad either way.

114
tools/port/verify-menu-audio Executable file
View File

@@ -0,0 +1,114 @@
#!/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 means IDENTICAL.
# No threshold: a bar here would be a number nobody measured.
n = min(len(ctrl), len(noop))
ok_silent = ctrl[:n].tobytes() == noop[:n].tobytes()
print("no-op presses vs bed alone : %s (%d samples)"
% ("IDENTICAL -- silent" if ok_silent else "DIFFER -- the port sounds a dead press", 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"