1
0
Fork 0
forked from hanez/xtc

Phase 15: host trusted LADSPA processors with persisted graph state

This commit is contained in:
Johannes Findeisen 2026-10-27 04:00:27 +01:00
commit f72314b5a3
24 changed files with 1046 additions and 70 deletions

View file

@ -246,3 +246,14 @@ acceptance sequence and transport semantics.
and numeric/layout tests (also included in `make test`). `make quality-preview`
builds the C example and writes raw/smoothed listening WAVs into BUILD_DIR without
playing them or configuring devices. See [AUDIO_QUALITY.md](AUDIO_QUALITY.md).
Phase 14/15 checks: `make test-processor test-plugins test-gui-plugins`.
The latter two need the system LADSPA development header; real-plugin acceptance
is separate (`make test-plugins-real`, paths listed in PLUGIN_HOSTING.md). With no
`ladspa.h`, the ordinary core/GUI still builds and plugin operations return
UNSUPPORTED; existing plugin metadata can still be loaded/saved. To exercise that
configuration explicitly, use a fresh BUILD_DIR with
`CPPFLAGS=-DXTC_DISABLE_LADSPA JACK_AVAILABLE=`. No dependency is downloaded.
Link static consumers with `-lm -ldl` plus the backend dependencies when used.
See LICENSES.md for header/plugin licensing and decision 0009 for rationale.
Recording stress/recovery is available with `make test-recording`.

View file

@ -37,3 +37,20 @@ and supply the notices/source/relinking facilities required by those exact
licenses and linking choices. FLTK's exception does not remove obligations of
other linked libraries. This review covers the current unmodified system-linked
build; static all-in-one bundles and external plugins need a separate review.
## Phase 15 LADSPA review
The optional external host includes the system LADSPA 1.1 header (`ladspa`
1.17-7), copyright Richard W.E. Furse, Paul Barton-Davis and Stefan Westerfeld,
LGPL-2.1-or-later. No header, implementation or plugin binary is copied into the
repository. The host uses the glibc dynamic loader (`-ldl`, LGPL-2.1-or-later),
with no discovery/runtime SDK library or new installed package. LADSPA is mature
and stable; it does not provide modern instrument/MIDI/opaque-state services.
The original fixture in tests/plugins is MIT repository code.
Acceptance dynamically loads the user's installed LADSPA sample amp/sine modules
(package metadata LGPL-2.1-or-later) and SWH inverter (swh-plugins 0.4.17-7,
GPL-2.0-or-later). Testing/loading a user-installed module does not grant the right
to bundle or relicense it. Redistributors must review each chosen module and the
combined distribution's obligations separately; xtc's MIT grant covers only its
original code. No third-party plugin is included in source archives or installs.

View file

