diff --git a/Makefile b/Makefile index a5715fe..5e30908 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 9a7454a..5ab047c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/BUILD.md b/docs/BUILD.md index 5d3b904..ff61481 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -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`. diff --git a/docs/LICENSES.md b/docs/LICENSES.md index c22a7f2..9e0a617 100644 --- a/docs/LICENSES.md +++ b/docs/LICENSES.md @@ -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. diff --git a/docs/PLUGIN_HOSTING.md b/docs/PLUGIN_HOSTING.md index 2dc8014..85dd2db 100644 --- a/docs/PLUGIN_HOSTING.md +++ b/docs/PLUGIN_HOSTING.md @@ -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. diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md index 9392586..964445b 100644 --- a/docs/PROJECT_FORMAT.md +++ b/docs/PROJECT_FORMAT.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 871fdc6..907cf64 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. diff --git a/docs/decisions/0009-parameter-processors-and-ladspa.md b/docs/decisions/0009-parameter-processors-and-ladspa.md new file mode 100644 index 0000000..a48ea3b --- /dev/null +++ b/docs/decisions/0009-parameter-processors-and-ladspa.md @@ -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. diff --git a/include/xtc/xtc_engine.h b/include/xtc/xtc_engine.h index 373976f..8181592 100644 --- a/include/xtc/xtc_engine.h +++ b/include/xtc/xtc_engine.h @@ -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 { diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h new file mode 100644 index 0000000..96f72b0 --- /dev/null +++ b/include/xtc/xtc_plugin.h @@ -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 diff --git a/packaging/xtc.pc.in b/packaging/xtc.pc.in index c3782eb..d109aff 100644 --- a/packaging/xtc.pc.in +++ b/packaging/xtc.pc.in @@ -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}" diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c index 4c4edd1..326193d 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -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; } } diff --git a/src/graph/graph.c b/src/graph/graph.c index b8fb64d..a5ad397 100644 --- a/src/graph/graph.c +++ b/src/graph/graph.c @@ -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; +} diff --git a/src/graph/graph_internal.h b/src/graph/graph_internal.h index 510e19b..b7c6466 100644 --- a/src/graph/graph_internal.h +++ b/src/graph/graph_internal.h @@ -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 { diff --git a/src/gui/graph_widget.cpp b/src/gui/graph_widget.cpp index dfa71ce..5de655a 100644 --- a/src/gui/graph_widget.cpp +++ b/src/gui/graph_widget.cpp @@ -1,4 +1,5 @@ #include "graph_widget.hpp" +#include "xtc/xtc_plugin.h" #include #include #include @@ -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(node.value)); else if (node.type == XTC_NODE_GAIN) std::snprintf(text, sizeof(text), "x %.2f", static_cast(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" diff --git a/src/gui/graph_widget.hpp b/src/gui/graph_widget.hpp index c33441e..ec0dc24 100644 --- a/src/gui/graph_widget.hpp +++ b/src/gui/graph_widget.hpp @@ -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_; diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index a254eb1..c7a054c 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include 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(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(context); + if (w->recorder_) { w->report_error("Stop recording before plugin changes",XTC_ERR_INVALID_STATE); return; } + auto *menu = static_cast(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(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); +} diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index 49b33e2..97cb643 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -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; diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index f828bc1..dbd9f64 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -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, diff --git a/src/plugin/ladspa.c b/src/plugin/ladspa.c new file mode 100644 index 0000000..9f6a640 --- /dev/null +++ b/src/plugin/ladspa.c @@ -0,0 +1,212 @@ +#include "xtc/xtc_plugin.h" +#include "xtc/xtc_dsp.h" +#include +#include +#include +#include +#include +/* 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() && !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 +#include +#include +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 diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 74e49fc..60204c4 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -15,6 +15,7 @@ #include #include #include +#include /* 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; diff --git a/tests/integration/test_plugins.c b/tests/integration/test_plugins.c new file mode 100644 index 0000000..d0e9a12 --- /dev/null +++ b/tests/integration/test_plugins.c @@ -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 +#include +#include +#include +#include +#include +#include +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,©) == 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,©) == 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,©,&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; +} diff --git a/tests/integration/test_release.py b/tests/integration/test_release.py index 2217178..b437479 100644 --- a/tests/integration/test_release.py +++ b/tests/integration/test_release.py @@ -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:]) diff --git a/tests/plugins/fixture.c b/tests/plugins/fixture.c new file mode 100644 index 0000000..2667b84 --- /dev/null +++ b/tests/plugins/fixture.c @@ -0,0 +1,51 @@ +/* MIT test fixture; not a redistributed third-party plugin. */ +#include +#include +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; }