Commit ab368180 by PLN (Algolia)

feat(pv-at): --track proves every control on a REAL track moves the sound (#74)

PLN's ask, verbatim: "all controls have impact on sound... even the crushes should
change noticeably". The existing suite proves the RIG works against a fixture;
this proves a TRACK is wired, which is the thing that bites at 160 BPM when a knob
turns out to do nothing.

    python3 tools/at --track live/.../gimme_acid.tidal --dry-run   # no audio
    python3 tools/at --track live/.../gimme_acid.tidal [--json]

Per control: park at REST, take TWO baselines, probe, restore, judge. Four
outcomes, not two — MOVED / NO_IMPACT / INCONCLUSIVE / UNMEASURABLE — because a
swing that does not clearly beat the pattern's own drift is genuinely unknown, and
calling that a pass is how a green suite comes to mean nothing. NO_SIGNAL is kept
separate from NO_IMPACT: "nothing was playing" and "the control is dead" have
different fixes, and conflating them once cost a whole evening chasing SuperDirt.

THE BUG THE FIRST DRY RUN FOUND, which is the real content of this commit

The plan for gimme_acid came back "0 bipolar, no CC 49/50/51 at all" — on a track
that applies gF1/gF2/gF3 to SIX orbits. gF1/gF2/gF3 are defined in BootTidal.hs,
not in the track, so lens.py's scan of the .tidal file alone was structurally
blind to the three DJ FILTERS and to EVERY MUTE in the rig. The controls PLN
reaches for most were the ones the acceptance test could not see.

Fixed by PARSING BootTidal.hs (parsers-over-copy — it is the source of truth and
it moves: gF1 was rewired onto gDJF, midiGGlobal was retired hours ago, and a
hardcoded table would rot into a confidently wrong report). boot_helpers() now
resolves:
  * `gF1 = gDJF "^49"` -> CC49, and BORROWS its lens from gDJF's body, since
    "gDJF" is not a keyword any table knows and the lpf/hpf evidence sits on two
    separate lines
  * `gM1 = gMask . gMute1` -> composition, inheriting CC41 AND CC73
  * a track's own `let gMute = ...` SHADOWS the boot one, as Tidal does
Result on gimme_acid: 18 controls -> 22, with 49/50/51 present, bipolar, resting
at 64 and probing downward. Across the OPAL set: 275 controls, 15-29 per track,
all three filters resolved in all 13, zero Ardour-owned CCs.

Two parse bugs found and fixed on the way, both by reading the output instead of
trusting it:
  * `mask "f*16"` was classified as DENSITY. An all-false mask is a MUTE — same
    keyword, opposite lens (rms vs onsets) — and my first all-false test asked
    "does it contain a 1", which the *16 repeat count satisfies. So gMute1/2/3,
    three textually IDENTICAL helpers, came back mute/density/density: rms would
    have been the wrong lens on two of PLN's three mutes, and two working mutes
    would have reported NO_IMPACT.
  * `gM3 = gMask . gMute3` swallowed the Launchpad block that follows it and
    reported CC 7 and CC 9 as mutes, because continuation lines were appended to
    whatever was defined last. A continuation now has to LOOK like one.

SAFETY
CC93 is added to a new lens.NEVER_PROBE. It arms panic — gPanic gates on "^93", so
sending it high silences every gPanic'd stream. "Probe every control" plus "one
control is a kill switch" is how a self-test mutes a rehearsal and gets blamed on
the rig. It is reported and never swept. CC77-84 stay excluded by construction in
lens AND re-asserted at the probe; CC77 down is total silence, so belt and braces
is proportionate there.
Controls are restored per control, not at exit, so Ctrl-C mid-run is safe. Restore
goes to Control.rest — 64 for a DJ filter, because djf 0.05 is a ~26 Hz low-pass,
i.e. silence, and parking one at 0 to get a "baseline" measures silence and then
calls the track broken (#48).

--dry-run exists deliberately: it prints the plan with no audio and no MIDI, so
the half that can be checked tonight — which controls were found, which lens each
gets, what will be sent, what will be restored — is checkable at all. "Built but
never run" is exactly how the LED work shipped a colour ramp that was never
called.

TESTS: +79 (22 in test_lens.py, 57 in the new test_track_runner.py), suite
345 -> 424, all green. Every boot-helper regression above is pinned, including a
parametrised all-false-mask table with `f*16` in it; each lens is asserted to have
a floor (2 * max(drift, 0) is zero, so without one any tiny swing looks
significant on a quiet capture); the probe is asserted to REFUSE all of CC77-84;
and the dry-run plan is built for all 13 OPAL tracks with the DJ filters asserted
bipolar-and-resting-at-64 in each.

STATUS: the judgement half is validated. The AUDIO half is built but unrun — it
needs monitors, and the house is quiet. First thing to run with sound up.
parent 3ab9ae03
...@@ -143,6 +143,13 @@ GATES = ("midiOn", "midiOff", "midiG", "novaOn", "novaOff", "range", ...@@ -143,6 +143,13 @@ GATES = ("midiOn", "midiOff", "midiG", "novaOn", "novaOff", "range",
DJ_FILTERS = frozenset({49, 50, 51}) DJ_FILTERS = frozenset({49, 50, 51})
# CC93 is the PANIC gate (gPanic reads "^93"; the SC bridge sets it from the
# 73+74+91+92 chord). Sending it high silences every gPanic'd stream. It is a global
# kill switch, not a musical control, so an acceptance test must never sweep it —
# "probe every control" plus "one control is a kill switch" is how a self-test
# silences a rehearsal and gets blamed on the rig. Reported, never probed.
NEVER_PROBE = frozenset({93})
@dataclass @dataclass
class Control: class Control:
...@@ -186,6 +193,30 @@ class Control: ...@@ -186,6 +193,30 @@ class Control:
f"on {orb}") f"on {orb}")
# An ALL-FALSE mask is a MUTE, not a density gate, and the difference decides the
# lens: `mask "f*16"` silences the stream (measure rms), `mask "t!7 f"` drops one
# event in eight (measure onsets). Same keyword, opposite measurements — the
# distinction lives in the mask PATTERN, not the function name.
#
# This was found by running the plan against gimme_acid: gMute1/2/3 are three
# identical `mask "f*16"` helpers and they came back classified as mute, density,
# density. rms would have been the wrong lens on two of PLN's three mutes.
MUTE_MASK = re.compile(r'\bmask\s+\(?\s*"([^"]*)"')
# Repeat/multiplier suffixes carry DIGITS that are not values: `f*16` is sixteen
# falses, and a naive "does it contain a 1" test reads that 16 as a true and calls a
# full mute a density gate. Strip the suffixes before looking at the values.
MASK_SUFFIX = re.compile(r"[*!@/]\s*[\d.]+")
def _is_mute_mask(text: str) -> bool:
"""True when a `mask` argument is all-false, i.e. the stream is silenced."""
for body in MUTE_MASK.findall(text):
values = MASK_SUFFIX.sub("", body)
if "t" not in values and "1" not in values:
return True
return False
def _effect_in(text: str) -> tuple[str, str, str] | None: def _effect_in(text: str) -> tuple[str, str, str] | None:
"""First effect keyword in a line, as (keyword, kind, lens). """First effect keyword in a line, as (keyword, kind, lens).
...@@ -194,6 +225,8 @@ def _effect_in(text: str) -> tuple[str, str, str] | None: ...@@ -194,6 +225,8 @@ def _effect_in(text: str) -> tuple[str, str, str] | None:
order alone would have handed every such line to whichever effect happened to sit order alone would have handed every such line to whichever effect happened to sit
higher in the list. higher in the list.
""" """
if _is_mute_mask(text):
return ("mask(all-false)", "mute", "rms")
best = None best = None
for kw, kind, lens in EFFECT_LENS: for kw, kind, lens in EFFECT_LENS:
for m in re.finditer(rf"\b{re.escape(kw)}\b", text): for m in re.finditer(rf"\b{re.escape(kw)}\b", text):
...@@ -240,6 +273,115 @@ def _scan(text: str) -> dict[int, tuple[str, str, str]]: ...@@ -240,6 +273,115 @@ def _scan(text: str) -> dict[int, tuple[str, str, str]]:
return out return out
# --------------------------------------------------------------------------- #
# BootTidal's own g* helpers
# --------------------------------------------------------------------------- #
# The reason this exists: gimme_acid applies gF1/gF2/gF3 on SIX orbits, and the
# first per-track plan reported "0 bipolar, no CC 49/50/51 at all". Those helpers
# are defined in BootTidal.hs, not in the track, so a scan of the .tidal file alone
# is blind to the three DJ filters — the controls PLN reaches for most. Same for
# gM1/gM2/gM3, which is every mute in the rig.
#
# PARSED, never copied (parsers-over-copy): BootTidal is the source of truth and it
# moves — gF1 was rewired onto gDJF, and midiGGlobal was retired on 2026-07-29. A
# hardcoded table would rot into a confidently wrong report, which is worse than no
# report. If BootTidal.hs is missing the functions below return {} and per-track
# analysis degrades to what the file itself says, rather than failing.
BOOTTIDAL = Path(__file__).resolve().parents[2] / "BootTidal.hs"
# ` gF1 = gDJF "^49"` / ` gM1 = gMask . gMute1`. A definition WITH a parameter
# (`gDJF ch = ...`) is deliberately excluded here: it has no CC of its own, its
# caller supplies one. Those are collected separately, as lens providers.
BOOT_DEF_RE = re.compile(r"^\s+(g[A-Za-z0-9_']*)\s*=\s*(\S.*)$")
BOOT_FUNC_RE = re.compile(r"^\s+(g[A-Za-z0-9_']*)\s+([a-z][A-Za-z0-9_']*)\s*=\s*(\S.*)$")
_BOOT_CACHE: dict[str, dict[str, dict[int, tuple[str, str, str]]]] = {}
def boot_helpers(path: Path | None = None) -> dict[str, dict[int, tuple[str, str, str]]]:
"""helper name -> {cc: (kind, lens, evidence)} for every g* in BootTidal.hs.
Resolves composition (`gM1 = gMask . gMute1` inherits both CCs) and borrows the
lens from a parameterised helper when one is applied (`gF1 = gDJF "^49"` gets
filter/centroid from gDJF's lpf+hpf body, since "gDJF" itself is not a keyword
any table would know).
"""
p = path or BOOTTIDAL
key = str(p)
if key in _BOOT_CACHE:
return _BOOT_CACHE[key]
out: dict[str, dict[int, tuple[str, str, str]]] = {}
try:
text = p.read_text()
except OSError:
_BOOT_CACHE[key] = out
return out
funcs: dict[str, str] = {} # parameterised: name -> body (lens provider)
defs: dict[str, str] = {} # zero-arg: name -> rhs
# A continuation must LOOK like one. Appending every indented line to whatever was
# defined last made `gM3 = gMask . gMute3` swallow the unrelated Launchpad block
# that follows it and report CC 7 and CC 9 as mutes. Real Haskell continuations
# open with an operator.
cont = re.compile(r"^\s+[.#$(),+|]")
last: tuple[dict, str] | None = None
for raw in text.splitlines():
code = strip_comment(raw)
if not code.strip():
last = None # a blank line ends any continuation
continue
mf = BOOT_FUNC_RE.match(code)
if mf:
funcs[mf.group(1)] = funcs.get(mf.group(1), "") + " " + mf.group(3)
last = (funcs, mf.group(1))
continue
md = BOOT_DEF_RE.match(code)
if md:
defs[md.group(1)] = md.group(2)
last = (defs, md.group(1))
continue
if last is not None and cont.match(code):
# gDJF's hpf line sits on its own line and carries half the lens evidence,
# so continuations have to be folded in — into the RIGHT definition.
store, name = last
store[name] += " " + code.strip()
else:
last = None
def classify(name: str, rhs: str) -> tuple[str, str, str] | None:
text_for_lens = rhs
for fname, fbody in funcs.items():
if re.search(rf"\b{re.escape(fname)}\b", rhs):
text_for_lens = f"{fbody} {rhs}"
break
return _effect_in(text_for_lens)
def ccs_of(name: str, depth: int = 0) -> dict[int, tuple[str, str, str]]:
if depth > 6 or name not in defs:
return {}
rhs = defs[name]
hit = classify(name, rhs)
got: dict[int, tuple[str, str, str]] = {}
for c in CC_RE.findall(rhs):
cc = int(c)
if cc in FORBIDDEN_CC:
continue
got[cc] = ((hit[1], hit[2], f"BootTidal {name} = {rhs.strip()[:70]}")
if hit else ("unknown", "", f"BootTidal {name}"))
for other in defs:
if other != name and re.search(rf"\b{re.escape(other)}\b", rhs):
for cc, meta in ccs_of(other, depth + 1).items():
got.setdefault(cc, meta)
return got
for name in defs:
got = ccs_of(name)
if got:
out[name] = got
_BOOT_CACHE[key] = out
return out
def controls(track: Track) -> list[Control]: def controls(track: Track) -> list[Control]:
"""Every CC this track binds, with the lens that can see it and the orbits it hits. """Every CC this track binds, with the lens that can see it and the orbits it hits.
...@@ -248,7 +390,9 @@ def controls(track: Track) -> list[Control]: ...@@ -248,7 +390,9 @@ def controls(track: Track) -> list[Control]:
a helper's CC only to the line that defines it would say "CC51 affects no orbits", a helper's CC only to the line that defines it would say "CC51 affects no orbits",
which is exactly backwards -- in you_my_sunshine that one helper owned FOUR. which is exactly backwards -- in you_my_sunshine that one helper owned FOUR.
""" """
helpers: dict[str, dict[int, tuple[str, str, str]]] = {} # BootTidal's helpers first, so a track's own `let` of the same name SHADOWS
# them — which is what Tidal does, and several tracks redefine gMute locally.
helpers: dict[str, dict[int, tuple[str, str, str]]] = dict(boot_helpers())
for line in track.lines: for line in track.lines:
code = strip_comment(line) code = strip_comment(line)
m = LET_RE.match(code) m = LET_RE.match(code)
......
...@@ -193,6 +193,139 @@ def test_lens_value_reads_every_declared_lens(): ...@@ -193,6 +193,139 @@ def test_lens_value_reads_every_declared_lens():
assert name in lens.LENS_UNITS assert name in lens.LENS_UNITS
# --------------------------------------------------------------- BootTidal helpers
# These exist because the first --track plan on gimme_acid reported "0 bipolar, no
# CC 49/50/51 at all" — on a track that applies gF1/gF2/gF3 to SIX orbits. The
# helpers live in BootTidal.hs, so scanning only the .tidal file was blind to the
# three DJ filters and to every mute in the rig.
def test_boot_helpers_finds_the_three_dj_filters():
h = lens.boot_helpers()
assert h["gF1"] == {49: ("filter", "centroid", h["gF1"][49][2])}
assert set(h["gF2"]) == {50}
assert set(h["gF3"]) == {51}
def test_the_dj_filter_lens_is_borrowed_from_the_parameterised_helper():
"""`gF1 = gDJF "^49"` carries no effect keyword of its own — "gDJF" is not in any
table. The lens has to come from gDJF's body (lpf + hpf), which sits on two
separate lines."""
assert lens.boot_helpers()["gF1"][49][1] == "centroid"
def test_boot_helpers_resolves_composition():
"""`gM1 = gMask . gMute1` inherits BOTH — the mask on CC41 and the mute on CC73."""
assert set(lens.boot_helpers()["gM1"]) == {41, 73}
assert set(lens.boot_helpers()["gM2"]) == {41, 74}
assert set(lens.boot_helpers()["gM3"]) == {41, 75}
def test_the_mutes_are_measured_by_LEVEL_and_the_mask_by_DENSITY():
"""Same keyword, opposite lenses. gMask is `mask "t!7 f"` (drops one event in
eight -> onsets); gMute is `mask "f*16"` (silences the stream -> rms)."""
h = lens.boot_helpers()
assert h["gMask"][41][1] == "onsets"
for n, cc in ((1, 73), (2, 74), (3, 75)):
assert h[f"gMute{n}"][cc][0] == "mute"
assert h[f"gMute{n}"][cc][1] == "rms"
def test_all_three_mutes_are_classified_IDENTICALLY():
"""The bug this caught: gMute1/2/3 are three textually identical helpers and they
came back mute, density, density. rms would have been the wrong lens on two of
PLN's three mutes, so two working mutes would have reported NO_IMPACT."""
h = lens.boot_helpers()
kinds = {(h[f"gMute{n}"][cc][0], h[f"gMute{n}"][cc][1])
for n, cc in ((1, 73), (2, 74), (3, 75))}
assert len(kinds) == 1
@pytest.mark.parametrize("body,is_mute", [
("f*16", True), # the regression: "16" contains a 1, read as a true value
("f*8", True),
("f", True),
("f f f f", True),
("f(4,32)", True), # all-false euclid still masks everything out
("0*16", True),
("t!7 f", False), # a density gate: seven pass, one drops
("t f", False),
("1 0 1 0", False),
("t*16", False),
])
def test_an_all_false_mask_is_a_mute_and_anything_else_is_density(body, is_mute):
assert lens._is_mute_mask(f'mask "{body}"') is is_mute
def test_a_continuation_line_cannot_leak_into_the_wrong_helper():
"""`gM3 = gMask . gMute3` is followed by the Launchpad block, and appending every
indented line to whatever was defined last made gM3 report CC 7 and CC 9 as
mutes. A continuation has to LOOK like one (open with an operator)."""
assert set(lens.boot_helpers()["gM3"]) == {41, 75}
assert 7 not in lens.boot_helpers()["gM3"]
def test_gpanic_is_found_but_marked_never_to_probe():
"""CC93 arms panic — it silences every gPanic'd stream. Reported, never swept:
"probe every control" plus "one control is a kill switch" is how a self-test
mutes a rehearsal and gets blamed on the rig."""
assert 93 in lens.boot_helpers()["gPanic"]
assert 93 in lens.NEVER_PROBE
def test_boot_helpers_never_returns_an_ardour_owned_cc():
for name, ccs in lens.boot_helpers().items():
assert not (set(ccs) & lens.FORBIDDEN_CC), name
def test_boot_helpers_degrades_to_empty_when_boottidal_is_missing(tmp_path):
"""Analysis must fall back to what the .tidal file itself says, not explode."""
assert lens.boot_helpers(tmp_path / "nope.hs") == {}
def test_a_track_using_gF1_gets_cc49_attributed_to_its_orbits():
"""The integration that the unit tests above cannot prove: the boot helper has to
reach `controls()` and land on the ORBITS that apply it."""
from pvlint.core import load
t = load(str(TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal"))
by_cc = {c.cc: c for c in lens.controls(t)}
assert 49 in by_cc and by_cc[49].orbits, "gF1's CC49 reached no orbit"
assert by_cc[49].kind == "filter"
assert by_cc[49].bipolar and by_cc[49].rest == 64
assert by_cc[49].probes[0] == 10
# and the mutes land on their orbits too
assert by_cc[75].orbits and by_cc[75].lens == "rms"
def test_a_local_let_shadows_the_boot_helper_of_the_same_name(tmp_path):
"""Tidal semantics: a track's own `let gMute = ...` wins. Several tracks redefine
these, and reporting the boot version would describe code that is not running."""
from pvlint.core import Track as T
t = T(path="x.tidal", text='let gMute1 = (midiOn "^60" (ply 4))\n\n'
'd1 $ gMute1 $ s "bd"\n')
by_cc = {c.cc: c for c in lens.controls(t)}
assert 60 in by_cc
assert by_cc[60].orbits == [1]
def test_every_opal_track_resolves_all_three_dj_filters():
"""The guard that would have caught the original miss. Every track in the set
applies gF1/gF2/gF3 somewhere, so all three must appear with orbits attached."""
import re as _re
from pvlint.core import load
setlist = TOOLS.parent / "armada" / "setlist_opal2026.txt"
rows = [ln.split("#")[0].strip() for ln in setlist.read_text().splitlines()]
tracks = [r for r in rows if r]
assert len(tracks) == 13
for rel in tracks:
t = load(str(TOOLS.parent / rel))
by_cc = {c.cc: c for c in lens.controls(t)}
text = (TOOLS.parent / rel).read_text()
for cc, helper in ((49, "gF1"), (50, "gF2"), (51, "gF3")):
if _re.search(rf"\b{helper}\b", text):
assert cc in by_cc, f"{rel}: {helper} applied but CC{cc} not found"
assert by_cc[cc].orbits, f"{rel}: CC{cc} attributed to no orbit"
# ------------------------------------------------------- corpus coverage # ------------------------------------------------------- corpus coverage
def test_the_classifier_covers_the_real_corpus(): def test_the_classifier_covers_the_real_corpus():
......
"""Tests for pv-at --track, the per-track control acceptance runner (#74).
PLN's ask: "all controls have impact on sound... even the crushes should change
noticeably". The runner answers that per control, and the answers it gives are only
worth anything if the JUDGEMENT is right — so this file tests the judgement (three
outcomes, drift gating, restore-to-rest, the kill-switch guard) without needing
audio. The audio half can only be validated with monitors up.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
TOOLS = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(TOOLS))
def _load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
m = importlib.util.module_from_spec(spec)
sys.modules[name] = m
spec.loader.exec_module(m)
return m
from at import lens # noqa: E402
at_main = _load("at_main", TOOLS / "at" / "__main__.py")
# --------------------------------------------------------------------------- #
# the three outcomes
# --------------------------------------------------------------------------- #
def _v(outcome: str, **kw):
return at_main.ControlVerdict(cc=kw.pop("cc", 49), kind=kw.pop("kind", "filter"),
lens=kw.pop("lens", "centroid"),
orbits=kw.pop("orbits", [1]),
outcome=outcome, **kw)
def test_moved_passes():
assert _v("MOVED").passed
def test_no_impact_fails():
"""A control that does nothing is the whole point of the test."""
assert not _v("NO_IMPACT").passed
def test_inconclusive_is_NOT_a_pass():
"""Two outcomes would force a lie. A swing that does not beat the pattern's own
drift is genuinely unknown, and calling it a pass is how a green suite comes to
mean nothing."""
assert not _v("INCONCLUSIVE").passed
def test_no_signal_fails_and_is_distinct_from_no_impact():
""""nothing was playing" and "the control is dead" have different fixes, and
reporting them the same way sent a whole evening chasing SuperDirt once."""
assert not _v("NO_SIGNAL").passed
assert _v("NO_SIGNAL").outcome != _v("NO_IMPACT").outcome
def test_unmeasurable_passes_because_it_is_not_a_defect():
"""pan and orbit cannot be seen by a mono level/spectrum probe. Reporting them as
broken every run would train PLN to ignore the report."""
assert _v("UNMEASURABLE", kind="pan", lens="").passed
# --------------------------------------------------------------------------- #
# the drift gate — a swing only counts if it beats the signal's own variation
# --------------------------------------------------------------------------- #
def test_every_lens_has_a_floor():
"""2 * max(drift, 0) is zero, so a lens whose drift happens to be ~0 on a quiet
capture would make ANY tiny swing look significant."""
for _kw, _kind, name in lens.EFFECT_LENS:
if name:
assert at_main._lens_floor(name) > 0, name
def test_the_floor_is_scaled_to_the_lens_unit():
"""A 1 dB rms change and a 1 Hz centroid change are not comparable quantities;
one floor for both would be meaningless in at least one of them."""
assert at_main._lens_floor("centroid") > at_main._lens_floor("rms")
assert at_main._lens_floor("low_ratio") < 1.0
def test_an_unknown_lens_gets_no_floor_rather_than_a_guessed_one():
assert at_main._lens_floor("something-new") == 0.0
# --------------------------------------------------------------------------- #
# safety: what may and may not be sent
# --------------------------------------------------------------------------- #
def test_the_panic_gate_is_never_swept():
"""CC93 arms panic and silences every gPanic'd stream. Probing it would mute the
rehearsal it is meant to validate."""
assert 93 in lens.NEVER_PROBE
ctl = lens.Control(cc=93, orbits=[1], kind="level", lens="rms")
v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
assert v.outcome == "UNMEASURABLE"
assert "kill switch" in v.detail
def test_an_unmeasurable_control_is_reported_without_sending_any_midi():
"""No port is touched, so this is safe to assert on with no rig at all."""
ctl = lens.Control(cc=8, orbits=[1], kind="pan", lens="")
v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
assert v.outcome == "UNMEASURABLE"
def test_a_control_with_no_orbit_is_inconclusive_not_probed():
ctl = lens.Control(cc=60, orbits=[], kind="crush", lens="centroid")
v = at_main._probe_control(ctl, seconds=1.0, port="0:0")
assert v.outcome == "INCONCLUSIVE"
def test_the_ardour_fader_bank_can_never_reach_the_probe():
"""Excluded by construction in lens, and asserted again at the probe. CC77 down
is total silence, so this is the one place belt AND braces is proportionate."""
for cc in range(77, 85):
assert cc in lens.FORBIDDEN_CC
ctl = lens.Control(cc=cc, orbits=[1], kind="level", lens="rms")
with pytest.raises(AssertionError):
at_main._probe_control(ctl, seconds=1.0, port="0:0")
def test_lens_never_returns_a_forbidden_cc_from_a_real_track():
from pvlint.core import load
t = load(str(TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal"))
assert not ({c.cc for c in lens.controls(t)} & lens.FORBIDDEN_CC)
# --------------------------------------------------------------------------- #
# rest values — the #48 footgun
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("cc", sorted(lens.DJ_FILTERS))
def test_a_dj_filter_rests_at_centre_and_probes_downward(cc):
"""djf 0.05 is a ~26 Hz low-pass, i.e. silence. Parking a DJF at 0 to establish a
"baseline" measures silence and then calls the track broken."""
c = lens.Control(cc=cc, orbits=[1], kind="filter", lens="centroid")
assert c.bipolar
assert c.rest == 64
assert c.probes[0] == 10 and 118 in c.probes
def test_a_unipolar_control_rests_at_zero():
c = lens.Control(cc=53, orbits=[1], kind="crush", lens="centroid")
assert not c.bipolar
assert c.rest == 0
assert c.probes[0] == 127
# --------------------------------------------------------------------------- #
# the dry run — the half that IS checkable without monitors
# --------------------------------------------------------------------------- #
OPAL = TOOLS.parent / "armada" / "setlist_opal2026.txt"
def _set_tracks():
return [ln.split("#")[0].strip() for ln in OPAL.read_text().splitlines()
if ln.split("#")[0].strip()]
@pytest.mark.parametrize("rel", _set_tracks())
def test_the_probe_plan_builds_for_every_track_in_the_set(rel, capsys):
""""Built but never run" is how the LED work shipped a colour ramp that was never
called. The plan is falsifiable with no audio, so it gets asserted."""
assert at_main.cmd_track_plan(TOOLS.parent / rel) == 0
out = capsys.readouterr().out
assert "no Ardour-owned CC in the plan" in out
assert "bipolar (rest at CENTRE 64" in out
@pytest.mark.parametrize("rel", _set_tracks())
def test_every_track_in_the_set_finds_its_dj_filters_as_bipolar(rel):
from pvlint.core import load
controls = lens.controls(load(str(TOOLS.parent / rel)))
bip = [c for c in controls if c.bipolar]
assert bip, f"{rel}: no bipolar control found — gF1/2/3 went missing"
assert all(c.rest == 64 for c in bip)
@pytest.mark.parametrize("rel", _set_tracks())
def test_every_track_binds_a_workable_number_of_controls(rel):
"""A track reporting 0 controls means the parse failed, not that the track is
simple. Measured range across the set on 2026-07-29: 15-29."""
from pvlint.core import load
n = len(lens.controls(load(str(TOOLS.parent / rel))))
assert 5 <= n <= 60, f"{rel}: {n} controls — the parse probably broke"
def test_the_plan_json_is_machine_readable(capsys):
import json
assert at_main.cmd_track_plan(
TOOLS.parent / "live/midi/nova/acid/gimme_acid.tidal", as_json=True) == 0
payload = json.loads(capsys.readouterr().out)
assert payload["status"] == "plan"
ccs = {c["cc"] for c in payload["controls"]}
assert {49, 50, 51} <= ccs
for c in payload["controls"]:
assert c["cc"] not in lens.FORBIDDEN_CC
assert c["rest"] in (0, 64)
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