Phase 15: host trusted LADSPA processors with persisted graph state
This commit is contained in:
parent
c3eadee891
commit
f72314b5a3
24 changed files with 1046 additions and 70 deletions
72
Makefile
72
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
52
docs/decisions/0009-parameter-processors-and-ladspa.md
Normal file
52
docs/decisions/0009-parameter-processors-and-ladspa.md
Normal 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.
|
||||
|
|
@ -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
39
include/xtc/xtc_plugin.h
Normal 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
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
212
src/plugin/ladspa.c
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
146
tests/integration/test_plugins.c
Normal file
146
tests/integration/test_plugins.c
Normal 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,©) == 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;
|
||||
}
|
||||
|
|
@ -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
51
tests/plugins/fixture.c
Normal 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; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue