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

@ -15,7 +15,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/plugin/ladspa.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -40,7 +40,7 @@ endif
all: $(BUILD_DIR)/libxtc.a $(BUILD_DIR)/xtc-check
$(BUILD_DIR)/xtc-check: $(BUILD_DIR)/src/main.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/libxtc.a: $(CORE_OBJECTS)
@mkdir -p $(@D)
@ -59,37 +59,37 @@ $(TEST_OBJECT): tests/unit/test_core.c
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
$(BUILD_DIR)/test_core: $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a -lm
$(CC) $(LDFLAGS) -o $@ $(TEST_OBJECT) $(BUILD_DIR)/libxtc.a -lm -ldl
$(BUILD_DIR)/test_offline: $(BUILD_DIR)/tests/unit/test_offline.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_realtime: $(BUILD_DIR)/tests/unit/test_realtime.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -pthread
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -pthread
$(BUILD_DIR)/test_backend: $(BUILD_DIR)/tests/unit/test_backend.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_graph_edits: $(BUILD_DIR)/tests/unit/test_graph_edits.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_project: $(BUILD_DIR)/tests/unit/test_project.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_audio: $(BUILD_DIR)/tests/unit/test_audio.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -pthread
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -pthread
$(BUILD_DIR)/test_midi: $(BUILD_DIR)/tests/unit/test_midi.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_mixer: $(BUILD_DIR)/tests/unit/test_mixer.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_native: $(BUILD_DIR)/tests/unit/test_native.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/native-gain: $(BUILD_DIR)/examples/native_gain.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
test: $(BUILD_DIR)/test_native $(BUILD_DIR)/native-gain $(BUILD_DIR)/test_mixer $(BUILD_DIR)/test_midi $(BUILD_DIR)/test_audio $(BUILD_DIR)/test_project $(BUILD_DIR)/test_graph_edits $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
$(if $(BACKEND_TEST),./$(BACKEND_TEST),@echo "JACK fault tests skipped: JACK development files unavailable")
@ -111,21 +111,21 @@ debug:
ifeq ($(JACK_AVAILABLE),yes)
$(BUILD_DIR)/tests/integration/test_jack_midi.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/test_jack_midi: $(BUILD_DIR)/tests/integration/test_jack_midi.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm -ldl
test-jack-midi: $(BUILD_DIR)/test_jack_midi
./$(BUILD_DIR)/test_jack_midi
$(BUILD_DIR)/tests/integration/test_jack_audio.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/test_jack_audio: $(BUILD_DIR)/tests/integration/test_jack_audio.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm -ldl
test-jack-audio: $(BUILD_DIR)/test_jack_audio
./$(BUILD_DIR)/test_jack_audio
$(BUILD_DIR)/tests/integration/test_jack.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/test_jack: $(BUILD_DIR)/tests/integration/test_jack.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm
$(CC) $(LDFLAGS) -o $@ $^ $(JACK_LIBS) -lm -ldl
test-jack: $(BUILD_DIR)/test_jack
./$(BUILD_DIR)/test_jack
@ -139,14 +139,14 @@ ifeq ($(GUI_AVAILABLE),yes)
gui: $(BUILD_DIR)/xtc
$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl
test-gui: $(BUILD_DIR)/test_gui
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
ifeq ($(JACK_AVAILABLE),yes)
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
test-gui-jack: $(BUILD_DIR)/test_gui_jack
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui_jack
@ -157,7 +157,7 @@ test-gui-jack:
endif
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
else
test-gui-jack test-gui:
@echo "FLTK development files are required for GUI tests."
@ -228,10 +228,10 @@ clean:
-include $(BUILD_DIR)/tests/unit/test_native.d $(BUILD_DIR)/examples/native_gain.d
$(BUILD_DIR)/test_performance: $(BUILD_DIR)/tests/integration/test_performance.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
$(BUILD_DIR)/test_recovery: $(BUILD_DIR)/tests/integration/test_recovery.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-recovery
test-performance: $(BUILD_DIR)/test_performance
@ -244,7 +244,7 @@ test-recovery: $(BUILD_DIR)/test_recovery
# Linux/GNU-compatible linker test; no instrumentation in production objects.
$(BUILD_DIR)/test_reliability: $(BUILD_DIR)/tests/integration/test_reliability.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free
.PHONY: test-reliability test-sanitize analyze
test-reliability: test $(BUILD_DIR)/test_reliability
@ -261,7 +261,7 @@ analyze:
.PHONY: test-workflow
ifeq ($(GUI_AVAILABLE)$(JACK_AVAILABLE),yesyes)
$(BUILD_DIR)/test_workflow: $(BUILD_DIR)/tests/integration/test_workflow.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/libxtc.a
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
test-workflow: $(BUILD_DIR)/test_workflow
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_workflow
@ -274,7 +274,7 @@ endif
-include $(BUILD_DIR)/tests/integration/test_workflow.d
$(BUILD_DIR)/test_quality: $(BUILD_DIR)/tests/unit/test_quality.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-quality
test-quality: $(BUILD_DIR)/test_quality
./$(BUILD_DIR)/test_quality
@ -282,23 +282,41 @@ test: test-quality
-include $(BUILD_DIR)/tests/unit/test_quality.d
$(BUILD_DIR)/quality-preview: $(BUILD_DIR)/examples/quality_preview.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: quality-preview
quality-preview: $(BUILD_DIR)/quality-preview
./$(BUILD_DIR)/quality-preview "$(abspath $(BUILD_DIR))/quality"
-include $(BUILD_DIR)/examples/quality_preview.d
$(BUILD_DIR)/test_recording: $(BUILD_DIR)/tests/integration/test_recording.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-recording
test-recording: $(BUILD_DIR)/test_recording
./$(BUILD_DIR)/test_recording
-include $(BUILD_DIR)/tests/integration/test_recording.d
$(BUILD_DIR)/test_processor: $(BUILD_DIR)/tests/unit/test_processor.o $(BUILD_DIR)/examples/processors.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-processor
test-processor: $(BUILD_DIR)/test_processor
./$(BUILD_DIR)/test_processor
test: test-processor
-include $(BUILD_DIR)/tests/unit/test_processor.d $(BUILD_DIR)/examples/processors.d
$(BUILD_DIR)/plugin-fixture.so: tests/plugins/fixture.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -shared -o $@ $<
$(BUILD_DIR)/test_plugins: $(BUILD_DIR)/tests/integration/test_plugins.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=dlopen,--wrap=dlclose
.PHONY: test-plugins test-plugins-real
test-plugins: $(BUILD_DIR)/test_plugins $(BUILD_DIR)/plugin-fixture.so
./$(BUILD_DIR)/test_plugins "$(abspath $(BUILD_DIR))/plugin-fixture.so" fixture
test-plugins-real: $(BUILD_DIR)/test_plugins
./$(BUILD_DIR)/test_plugins /usr/lib/ladspa/amp.so
./$(BUILD_DIR)/test_plugins /usr/lib/ladspa/inv_1429.so
./$(BUILD_DIR)/test_plugins /usr/lib/ladspa/sine.so 3
-include $(BUILD_DIR)/tests/integration/test_plugins.d
.PHONY: test-gui-plugins
test-gui-plugins: $(BUILD_DIR)/test_gui $(BUILD_DIR)/plugin-fixture.so
XTC_TEST_PLUGIN="$(abspath $(BUILD_DIR))/plugin-fixture.so" xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui

View file

