Commit 02cdd985 by PLN (Algolia)

feat(silent-eval): execute every track's dN against an empty control map

PLN, playing this morning: "ctrl_enter on do it right, i hear the 4-bar pattern
each bar lower, 4th barely audible, its clearly a xfade". Every dN is `xfade N`
with xfadeIn 4, so those four bars are the PREVIOUS pattern leaving — the new one
was already silent, and the crossfade was handing the bug a graceful exit.

Two guards existed and neither could see it:
  * check-boot.sh runs the g* HELPERS against an empty control map — but the
    dangerous "^NN" uses are in the TRACKS. `# crushbus 41 (range 16 4.5 "^53")`
    is do_it_right's line, not BootTidal's, so check-boot stayed green while that
    orbit emitted nothing.
  * check-tracks.sh measures real audio — ground truth, but it needs the rig, a
    quiet house and ~45s a track.

This is the missing middle: rewrite each `dN $ ...` into a plain binding on top
of BootTidal's dedented helper block (with the track's own `let`s shadowing, as
they do live), then QUERY it with a deliberately empty controls map. Pure pattern
evaluation — no stream, no scsynth, no MIDI, no port 6010 — so it is safe to run
mid-set and answers all 13 tracks without anyone's ears.

RESULT, and it is unambiguous:
    empty control map   23 orbits silent across 10 of 10 buildable tracks,
                        including desire d1-d6, i.e. the ENTIRE track
    with the #55 seed   ZERO. Every declared orbit emits events.
The seed is necessary AND sufficient. The tracks are not broken; the silence PLN
heard means the seed did not reach the running Tidal.

TWO BUGS FOUND IN THIS TOOL BEFORE TRUSTING IT — both would have been confidently
wrong findings, and both were caught by looking at output rather than at code:

1. The seed parser used `[^\]]+?` to capture `n <- [13..20] ++ [29..36]`, which
   stops at the first `]`. It silently produced 4 seeds instead of 51, so the
   "seeded" run was indistinguishable from the unseeded one. Now line-based.

2. Far worse: the first version queried ONE cycle and reported six orbits as
   silent. Every one was a false positive — `mask "<f!24 t!8>"`, `"<~ [~ ~ ~
   cheval]>"`, `n "<~ <~ 7> ~ 5>"` are alternations whose cycle 0 is empty BY
   DESIGN, risers that fire once every 8 or 32 bars. Collapsing the time axis
   cannot distinguish sparse from dead. Now queries a 64-cycle window and prints
   the first sounding cycle when it is later than 4.

