0020 — Plugin tiers are fixed by MEASURED latency; LV2 ships as tiered subpackages
Status: Accepted (built, including the tiered RPM packaging — 2026-07-29 note below) Date: 2026-07-15
Context
A live console must never host a latency-eater (linear-phase EQ, look-ahead limiter, FFT
denoiser, long convolution reverb). The catalog scans ~610 LV2 plugins, and the RPM originally
shipped none. The obvious signal — the plugin's declared lv2:latency port — cannot be
trusted: a measured round-trip pass over all 610 found 12 plugins that declare zero latency
but actually delay the signal (two SWH IIR crossovers by ~99 ms), and the inverse (SWH
artificialLatency declares 120000 frames, delays 0).
Decision
- Measure, don't declare. Every plugin's round-trip latency is measured offline (lilv
run()+ a unit impulse + onset detection,benchmark.py --measure). The measured figure fixes the tier:live(≤ 5 ms) /studio(> 5 ms). A plugin that can't be measured (instrument/no-audio-in, silent under defaults, won't instantiate) is quarantinedunclassified— never auto-live. - Two axes. Latency fixes the tier (automatic); curation splits recommended vs extra
within a tier (
curation.ts∪ the harvested zynthian effects set + a curator override list; an override may set any tier except promote tolivefrom a non-livemeasured tier — so a curator can rescue an unmeasurable-but-known-good effect tostudiocase-by-case, but a latency-eater can never reach the live path). - Ship as four RPM subpackages —
openmixer-plugins-{live,live-extra,studio,studio-extra}— with dependencies auto-derived (per-plugin owning RPM viarpm -qf→INDEX.json→ generatedplugin-deps.inc). The meta packageRecommends: openmixer-plugins-liveonly. - Catalog stored per-package (
data/plugins/<rpm>.json, sorted) so a plugin-package update is one reviewable file diff.
Consequences
- A live install physically cannot pull in a studio latency-eater; a 99 ms crossover can never slip into the live set on a false declaration.
- The committed catalog is a reproducible build input (no scan needed in a clean chroot).
-liveis aRecommends, not aRequires: the native EQ/gate/comp/delay/reverb (ADR 0021) make the console fully functional with zero LV2 plugins.- Same "measure the hardware/reality, don't trust a declaration" principle as ADR 0018 (clock).
Note — 2026-07-29: packaging landed; a third suitability axis has since appeared
The core of this ADR is implemented verbatim and is worth trusting: the 5 ms
live boundary (LIVE_MAX_MS = 5 in packages/catalog/src/tier.ts), the
quarantine of the ambiguous rather than a guess (tierForBounds in the same file), and
the curator-override rule that overrides may demote but never promote to live
(resolveTier in packages/catalog/src/curation-tier.ts).
Two things have moved since.
Packaging is done, not "in progress". The four subpackages are generated by
packages/catalog/tools/gen-plugin-deps.mjs, the generated
packaging/rpm/plugin-deps.inc is committed and pulled in by the spec's own
%include %{omx_plugin_inc} directive, and Recommends: %{name}-plugins-live
appears exactly as specified. The per-package catalogs are
packages/catalog/data/plugins/*.json plus INDEX.json.
"Two axes" is now three. This ADR says latency fixes the tier and curation
splits recommended-vs-extra within it. Measured per-plugin CPU cost has since
joined them as a first-class dimension —
the module doc comment in packages/catalog/src/cpu-cost.ts calls itself "the third
suitability dimension, alongside latency and topology", fed by tools/benchmark.py --cost
with provenance committed at packages/catalog/data/cpu-cost-provenance.json,
alongside destination-suitability.ts and family-standing.ts. Someone scoping
catalog work off this ADR alone will under-scope it.
The "~610 LV2 plugins" figure in Context is the size of the scan at the time; the catalog now holds 958 entries. The "12 of 610 declare zero latency but actually delay the signal" measurement is a dated finding and reads correctly as one.