@ -32,7 +32,9 @@ This repository is a starter scaffold, not a production DAW. It currently provid
- architecture, format, GUI, roadmap, and decision documents.
The JACK backend has passed software-loopback testing; physical-device playback
and capture are unverified. MIDI recording and plugin hosting are not implemented.
and capture are unverified. MIDI recording remains deferred. Bounded LADSPA
hosting supports the compatible mono processors documented in
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md).
## Build
@ -114,3 +116,8 @@ The project is scaffolded under the MIT License; see `LICENSE`. Review the licen
Audio timing, transitions, latency limits and listening previews are described
in [docs/AUDIO_QUALITY.md](docs/AUDIO_QUALITY.md). Human listening acceptance
for the Phase 12 transitions remains pending.
Phases 13–15 add no-clobber recording/recovery, parameterized native C processors,
and trusted in-process LADSPA hosting with persisted identity/control values.
See [RECOVERY.md](docs/RECOVERY.md), [NATIVE_API.md](docs/NATIVE_API.md) and
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md) for commands and limits.

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.

View file

@ -8,6 +8,8 @@ extern "C" {
#endif
typedef struct xtc_graph xtc_graph;
/* Control-thread independent model copy; out is NULL on failure. */
xtc_result xtc_graph_clone(const xtc_graph *source, xtc_graph **out);
typedef enum {
XTC_SIGNAL_AUDIO = 1,
@ -35,7 +37,8 @@ typedef enum {
XTC_NODE_OUTPUT,
XTC_NODE_AUDIO, /* Timeline mix source, output only. */
XTC_NODE_INSTRUMENT, /* Internal MIDI instrument, output only. */
XTC_NODE_NATIVE /* Trusted source/effect; see xtc_native.h. */
XTC_NODE_NATIVE, /* Trusted source/effect; see xtc_native.h. */
XTC_NODE_PLUGIN /* Persisted LADSPA source/effect; see xtc_plugin.h. */
} xtc_node_type;
typedef struct {

39
include/xtc/xtc_plugin.h Normal file
View file

@ -0,0 +1,39 @@
#ifndef XTC_PLUGIN_H
#define XTC_PLUGIN_H
#include "xtc_processor.h"
#ifdef __cplusplus
extern "C" {
#endif
#define XTC_PLUGIN_CONTROLS 16U
/* Plain copied model: no library handles or callback pointers. Absolute trusted
* module path, descriptor index + unique ID + label checked again on compile.
* Controls are actual LADSPA port indices, in ascending order. */
typedef struct {
char path[512], label[64];
unsigned int index, unique_id, inputs, control_count;
unsigned int ports[XTC_PLUGIN_CONTROLS];
float values[XTC_PLUGIN_CONTROLS];
} xtc_plugin_spec;
typedef struct xtc_plugin xtc_plugin;
/* Control thread only. Probe explicitly enumerates a file by descriptor index;
* NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading
* trusted libraries can execute arbitrary constructors. Never probe in audio. */
xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, xtc_plugin_spec *out);
/* Control-thread parameter names, computed ranges/defaults at this sample rate.
* Output has spec->control_count entries; IDs are LADSPA port indices. */
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *spec, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS]);
/* Metadata-only validation: never opens libraries. */
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *spec);
xtc_result xtc_plugin_create(const xtc_plugin_spec *spec, double rate, size_t frames, xtc_plugin **out);
void xtc_plugin_destroy(xtc_plugin *plugin); /* control, after quiescence */
/* Single audio owner. No loading/allocation/I/O in host process; trusted run()
* must obey realtime rules. No MIDI; max one mono input and one mono output.
* Transport reset retains LADSPA delay history (no realtime reset contract). */
xtc_result xtc_plugin_process(xtc_plugin *plugin, const float *input, float *output, size_t frames);
xtc_result xtc_graph_add_plugin(xtc_graph *graph, const xtc_plugin_spec *spec, size_t *id);
xtc_result xtc_graph_get_plugin(const xtc_graph *graph, size_t id, xtc_plugin_spec *out);
xtc_result xtc_graph_set_plugin(xtc_graph *graph, size_t id, const xtc_plugin_spec *spec);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -7,5 +7,5 @@ Name: xtc
Description: Modular C audio engine and project library
Version: @VERSION@
Requires.private: @JACK@
Libs: -L"${libdir}" -lxtc -lm
Libs: -L"${libdir}" -lxtc -lm -ldl
Cflags: -I"${includedir}"

View file