Honest limits: 3 of 13 tracks (perfect, mafia_sans_serif, the_revolution) do not
yet build in the harness — `cutoff` is ambiguous between BootTidal and
Sound.Tidal.Params, and one chord literal needs its type pinned. These are
reported as BUILD FAILED and explicitly never as a verdict about the music, so a
harness limit cannot masquerade as a finding.
parent d1b21c94
#!/usr/bin/env python3
"""silent-eval — prove every dN in a track EMITS EVENTS, cold, with no rig.
Why this exists (2026-07-29, J-5 to OPAL, #79)
----------------------------------------------
PLN, playing: "ctrl_enter on do it right, i hear the 4-bar pattern each bar
lower, 4th barely audible, its clearly a xfade".
Every `dN` is `xfade N` with `xfadeIn 4` (BootTidal.hs:100,126), so what you hear
for four bars after an eval is the PREVIOUS pattern leaving. If the new pattern is
silent, the crossfade hands it a graceful exit and the bug looks like a "drift".
The sound right after ctrl+enter is not evidence the eval worked — it is evidence
the LAST one did.
So the question is "why did the eval produce silence", and the classic answer on
this rig is #55: a bare "^NN" whose CC has never arrived yields NO EVENTS — not a
default of 0, NOTHING — and `#` cannot emit without a right-hand value, so one
unseeded control empties an entire orbit.
THE GAP THIS FILLS
------------------
Two guards already exist and neither can see this:
* tools/check-boot.sh runs the g* HELPERS against an empty control map. But
the dangerous "^NN" uses are in the TRACKS — `# crushbus 41 (range 16 4.5
"^53")` is do_it_right's line, not BootTidal's. check-boot is green while
that orbit is silent.
* tools/check-tracks.sh measures real audio, which is the ground truth, but it
needs the rig, a quiet house, and ~45s per track.
The middle tier was missing: EXECUTE the track's own patterns, with a
deliberately empty control map, and assert events come out. That is pure pattern
evaluation — no stream, no scsynth, no MIDI, no port 6010 — so it is safe to run
mid-set, and it answers all 13 tracks in one pass without anyone's ears.
METHOD
------
Dedent BootTidal's helper `let` block into a real module (the same extraction
check-boot.sh uses), append the track's own `let` bindings so local definitions
SHADOW the boot ones exactly as they do live, rewrite each `dN $ ...` into a
plain binding, and `query` it over cycle 0..1 with `M.empty` as the controls.
WHAT A FAILURE MEANS
--------------------
SILENT the orbit emits zero events on a fresh boot. It will be
inaudible until some knob is physically moved. This is the bug.
ok it emits events cold. (It can still be silenced by a control
that is moved to a bad VALUE — that is `gig-log.py controls`.)
BUILD FAILED the harness could not compile the track. Reported as its own
verdict and never as "silent": a harness limitation must never
masquerade as a finding about the music.
Usage
-----
tools/silent-eval.py # the whole OPAL setlist
tools/silent-eval.py live/techno/x.tidal # just these
tools/silent-eval.py --keep # keep the generated .hs to read
tools/silent-eval.py --seeded # query with the #55 seed applied
# -> proves the seed is the fix
Exit 0 = every declared orbit of every track emits events cold.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from pvlint.core import Track, load, strip_comment # noqa: E402
ROOT = Path(__file__).resolve().parent.parent
BOOT = ROOT / "BootTidal.hs"
SETLIST = ROOT / "armada" / "setlist_opal2026.txt"
# The helper let-block, located by CONTENT so edits to BootTidal do not rot this
# (same anchor check-boot.sh uses — keep them in step).
HELPER_ANCHOR = "let -- DPV specific parameters"
SEED_ANCHOR = "let _seed = concat"
ORBIT_START = re.compile(r"^(d(\d+))(\s|$)")
LET_RE = re.compile(r"^let\s+(\S+)")
def helper_block(boot: Path = BOOT) -> str:
"""BootTidal's g* helper block, dedented into module-level bindings."""
lines = boot.read_text().splitlines()
try:
start = next(i for i, l in enumerate(lines) if l.startswith(HELPER_ANCHOR))
except StopIteration:
raise SystemExit(f"silent-eval: no {HELPER_ANCHOR!r} in {boot}")
end = next(i for i in range(start + 1, len(lines)) if lines[i].startswith(":}"))
body = lines[start:end]
body[0] = body[0][len("let "):]
return "\n".join(l[4:] if l.startswith(" ") else l for l in body)
def seed_pairs(boot: Path = BOOT) -> list[tuple[str, float]]:
"""The #55 seed list, evaluated in Python so --seeded needs no Haskell.
Deliberately re-derived from the Haskell source rather than duplicated: a
seed list that drifts from BootTidal.hs would make this tool confidently
wrong in the reassuring direction.
"""
text = boot.read_text()
i = text.find(SEED_ANCHOR)
if i < 0:
return []
body = text[i:text.index("] :: [(String, Double)]", i)]
out: list[tuple[str, float]] = []
# Line-based on purpose. A single regex over the whole block cannot span
# `n <- [13..20] ++ [29..36]` without a `[^\]]` class that stops at the
# first `]` — which silently yielded 4 seeds instead of 60 and made a
# "seeded" run look identical to an unseeded one. Each seed entry is one
# line, so parse one line at a time and let a miss be visible.
for raw in body.splitlines():
line = strip_comment(raw)
m = re.search(r"\(\s*show n\s*,\s*([-\d.]+)\s*\)\s*\|\s*n\s*<-\s*(.+?)\s*\]\s*$",
line)
if m:
val = float(m.group(1))
for rng in m.group(2).split("++"):
rng = rng.strip()
rm = re.match(r"^\[\s*(\d+)\s*\.\.\s*(\d+)\s*\]$", rng)
if rm:
out += [(str(n), val)
for n in range(int(rm.group(1)), int(rm.group(2)) + 1)]
continue
lm = re.match(r"^\[([\d,\s]+)\]$", rng)
if lm:
out += [(n.strip(), val)
for n in lm.group(1).split(",") if n.strip()]
continue
for lit in re.finditer(r'\(\s*"(\d+)"\s*,\s*([-\d.]+)\s*\)', line):
out.append((lit.group(1), float(lit.group(2))))
return out
def track_lets(track: Track) -> list[str]:
"""The track's own top-level `let` bindings, as module bindings.
These SHADOW BootTidal's when a track redefines gMask/gMute/gM* — which most
of the OPAL set does — so the harness must carry them or it tests the wrong
definitions. In Haskell a later top-level binding is a duplicate rather than
a shadow, so callers rename the boot ones out of the way.
"""
out = []
for raw in track.lines:
code = strip_comment(raw)
if code.startswith("let ") and "=" in code:
out.append(code[len("let "):].rstrip())
return out
def orbit_bindings(track: Track) -> list[tuple[str, int, int, str]]:
"""(binding name, orbit number, 1-indexed line, Haskell source)."""
out = []
for orb in track.orbits():
lines = [strip_comment(l).rstrip() for l in orb.lines]
lines = [l for l in lines if l.strip()]
if not lines:
continue
m = ORBIT_START.match(lines[0])
if not m:
continue
name = f"orb{orb.number}_{orb.start}"
# `dN` -> `name = idcp` keeps BOTH shapes valid: `dN $ x` becomes
# `name = idcp $ x`, and a bare `dN` with the `$` on the next line
# becomes `name = idcp` + ` $ x`, which still parses as application.
lines[0] = f"{name} = idcp" + lines[0][len(m.group(1)):]
out.append((name, orb.number, orb.start, "\n".join(lines)))
return out
def build_module(track: Track, seeded: bool) -> str:
helpers = helper_block()
# A track that redefines a boot helper must WIN. Rename the boot definition
# rather than dropping it: something else in the block may still call it.
locals_ = track_lets(track)
names = {LET_RE.match("let " + l).group(1) for l in locals_ if LET_RE.match("let " + l)}
for n in names:
helpers = re.sub(rf"(?m)^{re.escape(n)}\b", f"boot_{n}", helpers)
binds = orbit_bindings(track)
seed = ""
if seeded:
pairs = seed_pairs()
entries = ", ".join(f'("{k}", VF {v})' for k, v in pairs)
seed = f"ctrls = M.fromList [{entries}]"
else:
seed = "ctrls = M.empty"
checks = "\n".join(
f' audible "d{n} (line {ln})" {name} failed' for name, n, ln, _ in binds)
body = "\n\n".join(src for _, _, _, src in binds)
return f"""{{-# LANGUAGE OverloadedStrings #-}}
{{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing -Wno-type-defaults #-}}
module Main where
import Sound.Tidal.Context
import qualified Data.Map.Strict as M
import System.Exit (exitFailure)
import Data.IORef
p :: Int -> ControlPattern -> IO ()
p = undefined
setI :: String -> Pattern Int -> IO ()
setI = undefined
-- `dN` is rewritten to `idcp`, so both `dN $ x` and a bare `dN` with the `$`
-- on the following line stay valid Haskell.
idcp :: ControlPattern -> ControlPattern
idcp = id
{helpers}
{chr(10).join(locals_)}
{body}
{seed}
-- Query a WINDOW, never a single cycle. Half this set's parts are alternations
-- whose cycle 0 is legitimately empty — `mask "<f!24 t!8>"` on a riser is silent
-- for 24 bars BY DESIGN — and a one-cycle query calls every one of them dead.
-- 64 covers the longest alternation in the corpus with room to spare.
cycles :: Time
cycles = 64
audible :: String -> ControlPattern -> IORef Bool -> IO ()
audible label pat failed = do
let evs = query pat (State (Arc 0 cycles) ctrls)
if null evs
then do putStrLn (" SILENT " ++ label ++ " (nothing in "
++ show (floor cycles :: Int) ++ " cycles)")
writeIORef failed True
else do
let firstAt = floor (minimum (map (start . part) evs)) :: Int
late = if firstAt > 4 then " [first sounds at cycle "
++ show firstAt ++ "]" else ""
putStrLn (" ok " ++ label ++ " -> "
++ show (length evs) ++ " event(s)" ++ late)
main :: IO ()
main = do
failed <- newIORef False
{checks if checks else ' return ()'}
bad <- readIORef failed
if bad then exitFailure else return ()
"""
def check_track(path: Path, seeded: bool, keep: bool) -> tuple[str, str]:
"""Returns (verdict, detail). verdict in ok / SILENT / BUILD."""
track = load(str(path))
if not track.orbits():
return "ok", " (no dN declarations)"
src = build_module(track, seeded)
with tempfile.TemporaryDirectory() as td:
work = Path(td)
hs = work / "Main.hs"
hs.write_text(src)
if keep:
dest = ROOT / "output" / f"silent-eval-{path.stem}.hs"
dest.parent.mkdir(exist_ok=True)
dest.write_text(src)
b = subprocess.run(
["ghc", "-package", "tidal", "-package", "containers",
"-o", str(work / "run"), "-outputdir", str(work / "o"), str(hs)],
capture_output=True, text=True)
if b.returncode != 0:
err = b.stderr.replace(str(hs), "<track>")
return "BUILD", err.strip()
r = subprocess.run([str(work / "run")], capture_output=True, text=True)
return ("SILENT" if r.returncode else "ok"), r.stdout.rstrip()
def setlist_tracks() -> list[Path]:
if not SETLIST.exists():
raise SystemExit(f"silent-eval: no setlist at {SETLIST}")
out = []
for raw in SETLIST.read_text().splitlines():
line = raw.split("#")[0].strip()
if line:
out.append(ROOT / line)
return out
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(
prog="silent-eval.py", description=__doc__.split("\n")[0],
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("tracks", nargs="*", type=Path)
ap.add_argument("--seeded", action="store_true",
help="query with the #55 boot seed applied (proves the fix)")
ap.add_argument("--keep", action="store_true",
help="write the generated Haskell to output/ for reading")
a = ap.parse_args(argv)
tracks = a.tracks or setlist_tracks()
mode = "WITH the #55 boot seed" if a.seeded else "with an EMPTY control map"
print(f"silent-eval: {len(tracks)} track(s), {mode}\n")
bad = built = 0
for t in tracks:
if not t.exists():
print(f" {t.name}: no such file")
bad += 1
continue
verdict, detail = check_track(t, a.seeded, a.keep)
if verdict == "BUILD":
print(f" {t.stem}: BUILD FAILED — harness limit, NOT a track verdict")
print("\n".join(" " + l for l in detail.splitlines()[:6]))
built += 1
continue
n_silent = detail.count("SILENT")
head = "ok" if verdict == "ok" else f"FAIL — {n_silent} orbit(s) SILENT"
print(f" {t.stem}: {head}")
for line in detail.splitlines():
if "SILENT" in line or verdict == "ok" and False:
print(" " + line.strip())
if verdict != "ok":
bad += 1
print()
if built:
print(f"silent-eval: {built} track(s) could not be built by the harness "
f"— fix the harness, do not read these as passes")
if bad:
print(f"silent-eval: FAIL — {bad} track(s) have an orbit that emits "
f"NOTHING on a fresh boot.")
return 1
print("silent-eval: OK — every declared orbit emits events.")
return 0
if __name__ == "__main__":
sys.exit(main())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment