Commit cd90dcb0 by PLN (Algolia)

fix(sc): every orbit's MiVerb/MiClouds/MiRipples send was a dead node

On every clean boot the server said, 84 lines of it:

    *** ERROR: SynthDef global_mi_verb2 not found
    FAILURE IN SERVER /s_new SynthDef not found

3 SynthDefs x 14 orbits x 2 lines. So '# verbwet', '# cloudswet' and
'# ripplesreson' did nothing on any orbit, for the whole session -- silently,
because a missing global effect is an absence of effect rather than a noise you
notice.

mi-UGens was installed correctly all along (11 .so + 11 classes) and the
SynthDefs were defined correctly in start_and_midi.scd:273-296. The bug was
ordering: SynthDef(...).add is ASYNCHRONOUS -- it compiles locally, sends
/d_recv and returns -- while the very next statement sends /s_new for those
names via initNodeTree. The /s_new overtook the /d_recv. One s.sync before the
registration, legal inside s.waitForBoot's Routine; the idiom was already
commented out twelve lines up. 84 errors before, 0 after.

spectral-freeze above deliberately does NOT need it: it replaces a def whose
synths are built per event, long after /d_recv has landed.

Monitors it as gear, since presence checks could not see any of this:
- mi-UGens extension now enumerates the 11 plugins AND 11 classes we use and
  names whichever is missing. d.is_dir() is true whether the directory holds a
  working install or a stale README.
- SuperDirt boot errors (new) scopes the journal to the CURRENT boot and FAILs
  on SynthDef-not-found / FAILURE IN SERVER, naming the distinct defs rather
  than one line per orbit. Generic question, so it catches the next
  async-ordering bug too. WARNs when parvagues-sc is down, which is the correct
  on-demand state before a set.

Both tested in both directions: FAIL on the recorded pre-fix journal window
naming all three defs, PASS on the post-fix boot. That test is what caught 're'
never having been imported in rig-doctor.py -- the new code was its first user,
so the check would have crashed the doctor the first time it found something.

Doctor: 47 checks, 0 fail, 10 warn, 37 pass.
parent 077e1db1
...@@ -2130,9 +2130,10 @@ scattered through this file resolve locally. ...@@ -2130,9 +2130,10 @@ scattered through this file resolve locally.
the next boot: `preload: warming the set's samples…` / the next boot: `preload: warming the set's samples…` /
`=== PRELOAD: 60/61 banks OK in 2.6 s ===`. `=== PRELOAD: 60/61 banks OK in 2.6 s ===`.
- 🔴 **The preload plan is fresh and aimed at the wrong set.** Re-running the - 🔴 **The preload plan is fresh and aimed at the wrong set.** Re-running the
same load after the fix still logged 29 lazy reads — and **all 29 were the same load after the fix still logged 41 lazy reads — and **all 41 were the
`rose` bank**, because `rose_rouge` is NOT in `armada/setlist_opal2026.txt`. `rose` bank**, because `rose_rouge` is NOT in `armada/setlist_opal2026.txt`.
Zero of the 60 preloaded banks lazy-loaded, so the mechanism is provably Zero of the 60 preloaded banks lazy-loaded, and SC logged ZERO `late`
messages in that window, so the mechanism is provably
working; it is warming the 16-track OPAL set while the track this box has working; it is warming the 16-track OPAL set while the track this box has
actually been playing (see the rig-state memory) is outside it. actually been playing (see the rig-state memory) is outside it.
This is `check-preload.sh`'s own documented failure one layer up: there, the This is `check-preload.sh`'s own documented failure one layer up: there, the
...@@ -2148,14 +2149,41 @@ scattered through this file resolve locally. ...@@ -2148,14 +2149,41 @@ scattered through this file resolve locally.
different things. Worth ten minutes: the mismatch banner is a real signal that different things. Worth ten minutes: the mismatch banner is a real signal that
currently cries wolf on a fresh plan, and a warning that fires when nothing is currently cries wolf on a fresh plan, and a warning that fires when nothing is
wrong is a warning nobody reads on the night it matters. wrong is a warning nobody reads on the night it matters.
- **Three global FX synthdefs are missing at every boot**: `global_mi_verb2`, - ~~**Three global FX synthdefs are missing at every boot**~~ **FIXED 2026-09-07
`global_mi_clouds2`, `global_mi_ripples2` — `*** ERROR: SynthDef ... not with one `s.sync`.** `global_mi_verb2` / `global_mi_clouds2` /
found` / `FAILURE IN SERVER /s_new SynthDef not found`, seen 2026-09-07 on a `global_mi_ripples2` were reported `SynthDef not found` on every clean boot —
clean boot. These are the mi-UGens global effect buses, so any track routing **84 error lines**, i.e. 3 defs × 14 orbits × 2 lines. So **every orbit's
to them gets silence from that send. `rig-doctor`'s mi-ugens check passes, MiVerb, MiClouds and MiRipples send was a dead node for the whole session**:
which means it is checking that the plugins are present rather than that the `# verbwet`, `# cloudswet`, `# ripplesreson` did nothing, silently, because a
synthdefs load — the same presence-versus-function gap as everywhere else this missing global effect is an absence of effect rather than a noise you notice.
week. **mi-UGens was installed perfectly all along** — 11 `.so` + 11 classes at
`~/.local/share/SuperCollider/Extensions/mi-UGens` — and the SynthDefs were
defined correctly at `start_and_midi.scd:273-296`. The bug was ordering:
`SynthDef(...).add` is ASYNCHRONOUS (compiles locally, sends `/d_recv`,
returns) and the very next statement, `~dirt.orbits.do { ... x.initNodeTree }`,
sends `/s_new` for those names. The `/s_new` overtook the `/d_recv`. One
`s.sync` before the registration, legal because we are inside
`s.waitForBoot`'s Routine — the idiom was already sitting commented out at
line 236. Verified: 84 errors before, **0 after**, same file one line apart.
(`spectral-freeze` above does NOT need it: it replaces a def whose synths are
built per event, long after `/d_recv` lands.)
- **`rig-doctor` now monitors mi as gear** (2026-09-07, PLN's ask). Two checks
where there was one presence test:
* `mi-UGens extension` enumerates the 11 plugins **and** the 11 classes we
actually use, and WARNs naming whichever is absent — `d.is_dir()` is true
whether the directory holds a working install or a stale README.
* `SuperDirt boot errors` (new, under SERVICES) scopes the journal to the
CURRENT boot and FAILs on `SynthDef ... not found` / `FAILURE IN SERVER`,
naming the distinct SynthDefs rather than repeating one line per orbit.
The question it asks is generic, so it catches the next async-ordering bug
too, not just this one. WARNs (not FAILs) when parvagues-sc is down, since
on-demand is the correct state before a set.
Both tested in BOTH directions: the boot-error check FAILs on the recorded
pre-fix journal window (naming all three defs) and PASSes on the post-fix
boot. That test is also what caught `re` never having been imported in
`rig-doctor.py` — my new code was its first user, so the check would have
crashed the doctor the first time it found something.
Doctor now: **47 checks, 0 fail, 10 warn, 37 pass.**
**The mechanism was never broken**: `gig-up.sh:182` already calls the checker **The mechanism was never broken**: `gig-up.sh:182` already calls the checker
and auto-fixes on a non-zero exit, and the checker does exit 1 correctly. and auto-fixes on a non-zero exit, and the checker does exit 1 correctly.
`gig-up.sh` simply has not been run on this box since the setlist grew. So `gig-up.sh` simply has not been run on this box since the setlist grew. So
......
...@@ -325,6 +325,33 @@ s.waitForBoot { ...@@ -325,6 +325,33 @@ s.waitForBoot {
}, [\ir]).add; }, [\ir]).add;
"[boot] spectral-freeze overridden: PV_MagFreeze (JoshPVUGens crashed scsynth 2026-08-15)".postln; "[boot] spectral-freeze overridden: PV_MagFreeze (JoshPVUGens crashed scsynth 2026-08-15)".postln;
// WAIT FOR THE SERVER TO HAVE THE SYNTHDEFS BEFORE ASKING FOR THEM.
//
// `SynthDef(...).add` is ASYNCHRONOUS: it compiles locally, sends /d_recv,
// and returns immediately. `initNodeTree` below sends /s_new for those very
// names. Without a sync the /s_new overtakes the /d_recv and every boot
// logged, in the server's own words (2026-09-07, on a clean boot):
//
// *** ERROR: SynthDef global_mi_verb2 not found
// FAILURE IN SERVER /s_new SynthDef not found
// *** ERROR: SynthDef global_mi_clouds2 not found
// *** ERROR: SynthDef global_mi_ripples2 not found
//
// So every orbit's MiVerb / MiClouds / MiRipples send was a dead node for
// the whole session: `# verbwet`, `# cloudswet`, `# ripplesreson` did
// nothing, silently, because a missing global effect is absence of an
// effect rather than an error you can hear. mi-UGens itself was installed
// correctly all along (11 .so + 11 classes) — which is why rig-doctor's
// presence check was green.
//
// `spectral-freeze` above does NOT need this: it replaces a SynthDef whose
// synths are built per EVENT, long after /d_recv has landed. These three
// are instantiated here, at boot, in the next statement.
//
// We are inside s.waitForBoot's Routine, so s.sync is legal here — the
// idiom is even sitting commented out at the top of this block.
s.sync;
// Register global effects on all orbits // Register global effects on all orbits
~dirt.orbits.do { |x| ~dirt.orbits.do { |x|
var verb = GlobalDirtEffect(\global_mi_verb, [\verbwet, \verbtime, \verbdamp, \verbhp, \verbfreeze, \verbdiff, \verbgain]); var verb = GlobalDirtEffect(\global_mi_verb, [\verbwet, \verbtime, \verbdamp, \verbhp, \verbfreeze, \verbdiff, \verbgain]);
......
...@@ -78,6 +78,7 @@ from __future__ import annotations ...@@ -78,6 +78,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
...@@ -245,17 +246,124 @@ def check_quarks() -> None: ...@@ -245,17 +246,124 @@ def check_quarks() -> None:
add("TOOLCHAIN", f"quark {name}", PASS, note) add("TOOLCHAIN", f"quark {name}", PASS, note)
# The Mutable Instruments UGens this rig actually uses. MiVerb / MiClouds /
# MiRipples are wired as GLOBAL effects on every orbit by start_and_midi.scd,
# so their absence is not "one synth missing" — it is every orbit's send going
# quiet. The rest back the mi* synths.
MI_PLUGINS = ("MiVerb", "MiClouds", "MiRipples", "MiPlaits", "MiRings",
"MiTides", "MiWarps", "MiOmi", "MiElements", "MiGrids", "MiMu")
# Global effects registered per orbit. Named `global_<name><numChannels>` on the
# server, so the boot log says e.g. `global_mi_verb2`.
MI_GLOBAL_FX = ("global_mi_verb", "global_mi_clouds", "global_mi_ripples")
def check_mi_ugens() -> None: def check_mi_ugens() -> None:
"""WARN not FAIL: without it, the mi* SynthDefs (miplaits, mirings, """Are the mi-UGens plugins there, in full, and for each thing we use?
mitides, miwarps, miomi, mielements) fail to build; everything else
still runs.""" Was a single `d.is_dir()`. That is true whether the directory holds eleven
working plugins or one stale README, and it was green on 2026-09-07 while
every orbit's MiVerb/MiClouds/MiRipples send was a dead node — see
check_sc_boot_errors, which is the half that can actually see that.
"""
d = MI_UGENS() d = MI_UGENS()
if d.is_dir(): if not d.is_dir():
add("TOOLCHAIN", "mi-UGens extension", PASS, str(d))
else:
add("TOOLCHAIN", "mi-UGens extension", WARN, f"missing at {d}", add("TOOLCHAIN", "mi-UGens extension", WARN, f"missing at {d}",
"needed only for mi* synths — install from " "needed for mi* synths AND for the per-orbit MiVerb/MiClouds/"
"MiRipples global sends — install from "
"https://github.com/v7b1/mi-UGens into that path") "https://github.com/v7b1/mi-UGens into that path")
return
have_so = {p.stem for p in d.glob("*.so")}
have_sc = {p.stem for p in (d / "Classes").glob("*.sc")}
missing_so = [n for n in MI_PLUGINS if n not in have_so]
missing_sc = [n for n in MI_PLUGINS if n not in have_sc]
if missing_so or missing_sc:
add("TOOLCHAIN", "mi-UGens extension", WARN,
f"{len(have_so)} .so + {len(have_sc)} classes at {d}; "
f"missing plugin(s): {', '.join(missing_so) or 'none'}; "
f"missing class(es): {', '.join(missing_sc) or 'none'}",
"rebuild/reinstall from https://github.com/v7b1/mi-UGens — a "
"partial install silently drops whichever synths need the absent "
"plugin, and MiVerb/MiClouds/MiRipples are wired to EVERY orbit")
else:
add("TOOLCHAIN", "mi-UGens extension", PASS,
f"all {len(MI_PLUGINS)} plugins + classes present at {d}")
def check_sc_boot_errors() -> None:
"""Did SuperDirt's LAST boot actually get everything onto the server?
This is the check that catches what presence cannot. `SynthDef(...).add` is
asynchronous — it sends /d_recv and returns — so anything that instantiates
those names in the next statement can overtake them. That is exactly what
happened to the three mi global effects: mi-UGens was installed perfectly,
the SynthDefs were defined correctly in start_and_midi.scd, and the server
still said
*** ERROR: SynthDef global_mi_verb2 not found
FAILURE IN SERVER /s_new SynthDef not found
on every single boot, leaving `# verbwet` / `# cloudswet` /
`# ripplesreson` as no-ops on every orbit for a whole session. Fixed with
one `s.sync` (2026-09-07); this check is what would have found it, and what
will find the next one — the question it asks is generic, not mi-specific.
Read-only, and deliberately scoped to the CURRENT boot: an error from a
session three days ago is history, not a preflight failure.
"""
rc, out, _ = run_cmd(["systemctl", "--user", "show", "-p", "ActiveState",
"-p", "ExecMainStartTimestamp", "parvagues-sc.service"])
state = started = ""
for line in out.splitlines():
if line.startswith("ActiveState="):
state = line.split("=", 1)[1].strip()
elif line.startswith("ExecMainStartTimestamp="):
started = line.split("=", 1)[1].strip()
if state != "active":
add("SERVICES", "SuperDirt boot errors", WARN,
f"parvagues-sc is {state or '?'} — nothing to inspect. This box "
f"runs SuperDirt on demand, so that is normal before a set; "
f"re-run after gig-up to check the boot.",
"systemctl --user start parvagues-sc && sleep 20 && "
"tools/rig-doctor.py")
return
if not started:
add("SERVICES", "SuperDirt boot errors", WARN,
"parvagues-sc is active but has no start timestamp to scope the "
"journal to", "")
return
rc, out, _ = run_cmd(["journalctl", "--user", "-u", "parvagues-sc",
"--since", started, "--no-pager"], timeout=20)
def is_error(line: str) -> bool:
# Explicit rather than a chain of or/and: `and` binds tighter than `or`,
# so the first draft read as A or (B and C) or D by accident and only
# happened to be right.
if "FAILURE IN SERVER" in line:
return True
return "SynthDef" in line and "not found" in line
bad = [l.strip() for l in out.splitlines() if is_error(l)]
# Name the distinct SynthDefs rather than repeating one line per orbit:
# a missing global effect fails once per orbit, so 3 defs read as 24 lines.
names = sorted({m.group(1) for l in bad
for m in [re.search(r"SynthDef (\S+) not found", l)] if m})
if not bad:
add("SERVICES", "SuperDirt boot errors", PASS,
f"no SynthDef/server errors since {started}")
else:
detail = (f"{len(bad)} server error line(s) since {started}"
+ (f"; missing SynthDef(s): {', '.join(names)}" if names else ""))
add("SERVICES", "SuperDirt boot errors", FAIL, detail,
"a SynthDef the boot ASKS FOR but the server does not have is a "
"silent dead node, not an audible failure. If the names are "
f"{'/'.join(MI_GLOBAL_FX)}, the cause is a missing `s.sync` between "
"`.add` and `initNodeTree` in start_and_midi.scd. Read the full "
"log: journalctl --user -u parvagues-sc -b --no-pager | grep -i synthdef")
def check_haskell() -> None: def check_haskell() -> None:
...@@ -1082,7 +1190,8 @@ def check_secrets() -> None: ...@@ -1082,7 +1190,8 @@ def check_secrets() -> None:
# Checks that take no argument. check_sample_coverage(bundle) is called # Checks that take no argument. check_sample_coverage(bundle) is called
# separately by run_all() since it needs the --bundle path. # separately by run_all() since it needs the --bundle path.
CHECKS = ( CHECKS = (
check_sc, check_quarks, check_mi_ugens, check_haskell, check_pulsar, check_sc, check_quarks, check_mi_ugens, check_sc_boot_errors,
check_haskell, check_pulsar,
check_ardour, check_pipewire, check_alsa_ffmpeg, check_python_modules, check_ardour, check_pipewire, check_alsa_ffmpeg, check_python_modules,
check_scd_sample_roots, check_scd_sample_roots,
check_synthdefs, check_synthdefs,
......
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