@ -17,6 +17,7 @@ struct xtc_plan {
float input_gain;
uint64_t position;
xtc_native **native;
xtc_plugin **plugins;
};
/**
@ -33,6 +34,11 @@ void xtc_plan_destroy(xtc_plan *plan)
for (i = 0; i < plan->node_count * 2; ++i)
xtc_native_destroy(plan->native[i]);
}
if (plan->plugins) {
size_t i;
for (i = 0; i < plan->node_count * 2; ++i) xtc_plugin_destroy(plan->plugins[i]);
}
free(plan->plugins);
free(plan->native);
free(plan->nodes);
free(plan->order);
@ -81,6 +87,7 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t
plan->max_frames = max_frames;
plan->sample_rate = sample_rate;
plan->input_gain = 1.0f;
plan->plugins = calloc(storage_nodes * 2U,sizeof(*plan->plugins));
plan->native = calloc(storage_nodes * 2U, sizeof(*plan->native));
plan->nodes = calloc(storage_nodes, sizeof(*plan->nodes));
plan->order = calloc(storage_nodes, sizeof(*plan->order));
@ -90,7 +97,7 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t
plan->buffers = calloc(storage_nodes * max_frames * 2U, sizeof(*plan->buffers));
plan->peaks = calloc(storage_nodes, sizeof(*plan->peaks));
plan->phases = calloc(storage_nodes, sizeof(*plan->phases));
if (plan->native == NULL || plan->nodes == NULL || plan->order == NULL || plan->offsets == NULL ||
if (plan->plugins == NULL || plan->native == NULL || plan->nodes == NULL || plan->order == NULL || plan->offsets == NULL ||
plan->sources == NULL || plan->render == NULL || plan->buffers == NULL || plan->peaks == NULL ||
plan->phases == NULL) {
xtc_plan_destroy(plan);
@ -116,6 +123,14 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t
: terminal);
}
for (node = 0; node < plan->node_count; ++node) {
if (plan->nodes[node].type == XTC_NODE_PLUGIN) {
size_t side;
for (side = 0; side < 2; ++side) {
result = xtc_plugin_create(&plan->nodes[node].plugin,sample_rate,max_frames,
&plan->plugins[side * plan->node_count + node]);
if (result != XTC_OK) { xtc_plan_destroy(plan); return result; }
}
}
if (plan->nodes[node].type == XTC_NODE_NATIVE) {
size_t side;
for (side = 0; side < 2; ++side) {
@ -211,19 +226,19 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
const float *midi_source = channel ? instrument_right : instrument;
for (index = 0U; index < plan->node_count; ++index) {
size_t node = plan->order[index];
xtc_node spec = plan->nodes[node];
const xtc_node *spec = &plan->nodes[node];
float *buffer = plan->buffers + (channel * plan->node_count + node) * plan->max_frames;
float peak = 0.0f;
double phase = plan->phases[node];
for (frame = 0U; frame < frame_count; ++frame) {
float sample = 0.0f;
if (spec.type == XTC_NODE_INSTRUMENT) {
if (spec->type == XTC_NODE_INSTRUMENT) {
sample = midi_source ? midi_source[frame] : 0.0f;
} else if (spec.type == XTC_NODE_AUDIO) {
} else if (spec->type == XTC_NODE_AUDIO) {
sample = audio_source ? audio_source[frame] : 0.0f;
} else if (spec.type == XTC_NODE_OSCILLATOR) {
} else if (spec->type == XTC_NODE_OSCILLATOR) {
sample = (float)sin(XTC_TAU * phase);
phase += (double)spec.value / plan->sample_rate;
phase += (double)spec->value / plan->sample_rate;
if (phase >= 1.0)
phase -= 1.0;
} else {
@ -232,16 +247,16 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
for (edge = plan->offsets[node]; edge < plan->offsets[node + 1U]; ++edge)
sample += plan->buffers[(channel * plan->node_count + plan->sources[edge]) * plan->max_frames +
frame];
if (spec.type == XTC_NODE_GAIN)
sample *= spec.value;
if (spec->type == XTC_NODE_GAIN)
sample *= spec->value;
}
sample = xtc_dsp_finite(sample);
buffer[frame] = sample;
if (fabsf(sample) > peak)
peak = fabsf(sample);
}
if (spec.type == XTC_NODE_NATIVE) {
xtc_native_block block = {spec.native.kind == XTC_NATIVE_EFFECT ? buffer : NULL,
if (spec->type == XTC_NODE_NATIVE) {
xtc_native_block block = {spec->native.kind == XTC_NATIVE_EFFECT ? buffer : NULL,
buffer,
frame_count,
plan->position,
@ -255,9 +270,18 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
return result;
}
}
if (spec->type == XTC_NODE_PLUGIN) {
xtc_result result = xtc_plugin_process(plan->plugins[channel * plan->node_count + node],
spec->plugin.inputs ? buffer : NULL,buffer,frame_count);
if (result != XTC_OK) {
memset(output,0,frame_count*sizeof(*output));
if (right) memset(right,0,frame_count*sizeof(*right));
return result;
}
}
if (!right || channel == 1)
plan->phases[node] = phase;
if (spec.type == XTC_NODE_METER)
if (spec->type == XTC_NODE_METER)
plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak;
}
}

View file

@ -180,6 +180,7 @@ xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, size_t por
type = graph->nodes[node_id].type;
if (port_id > 1U ||
(port_id == 0U && (type == XTC_NODE_OSCILLATOR || type == XTC_NODE_AUDIO || type == XTC_NODE_INSTRUMENT ||
(type == XTC_NODE_PLUGIN && !graph->nodes[node_id].plugin.inputs) ||
(type == XTC_NODE_NATIVE && graph->nodes[node_id].native.kind != XTC_NATIVE_EFFECT))) ||
(port_id == 1U && type == XTC_NODE_OUTPUT))
return XTC_ERR_NOT_FOUND;
@ -417,6 +418,27 @@ static xtc_result disconnect_ports(xtc_graph *graph, const xtc_connection_info *
return XTC_ERR_NOT_FOUND;
}
/* Independent control-thread model copy; no plugin code is loaded. */
xtc_result xtc_graph_clone(const xtc_graph *source, xtc_graph **out)
{
xtc_graph *graph;
xtc_result result;
if (!out) return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
if (!source) return XTC_ERR_INVALID_ARGUMENT;
result = xtc_graph_create(source->max_nodes, &graph);
if (result != XTC_OK)
return result;
graph->node_count = source->node_count;
graph->edge_count = source->edge_count;
graph->next_id = source->next_id;
graph->require_output_nodes = source->require_output_nodes;
memcpy(graph->nodes, source->nodes, source->node_count * sizeof(*graph->nodes));
memcpy(graph->edges, source->edges, source->edge_count * sizeof(*graph->edges));
*out = graph;
return XTC_OK;
}
/**
* @brief Applies an edit to a new graph copy, leaving the source unchanged.
*
@ -437,15 +459,8 @@ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *e
*out_graph = NULL;
if (source == NULL || edit == NULL)
return XTC_ERR_INVALID_ARGUMENT;
result = xtc_graph_create(source->max_nodes, &graph);
if (result != XTC_OK)
return result;
graph->node_count = source->node_count;
graph->edge_count = source->edge_count;
graph->next_id = source->next_id;
graph->require_output_nodes = source->require_output_nodes;
memcpy(graph->nodes, source->nodes, source->node_count * sizeof(*graph->nodes));
memcpy(graph->edges, source->edges, source->edge_count * sizeof(*graph->edges));
result = xtc_graph_clone(source,&graph);
if (result != XTC_OK) return result;
switch (edit->kind) {
case XTC_EDIT_ADD_NODE:
switch (edit->data.add.type) {
@ -540,3 +555,36 @@ xtc_result xtc_graph_get_native(const xtc_graph *graph, size_t id, xtc_native_de
*out = graph->nodes[i].native;
return XTC_OK;
}
xtc_result xtc_graph_add_plugin(xtc_graph *g, const xtc_plugin_spec *s, size_t *id)
{
xtc_result r = xtc_plugin_spec_validate(s);
if (r != XTC_OK) return r;
r = add_node(g,XTC_NODE_PLUGIN,0,id);
if (r == XTC_OK) g->nodes[g->node_count-1].plugin = *s;
return r;
}
xtc_result xtc_graph_get_plugin(const xtc_graph *g, size_t id, xtc_plugin_spec *out)
{
size_t i;
if (!g || !out) return XTC_ERR_INVALID_ARGUMENT;
i = node_index(g,id);
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
if (g->nodes[i].type != XTC_NODE_PLUGIN) return XTC_ERR_INVALID_ARGUMENT;
*out = g->nodes[i].plugin; return XTC_OK;
}
xtc_result xtc_graph_set_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *s)
{
size_t i;
xtc_result r = xtc_plugin_spec_validate(s);
if (!g || r != XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
i = node_index(g,id);
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
if (g->nodes[i].type != XTC_NODE_PLUGIN) return XTC_ERR_INVALID_ARGUMENT;
/* Parameter edits cannot replace identity/layout or invalidate wiring. */
if (strcmp(s->path,g->nodes[i].plugin.path) || strcmp(s->label,g->nodes[i].plugin.label) ||
s->index != g->nodes[i].plugin.index || s->unique_id != g->nodes[i].plugin.unique_id ||
s->inputs != g->nodes[i].plugin.inputs || s->control_count != g->nodes[i].plugin.control_count ||
memcmp(s->ports,g->nodes[i].plugin.ports,s->control_count*sizeof(*s->ports))) return XTC_ERR_UNSUPPORTED;
g->nodes[i].plugin = *s; return XTC_OK;
}

View file

