12 — Plugin Host, Patches & Mixer
Status · reconciled 2026-08-30. Engine milestones M1–M5 and the semantic musical-event refactor are implemented. M6 integrates physical controls and the target without changing these engine contracts. Project-wide status is in
10.
This document reframes what the sound engine is. Auvra is not “a synthesizer
with an app store later” — it is an instrument-shaped plugin host: synth
plugins make sound, a patch combines them into a playable setup, and an
integrated mixer with effect-plugin support glues it to the outputs. The
store (§7) ships plugins and patches. Everything
else in this blueprint (latency budget, control surface, appliance behaviour)
is unchanged and constrains the design here.
1. Concept & terms
Section titled “1. Concept & terms”| Term | Meaning |
|---|---|
| Plugin | A loadable audio module: instrument (synth) or effect. |
| Slot | A position in a patch hosting a synth-plugin instance, with a key/velocity zone and MIDI channel. Patches have unlimited slots (bounded only by CPU/RAM headroom). |
| Patch | The saved playable unit: slot plugins + their state, mixer state, effect chains, control-surface mappings. |
| Setlist | An ordered list of patches for live use, with preloading for instant switching. |
| Strip | The mixer channel belonging to a slot (or bus/master). |
| Built-in plugin | A plugin statically linked into the host (no dlopen), speaking the same host API as external ones. |
flowchart LR
subgraph PATCH["Patch"]
direction TB
S1["Slot 1<br/>synth plugin<br/>zone A0–B3"]
S2["Slot 2<br/>synth plugin<br/>zone C4–C8"]
S3["Slot 3<br/>synth plugin<br/>layer (full range)"]
SN["Slot N …<br/>(unlimited, CPU-bounded)"]
end
subgraph MIX["Mixer"]
direction TB
ST1["Strip 1<br/>inserts + sends"]
ST2["Strip 2<br/>inserts + sends"]
ST3["Strip 3<br/>inserts + sends"]
FXA["FX bus A<br/>(e.g. reverb plugin)"]
FXB["FX bus B<br/>(e.g. delay plugin)"]
MST["Master strip<br/>inserts + limiter"]
end
MIDI["USB-MIDI<br/>(keybed)"] --> PATCH
CTL["I²C control surface<br/>(via auvra-ctld)"] -->|"mappings"| PATCH
S1 --> ST1
S2 --> ST2
S3 --> ST3
ST1 --> MST
ST2 --> MST
ST3 --> MST
ST1 -.->|"send"| FXA
ST2 -.->|"send"| FXA
ST3 -.->|"send"| FXB
FXA --> MST
FXB --> MST
MST --> OUT["Audio interface"]
2. Plugin API strategy
Section titled “2. Plugin API strategy”Decision: CLAP is the native plugin format; the host core is format-agnostic so further formats can be added as backends later.
- CLAP is MIT-licensed, has a stable C ABI, no SDK agreement, and was designed (Bitwig/u-he) with modern hosting in mind — polyphonic modulation, per-note expression, a host thread-pool, and clean separation of parameters from GUI.1 A meaningful catalog of open-source instruments already ships CLAP builds (Surge XT, Dexed, Odin2, Airwindows…),2 which seeds the store with real content.
- The engine defines its own host-side plugin traits (instrument, effect,
parameters, state). CLAP is the first backend implementing them
(
auvra-clap); built-in plugins implement the traits directly and are statically linked — the same API is dogfooded from day one. - VST3 became a licensing non-issue: since 2025 the VST 3 SDK is offered under plain MIT (GPLv3/proprietary dual licensing was retired), so a VST3 backend is a realistic later addition — only the VST trademark/logo has usage rules.3 LV2 remains a candidate for the Linux-native corpus.4
- Parameters first, native GUI as workbench. Auvra’s touch-first performance
controls come from parameter metadata and CLAP
remote-controls; a native plugin editor is an optional setup surface, never the stage interface. The embedding architecture and minimal X11 kiosk decision are in16.
| Alternative | Verdict |
|---|---|
| Custom Auvra-only API | Perfect technical fit, empty ecosystem — every synth would need a port. Rejected as primary; the internal traits keep the door open. |
| VST3-first | MIT now, but C++-heavy SDK, GUI-centric culture, weaker headless story. Later backend, not the native format. |
| LV2-first | Linux-native and headless-friendly, but aging spec, fragmented extensions, thin commercial uptake. Possible later backend. |
3. Patch model
Section titled “3. Patch model”- A patch has unlimited synth slots (layers/splits — the S90 is a stage
keyboard, but the slot count is a data-model property, not a hardware one;
the practical limit is CPU/RAM headroom, enforced by the overload policy in
§5, not by the format). Each slot: plugin reference, key zone, velocity window, MIDI channel, transpose, its strip settings. The motor-fader bank pages across slots in banks (06-control-surface.md). - Patch file = versioned manifest + per-plugin opaque state blobs (keyed by plugin id + version, exactly what the plugin’s state extension returns). Plugins updating must load older blobs (CLAP convention).1
- Setlists order patches for a gig. The engine preloads the next patch’s plugins in the background (RAM is cheap on the N100); switching swaps the audio graph between blocks — target < 100 ms to playable.
- Release-tail handover: on switch, the outgoing graph keeps rendering into the master until voices decay (bounded, e.g. 2 s) while the new patch is already playable — no cut-off tails on stage.
4. Mixer
Section titled “4. Mixer”Decision: fixed topology, generous but bounded — not a free-form modular graph. Predictable real-time cost, an obvious touch UI, and a 1:1 mapping to the motor-fader bank beat arbitrary routing for a stage instrument.
- Slot strip: input trim → 4 insert slots (effect plugins) → pan → fader → mute, plus 2 sends (pre/post switchable).
- FX buses A/B: each an effect-plugin chain (up to 4), return to master.
- Master strip: 4 inserts → master fader → safety limiter (built-in, always last — appliance principle: the instrument must not clip a PA).
- Effects are plugins in those positions; the built-in set starts with utility EQ/comp/reverb/delay so a fresh device is complete without store content.
- Motor-faders page across slot faders in banks (+ master pinned); further
banks switch to sends (
06-control-surface.md).
The implemented mixer keeps one format-general effect path:
- Two smoothed pre/post-fader sends feed two per-graph FX buses. A retiring graph therefore retains its own delay/reverb tail.
- Slot, bus and master chains each host up to four effects. Built-in, CLAP and
missing-plugin placeholders use the same persisted
PluginRefcontract. PluginLocidentifies every slot/insert/bus/master owner for state capture, retirement, quarantine and native-editor routing.- Master inserts run before the one global master fader and brickwall limiter; no plugin can bypass the safety ceiling.
- Bypass is a live RT command and preserves the instance. Add/remove/replace/ reorder are structural patch edits and rebuild a graph off-thread; graph installation reasserts persisted bypass state to reconcile command races.
- Diagnostic built-ins provide delay, reverb, EQ and compression without changing the product rule that instruments and extensible effects are CLAP.
5. Real-time architecture
Section titled “5. Real-time architecture”The latency budget (01 §4) is unchanged: one
64-frame period @ 48 kHz for the whole graph.
- Audio thread: renders the precomputed graph order (slots → strips → buses → master); no allocation, no locks, no dlopen on this thread.
- Command ring: UI/ctld/MIDI-mapping changes go through lock-free SPSC rings into the audio thread. Parameter changes are smoothed in the engine.
- Musical-event routing: platform adapters normalize MIDI 1 byte streams
(and later MIDI 2 UMP) into the protocol-independent event contract in
05§3. Notes carry stable host ids; note on/off, controllers/pedals, pitch, pressure, and note expression are delivered sample-accurately. Zone membership and transposed key are recorded at note-on and replayed for note-off and per-note expression. - Plugin lifecycle (load, instantiate, activate, state restore) runs on a worker thread; activated instances are handed to the audio thread via RT-safe swap. Deactivation/unload likewise never blocks audio.
- Overload policy: since slot count is unlimited, this is the real bound — per-slot voice caps, a headroom meter in the UI, patch-load-time CPU estimation, and ordered degradation: voice stealing first, then bypassing insert FX, then muting the offending slot — never an xrun by design.
- Threads: RT audio (SCHED_FIFO), MIDI in, worker pool (also backs the CLAP thread-pool extension later), ctld/IPC, UI.
6. Trust & isolation
Section titled “6. Trust & isolation”Decision: plugins run in-process; safety comes from a curated, signed store plus a watchdog.
| Aspect | Choice |
|---|---|
| Hosting | In-process (lowest latency, no shared-memory IPC tax on a 4-core N100). |
| Store policy | Only curated, signed packages install; sideloading gated behind an explicit developer mode. |
| Crash containment | Host watchdog restarts the engine into the current patch in < 1 s (state is journaled after every edit); repeated crashes quarantine the offending plugin. |
| Revisit trigger | If the store ever accepts uncurated uploads, unverified plugins move to a sandbox process (hybrid model) — the host traits are transport-agnostic to keep this possible. |
Crash containment has two layers:
- In-process panic containment (
auvra-core::mixer). Every slot instrument and insert FX runs behind acatch_unwindguard on the audio thread (panics are unwind, and the guard sits inside the render path because the cpal callback is a C boundary a panic must not cross). A caught panic silences the block and bypasses that plugin (Slot.active=false/Insert.bypassed=true) — the rest of the patch keeps playing, no restart. Surfaced viaMeterSnapshot.plugin_panics(the app logs it off-thread). Contains panics, not C-ABI segfaults. - Supervisor watchdog (
auvra --supervise). Spawns the app as a child and, on a hard crash, relaunches it into the journaled active patch (Storewritesactive.json— patch id + its CLAP bundle blame-keys — on every switch and at startup; the child readsAUVRA_RESUME). A plugin that crashesBLAME_THRESHOLDtimes is quarantined (AUVRA_QUARANTINE→build_graphloads it bypassed, state blob preserved); a crash-loop that can’t be salvaged makes the supervisor give up rather than spin. This is the segfault half of the pair.
7. Store implications
Section titled “7. Store implications”Concept-level only here (own design doc later):
- Package = plugin binaries (x86-64 now, aarch64 when the platform migrates), metadata (id, version, vendor, category, param manifest), factory presets, signature.
- SDK story: “ship your existing CLAP” is the pitch; plus a Rust template crate for new plugins targeting Auvra’s built-in trait and CLAP export.
- Commercial plugins need offline-friendly licensing (a stage instrument may never be online) — activation-file model, no phone-home on boot. [verify]
8. Crate boundaries & milestone status
Section titled “8. Crate boundaries & milestone status”| Crate | Responsibility |
|---|---|
auvra-core |
Platform- and format-independent engine, graph, mixer, events, patches and control targets |
auvra-clap |
CLAP discovery, ownership, state, audio adapter and native editor integration |
auvra-platform |
SDL3/egui, cpal and midir adapters |
auvra-app |
Product orchestration, UI state, host thread and command wiring |
auvra-proto / auvra-ctld |
Versioned hardware contract and Linux control-node daemon |
| Milestone | Result | Status |
|---|---|---|
| M1 | Engine and plugin traits | Done |
| M2 | Lock-free mixer, limiter, telemetry and off-thread disposal | Done |
| M3 | Real CLAP instruments/effects, state and parameter introspection | Done |
| M4 | Slots, zones, layers/splits and stable-note event routing | Done |
| M5 | Patches/setlists, async ownership, preloading and tail-safe switching | Done |
| M6 | Control IPC, hardware mapping and target integration | Active |
The implementation depends on four invariants:
- CLAP owners are created, queried and destroyed on their owning host/UI thread;
only the
Sendprocessing adapter enters the audio graph. - The audio thread allocates and locks nothing. Commands, musical events, telemetry and disposal all use bounded RT-safe structures.
- A note’s slot membership and transposed keys are captured at note-on and replayed for note-off and per-note expression.
- Graph switches install a prebuilt graph immediately, release the outgoing graph through a bounded tail, then dispose it off-thread. Plugin owners outlive every active, retiring or preloaded adapter.
M6 does not redesign the engine. M6.1 provides versioned, reconnecting
auvra-ctld/app IPC for raw physical input and output feedback. M6.2 maps those
ids through platform-free hardware profiles onto the existing command/control
targets, then proves the complete stack in QEMU and on the N100. Project-wide
exit criteria live in
10.
9. Open questions [verify]
Section titled “9. Open questions [verify]”- CLAP thread-pool + voice-info extension coverage across the plugins we care about (Surge XT, Dexed, Odin2) — affects the worker-pool design.
- Parameter-metadata quality in the wild — largely answered by the
three-layer control model in
13§4 (patch-owned performance controls, seeded from CLAPremote-controls); residual risk is coverage of that extension among target plugins (measured at M3 for Surge XT: 5 curated pages incl. an 8-macro page —13§8.1; Dexed still to be spot-checked). - State-blob forward compatibility policy for store updates (pin old versions vs. require migration).
- Release-tail handover CPU cost at worst case (two full graphs for ~2 s) on the N100 — measure in M5.
- Wayland/KMS plugin-GUI embedding feasibility on the target (post-P6).
- Offline activation model acceptable to commercial vendors.
Footnotes
Section titled “Footnotes”-
CLAP — CLever Audio Plug-in API, MIT-licensed C ABI by Bitwig & u-he. https://cleveraudio.org/ · spec/repo https://github.com/free-audio/clap · Rust host/plugin bindings https://github.com/prokopyl/clack ↩ ↩2
-
Surge XT (GPLv3, ships CLAP) https://surge-synthesizer.github.io/; further CLAP OSS instruments: Dexed https://asb2m10.github.io/dexed/, Odin 2 https://thewavewarden.com/pages/odin-2, Airwindows consolidated https://www.airwindows.com/consolidated/. ↩
-
VST 3 SDK licensing: “VST 3 SDK is under MIT license. Licensing under GPLv3 and the Steinberg proprietary license is no longer available.” https://github.com/steinbergmedia/vst3sdk · https://www.steinberg.net/developers/vstsdk/ ↩
-
LV2 plugin standard. https://lv2plug.in/ ↩