xtc/docs/PLUGIN_HOSTING.md
2026-11-06 18:13:49 +01:00

18 KiB

External Plugin Hosting Evaluation

The original decision deferred external hosting. The native source API is useful for proving processor contracts without adding an SDK or scanning/loading code.

Candidate Benefits Work required before adoption
LV2 Open specifications, established Linux hosts/plugins RDF discovery and feature negotiation, URIDs, state, UI lifetime, worker requests and event mapping; likely additional discovery dependency
CLAP C ABI, explicit lifecycle and event model Extension negotiation, parameter/state persistence, host callbacks, main/audio-thread scheduling and UI integration
VST3 Broad plugin availability SDK/license review for the selected SDK release, C++ adapter outside the C core, component/controller and editor lifecycle, state and event translation

No candidate was linked, scanned or loaded. API/SDK and plugin licenses must be checked at the revision actually adopted; they are separate from xtc's MIT license. Supporting a format does not grant redistribution rights to individual plugins. Do not search user plugin directories from the callback.

Before implementation, require versioned module identity and state, a missing- plugin recovery policy, preparation failure/partial cleanup tests, bounded events and parameter publication, latency/tail handling, callback-safe unload after retirement, a GUI/main-thread boundary and an explicit crash-containment decision. Native ABI tests cover some ownership mechanics but do not prove third-party code safe. Event graph ports remain a gap; track instrument selection is described below. Choose one format with concrete user needs after those contracts are specified; do not introduce a general multi-format framework now. See decision 0008.


LADSPA Implementation (Supersedes the Earlier Deferral)

Decision 0009 selects a bounded LADSPA 1.1 host. xtc_plugin.h is the C API; no C++ engine or FLTK type is needed.

  1. Choose File → Load LADSPA plugin and a trusted absolute .so path. Loading executes native code immediately, including constructors during discovery.
  2. The descriptor browser lists names, labels, makers and source/effect roles. Select a row and Choose. Counts report compatible and skipped descriptors.
  3. In the Modular Graph, connect the new source/effect output to an output or other effect; effects have an optional mono input. Unconnected effect inputs receive external input, as other graph processors do.
  4. Select its node and use Edit → Selected plugin control. The editor shows name, maker, copyright, module path, descriptor index and unique ID, then actual port IDs, editable values, rate-adjusted bounds, defaults and discrete/ logarithmic hints. Apply validates all values and candidate initialization; Cancel and window close preserve the document. Toggled controls require 0/1 and integer controls require integral values.
  5. Save normally, reopen, reconnect external JACK playback if needed, and Play. Plugin changes stop/rebuild playback and participate in ordinary undo/redo.

The GUI uses only public graph/model/plugin APIs. Discovery probes explicit module files by descriptor index, bounded to 1024 descriptors. It never recursively scans system directories. The C API exposes probe, parameter metadata, instance create/process/destroy, and add/get/set graph configuration. Missing header means hosting returns UNSUPPORTED; metadata persistence continues to work.

Supported: 0/1 audio input, 1 output, <=64 total ports and <=16 input controls. Stereo graph channels are two independent mono instances. Stereo/multichannel LADSPA descriptors are rejected, rather than downmixed silently. Control output ports have private storage; no output-control GUI is implemented. No plugin UI, MIDI/events, sample automation, worker interface, latency compensation or opaque state is supplied by this first format. Parameter configuration is persisted; oscillator phase, held voices, delay history and tails are not. No smoothing is added around plan replacement. Integer LADSPA sample rates are obtained from the engine rate; practical backend rates are integral.

A new plan opens and owns its own modules and instances. Failed loading, incompatible identity/range/layout, or instantiate returning NULL unwinds the candidate and leaves active/pending plans intact. Only run and bounded host buffer copies execute in audio. Destruction and unloading happen after callback quiescence or retirement acknowledgement on the control thread. Transport reset retains LADSPA history; compile/reopen starts fresh. A LADSPA run callback has no error return. Nonfinite/subnormal samples become zero, but native crashes, corruption, runaway CPU, allocations or I/O inside a plugin cannot be contained or prevented. Use trusted plugins; hard-realtime properties are declarations, not proof. This is an in-process host, not a crash-isolation service.

