2026-10-27 01:21:04 +01:00
# External plugin hosting evaluation
2026-10-27 04:00:27 +01:00
Historical Phase 9 decision: defer external hosting. The native source API is useful
2026-10-27 01:21:04 +01:00
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 in this phase. 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 and custom-instrument GUI selection remain gaps.
Choose one format with concrete user needs after those contracts are specified;
do not introduce a general multi-format framework now. See decision 0008.
2026-10-27 04:00:27 +01:00
## Phase 15 implementation (supersedes the Phase 9 deferral)
Decision [0009 ](decisions/0009-parameter-processors-and-ladspa.md ) 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.
2026-10-27 16:24:21 +01:00
2. The descriptor browser lists names, labels, makers and source/effect roles.
Select a row and Choose. Counts report compatible and skipped descriptors.
2026-10-27 04:00:27 +01:00
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.
2026-10-27 16:24:21 +01:00
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.
2026-10-27 04:00:27 +01:00
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
```sh
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.
2026-10-27 16:24:21 +01:00
## Phase 25: 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. Built-in MIDI synthesis remains the only GUI MIDI
instrument. 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 phase 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.
### Phase 25 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.