Use installed plugins in the supported desktop host, save useful states, and understand their playback and export requirements.
What it is#
The native desktop backend hosts third-party CLAP, Audio Unit, and VST3 plugins — as track effect inserts or as the track's sound source (instrument) — behind one format-agnostic backend seam. Parameters are addressable by numeric id or display name, set to a constant, automated with a transport-clocked pattern lane, or driven by any control signal (LFOs, envelopes, follower sidechains, and live macro knobs). Format is chosen by the path; parameter values are normalized [0,1] across every format.
Surface#
Four builtins (top-level / track position only):
-- effect insert on a track bus (format dispatched by path):
lead = play pluck !"c3 e3 g3" |> plugin "Valhalla.clap"
comp = play drums beat |> plugin "/Library/Audio/Plug-Ins/VST3/Comp.vst3"
-- a track whose sound source IS a plugin instrument:
keys = instrument "Vital.clap" !"c3 e3 g3 c4"
bell = instrument "au:aumu:dls :appl" !"c5 e5" -- Apple DLSMusicDevice
-- select a plugin within a bundle, and set params (by id or by name):
pad = instrument "Surge.clap" !"c3" |> pluginId "org.surge-synth.surge-xt"
|> pluginParam 12 0.8
|> pluginParam "Filter Cutoff" 0.6
-- a NAMED preset from the workspace store — e.g. a Kontakt patch dialed in
-- once via the editor, saved from the picker's `preset` chip, then loaded
-- by name in any song (state applies first; declared params stay on top):
grand = instrument "Kontakt 8.vst3" !"c3 e3 g3" |> pluginPreset "grand piano"
-- MIDI CC lanes on an instrument — continuous articulation (mod-wheel
-- dynamics CC1, expression CC11, …); constant, pattern lane, or signal,
-- normalized [0,1] like pluginParam:
str = instrument "Kontakt 8.vst3" pat |> pluginPreset "strings"
|> ccLane 1 (lfoSine @ 4c * 0.3 + 0.55)
vla = instrument "Kontakt 8.vst3" mel |> ccLane 1 !"0.3 0.5 0.8 0.6"
-- pattern-valued param: a stepwise automation lane on the transport:
acid = play bass !"c2 c2 g2 c2" |> plugin "au:AUDistortion"
|> pluginParam "Wet/Dry Mix" !"0.2 0.8 [0.5 1]"
-- signal-valued param: any control expression, block-rate; a macro is a
-- live knob wired straight to the plugin parameter; follower sidechains:
wob = play bass !"c2 c2 g2 c2" |> plugin "Wobbler.clap"
|> pluginParam "Cutoff" (lfoSine @ 1/2c * 0.4 + 0.5)
|> pluginParam "Drive" (macro "drive" 0.3)
Dispatch is by path suffix: .clap → CLAP, .vst3 → VST3, au:type:sub:manu → Audio Unit (the space-padded FourCC triple, e.g. au:aufx:dist:appl). A bare reference ("Kontakt 8.vst3", or even "Kontakt 8" with no extension) resolves against the standard system + user plugin directories at activation (plugin::resolve_plugin_ref, shared with volta autosample --plugin) — song files never need machine paths.
A song may load only installed plugins. Loading a plugin runs its native code, so a path a song names must resolve inside /Library/Audio/Plug-Ins or ~/Library/Audio/Plug-Ins (VST3, CLAP, Components), or a folder listed in the VOLTA_PLUGIN_PATH environment variable (colon-separated, for a plugin developer's build folder — a song cannot set it). Anything else — a path beside a mounted disk image, a relative path, a .. escape — is refused at activation with a message naming the folders (plugin::confine_song_plugin_ref, checked on the resolved path). au: references name a registered component and pass. Operator tools whose path the person typed (volta-pilot, volta autosample --plugin) use PluginHost::trusting_any_path. While the desktop is in a jam room no new plugin activates (PluginHost::set_suspended): the room's text is a peer's to write, and a browser peer could not host one anyway. A pluginParam key is a host-stable numeric id or a case-insensitive name (exact first, else a unique substring match); a missing or ambiguous name warns with candidates and is skipped (never misdirected at id 0).
How it's implemented#
- Compiler:
builtins/plugins.rs— the descriptor family that ownsplugin/pluginId/pluginParam/pluginPreset/ccLane: their ports, arities, cadences, and the string-LITERAL operand each of the three name ports takes.instrumentis a track-source descriptor inbuiltins/track_sources.rs. Both lower inlower/track.rs. Plugin hosting is behind cargo features in the ENGINE only; the compiler registers these names unconditionally, so a song's IR does not depend on how the host was built. IR:ir::PluginParam { name, value, pattern, signal },TrackEffect::Plugin,Track.plugin+ the$pluginsentinel synth. A signal-valued param lowers vialower_signal_synthinto an anonymous single-outsynth graph onPluginParam.signal(the macro-initial const-fold runs after the signal probe, so a macro-valued param stays a live lane). - Engine (
engine/src/plugin/): thePluginBackend: Sendtrait (backend.rs) withParamSpec { id, name, min, max, default },resolve_param_key, andwalk_param_lane; per-formatclap_backend.rs/au_backend.rs/vst3_backend.rs;mod.rsdispatches by path. Effects wire througheffect.rs::build_plugin_effect; instruments throughlib.rs::build_track_instruments. Signal lanes compile toTrackPlan.signal_lanes(graph.rs::from_module) and render as one persistent voice per lane in a dedicatedparam_arena(a secondVoiceArena, so modulation never sums into the audible mix), anchored at transport beat 0. Pattern lanes apply sample-accurately on VST3/CLAP, block-rate (~2.7 ms) on AU. A hot-swap carries live instances over when the same plugin sits at the same track / role / slot (state, editor binding, tails, in-flight notes survive). AU and VST3 instantiate on the calling thread (some AUs assert the main queue); AU registry lookups (AudioComponentFindNext, by-name enumeration), which wait on theAudioComponentRegistrardaemon, run on a helper thread bounded byau_backend.rs::REGISTRY_TIMEOUT(20 s), so a wedged registrar makes the plugin unavailable instead of freezing the load. Behindengine/Cargo.tomlfeature gates:plugin-host(umbrella) +clap-host/au-host/vst3-host. - Live editors, all formats (
engine-host/src/plugin_window.rs): AU opens its Cocoa view (custom orAUGenericView); VST3 and CLAP open through the format-agnosticplugin::create_editor_viewseam. VST3: a persistentIEditController(single-component cast, or instantiated +IConnectionPoint-connected +setComponentState-synced) wired with the hostIComponentHandler, whoseperformEditroutes through a lock-guarded queue the audio thread drains — editor knob-turns change the playing sound; teardown lives in the sharedArc<Vst3Core>keepalive (never under an open editor). CLAP: the worker checks its!Sendinstance out to the OS main thread (ClapMainShuttle), theclap.guiextension embeds Cocoa; a retire while the editor is open defers teardown until the window closes (the worker's on-loan ledger). - State save/restore:
EngineHandle::save_plugin_states/restore_plugin_states(AUClassInfo, VST3IComponent::get/setState, CLAPstateextension via the worker), persisted as the<song>.volta.state.jsonsidecar — written on save and restored after a song load (JSON contract:{track, role, slot, format, state: base64}). - CC lanes (
ccLane, gap M1 closed 2026-08-01):PluginCcridesPluginInstrumentSpec.ccs(instrument-only — inserts have no MIDI input); the wrapper walks CC pattern lanes beside param lanes and routes per format at render: VST3 has no CC input events at all, so the wrapper asks the controller'sIMidiMappingwhich ParamID the plugin bound to that controller (cached, warmed control-side at load) and sends normalized param changes; AU gets real0xB0bytes down the note path (MusicDeviceMIDIEvent/event-list, same UMP latch); CLAP getsclap_event_midimerged time-sorted into the input queue. Signal-valuedccLanes ride the same anonymous-modulation voices aspluginParamsignals (SignalLanePlan.cc/ResolvedSignalLane.cc); hot-swap reuse re-applies CC values/lanes like params. Unit-tested via a mock backend (plugin::hosted::cc_tests). - volta-pilot (
app-native/src/pilot.rs, binvolta-pilot, macOS +plugins): scripted plugin-editor automation — hosts one instrument, opens its REAL editor in anNSWindow, and obeys a command file (shot/click/drag/type/drop/probe/save/quit) so an agent can see the GUI (own-windowCGWindowListCreateImage— no Screen Recording permission), drive it, verify sound offline (proberenders a note and reports the peak), and snapshot state straight into the preset store. The workhorse discovery: Kontakt 8's Qt/NTK UI ignores synthesized NSEvents (posted OR sent), but file loading is drag-and-drop, and the drop side of AppKit is a protocol call —drop x y file.nkifabricates anNSDraggingInfo(define_class) over awriteObjects:pasteboard and walksdraggingEntered:→performDragOperation:on the hit-testedQNSViewdirectly. Drops land on the rack strip (y≈70), not the browser page. One instance per preset (a second drop STACKS in the rack);.nkis enumerate from/Users/Shared/*/Instruments. This mapped the wholesodiumroster unattended. - Named presets (
engine/src/plugin/preset.rs): the same opaque blobs under a name, stored at~/Documents/Volta/presets/<plugin-label>/ <name>.<format>.state(raw bytes — Kontakt states run to megabytes;preset::set_rootoverrides the root).|> pluginPreset "name"rides the IR (PluginInstrumentSpec.preset/TrackEffect::Plugin.preset), applies at activation before declared params, defeats hot-swap reuse when it changes, and warns (default sound) on a miss. Capture/audition surface: the picker'spresetchip →EngineHandle::save_plugin_preset/plugin_presets/load_plugin_preset(per-instance, beside the sidecar pair). The offline bake path isvolta autosample --plugin— see Capture an instrument as samples. - Desktop app (
app-native/, volta-studio): enables all three host features and implements the workbench's plugin bridge calls (open_plugin_editor, the loaded-plugin list, save/restore of plugin state) inapp-native/src/main.rs. Picker + param-knob UI isui/src/pluginpicker.rs— the headerplugindropdown: installed-plugin search + insert-at-cursor, a per-instance knob browser with live values, and preview / live-editor buttons. (The old Tauri shellapp-tauri/and itsruntime/src/plugin-picker.tsweb picker were removed 2026-07-20 / 2026-08-09.)
Notes & limits#
- Native-only — there is no plugin hosting in the browser build.
- Build — hosting is ON by default in
volta-studio(the crate's defaultpluginsfeature → engineclap-host/au-host/vst3-host+volta-engine-host/plugin-ui);make run-studioand a plaincargo run -p volta-studioboth get the headerpluginmenu.--no-default-featuresdrops it. Do not reach for--features plugin-host: that isvolta-engine's umbrella feature — from the workspace root it resolves against the engine, builds clean, and leaves the shell'spluginscfgs (and the menu) off. - Latency & PDC — every backend reports declared latency (
PluginBackend::latency_samples, queried once at activate: CLAPlatencyext / VST3getLatencySamples/ AUkAudioUnitProperty_Latency), carried inLoadedPlugin.latency; the engine delays lighter tracks at the master sum to compensate — see Mixing tracks and buses §6.5 for semantics and limits. - Values are normalized
[0,1]across all formats (VST3 directly; AU/CLAP rescale through the declared[min, max]); an undeclared id passes through unmapped. panTrackon a plugin track uses the same pan law as a native patch (Mixing tracks and buses §3.5). A hosted instrument's audio is one continuous stream, so the pan applies to its bus, sampled at the block's cycle position (per-event pan likejuxcannot apply the way it does per-voice) — but the law is now the engine's singlevoice::pan_gains_at, so|> panTrack 0.5means one thing regardless of what is making the sound. It used to be the W3CStereoPannerNodestereo-input law: on a mono bus the peak channel ran +3.01 / +4.42 / +5.33 / +5.85 / +6.02 dB above the native path at pan 0 / 0.25 / 0.5 / 0.75 / 1, with total power up tog_L²+g_R² = 4— so substituting a hosted CLAP/VST for a native patch on a panned track jumped 5–6 dB and could drive the master limiter (dsp-quality-audit-2026-08-03.md§A3). The cost of one law is that a genuinely stereo plugin bus is balanced rather than image-rotated: hard over, the far channel's content is attenuated away instead of folded into the near one — the same thingpanTrackdoes to a stereo native voice, and what the mixer strip's ownpandoes to a bus. Pinned bylib::tests::plugin_bus_pan_is_the_engine_pan_law.- Only the first input + first output bus are wired (multi-port deferred). AU
.componentpath resolution is blocked on a newerplistcrate. CLAP editor caveats: no host timer/gui-request extensions are vended yet (plugins animate via their own timers), and state save fails soft (None) while that plugin's editor window is open (the instance is on loan). Control-thread waits on the CLAP worker are bounded (plugin::await_worker): activation gives up after 20 s and becomes an activation error; state save/restore and the editor checkout give up after 5 s and fail soft, so a wedged plugin never freezes the workbench. Window paint itself is not verifiable headlessly — the editor plumbing is (see thevst3_editor_*/state tests and the CLAP shuttle tests). - Discovery helper:
cargo run -p volta-engine --features au-host --example plugin-params -- "au:AUDistortion"dumps a plugin's declared parameter table.