Skip to content

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.

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"]

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 in 16.
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.
  • 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.

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 PluginRef contract.
  • PluginLoc identifies 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.

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.

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 a catch_unwind guard 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 via MeterSnapshot.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 (Store writes active.json — patch id + its CLAP bundle blame-keys — on every switch and at startup; the child reads AUVRA_RESUME). A plugin that crashes BLAME_THRESHOLD times is quarantined (AUVRA_QUARANTINE → build_graph loads 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.

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]
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:

  1. CLAP owners are created, queried and destroyed on their owning host/UI thread; only the Send processing adapter enters the audio graph.
  2. The audio thread allocates and locks nothing. Commands, musical events, telemetry and disposal all use bounded RT-safe structures.
  3. A note’s slot membership and transposed keys are captured at note-on and replayed for note-off and per-note expression.
  4. 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.

  1. CLAP thread-pool + voice-info extension coverage across the plugins we care about (Surge XT, Dexed, Odin2) — affects the worker-pool design.
  2. Parameter-metadata quality in the wild — largely answered by the three-layer control model in 13 §4 (patch-owned performance controls, seeded from CLAP remote-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).
  3. State-blob forward compatibility policy for store updates (pin old versions vs. require migration).
  4. Release-tail handover CPU cost at worst case (two full graphs for ~2 s) on the N100 — measure in M5.
  5. Wayland/KMS plugin-GUI embedding feasibility on the target (post-P6).
  6. Offline activation model acceptable to commercial vendors.
  1. 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

  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/. ↩

  3. 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/ ↩

  4. LV2 plugin standard. https://lv2plug.in/ ↩