@ -1,6 +1,6 @@
# External plugin hosting evaluation
Decision: defer external hosting after Phase 9. The native source API is useful
Historical Phase 9 decision: defer 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 |
@ -22,3 +22,84 @@ 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.
## 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.
2. The dialog lists compatible descriptor indices/labels. Select an index.
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 dialog shows
actual port IDs, names, values and rate-adjusted ranges. Enter a port and value.
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
```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.

View file

@ -1,6 +1,6 @@
# Project format v4 (reads v1/v2/v3)
# Project format v5 (reads v1/v2/v3/v4)
Phases 5–8 use a bounded text format implemented in C99 without a parser library.
Phases 5–15 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
files; MIDI notes are stored inline.
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
Counts determine the number of subsequent records:
```text
XTC_PROJECT 4
XTC_PROJECT 5
APP 0 1 0
TEMPO 120000
IDS 1 1
@ -39,30 +39,30 @@ END
- `GRAPH`: output-node policy (0/1), next node ID, node count, edge count.
Nodes retain stable IDs (limited to the host's size_t range). No pointers or
compiled state are serialized.
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source, 6 instrument), and exactly eight
- `NODE`: ID, type (1 gain, 2 oscillator, 3 meter, 4 output, 5 audio source, 6 instrument, 8 LADSPA plugin), and exactly eight
hexadecimal digits encoding the IEEE binary32 parameter bits, independent
of locale and host byte order. Nonfinite values are rejected.
- `EDGE`: source and destination node IDs. Port directions are implied
by node type; endpoints, duplicates and cycles are validated by the graph API.
- `TRACK`: ID and hex-encoded name bytes. In v4 each is immediately followed
- `TRACK`: ID and hex-encoded name bytes. In v4/v5 each is immediately followed
by `MIX gain_bits pan_bits mute solo`: finite binary32 gain 0..4, pan -1..1
encoded as eight hex digits, mute and solo unsigned 0/1.
- `CLIP`: ID, track ID, start tick, length tick, source offset tick, source
length tick, hex-encoded name bytes. Trimming
preserves source bounds, so a region can re-extend only within its original
length. Overlaps are allowed and audio clips mix through the audio source node.
- `ASSET`: required after each CLIP in v2/v3/v4; "-" means no audio asset, otherwise
- `ASSET`: required after each CLIP in v2/v3/v4/v5; "-" means no audio asset, otherwise
hex-encoded absolute path bytes, <=511 bytes after decoding, no embedded NUL
or ASCII control characters. Each clip directly references a WAV; no sample
data, pointers or DSP state are embedded. Paths can contain spaces.
- `MIDI`: required after each ASSET in v3/v4; 0 for silent/audio regions, 1 for
- `MIDI`: required after each ASSET in v3/v4/v5; 0 for silent/audio regions, 1 for
MIDI clips. MIDI clips must have no asset.
- `NOTES`: next stable note ID and global note count, followed by that many
`NOTE id clip_id start_tick length_tick pitch velocity channel` records.
Notes reference MIDI clips; start/length are source-relative, positive length
within source bounds. Pitch is 0..127, velocity 1..127, channel 0..15.
Note IDs are unique, monotonic uint64 IDs with UINT64_MAX reserved.
- `AUTOMATION`: global point count, after the last NOTE and before END in v4.
- `AUTOMATION`: global point count, after the last NOTE and before END in v4/v5.
Follow with `POINT track_id parameter tick value_bits` for each point.
Parameter 0 is gain (0..4), 1 pan (-1..1); values use finite binary32 hex.
Track must exist; tick <= MAX_TICK; duplicate track/parameter/tick is invalid.
@ -82,8 +82,8 @@ node types 1..4, migrating their clips to empty asset paths. Version 2 accepts
node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes.
Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate
all tracks to unity gain, centered pan, mute/solo off and no automation.
Saving always writes v4. Unknown versions are rejected without replacing the destination; older
applications cannot read v4. Migration and maximum-path roundtrips are tested.
Saving always writes v5. Unknown versions are rejected without replacing the destination; older
applications cannot read v5. Migration and maximum-path roundtrips are tested.
## Ownership and failures
@ -108,9 +108,33 @@ changing its IDs or arrangement. See AUDIO_FILES.md for recovery and limitations
## Phase 9 extension and recovery policy
Format remains v4. Native processor descriptors are process-local and have no
Phase 9 kept format v4. Native processor descriptors are process-local and have no
serialized representation; saving a native graph returns UNSUPPORTED and retains
the existing destination. Unknown file versions/types remain rejected. See
NATIVE_API.md for the extension boundary and RECOVERY.md for write-failure, killed
writer and unfinished-recording behavior. Metadata validation is available via
`xtc-check --validate PROJECT` without a GUI or server; it does not load assets.
## Phase 15: version 5 plugin metadata
The writer now emits `XTC_PROJECT 5`; versions 1–4 still load. All v4 records and
order remain the same. Node type 8 is a LADSPA plugin; native pointer-bearing type
7 is still unsupported in files. Immediately after each type-8 `NODE id 8
00000000` record come:
```text
PLUGIN unique_id descriptor_index mono_inputs control_count hex_label
MODULE hex_absolute_module_path
CONTROL port_index float32_hex_bits
```
There are exactly `control_count` CONTROL records. Label length is 1–63 bytes,
module path 1–511 bytes (absolute), both hex-encoded with no embedded NUL. Index
is <1024, mono_inputs 0/1, controls <=16, control port indices <64 strictly
increasing, and values finite. Unique ID fits uint32. The one output is implicit.
Structural validation never opens libraries. On graph compilation the host checks
identity, layout, input control port IDs and rate-adjusted legal ranges against
the module. Unknown nodes/versions fail without mutating the current project.
Missing modules remain preserved by metadata load/save; GUI installation rejects
them with a repair message. No transient DSP state, pointer or plugin binary is
stored. See PLUGIN_HOSTING.md and decision 0009 for supported processors/limits.