@ -3,6 +3,7 @@
#include "xtc/xtc_engine.h"
#include "xtc/xtc_native.h"
#include "xtc/xtc_plugin.h"
#define XTC_MAX_NODES 256U
#define XTC_MAX_FRAMES 65536U
@ -12,6 +13,7 @@ typedef struct {
float value;
size_t id;
xtc_native_descriptor native;
xtc_plugin_spec plugin;
} xtc_node;
typedef struct {

View file

@ -1,4 +1,5 @@
#include "graph_widget.hpp"
#include "xtc/xtc_plugin.h"
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/fl_draw.H>
@ -24,6 +25,8 @@ const char *node_name(xtc_node_type type)
return "Gain";
case XTC_NODE_METER:
return "Meter";
case XTC_NODE_PLUGIN:
return "LADSPA plugin";
case XTC_NODE_NATIVE:
return "Native processor";
case XTC_NODE_INSTRUMENT:
@ -371,7 +374,11 @@ void GraphWidget::draw()
std::snprintf(text, sizeof(text), "%.0f Hz", static_cast<double>(node.value));
else if (node.type == XTC_NODE_GAIN)
std::snprintf(text, sizeof(text), "x %.2f", static_cast<double>(node.value));
else
else if (node.type == XTC_NODE_PLUGIN) {
xtc_plugin_spec plugin{};
if (xtc_graph_get_plugin(graph_,node.id,&plugin) == XTC_OK)
std::snprintf(text,sizeof(text),"%.20s",plugin.label);
} else
std::snprintf(text, sizeof(text), "%s",
node.type == XTC_NODE_METER ? "Peak pass-through"
: node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source"

View file

@ -64,6 +64,8 @@ class GraphWidget : public Fl_Widget {
*
* @return Borrowed graph pointer; may be NULL when none was supplied.
*/
size_t selected_node() const { return selected_node_; }
const xtc_graph *graph() const
{
return graph_;

View file

@ -17,6 +17,9 @@
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <cstdlib>
#include <cmath>
#include <cerrno>
namespace {
/**
@ -72,6 +75,8 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("Audio/Panic", FL_F + 9, panic_callback, this);
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
menu->add("File/Load LADSPA plugin", 0, plugin_callback, this);
menu->add("Edit/Selected plugin control", 0, plugin_callback, this);
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
menu->add("Audio/Start engine", FL_F + 5, play_callback, this);
menu->add("Audio/Stop engine", FL_F + 8, stop_callback, this);
@ -841,6 +846,11 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
if (r == XTC_OK && !ready_)
r = xtc_midi_plan_create(candidate, 48000, &offline_midi);
xtc_midi_plan_destroy(offline_midi);
if (r == XTC_OK && !ready_) {
xtc_plan *graph = nullptr;
r = xtc_graph_compile(xtc_project_graph(candidate),48000,256,&graph);
xtc_plan_destroy(graph);
}
if (r != XTC_OK) {
xtc_audio_plan_destroy(offline);
refresh_status(); // A failed install may have stopped playback.
@ -891,6 +901,7 @@ xtc_result MainWindow::open_project(const char *path)
{
if (path == nullptr)
return XTC_ERR_INVALID_ARGUMENT;
project_issue_.clear();
std::string loaded_path(path);
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_create(&candidate);
@ -898,9 +909,30 @@ xtc_result MainWindow::open_project(const char *path)
r = xtc_project_load(candidate, path);
if (r == XTC_OK)
r = install_project(candidate);
if (r != XTC_OK)
if (r != XTC_OK) {
if (candidate) {
const xtc_graph *graph = xtc_project_graph(candidate);
size_t nodes = 0, edges = 0;
xtc_graph_get_counts(graph,&nodes,&edges);
for (size_t i = 0; i < nodes; ++i) {
xtc_node_info node{}; xtc_plugin_spec spec{};
if (xtc_graph_get_node(graph,i,&node) != XTC_OK || node.type != XTC_NODE_PLUGIN ||
xtc_graph_get_plugin(graph,node.id,&spec) != XTC_OK) continue;
double rate = 48000; xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin *check = nullptr;
xtc_result plugin_result = xtc_plugin_create(&spec,rate,256,&check);
xtc_plugin_destroy(check);
if (plugin_result != XTC_OK) {
project_issue_ = std::string("Missing/incompatible plugin ") + spec.label + " at " + spec.path +
"; restore a compatible module and retry Open. Saved document retained";
report_error(project_issue_.c_str(),plugin_result);
break;
}
}
}
xtc_project_destroy(candidate);
else {
} else {
path_ = loaded_path;
copy_label(path_.c_str());
}
@ -945,6 +977,7 @@ void MainWindow::file_callback(Fl_Widget *widget, void *context)
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
const char *action = menu->mvalue()->label();
xtc_result r = XTC_OK;
window->project_issue_.clear();
if (std::strcmp(action, "New") == 0) {
if (!window->discard_changes())
return;
@ -972,7 +1005,7 @@ void MainWindow::file_callback(Fl_Widget *widget, void *context)
}
}
if (r != XTC_OK)
window->report_error("Project operation failed", r);
window->report_error(window->project_issue_.empty() ? "Project operation failed" : window->project_issue_.c_str(), r);
}
/**
@ -1302,3 +1335,101 @@ void MainWindow::history_callback(Fl_Widget *widget, void *context)
if (r != XTC_OK && r != XTC_ERR_NOT_FOUND)
window->report_error("Undo/redo failed", r);
}
xtc_result MainWindow::install_plugin_graph(xtc_graph *graph)
{
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_set_graph(candidate,graph);
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate);
else mark_dirty();
return r;
}
xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *id)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
double rate = 48000;
xtc_backend_status status{};
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
xtc_plugin_spec spec{};
xtc_result r = xtc_plugin_probe(path,index,rate,&spec);
xtc_graph *graph = nullptr; size_t added = SIZE_MAX;
if (r == XTC_OK) r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
if (r == XTC_OK) r = install_plugin_graph(graph);
xtc_graph_destroy(graph);
if (r == XTC_OK && id) *id = added;
return r;
}
xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
xtc_plugin_spec spec{};
xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
if (r != XTC_OK) return r;
size_t i;
for (i = 0; i < spec.control_count && spec.ports[i] != port; ++i) {}
if (i == spec.control_count) return XTC_ERR_NOT_FOUND;
spec.values[i] = value;
xtc_graph *graph = nullptr;
r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
if (r == XTC_OK) r = xtc_graph_set_plugin(graph,id,&spec);
/* Compile before adoption even with a mocked/disconnected backend. */
xtc_plan *check = nullptr;
xtc_backend_status status{}; double rate = 48000;
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check);
xtc_plan_destroy(check);
if (r == XTC_OK) r = install_plugin_graph(graph);
xtc_graph_destroy(graph); return r;
}
void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
{
auto *w = static_cast<MainWindow *>(context);
if (w->recorder_) { w->report_error("Stop recording before plugin changes",XTC_ERR_INVALID_STATE); return; }
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
const char *label = menu->mvalue()->label();
xtc_result r;
if (std::strcmp(label,"Load LADSPA plugin") == 0) {
Fl_Native_File_Chooser chooser;
chooser.title("Load trusted LADSPA module (executes native code)"); chooser.filter("LADSPA\t*.so");
if (chooser.show() != 0 || !chooser.filename()) return;
double rate = 48000; xtc_backend_status status{};
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
std::string choices = "Compatible descriptors (index: label):\n";
for (unsigned i = 0; i < 1024; ++i) {
xtc_plugin_spec spec{};
r = xtc_plugin_probe(chooser.filename(),i,rate,&spec);
if (r == XTC_ERR_NOT_FOUND) break;
if (r == XTC_OK) choices += std::to_string(i) + ": " + spec.label + "\n";
}
choices += "Enter descriptor index:";
const char *answer = fl_input("%s","0",choices.c_str());
if (!answer) return;
char *end = nullptr; errno = 0; unsigned long index = std::strtoul(answer,&end,10);
if (errno || !answer[0] || *end || index >= 1024) r = XTC_ERR_INVALID_ARGUMENT;
else r = w->add_plugin(chooser.filename(),static_cast<unsigned int>(index));
} else {
size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{};
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec);
if (r == XTC_OK) {
std::string prompt = std::string(spec.label) + " control ports and values:\n";
xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};
double rate = 48000; xtc_backend_status status{};
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
r = xtc_plugin_parameters(&spec,rate,info);
if (r != XTC_OK) { w->report_error("Read plugin controls failed",r); return; }
for (unsigned i = 0; i < spec.control_count; ++i)
prompt += std::to_string(spec.ports[i]) + ": " + info[i].name + " = " + std::to_string(spec.values[i]) +
" [" + std::to_string(info[i].minimum) + ", " + std::to_string(info[i].maximum) + "]\n";
prompt += "Enter port and value (for example: 2 0.5):";
const char *answer = fl_input("%s","",prompt.c_str());
if (!answer) return;
unsigned port; float value; char extra;
if (std::sscanf(answer,"%u %f %c",&port,&value,&extra) != 2 || !std::isfinite(value)) r = XTC_ERR_INVALID_ARGUMENT;
else r = w->set_plugin_control(id,port,value);
}
}
if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r);
}

