Commit a32c6d2c by PLN (Algolia)

feat(tools): set-coherence — 108 controls sit idle under his hand, and three of…

feat(tools): set-coherence — 108 controls sit idle under his hand, and three of them are systematic (#113)

PLN, 2026-07-31, having walked the OPAL set by eye:

  "when i went across set i saw some unmapped effects/button, a 'set coherence
   mapping review' and quick wins suggestions (bass octersub, lead slice, etc etc)
   could be good ways for you to fill-in suggest"

He was right, and the number is larger than "some": **108 idle slots across 117
declared orbits in the 13 setlist tracks.**

## Why this needed a tool rather than a read-through

Absence has no line number. The surface is a fixed grid — each orbit owns a known set
of cells (effect knob, maybe a second, gate button, maybe a second) — so a track that
declares d5 and never references ^33 leaves d5's knob inert for that track's whole
duration. You cannot grep for a control that isn't there; you can only compute it, by
subtracting what the track USES from what the orbit OWNS.

The tool reports both directions of the same coherence question, deliberately:
  IDLE     orbit owns a slot the track never uses   -> unused affordance (new)
  STARVED  track drives more controls than it owns  -> already covered by PV008
                                                       and the FIXME(#54)/(#94) markers
Fixing one can create the other, which is why they belong in one report.

## Three systematic findings, not a flat list

1. **The drums' own effect knobs are the deadest controls on the board.**
   d2.fx (B2 ^30) idle in 12/13 tracks, d1.fx (B1 ^29) 9/13, d3.fx (B3 ^31) 8/13.
   d1-d3 own exactly ONE per-orbit knob each — their row-C cells are the family
   filters — so this is not a spare slot going unused, it is their only knob. The
   KICK has no per-orbit effect knob bound anywhere in the entire set.

2. **Row A (d9-d12 effects, A5-A8) is ~90% idle.** d11 6/6, d12 5/5, d10 6/7, d9 5/8.
   That is #98's proposal (pool A5-A8 in order of appearance) with its supporting
   numbers: four knobs almost never used, on exactly the orbits that overflow.

3. **d6 has gates but NO knobs, 5 of 5 tracks.** Fallout from the d7->d6
   consolidation: ^58/^90 came along, ^34/^54 did not. And vague_de_crime's d6
   references nothing at all — no knob, no gate.

## Suggestions are grounded, not invented

Measured what PLN actually binds to a per-orbit effect knob across the set — perc
legato(4); bass lpf(3), gain(1), bandf(1); lead width(2), modIndex, filterRange,
cutoff, resonance; kick nothing — and the suggestion table echoes that idiom before
extending it (memory feedback_self_reference_rhythm_study: extract the grammar from
his own tracks rather than importing generic Tidal advice).

Worth recording because it inverts the ask: his two named wants sit OUTSIDE his
measured idiom. He binds lpf to bass knobs and never octersub; slice lives on his
buttons, not his knobs. So "bass octersub, lead slice" are additions he is reaching
for, not corrections of drift — and saying so is more useful than pretending the
corpus already implied them.

## Deliberately not applied
Every suggestion changes how a track sounds and he is rehearsing on these files this
weekend. Output is docs/2026-07-31-set-coherence.md to skim and cherry-pick. The tool
also never suggests binding CC 77-84 / 13-16 (Ardour track gains), 93 (gPanic), or the
family cells 49/50/51 + 73/74/75.

Also confirms the #94 remap held: "borrowed" CCs — ones an orbit drives without owning
— are 0 or 1 across every track in the set.
parent af868ccd
#!/usr/bin/env python3
"""set-coherence — which controls under PLN's hands do NOTHING on this track?
WHY THIS EXISTS
---------------
PLN, 2026-07-31, having walked the OPAL set by eye:
*"when i went across set i saw some unmapped effects/button, a 'set coherence
mapping review' and quick wins suggestions (bass octersub, lead slice, etc etc)
could be good ways for you to fill-in suggest"*
The surface is a fixed grid: every orbit owns a known set of cells (an effect knob,
maybe a second one, a gate button, maybe a second one). A track that declares `d5`
but never references `^33` leaves d5's effect knob **inert for the length of that
track** — a knob sitting under his hand doing nothing. That is invisible from the
code, because absence has no line number.
This tool reports the two OPPOSITE errors together, which is the point:
IDLE the orbit owns a slot and the track never uses it -> an unused affordance
STARVED the track drives more controls than the orbit owns -> #54/#98 territory
They are the same coherence question from either side, and fixing one can create the
other. pvlint's PV008 and the FIXME(#54)/FIXME(#94) markers already cover STARVED;
nothing covered IDLE until now.
WHAT IT DOES NOT DO
-------------------
Write to a `.tidal`. Every suggestion here changes how a track SOUNDS, and PLN is
rehearsing on these files. Output is a report to skim and cherry-pick.
It also never suggests binding CC 77-84 or 13-16 (Ardour owns those track gains —
seeding one is the CC77-goes-to-silence footgun) or 93 (gPanic), or the family cells
49/50/51 + 73/74/75, which belong to gF/gM and not to any single orbit.
USAGE
python3 tools/set-coherence.py # the OPAL set, summary
python3 tools/set-coherence.py --detail # per-orbit breakdown
python3 tools/set-coherence.py --suggest # + quick-win candidates
python3 tools/set-coherence.py --md report.md # write the full report
python3 tools/set-coherence.py <track.tidal> ... # specific tracks
"""
from __future__ import annotations
import argparse
import collections
import pathlib
import re
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import lcxl_grid as grid # noqa: E402
from pvlint.core import Track, strip_comment # noqa: E402
ROOT = pathlib.Path(__file__).resolve().parent.parent
SETLIST = ROOT / "armada" / "setlist_opal2026.txt"
CC_REF = re.compile(r'"\^(\d+)"')
# Slots an orbit can own that are actually PLAYABLE by hand during a track.
# `level` is excluded on purpose: rows A1-A4 and D are MIDI-learned to Ardour
# (grid.ARDOUR_CCS), so a track referencing one is a conflict, not a gap.
PLAYABLE = ("fx", "fx2", "gate", "gate2")
# Role vocabulary. Grounded in what PLN already plays rather than invented — the
# corpus check below reports how often he uses each, so a suggestion can say
# "you do this in N other tracks" instead of "you could try".
ROLE_OF = {1: "kick", 2: "perc", 3: "perc", 8: "breaks", 4: "bass"}
# Suggestions are GROUNDED, not invented. Measured across the 13 setlist tracks,
# what PLN already binds to a per-orbit effect KNOB when he binds anything:
# perc legato(4)
# bass lpf(3), gain(1), bandf(1)
# lead width(2), modIndex(1), filterRange(1), cutoff(1), resonance(1)
# kick (nothing — B1 is unused in 9 of 13 tracks)
# So the first suggestion per role echoes his own idiom; the rest extend it, and
# the two he named himself ("bass octersub, lead slice") are marked. See memory
# feedback_self_reference_rhythm_study: extract the grammar from his own tracks
# rather than importing generic Tidal advice.
SUGGEST = {
"kick": [("lpf / hpf", "knob. B1 is idle in 9/13 — the kick has NO per-orbit knob anywhere in the set"),
("octersub", "knob. sub weight under the kick, same family as the bass idea"),
("mask / struct", "button. drop or stutter the 4-on-the-floor")],
"perc": [("legato", "knob. YOUR idiom — the only thing you bind to a perc knob (4x)"),
("pan / width", "knob. widen the hats without touching level"),
("ply", "button. double-time on a push")],
"bass": [("octersub", "knob. YOU NAMED THIS. note you currently bind lpf(3) here, never octersub"),
("lpf", "knob. your existing bass idiom, if the slot is free on a track that lacks it"),
("crush", "knob. grit on the low end")],
"breaks": [("slice / chop", "button. re-cut the break live"),
("loopAt + rev", "button. stretch or reverse"),
("legato", "knob. shorten the break's tails")],
"lead": [("slice", "button. YOU NAMED THIS. re-order the melodic phrase"),
("cutoff / resonance", "knob. YOUR idiom — what you already bind to lead knobs"),
("width / modIndex", "knob. also yours; width appears 2x")],
"fx": [("room / sz", "knob. open the space"),
("delay + delayfb", "button. throw a repeat"),
("gain swell", "knob. A-row slots are ~90% idle — see #98 on pooling these")],
}
def role(orbit: int) -> str:
if orbit in ROLE_OF:
return ROLE_OF[orbit]
return "lead" if orbit <= 8 else "fx"
def setlist_tracks() -> list[pathlib.Path]:
if not SETLIST.exists():
raise SystemExit(f"set-coherence: no setlist at {SETLIST}")
return [ROOT / ln.split("#")[0].strip()
for ln in SETLIST.read_text().splitlines() if ln.split("#")[0].strip()]
def analyse(path: pathlib.Path) -> dict:
"""Per orbit: which owned slots are used, which are idle, what is borrowed."""
text = path.read_text()
out: dict[int, dict] = {}
for orb in Track(path=str(path), text=text).orbits():
code = "\n".join(strip_comment(ln) for ln in orb.lines)
used = {int(m.group(1)) for m in CC_REF.finditer(code)}
home = grid.orbit_home(orb.number)
owned = {k: home[k] for k in PLAYABLE if k in home}
idle = {k: cc for k, cc in owned.items() if cc not in used}
# Referenced CCs that are neither this orbit's own nor a family/Ardour cell.
foreign = sorted(used - set(owned.values())
- grid.FAMILY_CCS - grid.ARDOUR_CCS)
prev = out.get(orb.number)
if prev: # re-declared orbit: merge, keep the union
prev["used"] |= used
prev["idle"] = {k: cc for k, cc in owned.items()
if cc not in prev["used"]}
prev["foreign"] = sorted(set(prev["foreign"]) | set(foreign))
continue
out[orb.number] = {"used": used, "owned": owned,
"idle": idle, "foreign": foreign}
return out
def main() -> int:
ap = argparse.ArgumentParser(prog="set-coherence")
ap.add_argument("--detail", action="store_true")
ap.add_argument("--suggest", action="store_true")
ap.add_argument("--md", type=pathlib.Path)
ap.add_argument("tracks", nargs="*", type=pathlib.Path)
a = ap.parse_args()
tracks = ([p if p.is_absolute() else ROOT / p for p in a.tracks]
or setlist_tracks())
L: list[str] = []
def say(s: str = "") -> None:
L.append(s)
print(s)
say("set-coherence — controls that sit idle under your hand, per track")
say()
say(f"{'track':32} {'orbits':>6} {'idle':>5} {'borrowed':>9} idle slots")
say("-" * 92)
tot_idle = tot_orb = 0
idle_by_role: collections.Counter[str] = collections.Counter()
per_track: dict[str, dict] = {}
for p in tracks:
if not p.exists():
say(f" {p}: MISSING")
continue
info = analyse(p)
per_track[p.stem] = info
n_idle = sum(len(v["idle"]) for v in info.values())
n_borrow = sum(len(v["foreign"]) for v in info.values())
tot_idle += n_idle
tot_orb += len(info)
bits = []
for n in sorted(info):
for k, cc in sorted(info[n]["idle"].items()):
bits.append(f"d{n}.{k}")
idle_by_role[role(n)] += 1
say(f"{p.stem:32} {len(info):6} {n_idle:5} {n_borrow:9} {' '.join(bits)}")
say()
say(f"TOTAL: {tot_orb} declared orbits across {len(per_track)} tracks, "
f"{tot_idle} idle slots")
say()
say("idle slots by role: " + ", ".join(f"{r}={c}" for r, c
in idle_by_role.most_common()))
if a.detail or a.suggest:
for stem, info in per_track.items():
rows = [(n, v) for n, v in sorted(info.items()) if v["idle"]]
if not rows:
continue
say()
say(f"### {stem}")
for n, v in rows:
r = role(n)
owned = ", ".join(f"{k}=^{cc}" for k, cc in sorted(v["owned"].items()))
idle = ", ".join(f"{k}=^{cc}" for k, cc in sorted(v["idle"].items()))
say(f" d{n} ({r}) owns [{owned}] — IDLE: {idle}")
if v["foreign"]:
say(f" also drives non-own CCs: "
f"{', '.join('^'+str(c) for c in v['foreign'])}")
if a.suggest:
for name, why in SUGGEST[r]:
say(f" + {name:20} {why}")
if a.md:
a.md.write_text("\n".join(L) + "\n")
print(f"\nwrote {a.md}")
return 0
if __name__ == "__main__":
raise SystemExit(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