Project v5 stores absolute module path, index, label, LADSPA unique ID, mono input count and ordered input-control port/value pairs. Restore verifies identity, layout, parameter ports and ranges against the loaded module. No pointers are serialized. Relocating a module requires restoring it at its saved absolute path or deliberately modifying its metadata through a C client. GUI Open reports the missing/incompatible identity/path and retains the current document. Metadata load/save and xtc-check --validate do not execute plugin code and do not certify module availability. LADSPA IDs are not cryptographic binary/version identities.


Repeatable Acceptance

make all test gui test-plugins test-gui-plugins analyze
make test-plugins-real

The first plugin target builds an original MIT shared-library fixture with gain, source, unsupported stereo and failed-initialization descriptors. Tests exercise identity/range errors, exact in-place gain, save/reopen/exact rendering, missing module metadata preservation and 100 plan replacements with callback guards against host allocation/free/dlopen/dlclose. GUI commands cover add, control edit, invalid-value rollback, undo/redo and save/New/reopen. ASan/UBSan/leak checks cover host/fixture lifetimes; plugin code from system packages is not instrumented.

test-plugins-real requires these separately installed modules (nothing is bundled or installed by the target):

Module / descriptor Tested behavior
/usr/lib/ladspa/amp.so, 0 amp_mono One input, gain control, finite nonzero render, exact save/reopen render
/usr/lib/ladspa/inv_1429.so, 0 inv One input, no controls, exact save/reopen render
/usr/lib/ladspa/sine.so, 3 sine_fcac No input, frequency/amplitude controls, exact save/reopen render

Tested packages: ladspa 1.17-7 and swh-plugins 0.4.17-7. These zero-latency processors define the initial compatible set; other plugins remain unqualified. Physical listening and plugin CPU/endurance qualification remain pending.


Discovery, Editing and Graph Lifecycle

Discovery remains explicit-file only, bounded to 1024 descriptor indices; no recursive scan, plugin database or new dependency is introduced. Unsupported layouts are skipped with counts; source/effect roles describe audio ports, not MIDI instrument capabilities. Modern track instruments are described below. LADSPA sources can be identified, edited, replaced and removed in the graph but cannot receive MIDI events.

Edit → Insert LADSPA on selected wire replaces one existing connection with source -> effect -> destination, as one undo step. Only one-input effects are accepted; stale wires, capacity failures and initialization failures retain the original graph. Edit → Replace selected plugin accepts the same audio input layout, keeps the node ID and all wires, and installs the new descriptor's default controls with no inherited automation. It can replace from another module. A source/effect layout mismatch is rejected. Undo restores the old identity, values and automation. Every candidate is compiled before adoption even when the backend is disconnected; no intermediate graph is published.

Ordering is graph signal flow, not descriptor order, creation order or a flat rack. Use the graph's connection/disconnection tools to place effects in series or parallel. Insertion on a wire preserves its endpoints and changes only that stage. Select a plugin and Delete to remove it and incident wires, then reconnect as desired; deletion does not automatically bridge or sum routes. A now-unwired effect receives external audio, following existing graph semantics. Undo/redo restores removal and routing. Independent bus inserts are still outside the shared-source graph model.

Reset defaults in the editor previews every control's declared/default-derived value. Apply also clears that plugin's automation; Cancel preserves it. The Edit → Reset selected plugin command does the same in one undo step. The public control-thread xtc_plugin_reset can reset one port (removing only its lane) or UINT32_MAX for all controls. Automation otherwise overrides static values, so editing a static control does not remove an existing lane.

xtc_plugin_describe returns copied name/maker/copyright (bounded/truncated), parameter ranges/defaults and integer/toggle/logarithmic/sample-rate/boundedness flags. Missing maker/copyright receive explicit fallback labels. LADSPA has no standard unit metadata: unit stays empty and the editor says unspecified rather than guessing Hz/dB from names. Numeric inputs use linear parameter values; logarithmic is a descriptor hint, not an extra host conversion. Unbounded ends are labeled unbounded; finite float extrema remain the validation bounds. Sample-rate hints scale bounds and defaults once at the engine's actual rate; nonrepresentable scaled bounds are rejected. Discrete defaults are rounded and validated; impossible defaults fail discovery rather than reaching the engine.

The generic modal editor owns copied metadata and temporary widget values only. It never owns or retains module handles, descriptors or audio instance pointers. Closing it cannot unload active DSP; Apply builds a new immutable plan before installation and existing retirement/quiescence rules release old instances. No embedded third-party editor UI is supported.

Compatibility and recovery

LADSPA 1.1 descriptor identity is module path + index + label + unique ID, with exact supported port/control layout verified on restore. A unique ID is not a binary version/hash guarantee. Parameter values and bounded automation persist in existing project v9 records; this work adds no file format change. Transient history/tails and opaque state remain unsupported. Native processor ABI 2 and its exact struct_size contract are unchanged; it remains parameter-only state. Consumers rebuild against public headers; the new copied details API adds no callbacks or GUI types to the C core.

Missing/incompatible modules reject GUI Open with the offending label/path and leave the current project intact. Restore a compatible module at that path and retry; use metadata-only validation/save to preserve an unavailable module's records. No dummy replacement silently removes its controls. Failed discovery, replacement or instantiate reports an error and retains the current document. A LADSPA run callback has no error return; finite-sample sanitation does not contain crashes, hangs, memory corruption or prohibited work inside a plugin. Only trusted in-process plugins are supported. See RECOVERY.md.

Verification

Fixture tests cover copied identity/hints, integer and toggle defaults and range rejection, logarithmic/sample-rate hints, scaled-range overflow rejection, reset and unchanged-on-error behavior, parameter/automation save/reopen and exact rendering. Noncommutative gain/offset effects prove that graph order changes output as expected. GUI tests drive the actual chooser/editor, reset/Apply and Cancel, replacement layout/init/missing-module rollback, insertion/stale-wire failure, deletion and undo. Existing fixture tests exercise 100 live plan replacements with host allocation/free/dlopen/dlclose guards. Native ABI tests cover process-error silence/reset. The separately installed amp/inverter/sine modules remain the qualified LADSPA set. Human listening, arbitrary third-party editors/plugins and extended realtime endurance remain unverified.


Parameter Targets

xtc_control.h supplies common stable owner/parameter IDs and control-thread metadata/edit APIs for track/bus gain/pan and LADSPA input ports. The GUI's plugin automation command uses this contract. Linear/hold LADSPA encoding and its integer/toggle restrictions remain unchanged; v10 adds mixer interpolation and live-sine CC maps without changing plugin state records. Direct static edits do not override existing lanes. Live CCs do not map to plugins; missing targets or modules fail without installing partial state. See CONTROL.md for timing, precedence, reset and backward-jump behavior.


CLAP, VST2 and VST3 Effect Loaders

File → Load plugin now detects LADSPA, CLAP, VST2 and VST3 entry points in an explicit trusted Linux module. Insert plugin on selected wire and Replace selected plugin use the same browser and transactional graph preparation. The identity/editor dialog names the format and stable ID. Existing LADSPA controls and automation continue to work. Modern effects currently use their default configuration: the editor shows identity but no editable parameters.

Choose the actual binary: .clap for CLAP, a VST2 .so exporting VSTPluginMain, or a VST3 bundle's Contents/<architecture>-linux/<module>.so exporting ModuleEntry, ModuleExit and GetPluginFactory. The C API also accepts conventional .vst3 bundle paths and resolves the matching architecture and bundle-stem binary. The file chooser may require selecting the inner binary; no recursive plugin-folder scan is performed. Auto-detection prefers CLAP, then VST2, then VST3, otherwise LADSPA; the C API also provides xtc_plugin_probe_format for an explicit format.

The modern subset is one mono audio input and one mono output, float32, default configuration, zero reported latency. Stereo graph playback uses independent instances. For the modern effect subset, true stereo/multichannel/sidechain buses, instruments, MIDI/events, custom editors, parameter changes/automation, programs/presets, opaque state, transport/time information, workers and latency compensation are not implemented for these three formats. Modern metadata with controls/automation is rejected, not silently ignored. VST3 event buses and CLAP note ports are rejected. Plugins requiring unsupported host extensions may refuse initialization. This is not a claim that all CLAP/VST2/VST3 plugins will load or sound identically after updates.

Only original effect fixtures have been qualified for the new formats. Trusted plugin code is in-process and can crash/hang or violate realtime rules. No plugin binary is included in packages. A VST2 ABI adapter does not provide rights to the discontinued SDK or plugin redistribution. See decision 0010 and LICENSES.md.

The control/main thread owns discovery, activation, module reference accounting, retirement and unloading. CLAP's start-processing call runs on the audio owner; main-thread callbacks are coalesced and serviced via xtc_plugin_idle (GUI timer, currently 100 ms). C hosts using CLAP must pump that API on their serialized main thread while instances live, and stop/join audio before destruction. Restart and VST2 I/O-change requests make subsequent blocks fail/silence; stop and rebuild or reload the plugin rather than reconfiguring inside the callback. CLAP process errors and VST3 failure results silence a block. VST2 has no process error return.

make all test gui test-modern-plugins test-gui-modern-plugins
make test-sanitize analyze

Original CLAP/VST2/VST3 shared-library fixtures exercise exact in-place and stereo processing, entry/instance lifetimes, rejected layouts, partial initialization, CLAP start/process failures, graph replacement rollback, 50 plan retirements per format, missing modules, v11 save/reopen and GUI add/browser/undo/reopen. Host allocation/loading/unloading guards remain active while rendering. This evidence qualifies the subset above, not arbitrary external plugins or physical audio.


Instrument Support

MIDI tracks can bind CLAP, VST2 and VST3 instruments. In the Track menu, select Load instrument plugin for the selected sequencer track; Use built-in instrument removes the binding. Assignment also selects the track for live MIDI. Live MIDI to selected instrument changes that target; Live MIDI to built-in sine restores the legacy live source. MIDI clip notes always go to their own track's instrument. Independent instances feed track gain/pan, mute/solo, bus routing and automation. Bindings/live target save in v12.

Supply the actual trusted Linux binary (including the inner .so of a VST3 bundle). Supported instruments have zero audio inputs, one mono output, float32 processing, zero latency and one note/event input. Default configuration only: no custom editor, opaque state/preset restoration, modern parameter editing/automation, stereo/multichannel instruments or MIDI output. No arbitrary third-party instrument is qualified by the original ABI fixture tests.

CLAP accepts MIDI or native notes; VST2 uses MIDI events; VST3 uses IEventList note events. Live CC reaches CLAP MIDI/VST2; native CLAP/VST3 have no controller mapping (including sustain). Live MIDI goes to exactly one selected target. Seeks/loops retrigger notes crossing the destination with fresh envelopes; plugin oscillator/opaque DSP state cannot be reconstructed. Panic delivers all-off (VST3 held-key note-offs); plugin release tails may remain. Processing uses preallocated 256-frame chunks and 256 incoming events per instance/chunk; overflow or processing failure silences the block and schedules reset events. The existing 32 simultaneous scheduled-note and 4096 stored-note bounds remain. Do not unload/replace modules during playback; candidate plans are prepared off the callback and old instances are reclaimed on the control thread. Missing references remain inspectable without loading; failed GUI preparation retains the current project. Save a backup for applications limited to v11 or earlier.

Phase 106 adds official SDK examples and qualification, including VST3 process context (sample position, rate, tempo and musical position), an SDK-built gain/synth pair and a WAV render example. Native Windows ASIO remains in progress; the optional ASIO SDK example currently provides device diagnostics.