View file

@ -7,6 +7,7 @@
#include "xtc/xtc_project.h"
#include "xtc/xtc_backend.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
class GraphWidget;
class MixerWidget;
@ -18,6 +19,9 @@ class Fl_Tabs;
class MainWindow : public Fl_Double_Window {
public:
/* Control-thread plugin commands, transactional model adoption. */
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
xtc_result set_plugin_control(size_t id, unsigned int port, float value);
/**
* @brief Constructs the window, its widgets, and the initial project.
*
@ -284,10 +288,13 @@ class MainWindow : public Fl_Double_Window {
* @param widget Triggering Fl_Menu_Bar whose selected label names the action.
* @param context MainWindow * owning the project and file path.
*/
static void plugin_callback(Fl_Widget *widget, void *context);
xtc_result install_plugin_graph(xtc_graph *graph);
static void file_callback(Fl_Widget * widget, void *context);
GraphWidget *graph_widget_ = nullptr;
Fl_Box *graph_state_ = nullptr;
xtc_backend *backend_ = nullptr;
std::string project_issue_;
bool ready_ = false;
bool running_ = false;
bool closing_ = false;

View file

@ -125,6 +125,19 @@ static int decode_name(const char *text, char *name)
return xtc_project_valid_name(name);
}
/* Binary-safe nonempty hexadecimal strings, excluding embedded NUL. */
static int decode_bytes(const char *text, char *out, size_t capacity)
{
size_t n = strlen(text), i;
if (!n || n % 2 || n / 2 >= capacity) return 0;
for (i = 0; i < n/2; ++i) {
int hi = hex_digit(text[2*i]), lo = hex_digit(text[2*i+1]);
if (hi < 0 || lo < 0 || (!hi && !lo)) return 0;
out[i] = (char)(hi*16+lo);
}
out[n/2] = 0; return 1;
}
/**
* @brief Decodes eight hexadecimal IEEE-754 bits into a finite float.
*
@ -178,7 +191,7 @@ static xtc_result read_project(reader *input, xtc_project *project)
xtc_graph *graph = project->graph;
READ("XTC_PROJECT", 2U);
NUM(1U, version);
if (version < 1U || version > 4U)
if (version < 1U || version > 5U)
return XTC_ERR_UNSUPPORTED;
format_version = version;
READ("APP", 4U);
@ -212,9 +225,11 @@ static xtc_result read_project(reader *input, xtc_project *project)
if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN ||
version > (format_version == 1U ? XTC_NODE_OUTPUT
: format_version == 2U ? XTC_NODE_AUDIO
: XTC_NODE_INSTRUMENT) ||
: format_version < 5U ? XTC_NODE_INSTRUMENT
: XTC_NODE_PLUGIN) ||
!decode_float(input->tokens[3], &node.value))
return XTC_ERR_FORMAT;
if (version == XTC_NODE_NATIVE) return XTC_ERR_UNSUPPORTED;
node.id = (size_t)value;
node.type = (xtc_node_type)version;
if (node.type == XTC_NODE_OSCILLATOR && node.value < 0.0f)
@ -223,6 +238,25 @@ static xtc_result read_project(reader *input, xtc_project *project)
node.type == XTC_NODE_INSTRUMENT) &&
node.value != 0.0f)
return XTC_ERR_FORMAT;
if (node.type == XTC_NODE_PLUGIN) {
xtc_plugin_spec *p = &node.plugin;
size_t k;
if (node.value != 0) return XTC_ERR_FORMAT;
READ("PLUGIN",6U);
NUM(1U,value); if (value > UINT32_MAX) return XTC_ERR_FORMAT; p->unique_id = (unsigned int)value;
NUM(2U,value); if (value >= 1024) return XTC_ERR_FORMAT; p->index = (unsigned int)value;
NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; p->inputs = (unsigned int)value;
NUM(4U,value); if (value > XTC_PLUGIN_CONTROLS) return XTC_ERR_FORMAT; p->control_count = (unsigned int)value;
if (!decode_bytes(input->tokens[5],p->label,sizeof(p->label))) return XTC_ERR_FORMAT;
READ("MODULE",2U);
if (!decode_bytes(input->tokens[1],p->path,sizeof(p->path))) return XTC_ERR_FORMAT;
for (k = 0; k < p->control_count; ++k) {
READ("CONTROL",3U); NUM(1U,value);
if (value >= 64 || !decode_float(input->tokens[2],p->values+k)) return XTC_ERR_FORMAT;
p->ports[k] = (unsigned int)value;
}
if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT;
}
for (j = 0U; j < i; ++j)
if (graph->nodes[j].id == node.id)
return XTC_ERR_FORMAT;
@ -389,7 +423,7 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
return XTC_ERR_UNSUPPORTED;
if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)
return XTC_ERR_UNSUPPORTED;
if (fprintf(file, "XTC_PROJECT 4\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
if (fprintf(file, "XTC_PROJECT 5\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id,
project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count,
graph->edge_count) < 0)
@ -400,6 +434,17 @@ static xtc_result write_project(FILE *file, const xtc_project *project)
if (fprintf(file, "NODE %zu %u %08" PRIx32 "\n", graph->nodes[i].id, (unsigned int)graph->nodes[i].type, bits) <
0)
return XTC_ERR_IO;
if (graph->nodes[i].type == XTC_NODE_PLUGIN) {
const xtc_plugin_spec *p = &graph->nodes[i].plugin;
size_t k;
if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT;
if (fprintf(file,"PLUGIN %u %u %u %u ",p->unique_id,p->index,p->inputs,p->control_count) < 0 ||
!write_name(file,p->label) || fputs("MODULE ",file) == EOF || !write_name(file,p->path)) return XTC_ERR_IO;
for (k = 0; k < p->control_count; ++k) {
uint32_t value; memcpy(&value,p->values+k,4);
if (fprintf(file,"CONTROL %u %08" PRIx32 "\n",p->ports[k],value) < 0) return XTC_ERR_IO;
}
}
}
for (i = 0U; i < graph->edge_count; ++i)
if (fprintf(file, "EDGE %zu %zu\n", graph->nodes[graph->edges[i].source].id,

212
src/plugin/ladspa.c Normal file
View file

@ -0,0 +1,212 @@
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_dsp.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <stdio.h>
/* Header-only optional dependency; absent development header gives explicit
* unsupported stubs, preserving metadata-only load/save of plugin documents. */
#ifdef __has_include
#if __has_include(<ladspa.h>) && !defined(XTC_DISABLE_LADSPA)
#define XTC_HAVE_LADSPA 1
#endif
#endif
static int terminated(const char *s, size_t n) { return memchr(s, 0, n) != NULL; }
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
{
size_t i;
if (!s || !terminated(s->path,sizeof(s->path)) || s->path[0] != '/' ||
!terminated(s->label,sizeof(s->label)) || !s->label[0] || s->index >= 1024 ||
s->inputs > 1 || s->control_count > XTC_PLUGIN_CONTROLS) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < s->control_count; ++i)
if (s->ports[i] >= 64 || (i && s->ports[i] <= s->ports[i-1]) || !isfinite(s->values[i]))
return XTC_ERR_INVALID_ARGUMENT;
return XTC_OK;
}
#ifdef XTC_HAVE_LADSPA
#include <ladspa.h>
#include <dlfcn.h>
#include <limits.h>
struct xtc_plugin {
void *module;
const LADSPA_Descriptor *descriptor;
LADSPA_Handle handle;
float *input, *output;
float controls[64];
size_t frames;
int activated;
};
static xtc_result load(const char *path, unsigned int index, void **module, const LADSPA_Descriptor **descriptor)
{
LADSPA_Descriptor_Function function;
void *symbol;
if (!path || path[0] != '/' || strlen(path) >= 512 || index >= 1024) return XTC_ERR_INVALID_ARGUMENT;
*module = dlopen(path,RTLD_NOW | RTLD_LOCAL);
if (!*module) return XTC_ERR_NOT_FOUND;
symbol = dlsym(*module,"ladspa_descriptor");
if (!symbol || sizeof(function) != sizeof(symbol)) { dlclose(*module); *module = NULL; return XTC_ERR_UNSUPPORTED; }
memcpy(&function,&symbol,sizeof(function));
*descriptor = function(index);
if (!*descriptor) { dlclose(*module); *module = NULL; return XTC_ERR_NOT_FOUND; }
return XTC_OK;
}
static int bounds(const LADSPA_PortRangeHint *h, double rate, float value)
{
double scale = LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) ? rate : 1;
if (!isfinite(value) ||
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && value < h->LowerBound * scale) ||
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && value > h->UpperBound * scale) ||
(LADSPA_IS_HINT_INTEGER(h->HintDescriptor) && value != floorf(value)) ||
(LADSPA_IS_HINT_TOGGLED(h->HintDescriptor) && value != 0 && value != 1)) return 0;
return 1;
}
static float initial(const LADSPA_PortRangeHint *h, double rate)
{
unsigned long hint = h->HintDescriptor & LADSPA_HINT_DEFAULT_MASK;
double scale = LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) ? rate : 1;
double lo = LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) ? h->LowerBound * scale : 0;
double hi = LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? h->UpperBound * scale : 0;
double v = 0, fraction = .5;
if (hint == LADSPA_HINT_DEFAULT_1) v = 1;
else if (hint == LADSPA_HINT_DEFAULT_100) v = 100;
else if (hint == LADSPA_HINT_DEFAULT_440) v = 440;
else if (hint == LADSPA_HINT_DEFAULT_MINIMUM) v = lo;
else if (hint == LADSPA_HINT_DEFAULT_MAXIMUM) v = hi;
else if (hint == LADSPA_HINT_DEFAULT_LOW || hint == LADSPA_HINT_DEFAULT_MIDDLE || hint == LADSPA_HINT_DEFAULT_HIGH) {
if (hint == LADSPA_HINT_DEFAULT_LOW) fraction = .25;
if (hint == LADSPA_HINT_DEFAULT_HIGH) fraction = .75;
v = LADSPA_IS_HINT_LOGARITHMIC(h->HintDescriptor) && lo > 0 && hi > 0 ?
exp(log(lo)*(1-fraction)+log(hi)*fraction) : lo*(1-fraction)+hi*fraction;
}
if (LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && v < lo) v = lo;
if (LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && v > hi) v = hi;
if (LADSPA_IS_HINT_INTEGER(h->HintDescriptor)) v = round(v);
if (LADSPA_IS_HINT_TOGGLED(h->HintDescriptor)) v = v >= .5 ? 1 : 0;
return (float)v;
}
static xtc_result inspect(const LADSPA_Descriptor *d, double rate, xtc_plugin_spec *s)
{
unsigned long i; unsigned outputs = 0;
if (!d->Label || !d->Label[0] || strlen(d->Label) >= sizeof(s->label) || d->UniqueID > UINT32_MAX ||
!d->PortDescriptors || !d->PortRangeHints || !d->PortCount || d->PortCount > 64 ||
!d->instantiate || !d->connect_port || !d->run || !d->cleanup) return XTC_ERR_UNSUPPORTED;
s->unique_id = (unsigned int)d->UniqueID; strcpy(s->label,d->Label);
for (i = 0; i < d->PortCount; ++i) {
LADSPA_PortDescriptor p = d->PortDescriptors[i];
if (!!LADSPA_IS_PORT_INPUT(p) == !!LADSPA_IS_PORT_OUTPUT(p) ||
!!LADSPA_IS_PORT_AUDIO(p) == !!LADSPA_IS_PORT_CONTROL(p)) return XTC_ERR_UNSUPPORTED;
if (LADSPA_IS_PORT_AUDIO(p)) {
if (LADSPA_IS_PORT_INPUT(p)) ++s->inputs; else ++outputs;
} else if (LADSPA_IS_PORT_INPUT(p)) {
size_t n = s->control_count;
if (n == XTC_PLUGIN_CONTROLS) return XTC_ERR_UNSUPPORTED;
{
const LADSPA_PortRangeHint *h = d->PortRangeHints+i;
if ((LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && !isfinite(h->LowerBound)) ||
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && !isfinite(h->UpperBound)) ||
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) &&
h->LowerBound > h->UpperBound)) return XTC_ERR_UNSUPPORTED;
}
s->ports[n] = (unsigned int)i; s->values[n] = initial(d->PortRangeHints+i,rate);
if (!bounds(d->PortRangeHints+i,rate,s->values[n])) return XTC_ERR_UNSUPPORTED;
++s->control_count;
}
}
return outputs == 1 && s->inputs <= 1 ? XTC_OK : XTC_ERR_UNSUPPORTED;
}
xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, xtc_plugin_spec *out)
{
void *module = NULL; const LADSPA_Descriptor *d; xtc_plugin_spec s = {0}; xtc_result r;
if (!out || !isfinite(rate) || rate < 1 || rate > 768000) return XTC_ERR_INVALID_ARGUMENT;
r = load(path,index,&module,&d); if (r != XTC_OK) return r;
r = inspect(d,rate,&s);
if (r == XTC_OK) { strcpy(s.path,path); s.index = index; *out = s; }
dlclose(module); return r;
}
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
{
void *module = NULL; const LADSPA_Descriptor *d; xtc_plugin_spec actual = {0};
xtc_result r; size_t i;
if (!out || xtc_plugin_spec_validate(s) != XTC_OK || !isfinite(rate) || rate < 1 || rate > 768000)
return XTC_ERR_INVALID_ARGUMENT;
r = load(s->path,s->index,&module,&d); if (r != XTC_OK) return r;
r = inspect(d,rate,&actual);
if (r == XTC_OK && (actual.unique_id != s->unique_id || strcmp(actual.label,s->label) ||
actual.control_count != s->control_count || actual.inputs != s->inputs ||
memcmp(actual.ports,s->ports,s->control_count*sizeof(*s->ports)))) r = XTC_ERR_UNSUPPORTED;
if (r == XTC_OK) for (i = 0; i < s->control_count; ++i) {
const LADSPA_PortRangeHint *h = d->PortRangeHints+s->ports[i];
double scale = LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) ? rate : 1;
xtc_parameter_info p = {0}; p.id = s->ports[i];
snprintf(p.name,sizeof(p.name),"%s",d->PortNames && d->PortNames[p.id] ? d->PortNames[p.id] : "Control");
p.minimum = LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) ? (float)(h->LowerBound*scale) : -FLT_MAX;
p.maximum = LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? (float)(h->UpperBound*scale) : FLT_MAX;
p.default_value = actual.values[i]; out[i] = p;
}
dlclose(module); return r;
}
void xtc_plugin_destroy(xtc_plugin *p)
{
if (!p) return;
if (p->handle) {
if (p->activated && p->descriptor->deactivate) p->descriptor->deactivate(p->handle);
p->descriptor->cleanup(p->handle);
}
if (p->module) dlclose(p->module);
free(p->input); free(p->output); free(p);
}
xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frames, xtc_plugin **out)
{
xtc_plugin *p; xtc_plugin_spec actual = {0}; xtc_result r; unsigned long i; size_t c;
if (!out) return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
r = xtc_plugin_spec_validate(s); if (r != XTC_OK) return r;
if (!isfinite(rate) || rate < 1 || rate > 768000 || !frames || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
p = calloc(1,sizeof(*p)); if (!p) return XTC_ERR_OUT_OF_MEMORY;
r = load(s->path,s->index,&p->module,&p->descriptor); if (r != XTC_OK) goto fail;
r = inspect(p->descriptor,rate,&actual); if (r != XTC_OK) goto fail;
r = XTC_ERR_UNSUPPORTED;
if (strcmp(actual.label,s->label) || actual.unique_id != s->unique_id || actual.inputs != s->inputs ||
actual.control_count != s->control_count) goto fail;
for (c = 0; c < s->control_count; ++c) {
if (s->ports[c] != actual.ports[c] || !bounds(p->descriptor->PortRangeHints+s->ports[c],rate,s->values[c])) goto fail;
p->controls[s->ports[c]] = s->values[c];
}
p->frames = frames;
p->input = calloc(frames,sizeof(float)); p->output = calloc(frames,sizeof(float));
r = XTC_ERR_OUT_OF_MEMORY; if (!p->input || !p->output) goto fail;
p->handle = p->descriptor->instantiate(p->descriptor,(unsigned long)rate);
r = XTC_ERR_INVALID_STATE; if (!p->handle) goto fail;
for (i = 0; i < p->descriptor->PortCount; ++i) {
LADSPA_PortDescriptor port = p->descriptor->PortDescriptors[i];
p->descriptor->connect_port(p->handle,i,LADSPA_IS_PORT_CONTROL(port) ? p->controls+i :
LADSPA_IS_PORT_INPUT(port) ? p->input : p->output);
}
if (p->descriptor->activate) p->descriptor->activate(p->handle);
p->activated = 1; *out = p; return XTC_OK;
fail:
xtc_plugin_destroy(p); return r;
}
xtc_result xtc_plugin_process(xtc_plugin *p, const float *input, float *output, size_t frames)
{
size_t i;
if (!p || !output || frames > p->frames) return XTC_ERR_INVALID_ARGUMENT;
if (!frames) return XTC_OK;
for (i = 0; i < frames; ++i) p->input[i] = input ? xtc_dsp_finite(input[i]) : 0;
memset(p->output,0,frames*sizeof(float));
p->descriptor->run(p->handle,(unsigned long)frames);
for (i = 0; i < frames; ++i) output[i] = xtc_dsp_finite(p->output[i]);
return XTC_OK;
}
#else
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double r, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
{ (void)s; (void)r; (void)out; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_plugin_probe(const char *p, unsigned int i, double r, xtc_plugin_spec *s)
{ (void)p; (void)i; (void)r; (void)s; return XTC_ERR_UNSUPPORTED; }
xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double r, size_t f, xtc_plugin **out)
{ (void)s; (void)r; (void)f; if (!out) return XTC_ERR_INVALID_ARGUMENT; *out = NULL; return XTC_ERR_UNSUPPORTED; }
void xtc_plugin_destroy(xtc_plugin *p) { (void)p; }
xtc_result xtc_plugin_process(xtc_plugin *p, const float *i, float *o, size_t f)
{ (void)p; (void)i; (void)o; (void)f; return XTC_ERR_UNSUPPORTED; }
#endif

View file

@ -15,6 +15,7 @@
#include <cstring>
#include <cmath>
#include <initializer_list>
#include <cstdlib>
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
* timers run under Xvfb; no server/device is required or touched. */
@ -456,6 +457,45 @@ int main(int argc, char **argv)
window.do_callback();
}
CHECK(alive == 0U);
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);
auto *sequence = timeline(&window);
size_t id = SIZE_MAX;
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
xtc_plugin_spec spec{};
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
CHECK(window.set_plugin_control(id,spec.ports[0],.5f) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
CHECK(window.set_plugin_control(id,spec.ports[0],3) == XTC_ERR_UNSUPPORTED);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
const xtc_project *before = sequence->project();
CHECK(window.add_plugin("/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(window.add_plugin(module,2) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
CHECK(window.save_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK);
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
CHECK(xtc_project_create(&missing) == XTC_OK);
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
std::strcpy(spec.path,"/absent/xtc-test-plugin.so");
CHECK(xtc_graph_add_plugin(graph,&spec,&absent) == XTC_OK);
CHECK(xtc_project_set_graph(missing,graph) == XTC_OK);
CHECK(xtc_project_save(missing,"/tmp/xtc-gui-missing-plugin.xtc") == XTC_OK);
before = sequence->project();
CHECK(window.open_project("/tmp/xtc-gui-missing-plugin.xtc") == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(contains(&window,"/absent/xtc-test-plugin.so"));
CHECK(window.add_plugin(module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
xtc_project_destroy(missing); xtc_graph_destroy(graph);
unlink("/tmp/xtc-gui-missing-plugin.xtc");
unlink("/tmp/xtc-gui-plugin-test.xtc");
window.do_callback();
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -0,0 +1,146 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_project.h"
#include "xtc/xtc_realtime.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
static int audio;
void *__real_malloc(size_t);
void *__real_calloc(size_t,size_t);
void *__real_realloc(void *,size_t);
void __real_free(void *);
void *__real_dlopen(const char *,int);
int __real_dlclose(void *);
void *__wrap_malloc(size_t n) { assert(!audio); return __real_malloc(n); }
void *__wrap_calloc(size_t n,size_t z) { assert(!audio); return __real_calloc(n,z); }
void *__wrap_realloc(void *p,size_t n) { assert(!audio); return __real_realloc(p,n); }
void __wrap_free(void *p) { assert(!audio); __real_free(p); }
void *__wrap_dlopen(const char *p,int flags) { assert(!audio); return __real_dlopen(p,flags); }
int __wrap_dlclose(void *p) { assert(!audio); return __real_dlclose(p); }
static void graph_roundtrip(const xtc_plugin_spec *spec)
{
xtc_graph *g = NULL; xtc_plan *p = NULL, *again = NULL;
xtc_project *project = NULL, *loaded = NULL;
xtc_plugin_spec copy;
char path[] = "/tmp/xtc-plugin-project-XXXXXX";
float input[31], out[31], expected[31]; size_t i, plugin, output;
int fd = mkstemp(path); assert(fd >= 0); close(fd);
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,spec,&plugin) == XTC_OK);
assert(xtc_graph_add_output(g,&output) == XTC_OK);
assert(xtc_graph_connect(g,plugin,output) == XTC_OK);
assert(xtc_graph_compile(g,48000,31,&p) == XTC_OK);
for (i = 0; i < 31; ++i) input[i] = (float)i / 100;
audio = 1;
assert(xtc_plan_process(p,input,expected,31) == XTC_OK);
audio = 0;
{ int nonzero = 0; for (i = 0; i < 31; ++i) if (expected[i] != 0) nonzero = 1; assert(nonzero); }
assert(xtc_project_create(&project) == XTC_OK);
assert(xtc_project_set_graph(project,g) == XTC_OK);
assert(xtc_project_save(project,path) == XTC_OK);
assert(xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),plugin,&copy) == XTC_OK);
assert(!strcmp(copy.path,spec->path) && !strcmp(copy.label,spec->label));
assert(xtc_graph_compile(xtc_project_graph(loaded),48000,31,&again) == XTC_OK);
audio = 1;
assert(xtc_plan_process(again,input,out,31) == XTC_OK);
audio = 0;
for (i = 0; i < 31; ++i) assert(out[i] == expected[i] && isfinite(out[i]));
xtc_plan_destroy(again); again = NULL;
if (spec->control_count) {
char malformed[] = "/tmp/xtc-plugin-malformed-XXXXXX", record[1536];
int descriptor = mkstemp(malformed);
FILE *source = fopen(path,"r"), *dest = fdopen(descriptor,"w");
assert(descriptor >= 0 && source && dest);
while (fgets(record,sizeof(record),source)) {
if (!strncmp(record,"CONTROL ",8)) fputs("CONTROL 64 3f800000\n",dest);
else fputs(record,dest);
}
assert(!ferror(source)); fclose(source); assert(fclose(dest) == 0);
assert(xtc_project_load(loaded,malformed) == XTC_ERR_FORMAT);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),plugin,&copy) == XTC_OK);
assert(!strcmp(copy.path,spec->path));
unlink(malformed);
}
/* Metadata-only loading/saving preserves absent modules. */
strcpy(copy.path,"/nonexistent/xtc-preserved-plugin.so");
xtc_graph_destroy(g); g = NULL;
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,&copy,&plugin) == XTC_OK);
assert(xtc_project_set_graph(project,g) == XTC_OK);
assert(xtc_project_save(project,path) == XTC_OK);
xtc_project_destroy(loaded); loaded = NULL;
assert(xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_graph_compile(xtc_project_graph(loaded),48000,31,&again) == XTC_ERR_NOT_FOUND && !again);
assert(xtc_project_save(loaded,path) == XTC_OK);
xtc_graph_destroy(g); xtc_project_destroy(project); xtc_project_destroy(loaded);
xtc_plan_destroy(p); unlink(path);
/* Repeated publication, audio adoption and control retirement. */
{
xtc_realtime *rt = NULL;
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,spec,&plugin) == XTC_OK);
assert(xtc_realtime_create(48000,31,&rt) == XTC_OK);
for (i = 0; i < 100; ++i) {
assert(xtc_realtime_submit(rt,g) == XTC_OK);
audio = 1;
assert(xtc_realtime_process(rt,input,out,31) == XTC_OK);
audio = 0;
xtc_realtime_collect(rt);
}
xtc_realtime_destroy(rt); xtc_graph_destroy(g);
}
}
int main(int argc, char **argv)
{
xtc_plugin_spec s, bad; xtc_plugin *p = NULL; float data[17]; size_t i;
assert(argc == 2 || argc == 3);
assert(xtc_plugin_probe(argv[1],argc == 3 && strcmp(argv[2],"fixture") ? (unsigned int)strtoul(argv[2],NULL,10) : 0,48000,&s) == XTC_OK);
printf("Testing %s (%u controls, %u inputs)\n",s.label,s.control_count,s.inputs);
graph_roundtrip(&s);
{
xtc_parameter_info info[XTC_PLUGIN_CONTROLS];
assert(xtc_plugin_parameters(&s,48000,info) == XTC_OK);
for (i = 0; i < s.control_count; ++i) assert(info[i].id == s.ports[i] && info[i].name[0]);
}
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_OK);
for (i = 0; i < 17; ++i) data[i] = .5f;
audio = 1;
assert(xtc_plugin_process(p,data,data,17) == XTC_OK);
audio = 0;
for (i = 0; i < 17; ++i) assert(isfinite(data[i]));
xtc_plugin_destroy(p); p = NULL;
bad = s; ++bad.unique_id;
assert(xtc_plugin_create(&bad,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
bad = s; if (bad.control_count) {
bad.values[0] = NAN; assert(xtc_plugin_create(&bad,48000,17,&p) == XTC_ERR_INVALID_ARGUMENT);
}
if (argc == 3 && !strcmp(argv[2],"fixture")) {
assert(s.control_count == 1 && s.values[0] == 1);
s.values[0] = 2;
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_OK);
for (i = 0; i < 17; ++i) data[i] = .5f;
audio = 1;
assert(xtc_plugin_process(p,data,data,17) == XTC_OK);
audio = 0;
for (i = 0; i < 17; ++i) assert(data[i] == 1);
xtc_plugin_destroy(p); p = NULL;
graph_roundtrip(&s);
s.values[0] = 3; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
assert(xtc_plugin_probe(argv[1],1,48000,&s) == XTC_OK && !s.inputs);
graph_roundtrip(&s);
assert(xtc_plugin_probe(argv[1],2,48000,&s) == XTC_ERR_UNSUPPORTED);
assert(xtc_plugin_probe(argv[1],3,48000,&s) == XTC_OK);
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_INVALID_STATE && !p);
assert(xtc_plugin_probe(argv[1],4,48000,&s) == XTC_ERR_NOT_FOUND);
}
puts("Plugin lifecycle, identity, layouts, processing and persisted graph checks passed.");
return 0;
}

