Commit cfa56b69 by PLN (Algolia)

fix(desire): one misplaced paren silenced all nine orbits — and a lens that can see a filter

desire.tidal measured 0/9 orbits: every declared channel SILENT AT SOURCE, master
bus at -inf. Not the sample-buffer bug (b4f62757 fixed that), not a fader — the
file simply never compiled.

    off 0.125 (|+ note 12 . (|* gain 0.6))

`.` binds tighter than the section, so this parses as `|+ (note 12 . (|* gain
0.6))` — composing a function into a ValueMap. GHC's own words: "'(.)' is applied
to too few arguments". The fix is one pair of parens: `(|+ note 12) . (|* gain
0.6)`. After it, 9/9 orbits measure STEADY through SC and Ardour.

WHY IT COST A WHOLE TRACK. desire.tidal has ZERO blank lines, and a blank line is
Tidal's block separator — so the file is ONE block, and a block either compiles
entirely or not at all. One character took nine orbits. This is the third track
this week where "the rig is silent" was really "the code did not compile", and it
was only visible because the editor stopped deleting the word "error" from GHC
messages (fork commit 374d8a3): PLN read the EVAL ERROR notification off his
screen and forwarded it. Blank lines between orbits are free gig insurance.

Also lands tools/control-lens.py, the instrument for the other half of the
question. probe-chain proves an orbit makes LEVEL; it cannot prove a control
WORKS, because rms is blind to a DJ filter or a bit-crusher — those move timbre
at roughly constant loudness. So control-lens measures the right quantity per
control kind: spectral centroid + band energies for filter/crush, rms for
gain/mute, onset rate for ply/density. It drives the CC through the same path a
physical knob takes (aseqsend -> SC MIDIFunc.cc -> /ctrl -> sStateMV, reused from
lcxl-init rather than reinvented), captures before and after, and refuses to
guess: a contaminated tap is DISCARDED, a silent baseline is SKIPPED (else one
dead fader reads as twelve dead knobs), and "no measured change" is reported as
NO CHANGE, never softened to "subtle". It also hard-refuses CC 77-84, which are
MIDI-learned to Ardour's track gains.
parent 3a94a97a
...@@ -49,7 +49,7 @@ d4 $ gF2 $ gM3 -- BASS (the FIXME): D minor, grounded in measured key (corr=0.90 ...@@ -49,7 +49,7 @@ d4 $ gF2 $ gM3 -- BASS (the FIXME): D minor, grounded in measured key (corr=0.90
# gain 1.15 # gain 1.15
d6 $ gF1 $ gM2 -- ACID ARP: D minor, 4/16 square rises, ^59-driven d6 $ gF1 $ gM2 -- ACID ARP: D minor, 4/16 square rises, ^59-driven
$ midiOn "^59" (arp "<up updown pinkyup speedupdown>") $ midiOn "^59" (arp "<up updown pinkyup speedupdown>")
$ off 0.125 (|+ note 12 . (|* gain 0.6)) $ off 0.125 ((|+ note 12) . (|* gain 0.6))
$ note (scale "aeolian" (run "<4 16>") + "<0 3 5 3>" + 2) -- +2 = D root $ note (scale "aeolian" (run "<4 16>") + "<0 3 5 3>" + 2) -- +2 = D root
# s "superzow" # voice 0.05 # s "superzow" # voice 0.05
# lpf (range 500 5000 "^59") # resonance 0.35 # lpf (range 500 5000 "^59") # resonance 0.35
......
#!/usr/bin/env python3
"""control-lens — prove a control actually CHANGES THE SOUND, not just that it exists.
Why this exists (2026-07-28, J-7 to OPAL)
-----------------------------------------
PLN's ask was precise: *"send fake midi events for each, thus test all controls
have impact on sound ;) even the crushes should change noticeably what you
see/'hear'"*. The word that matters is **noticeably**. Two prior tools each
answer half the question and neither answers this one:
* `lcxl-init.py` proves a CC was SENT. It cannot tell you anything landed.
* `probe-chain.py` proves an orbit makes LEVEL. But **rms cannot see a filter
or a bit-crusher** — those move TIMBRE, sometimes at constant loudness. A
"no change in dBFS" verdict on a DJ filter is not a negative result, it is a
meaningless one. Judging a filter by its loudness is the same class of
mistake as judging a fade by an aggregate (feedback_measure_the_time_axis).
So this tool measures the RIGHT QUANTITY PER CONTROL KIND:
filter / crush -> SPECTRAL CENTROID + band energies (timbre moved?)
gain / mute -> rms dBFS (level moved?)
ply / density -> ONSET COUNT per second (more events?)
Method, per control: capture a BASELINE at the seeded value, then drive the CC to
each probe value through **the same path a physical knob takes** (aseqsend -> SC's
MIDIFunc.cc -> /ctrl -> Tidal's sStateMV — reused verbatim from lcxl-init.py, not
reinvented), re-capture, and compare. A control whose every measurement sits
inside the noise floor of the baseline is reported DEAD, which is a finding: it
means the knob does nothing and today that is completely invisible.
Honesty rules, learned the hard way
-----------------------------------
* A capture whose tap did not verify is DISCARDED, never interpreted (872676a).
* "No measured change" is reported as NO CHANGE, never softened into "subtle" —
a measurement tool that invents an effect is not a tool.
* The comparison is only valid if the ORBIT WAS AUDIBLE in the baseline. A
silent orbit yields "no change" for every control on it, which would frame a
dead fader as twelve dead knobs. Silent baseline -> SKIPPED, loudly.
Usage
-----
tools/control-lens.py --orbit 4 --cc 49 --kind filter --values 0 64 127
tools/control-lens.py --orbit 1 --cc 91 --kind density --values 0 127
tools/control-lens.py --spec live/techno/vague_de_crime.tidal # (see --spec)
Needs numpy (system python3 has it). Requires SC + the track already playing —
this tool never boots or evaluates anything, so it is safe to run while a set is
up.
"""
from __future__ import annotations
import argparse
import importlib.util
import math
import subprocess
import sys
import tempfile
import time
import wave
from pathlib import Path
import numpy as np
TOOLS = Path(__file__).resolve().parent
def _load(name: str, path: Path):
"""Import a module whose filename is not a valid identifier (probe-chain)."""
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# Reuse, do not reimplement: the tap machinery and the MIDI send path are both
# already debugged, and a second copy of either would drift out of agreement with
# the tool PLN actually trusts.
probe = _load("probe_chain", TOOLS / "probe-chain.py")
lcxl = _load("lcxl_init", TOOLS / "lcxl-init.py")
SR = probe.RATE
# A control is only credible as "effective" if it moves its own measure by more
# than session-to-session wobble. These thresholds are deliberately generous:
# the question is "noticeably", not "at all", and a false ACTIVE is worse than a
# false NO CHANGE (it would send PLN on stage trusting a dead knob).
THRESH = {
"filter": ("centroid", 12.0, "% centroid shift"),
"crush": ("centroid", 12.0, "% centroid shift"),
"gain": ("rms_db", 3.0, "dB rms"),
"density": ("onsets", 20.0, "% onsets/s"),
}
def capture(orbit: int, seconds: float) -> tuple[np.ndarray | None, str]:
"""Mono float32 of SuperCollider's output for one orbit, or (None, reason)."""
tmp = Path(tempfile.mkdtemp(prefix="control-lens-"))
tap = probe.Tap(f"d{orbit}_sc", probe.sc_ports_for(orbit), tmp)
node = f"lens_d{orbit}"
try:
tap.start(node)
time.sleep(1.2) # let the stream appear in the graph
tap.link(node)
time.sleep(seconds)
ok, actual = tap.verify() # while the node is still alive
finally:
tap.stop()
if not ok:
return None, f"tap contaminated (measured {actual})"
if tap.linked < 1:
return None, "tap linked nothing"
try:
with wave.open(str(tap.path), "rb") as w:
if w.getsampwidth() != 2:
return None, f"unexpected sample width {w.getsampwidth()}"
raw = w.readframes(w.getnframes())
ch = w.getnchannels()
except Exception as exc: # noqa: BLE001 — any decode failure is a discard
return None, f"unreadable capture: {exc}"
sig = np.frombuffer(raw, dtype="<i2").astype(np.float64) / 32768.0
if ch > 1:
sig = sig.reshape(-1, ch).mean(axis=1)
if sig.size < SR // 2:
return None, "capture too short"
return sig, ""
def onsets_per_s(sig: np.ndarray) -> float:
"""Crude but honest onset rate: peaks in a 10 ms-hop energy envelope.
Deliberately not librosa — this runs on the live rig under system python3,
and the question ("did event density change?") needs a RELATIVE number, not
a musicologically correct one.
"""
hop = SR // 100
n = len(sig) // hop
if n < 8:
return 0.0
env = np.sqrt(np.array([np.mean(sig[i*hop:(i+1)*hop] ** 2) for i in range(n)]) + 1e-12)
# Onset = a rise well above the local floor, with a 60 ms refractory gap so
# one transient is not counted six times.
floor = np.median(env)
thresh = max(floor * 3.0, env.max() * 0.12)
count, last = 0, -99
for i in range(1, n):
if env[i] > thresh and env[i] > env[i-1] * 1.6 and i - last > 6:
count += 1
last = i
return count / (n / 100.0)
def measure(sig: np.ndarray) -> dict:
w = np.hanning(len(sig))
S = np.abs(np.fft.rfft(sig * w)) ** 2
f = np.fft.rfftfreq(len(sig), 1 / SR)
tot = S.sum() + 1e-20
bands = {
"<150": 100 * S[f < 150].sum() / tot,
"150-2k": 100 * S[(f >= 150) & (f < 2000)].sum() / tot,
"2k+": 100 * S[f >= 2000].sum() / tot,
}
return {
"rms_db": 20 * math.log10(math.sqrt(float(np.mean(sig ** 2))) + 1e-9),
"centroid": float((f * S).sum() / tot),
"onsets": onsets_per_s(sig),
"bands": bands,
}
def pct(new: float, old: float) -> float:
return 100.0 * (new - old) / old if old > 1e-9 else float("inf")
def delta_for(kind: str, base: dict, test: dict) -> tuple[float, float, str]:
"""(measured delta, threshold, unit label) for this control kind."""
key, thr, unit = THRESH[kind]
if key == "rms_db":
return test["rms_db"] - base["rms_db"], thr, unit
return pct(test[key], base[key]), thr, unit
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--orbit", type=int, required=True, help="dN to listen to")
ap.add_argument("--cc", type=int, required=True, help="the control's CC number")
ap.add_argument("--kind", choices=sorted(THRESH), required=True,
help="what to MEASURE: filter/crush -> centroid, gain -> rms, "
"density -> onsets")
ap.add_argument("--values", nargs="+", type=int, required=True,
help="CC values to drive (first capture is the baseline BEFORE "
"any send)")
ap.add_argument("--restore", type=int, default=None,
help="CC value to leave the control at (default: leave last)")
ap.add_argument("-s", "--seconds", type=float, default=8.0,
help="capture length per value (default 8s ~ 4 bars at 120)")
ap.add_argument("--label", default="", help="name for the report line")
args = ap.parse_args()
if 77 <= args.cc <= 84:
print("control-lens: REFUSING — CC 77-84 are MIDI-learned to ARDOUR TRACK "
"GAINS (77 is global gain; down = total silence). Driving them "
"desyncs the physical faders.", file=sys.stderr)
return 2
port = lcxl.sc_midi_input_port()
if not port:
print("control-lens: FAIL — no SuperCollider MIDI input port; is sclang up?",
file=sys.stderr)
return 2
name = args.label or f"CC{args.cc}"
print(f"control-lens: d{args.orbit} / {name} / kind={args.kind} "
f"via SC MIDI in ({port})\n")
base, why = capture(args.orbit, args.seconds)
if base is None:
print(f"control-lens: DISCARD — baseline unusable: {why}", file=sys.stderr)
return 2
b = measure(base)
if b["rms_db"] < -70.0:
print(f"control-lens: SKIPPED — d{args.orbit} is SILENT at baseline "
f"({b['rms_db']:.1f} dB rms). Every control would read 'no change'; "
f"fix the orbit first, then judge its controls.", file=sys.stderr)
return 3
print(f" {'value':>6s} {'rms dB':>8s} {'centroid':>9s} {'onset/s':>8s} "
f"{'<150':>5s} {'150-2k':>6s} {'2k+':>5s} delta")
print(f" {'base':>6s} {b['rms_db']:8.1f} {b['centroid']:8.0f}Hz "
f"{b['onsets']:8.1f} {b['bands']['<150']:5.1f} "
f"{b['bands']['150-2k']:6.1f} {b['bands']['2k+']:5.1f}")
best, rows = 0.0, []
for v in args.values:
if not lcxl.send_cc(port, args.cc, v):
print(f" {v:6d} SEND FAILED — not judged", file=sys.stderr)
continue
time.sleep(1.5) # let the change take on the next events
sig, why = capture(args.orbit, args.seconds)
if sig is None:
print(f" {v:6d} DISCARD ({why})", file=sys.stderr)
continue
m = measure(sig)
d, thr, unit = delta_for(args.kind, b, m)
best = max(best, abs(d))
rows.append((v, m, d, unit))
print(f" {v:6d} {m['rms_db']:8.1f} {m['centroid']:8.0f}Hz "
f"{m['onsets']:8.1f} {m['bands']['<150']:5.1f} "
f"{m['bands']['150-2k']:6.1f} {m['bands']['2k+']:5.1f} "
f"{d:+.1f} {unit}")
if args.restore is not None:
lcxl.send_cc(port, args.cc, args.restore)
print(f"\n restored CC{args.cc} -> {args.restore}")
_, thr, unit = THRESH[args.kind]
if not rows:
print("\ncontrol-lens: NO VERDICT — every capture was discarded.",
file=sys.stderr)
return 2
if best >= thr:
print(f"\ncontrol-lens: {name} is EFFECTIVE — best swing {best:.1f} {unit} "
f"(>= {thr:g} threshold)")
return 0
print(f"\ncontrol-lens: {name} shows NO CHANGE — best swing only {best:.1f} "
f"{unit} (< {thr:g}). Either the control is not wired to this orbit, or "
f"it is wired but inaudible. Do NOT read this as 'subtle'.",
file=sys.stderr)
return 1
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