View file

@ -294,6 +294,15 @@ state are explicitly outside this parameter-only interface.
- Plugin loading and destruction do not occur in the real-time callback.
- The chosen format's dependencies and licensing are documented.
Implemented: bounded LADSPA 1.1 C host with explicit file/index discovery,
control-thread loading/lifecycle, mono source/effect graph mapping, independent
stereo instances, metadata/range checks and v5 identity/control persistence.
Public-API GUI load/configure commands support ordinary routing/save/reopen and
undo. Fixture/error/retirement/GUI tests and installed amp/inverter/sine acceptance
pass. See decision 0009, PLUGIN_HOSTING.md and LICENSES.md. MIDI, true multichannel
layouts, opaque state, editors, latency compensation and crash isolation are
unsupported; plugin realtime behavior and physical listening are unqualified.
## Phase 16 — Automation and Expressive Control
**Goal:** Make automation and MIDI control reliable, sample-timed, and usable.

View file

@ -0,0 +1,52 @@
# 0009 — Parameter processors and bounded LADSPA hosting
Status: accepted, Phases 14–15 (2026-10-27).
Native ABI 1 remains compatible. ABI 2 adds explicitly parameter-only configuration,
metadata, validation and sample-offset parameter/MIDI ordering. It is a public C
facility; opaque state and automatic native descriptor registration in project
files remain unsupported. Native instrument and gain examples exercise the
interface before adding a dynamically loaded module lifetime.
Select LADSPA 1.1 for the first external host. Reconsidering the Phase 9 candidates,
LV2 needs RDF/features/URIDs/workers, CLAP needs host scheduling/extension
negotiation, and VST3 needs a separate C++ SDK adapter and release-specific license
review. LADSPA adds a mature Linux C ABI, installed real plugins, straightforward
port negotiation and no RDF/SDK runtime library. Its specification is old and
stable, not an actively expanding instrument/event ecosystem. This trades MIDI,
editors, opaque state and modern automation for a small testable first host.
No general multi-format abstraction is introduced.
Use the installed LGPL-2.1-or-later header, system dynamic loader (`-ldl`), and
user-selected, trusted module files. Neither the header nor plugin binaries are
vendored. Header absence or `XTC_DISABLE_LADSPA` yields unsupported hosting stubs;
metadata can still be loaded/saved. Plugin licenses are independent of the host
and require review before distribution. See LICENSES.md.
Support zero/one mono audio input and exactly one mono output, at most 64 ports
and 16 input controls. Graph stereo consists of independent instances, not a
stereo LADSPA layout. Reject incompatible layouts and identities explicitly.
Control outputs are connected to private storage, not exposed as graph events.
LADSPA has no MIDI or opaque state API: save control values only. Parameter
changes rebuild plans on the control thread; sample automation is deferred to
Phase 16. Rate-scaled bounds are recalculated at compile. Instance history/tails
reset on recompile, not every transport seek (LADSPA has no realtime reset rule).
Latency compensation is unsupported: the supported set is zero-latency amp,
inverter and sine source, not arbitrary lookahead/delay/routing plugins.
Discovery, dlopen, descriptor calls, instantiate, connect, activate, deactivate,
cleanup and dlclose are control-thread operations. Only preallocated copies and
run execute in audio; the usual pending/retired plan handoff owns module handles.
Partial initialization unwinds on the control thread. The host cannot enforce
third-party callback behavior, bound CPU time, catch native crashes/hangs, or
undo memory corruption. There is no sandbox or crash recovery inside the audio
process. A fault may kill xtc; previously saved documents/recording recovery files
remain subject to their ordinary durability guarantees. Test only trusted modules.
Project v5 stores plain identity/layout/control metadata. Parsing never opens
modules. Compile failure preserves the source document and active plan. GUI Open
rejects missing/incompatible modules, retains its current document, and reports
saved module identity/path. Restore the compatible module at that path and retry;
a metadata-only C client can preserve/save the document without the module.
No silent replacement or deletion occurs. Same-ID/label/ports do not authenticate
binaries or guarantee unchanged sound across plugin upgrades.