View file

@ -40,7 +40,7 @@ def main():
make = shlex.split(env.get('MAKE', 'make'))
with log.open('w') as output:
try:
subprocess.run(make + ['release'], cwd=project, env=env, stdout=output,
subprocess.run(make + ['BUILD_DIR=build', 'release'], cwd=project, env=env, stdout=output,
stderr=subprocess.STDOUT, check=True)
except subprocess.CalledProcessError:
print(log.read_text()[-12000:])

51
tests/plugins/fixture.c Normal file
View file

@ -0,0 +1,51 @@
/* MIT test fixture; not a redistributed third-party plugin. */
#include <ladspa.h>
#include <stdlib.h>
typedef struct { float *in, *out, *gain; } instance;
static unsigned alive, runs;
unsigned xtc_fixture_alive(void) { return alive; }
unsigned xtc_fixture_runs(void) { return runs; }
static LADSPA_Handle create(const LADSPA_Descriptor *d, unsigned long rate)
{
instance *p;
(void)rate;
if (d->UniqueID == 900003) return NULL;
p = calloc(1,sizeof(*p)); if (p) ++alive; return p;
}
static void connect(LADSPA_Handle h, unsigned long port, LADSPA_Data *data)
{
instance *p = h;
if (port == 0) p->in = data;
else if (port == 1) p->out = data;
else p->gain = data;
}
static void run(LADSPA_Handle h, unsigned long frames)
{
instance *p = h; unsigned long i; ++runs;
for (i = 0; i < frames; ++i) p->out[i] = p->in[i] * *p->gain;
}
static void source(LADSPA_Handle h, unsigned long frames)
{ instance *p = h; unsigned long i; ++runs; for (i = 0; i < frames; ++i) p->in[i] = .25f; }
static void destroy(LADSPA_Handle h) { if (h) { --alive; free(h); } }
static const LADSPA_PortDescriptor ports[] = {
LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO,
LADSPA_PORT_INPUT|LADSPA_PORT_CONTROL};
static const LADSPA_PortDescriptor source_ports[] = {LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO};
static const LADSPA_PortDescriptor stereo_ports[] = {
LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO,
LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO};
static const char * const names[] = {"Input","Output","Gain","Right output"};
static const LADSPA_PortRangeHint hints[4] = {{0,0,0},{0,0,0},
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1,0,2},{0,0,0}};
static const LADSPA_Descriptor descriptors[] = {
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.PortCount=3,.PortDescriptors=ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
{.UniqueID=900001,.Label="xtc_test_source",.Name="Test source",.PortCount=1,.PortDescriptors=source_ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=source,.cleanup=destroy},
{.UniqueID=900002,.Label="xtc_test_stereo",.Name="Unsupported stereo",.PortCount=4,.PortDescriptors=stereo_ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
{.UniqueID=900003,.Label="xtc_test_fail",.Name="Initialization failure",.PortCount=3,.PortDescriptors=ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy}
};
const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
{ return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }