Phase 25: improve LADSPA discovery, parameter editing and plugin lifecycle
This commit is contained in:
parent
2fd7724065
commit
7bff7b535d
18 changed files with 591 additions and 42 deletions
10
Makefile
10
Makefile
|
|
@ -139,14 +139,14 @@ endif
|
||||||
ifeq ($(GUI_AVAILABLE),yes)
|
ifeq ($(GUI_AVAILABLE),yes)
|
||||||
gui: $(BUILD_DIR)/xtc
|
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
|
$(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)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl
|
||||||
|
|
||||||
test-gui: $(BUILD_DIR)/test_gui
|
test-gui: $(BUILD_DIR)/test_gui
|
||||||
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||||
|
|
||||||
ifeq ($(JACK_AVAILABLE),yes)
|
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
|
$(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)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
||||||
|
|
||||||
test-gui-jack: $(BUILD_DIR)/test_gui_jack
|
test-gui-jack: $(BUILD_DIR)/test_gui_jack
|
||||||
|
|
@ -157,7 +157,7 @@ test-gui-jack:
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
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
|
$(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)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
||||||
else
|
else
|
||||||
test-gui-jack test-gui:
|
test-gui-jack test-gui:
|
||||||
|
|
@ -261,7 +261,7 @@ analyze:
|
||||||
|
|
||||||
.PHONY: test-workflow
|
.PHONY: test-workflow
|
||||||
ifeq ($(GUI_AVAILABLE)$(JACK_AVAILABLE),yesyes)
|
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
|
$(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)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) $(JACK_LIBS) -lm -ldl
|
||||||
|
|
||||||
test-workflow: $(BUILD_DIR)/test_workflow
|
test-workflow: $(BUILD_DIR)/test_workflow
|
||||||
|
|
@ -407,3 +407,5 @@ test-buses: $(BUILD_DIR)/test_buses
|
||||||
./$(BUILD_DIR)/test_buses
|
./$(BUILD_DIR)/test_buses
|
||||||
test: test-buses
|
test: test-buses
|
||||||
-include $(BUILD_DIR)/tests/unit/test_buses.d
|
-include $(BUILD_DIR)/tests/unit/test_buses.d
|
||||||
|
|
||||||
|
-include $(BUILD_DIR)/src/gui/plugin_dialog.d
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,7 @@ indicators and atomic unity-gain insertion into a wire. See
|
||||||
|
|
||||||
Phase 24 adds one-level mix buses, routing, coherent track/bus peaks and master
|
Phase 24 adds one-level mix buses, routing, coherent track/bus peaks and master
|
||||||
clipping indicators, persisted in v9 (reading v1–v8). See [MIXER.md](docs/MIXER.md).
|
clipping indicators, persisted in v9 (reading v1–v8). See [MIXER.md](docs/MIXER.md).
|
||||||
|
|
||||||
|
Phase 25 adds LADSPA descriptor browsing, an identity/parameter editor with
|
||||||
|
reset, transactional replacement and insertion into graph wires. See
|
||||||
|
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md) for compatibility and lifecycle limits.
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,10 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
|
||||||
bus plugin inserts, per-track aux sends and sidechains remain deferred. Track
|
bus plugin inserts, per-track aux sends and sidechains remain deferred. Track
|
||||||
meters are pre-bus; bus meters are pre-graph; master is post-graph. Sample peaks
|
meters are pre-bus; bus meters are pre-graph; master is post-graph. Sample peaks
|
||||||
and sticky clipping do not provide loudness, true-peak analysis or limiting.
|
and sticky clipping do not provide loudness, true-peak analysis or limiting.
|
||||||
|
|
||||||
|
- Plugin discovery is an explicit-file descriptor browser, not a directory scan.
|
||||||
|
Generic parameter dialogs own copied metadata; no native plugin editors or
|
||||||
|
custom MIDI instrument chooser. LADSPA units are unspecified. Replacement
|
||||||
|
requires the same mono audio layout and resets controls/automation. Missing
|
||||||
|
modules reject Open while retaining the current project; GUI module relink for
|
||||||
|
unopened documents is deferred. In-process plugin crashes/hangs are uncontained.
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,17 @@ LADSPA 1.1 host. `xtc_plugin.h` is the C API; no C++ engine or FLTK type is need
|
||||||
|
|
||||||
1. Choose **File → Load LADSPA plugin** and a trusted absolute `.so` path. Loading
|
1. Choose **File → Load LADSPA plugin** and a trusted absolute `.so` path. Loading
|
||||||
executes native code immediately, including constructors during discovery.
|
executes native code immediately, including constructors during discovery.
|
||||||
2. The dialog lists compatible descriptor indices/labels. Select an index.
|
2. The descriptor browser lists names, labels, makers and source/effect roles.
|
||||||
|
Select a row and Choose. Counts report compatible and skipped descriptors.
|
||||||
3. In the Modular Graph, connect the new source/effect output to an output or
|
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
|
other effect; effects have an optional mono input. Unconnected effect inputs
|
||||||
receive external input, as other graph processors do.
|
receive external input, as other graph processors do.
|
||||||
4. Select its node and use **Edit → Selected plugin control**. The dialog shows
|
4. Select its node and use **Edit → Selected plugin control**. The editor shows
|
||||||
actual port IDs, names, values and rate-adjusted ranges. Enter a port and value.
|
name, maker, copyright, module path, descriptor index and unique ID, then
|
||||||
Toggled controls require 0/1 and integer controls require integral values.
|
actual port IDs, editable values, rate-adjusted bounds, defaults and discrete/
|
||||||
|
logarithmic hints. Apply validates all values and candidate initialization;
|
||||||
|
Cancel and window close preserve the document. Toggled controls require 0/1
|
||||||
|
and integer controls require integral values.
|
||||||
5. Save normally, reopen, reconnect external JACK playback if needed, and Play.
|
5. Save normally, reopen, reconnect external JACK playback if needed, and Play.
|
||||||
Plugin changes stop/rebuild playback and participate in ordinary undo/redo.
|
Plugin changes stop/rebuild playback and participate in ordinary undo/redo.
|
||||||
|
|
||||||
|
|
@ -103,3 +107,89 @@ bundled or installed by the target):
|
||||||
Tested packages: ladspa 1.17-7 and swh-plugins 0.4.17-7. These zero-latency
|
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.
|
processors define the initial compatible set; other plugins remain unqualified.
|
||||||
Physical listening and plugin CPU/endurance qualification remain pending.
|
Physical listening and plugin CPU/endurance qualification remain pending.
|
||||||
|
|
||||||
|
## Phase 25: discovery, editing and graph lifecycle
|
||||||
|
|
||||||
|
Discovery remains explicit-file only, bounded to 1024 descriptor indices; no
|
||||||
|
recursive scan, plugin database or new dependency is introduced. Unsupported
|
||||||
|
layouts are skipped with counts; source/effect roles describe audio ports, not
|
||||||
|
MIDI instrument capabilities. Built-in MIDI synthesis remains the only GUI MIDI
|
||||||
|
instrument. LADSPA sources can be identified, edited, replaced and removed in
|
||||||
|
the graph but cannot receive MIDI events.
|
||||||
|
|
||||||
|
**Edit → Insert LADSPA on selected wire** replaces one existing connection with
|
||||||
|
`source -> effect -> destination`, as one undo step. Only one-input effects are
|
||||||
|
accepted; stale wires, capacity failures and initialization failures retain the
|
||||||
|
original graph. **Edit → Replace selected plugin** accepts the same audio input
|
||||||
|
layout, keeps the node ID and all wires, and installs the new descriptor's default
|
||||||
|
controls with no inherited automation. It can replace from another module.
|
||||||
|
A source/effect layout mismatch is rejected. Undo restores the old identity,
|
||||||
|
values and automation. Every candidate is compiled before adoption even when
|
||||||
|
the backend is disconnected; no intermediate graph is published.
|
||||||
|
|
||||||
|
Ordering is graph signal flow, not descriptor order, creation order or a flat
|
||||||
|
rack. Use the graph's connection/disconnection tools to place effects in series
|
||||||
|
or parallel. Insertion on a wire preserves its endpoints and changes only that
|
||||||
|
stage. Select a plugin and Delete to remove it and incident wires, then reconnect
|
||||||
|
as desired; deletion does not automatically bridge or sum routes. A now-unwired
|
||||||
|
effect receives external audio, following existing graph semantics. Undo/redo
|
||||||
|
restores removal and routing. Independent bus inserts are still outside the
|
||||||
|
shared-source graph model.
|
||||||
|
|
||||||
|
Reset defaults in the editor previews every control's declared/default-derived
|
||||||
|
value. Apply also clears that plugin's automation; Cancel preserves it. The
|
||||||
|
**Edit → Reset selected plugin** command does the same in one undo step. The
|
||||||
|
public control-thread `xtc_plugin_reset` can reset one port (removing only its
|
||||||
|
lane) or UINT32_MAX for all controls. Automation otherwise overrides static
|
||||||
|
values, so editing a static control does not remove an existing lane.
|
||||||
|
|
||||||
|
`xtc_plugin_describe` returns copied name/maker/copyright (bounded/truncated),
|
||||||
|
parameter ranges/defaults and integer/toggle/logarithmic/sample-rate/boundedness
|
||||||
|
flags. Missing maker/copyright receive explicit fallback labels. LADSPA has no
|
||||||
|
standard unit metadata: unit stays empty and the editor says unspecified rather
|
||||||
|
than guessing Hz/dB from names. Numeric inputs use linear parameter values;
|
||||||
|
logarithmic is a descriptor hint, not an extra host conversion. Unbounded ends
|
||||||
|
are labeled unbounded; finite float extrema remain the validation bounds.
|
||||||
|
Sample-rate hints scale bounds and defaults once at the engine's actual rate;
|
||||||
|
nonrepresentable scaled bounds are rejected. Discrete defaults are rounded and
|
||||||
|
validated; impossible defaults fail discovery rather than reaching the engine.
|
||||||
|
|
||||||
|
The generic modal editor owns copied metadata and temporary widget values only.
|
||||||
|
It never owns or retains module handles, descriptors or audio instance pointers.
|
||||||
|
Closing it cannot unload active DSP; Apply builds a new immutable plan before
|
||||||
|
installation and existing retirement/quiescence rules release old instances.
|
||||||
|
No embedded third-party editor UI is supported.
|
||||||
|
|
||||||
|
### Compatibility and recovery
|
||||||
|
|
||||||
|
LADSPA 1.1 descriptor identity is module path + index + label + unique ID, with
|
||||||
|
exact supported port/control layout verified on restore. A unique ID is not a
|
||||||
|
binary version/hash guarantee. Parameter values and bounded automation persist
|
||||||
|
in existing project v9 records; this phase adds no file format change. Transient
|
||||||
|
history/tails and opaque state remain unsupported. Native processor ABI 2 and
|
||||||
|
its exact `struct_size` contract are unchanged; it remains parameter-only state.
|
||||||
|
Consumers rebuild against public headers; the new copied details API adds no
|
||||||
|
callbacks or GUI types to the C core.
|
||||||
|
|
||||||
|
Missing/incompatible modules reject GUI Open with the offending label/path and
|
||||||
|
leave the current project intact. Restore a compatible module at that path and
|
||||||
|
retry; use metadata-only validation/save to preserve an unavailable module's
|
||||||
|
records. No dummy replacement silently removes its controls. Failed discovery,
|
||||||
|
replacement or instantiate reports an error and retains the current document.
|
||||||
|
A LADSPA `run` callback has no error return; finite-sample sanitation does not
|
||||||
|
contain crashes, hangs, memory corruption or prohibited work inside a plugin.
|
||||||
|
Only trusted in-process plugins are supported. See RECOVERY.md.
|
||||||
|
|
||||||
|
### Phase 25 verification
|
||||||
|
|
||||||
|
Fixture tests cover copied identity/hints, integer and toggle defaults and range
|
||||||
|
rejection, logarithmic/sample-rate hints, scaled-range overflow rejection, reset
|
||||||
|
and unchanged-on-error behavior, parameter/automation save/reopen and exact
|
||||||
|
rendering. Noncommutative gain/offset effects prove that graph order changes
|
||||||
|
output as expected. GUI tests drive the actual chooser/editor, reset/Apply and
|
||||||
|
Cancel, replacement layout/init/missing-module rollback, insertion/stale-wire
|
||||||
|
failure, deletion and undo. Existing fixture tests exercise 100 live plan
|
||||||
|
replacements with host allocation/free/dlopen/dlclose guards. Native ABI tests
|
||||||
|
cover process-error silence/reset. The separately installed amp/inverter/sine
|
||||||
|
modules remain the qualified LADSPA set. Human listening, arbitrary third-party
|
||||||
|
editors/plugins and extended realtime endurance remain unverified.
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,21 @@ float32 WAV from complete on-disk frames, ignoring a trailing partial frame.
|
||||||
It never changes the source or overwrites an existing destination. This
|
It never changes the source or overwrites an existing destination. This
|
||||||
supersedes the earlier lack of a WAV recovery tool described above. See
|
supersedes the earlier lack of a WAV recovery tool described above. See
|
||||||
AUDIO_FILES.md for format/ownership/size limits and `make test-recording`.
|
AUDIO_FILES.md for format/ownership/size limits and `make test-recording`.
|
||||||
|
|
||||||
|
## Plugin load and replacement failures (Phase 25)
|
||||||
|
|
||||||
|
GUI Open reports an unavailable/incompatible plugin's label and absolute module
|
||||||
|
path and retains the current document. Preserve the saved file; restore a
|
||||||
|
compatible module at its original path and retry Open. `xtc-check --validate`
|
||||||
|
checks metadata without executing plugins, so success does not certify that a
|
||||||
|
module exists or initializes. Metadata-only C load/save preserves unavailable
|
||||||
|
plugin records. There is no automatic missing-plugin substitution or GUI relink
|
||||||
|
for an unopened document.
|
||||||
|
|
||||||
|
For an already loaded project, Replace selected plugin preserves wires and node
|
||||||
|
ID only for the same audio layout. New defaults replace controls/automation;
|
||||||
|
Undo restores the original configuration. Failed discovery/compile/instantiate
|
||||||
|
retains the original graph, controls and history position. Cancel in the generic
|
||||||
|
parameter editor discards its temporary values. Reset defaults clears automation
|
||||||
|
only when applied. No host recovery can contain a crash or hang in trusted native
|
||||||
|
plugin code; LADSPA has no runtime error-return channel.
|
||||||
|
|
|
||||||
|
|
@ -625,10 +625,35 @@ Make instruments and effects practical to use, manage, and diagnose in real proj
|
||||||
- Document plugin API versioning and compatibility expectations.
|
- Document plugin API versioning and compatibility expectations.
|
||||||
|
|
||||||
### Acceptance criteria
|
### Acceptance criteria
|
||||||
- Users can identify and manage loaded instruments and effects without editing configuration files.
|
- [x] Users can identify and manage loaded instruments and effects without editing configuration files.
|
||||||
- Missing or incompatible plugins produce actionable diagnostics and do not silently corrupt projects.
|
- [x] Missing or incompatible plugins produce actionable diagnostics and do not silently corrupt projects.
|
||||||
- Plugin state restoration is tested for supported plugin types.
|
- [x] Plugin state restoration is tested for supported plugin types.
|
||||||
- The engine remains safe if plugin initialization or processing fails, within the documented failure model.
|
- [x] The engine remains safe if plugin initialization or processing fails, within the documented failure model.
|
||||||
|
|
||||||
|
Implemented for the supported LADSPA 1.1 mono effects/audio sources: explicit
|
||||||
|
module descriptor browser, copied identity and control metadata, generic parameter
|
||||||
|
editor with validated Apply/Cancel and defaults reset, compatible-layout
|
||||||
|
replacement preserving node ID/wires, and atomic effect insertion on a selected
|
||||||
|
wire. Graph connections define order and existing deletion removes incident
|
||||||
|
wires. Edits integrate with undo/redo and compile before adoption. State uses
|
||||||
|
existing v9 parameter/automation records; missing/incompatible plugins retain the
|
||||||
|
current document and report recovery guidance. Native processor ABI 2 remains
|
||||||
|
unchanged. See PLUGIN_HOSTING.md and RECOVERY.md.
|
||||||
|
|
||||||
|
Validation: warning-enabled core/GUI builds; full unit/integration suite;
|
||||||
|
fixture and installed amp/inverter/sine plugin tests; actual chooser/editor GUI
|
||||||
|
tests; reset/cancel, insertion, replacement, removal and undo; layout/init/missing
|
||||||
|
module rollback; exact persisted rendering and noncommutative processing order;
|
||||||
|
callback allocation/module-lifetime guards, reliability and static analysis;
|
||||||
|
software JACK GUI/audio/MIDI and workflow; dependency-free build; installation
|
||||||
|
and package relocation; ASan/UBSan including generic-dialog lifetimes (existing
|
||||||
|
fontconfig leak suppression for GUI/system font allocations).
|
||||||
|
|
||||||
|
Qualification remains the documented in-process failure model: LADSPA cannot
|
||||||
|
signal run failure or contain crashes/hangs. No external editor, opaque state,
|
||||||
|
MIDI-capable plugin instrument, recursive scanner or new format was added.
|
||||||
|
Physical listening and arbitrary third-party plugin/endurance qualification
|
||||||
|
remain pending.
|
||||||
|
|
||||||
## Phase 26 — Automation and Control Mapping
|
## Phase 26 — Automation and Control Mapping
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,3 +130,11 @@ Phase 24: Mixer also offers Add bus and a Route choice for ordinary tracks.
|
||||||
Select Track/Bus strips to edit their controls and automation. Master and selected
|
Select Track/Bus strips to edit their controls and automation. Master and selected
|
||||||
strip meters show dBFS with 20 dB/s release; Reset peaks clears latched clipping.
|
strip meters show dBFS with 20 dB/s release; Reset peaks clears latched clipping.
|
||||||
See MIXER.md for solo rules, gain staging and meter taps.
|
See MIXER.md for solo rules, gain staging and meter taps.
|
||||||
|
|
||||||
|
Phase 25: File → Load LADSPA plugin offers a descriptor browser. Select a graph
|
||||||
|
plugin and Edit → Selected plugin control for identity, values, ranges and
|
||||||
|
Reset defaults; Apply commits, Cancel discards. Edit also offers replacement,
|
||||||
|
reset and insertion of an effect on a selected wire. Graph wires determine
|
||||||
|
processing order; Delete removes a selected plugin and its wires. Changes are
|
||||||
|
undoable. Replacement keeps compatible audio wiring but resets plugin controls
|
||||||
|
and automation. See PLUGIN_HOSTING.md for supported formats and failure behavior.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,28 @@ typedef struct {
|
||||||
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
|
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
|
||||||
xtc_plugin_point points[XTC_PLUGIN_POINTS];
|
xtc_plugin_point points[XTC_PLUGIN_POINTS];
|
||||||
} xtc_plugin_spec;
|
} xtc_plugin_spec;
|
||||||
|
/* Copied discovery metadata, never descriptor/module pointers. LADSPA provides
|
||||||
|
* no standardized units; unit is empty. Flags describe input control hints. */
|
||||||
|
#define XTC_PLUGIN_INTEGER 1U
|
||||||
|
#define XTC_PLUGIN_TOGGLED 2U
|
||||||
|
#define XTC_PLUGIN_LOGARITHMIC 4U
|
||||||
|
#define XTC_PLUGIN_SAMPLE_RATE 8U
|
||||||
|
#define XTC_PLUGIN_BOUNDED_BELOW 16U
|
||||||
|
#define XTC_PLUGIN_BOUNDED_ABOVE 32U
|
||||||
|
typedef struct {
|
||||||
|
char name[128], maker[128], copyright[128];
|
||||||
|
unsigned int hard_realtime;
|
||||||
|
xtc_parameter_info parameters[XTC_PLUGIN_CONTROLS];
|
||||||
|
unsigned int flags[XTC_PLUGIN_CONTROLS];
|
||||||
|
} xtc_plugin_details;
|
||||||
|
/* Control-thread identity/layout verification and copied human-readable hints. */
|
||||||
|
xtc_result xtc_plugin_describe(const xtc_plugin_spec *spec, double rate, xtc_plugin_details *out);
|
||||||
|
/* Reset static values to defaults, removing automation for the chosen port.
|
||||||
|
* UINT32_MAX resets all controls. Failure leaves spec unchanged. */
|
||||||
|
xtc_result xtc_plugin_reset(xtc_plugin_spec *spec, double rate, unsigned int port);
|
||||||
|
/* Replace identity/controls at a stable node ID; only identical audio layouts
|
||||||
|
* are accepted, preserving all wires. Metadata-only; compile before adoption. */
|
||||||
|
xtc_result xtc_graph_replace_plugin(xtc_graph *graph, size_t id, const xtc_plugin_spec *spec);
|
||||||
typedef struct xtc_plugin xtc_plugin;
|
typedef struct xtc_plugin xtc_plugin;
|
||||||
/* Control thread only. Probe explicitly enumerates a file by descriptor index;
|
/* Control thread only. Probe explicitly enumerates a file by descriptor index;
|
||||||
* NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading
|
* NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,18 @@ xtc_result xtc_graph_set_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *
|
||||||
g->nodes[i].plugin = *s; return XTC_OK;
|
g->nodes[i].plugin = *s; return XTC_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xtc_result xtc_graph_replace_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *s)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
if (!g || xtc_plugin_spec_validate(s) != 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;
|
||||||
|
if (g->nodes[i].plugin.inputs != s->inputs) return XTC_ERR_UNSUPPORTED;
|
||||||
|
g->nodes[i].plugin = *s;
|
||||||
|
return XTC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
xtc_result xtc_graph_analyze(const xtc_graph *graph, xtc_routing_info *items, size_t capacity, size_t *count)
|
xtc_result xtc_graph_analyze(const xtc_graph *graph, xtc_routing_info *items, size_t capacity, size_t *count)
|
||||||
{
|
{
|
||||||
xtc_routing_info info[XTC_MAX_NODES] = {{0}};
|
xtc_routing_info info[XTC_MAX_NODES] = {{0}};
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class GraphWidget : public Fl_Widget {
|
||||||
*/
|
*/
|
||||||
xtc_result add_node(xtc_node_type type);
|
xtc_result add_node(xtc_node_type type);
|
||||||
xtc_result insert_gain();
|
xtc_result insert_gain();
|
||||||
|
bool selected_connection(xtc_connection_info &out) const { if (!edge_selected_) return false; out = selected_edge_; return true; }
|
||||||
bool auto_layout(); // Presentation only, no engine publication or undo step.
|
bool auto_layout(); // Presentation only, no engine publication or undo step.
|
||||||
bool routing(size_t id, xtc_routing_info &info) const;
|
bool routing(size_t id, xtc_routing_info &info) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "sequencer_widget.hpp"
|
#include "sequencer_widget.hpp"
|
||||||
#include "piano_roll.hpp"
|
#include "piano_roll.hpp"
|
||||||
#include "mixer_widget.hpp"
|
#include "mixer_widget.hpp"
|
||||||
|
#include "plugin_dialog.hpp"
|
||||||
#include <FL/Fl_Native_File_Chooser.H>
|
#include <FL/Fl_Native_File_Chooser.H>
|
||||||
#include "xtc/xtc_version.h"
|
#include "xtc/xtc_version.h"
|
||||||
|
|
||||||
|
|
@ -76,6 +77,9 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
||||||
menu->add("View/Piano roll", FL_CTRL + '4', piano_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("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
|
||||||
menu->add("File/Load LADSPA plugin", 0, plugin_callback, this);
|
menu->add("File/Load LADSPA plugin", 0, plugin_callback, this);
|
||||||
|
menu->add("Edit/Replace selected plugin", 0, plugin_callback, this);
|
||||||
|
menu->add("Edit/Insert LADSPA on selected wire", 0, plugin_callback, this);
|
||||||
|
menu->add("Edit/Reset selected plugin", 0, plugin_callback, this);
|
||||||
menu->add("Edit/Selected plugin automation", 0, plugin_callback, this);
|
menu->add("Edit/Selected plugin automation", 0, plugin_callback, this);
|
||||||
menu->add("Edit/Selected plugin control", 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("File/Quit", FL_CTRL + 'q', close_callback, this);
|
||||||
|
|
@ -1383,11 +1387,58 @@ xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *
|
||||||
xtc_graph *graph = nullptr; size_t added = SIZE_MAX;
|
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_clone(xtc_project_graph(project_.get()),&graph);
|
||||||
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
|
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
|
||||||
|
xtc_plan *check = nullptr;
|
||||||
|
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);
|
if (r == XTC_OK) r = install_plugin_graph(graph);
|
||||||
xtc_graph_destroy(graph);
|
xtc_graph_destroy(graph);
|
||||||
|
if (r == XTC_OK) graph_widget_->select_node(added);
|
||||||
if (r == XTC_OK && id) *id = added;
|
if (r == XTC_OK && id) *id = added;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int index)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (r == XTC_OK) r = apply_plugin_spec(id,spec,true);
|
||||||
|
if (r == XTC_OK) graph_widget_->select_node(id);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
xtc_result MainWindow::reset_plugin(size_t id, unsigned int port)
|
||||||
|
{
|
||||||
|
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_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
|
||||||
|
if (r == XTC_OK) r = xtc_plugin_reset(&spec,rate,port);
|
||||||
|
return r == XTC_OK ? apply_plugin_spec(id,spec) : r;
|
||||||
|
}
|
||||||
|
xtc_result MainWindow::insert_plugin(const xtc_connection_info &wire, 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);
|
||||||
|
if (r == XTC_OK && !spec.inputs) r = XTC_ERR_UNSUPPORTED;
|
||||||
|
xtc_graph *graph = nullptr; size_t added = SIZE_MAX;
|
||||||
|
xtc_graph_edit edit{}; edit.kind = XTC_EDIT_DISCONNECT; edit.data.connection = wire;
|
||||||
|
if (r == XTC_OK) r = xtc_graph_apply_edit(xtc_project_graph(project_.get()),&edit,&graph,nullptr);
|
||||||
|
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
|
||||||
|
if (r == XTC_OK) r = xtc_graph_connect_ports(graph,wire.source_node,wire.source_port,added,0);
|
||||||
|
if (r == XTC_OK) r = xtc_graph_connect_ports(graph,added,1,wire.destination_node,wire.destination_port);
|
||||||
|
xtc_plan *check = nullptr;
|
||||||
|
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);
|
||||||
|
if (r == XTC_OK) { graph_widget_->select_node(added); if (id) *id = added; }
|
||||||
|
return r;
|
||||||
|
}
|
||||||
xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value)
|
xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value)
|
||||||
{
|
{
|
||||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||||
|
|
@ -1419,11 +1470,11 @@ xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &
|
||||||
}
|
}
|
||||||
return apply_plugin_spec(id,spec);
|
return apply_plugin_spec(id,spec);
|
||||||
}
|
}
|
||||||
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec)
|
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace)
|
||||||
{
|
{
|
||||||
xtc_graph *graph = nullptr;
|
xtc_graph *graph = nullptr;
|
||||||
xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
||||||
if (r == XTC_OK) r = xtc_graph_set_plugin(graph,id,&spec);
|
if (r == XTC_OK) r = replace ? xtc_graph_replace_plugin(graph,id,&spec) : xtc_graph_set_plugin(graph,id,&spec);
|
||||||
/* Compile before adoption even with a mocked/disconnected backend. */
|
/* Compile before adoption even with a mocked/disconnected backend. */
|
||||||
xtc_plan *check = nullptr;
|
xtc_plan *check = nullptr;
|
||||||
xtc_backend_status status{}; double rate = 48000;
|
xtc_backend_status status{}; double rate = 48000;
|
||||||
|
|
@ -1440,30 +1491,52 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
||||||
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
|
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
|
||||||
const char *label = menu->mvalue()->label();
|
const char *label = menu->mvalue()->label();
|
||||||
xtc_result r;
|
xtc_result r;
|
||||||
if (std::strcmp(label,"Load LADSPA plugin") == 0) {
|
if (std::strcmp(label,"Reset selected plugin") == 0) {
|
||||||
|
r = w->reset_plugin(w->graph_widget_->selected_node());
|
||||||
|
} else if (std::strcmp(label,"Load LADSPA plugin") == 0 ||
|
||||||
|
std::strcmp(label,"Replace selected plugin") == 0 ||
|
||||||
|
std::strcmp(label,"Insert LADSPA on selected wire") == 0) {
|
||||||
|
const size_t selected = w->graph_widget_->selected_node();
|
||||||
|
xtc_connection_info wire{};
|
||||||
|
const bool insert = std::strcmp(label,"Insert LADSPA on selected wire") == 0;
|
||||||
|
const bool replace = std::strcmp(label,"Replace selected plugin") == 0;
|
||||||
|
if (insert && !w->graph_widget_->selected_connection(wire)) {
|
||||||
|
w->report_error("Select an existing wire before inserting an effect",XTC_ERR_INVALID_STATE); return;
|
||||||
|
}
|
||||||
|
if (replace) {
|
||||||
|
xtc_plugin_spec old{};
|
||||||
|
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),selected,&old);
|
||||||
|
if (r != XTC_OK) { w->report_error("Select a LADSPA node to replace",r); return; }
|
||||||
|
}
|
||||||
Fl_Native_File_Chooser chooser;
|
Fl_Native_File_Chooser chooser;
|
||||||
chooser.title("Load trusted LADSPA module (executes native code)"); chooser.filter("LADSPA\t*.so");
|
chooser.title("Choose trusted LADSPA module (executes native code)"); chooser.filter("LADSPA\t*.so");
|
||||||
if (chooser.show() != 0 || !chooser.filename()) return;
|
if (chooser.show() != 0 || !chooser.filename()) return;
|
||||||
double rate = 48000; xtc_backend_status status{};
|
double rate = 48000; xtc_backend_status status{};
|
||||||
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||||
std::string choices = "Compatible descriptors (index: label):\n";
|
unsigned int index = 0;
|
||||||
for (unsigned i = 0; i < 1024; ++i) {
|
xtc_plugin_spec probe{};
|
||||||
xtc_plugin_spec spec{};
|
r = xtc_plugin_probe(chooser.filename(),0,rate,&probe);
|
||||||
r = xtc_plugin_probe(chooser.filename(),i,rate,&spec);
|
if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) {
|
||||||
if (r == XTC_ERR_NOT_FOUND) break;
|
w->report_error("Cannot discover LADSPA module; check path, dependencies and ladspa_descriptor export",r); return;
|
||||||
if (r == XTC_OK) choices += std::to_string(i) + ": " + spec.label + "\n";
|
|
||||||
}
|
}
|
||||||
choices += "Enter descriptor index:";
|
if (!xtc_choose_plugin(chooser.filename(),rate,index)) return;
|
||||||
const char *answer = fl_input("%s","0",choices.c_str());
|
if (replace) r = w->replace_plugin(selected,chooser.filename(),index);
|
||||||
if (!answer) return;
|
else if (insert) r = w->insert_plugin(wire,chooser.filename(),index);
|
||||||
char *end = nullptr; errno = 0; unsigned long index = std::strtoul(answer,&end,10);
|
else r = w->add_plugin(chooser.filename(),index);
|
||||||
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 {
|
} else {
|
||||||
size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{};
|
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);
|
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec);
|
||||||
if (r == XTC_OK) {
|
if (r == XTC_OK) {
|
||||||
std::string prompt = std::string(spec.label) + " control ports and values:\n";
|
std::string prompt = std::string(spec.label) + " control ports and values:\n";
|
||||||
|
if (std::strcmp(label,"Selected plugin control") == 0) {
|
||||||
|
double rate = 48000; xtc_backend_status status{};
|
||||||
|
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||||
|
xtc_plugin_details details{}; xtc_plugin_spec edited{};
|
||||||
|
r = xtc_plugin_describe(&spec,rate,&details);
|
||||||
|
if (r == XTC_OK && xtc_edit_plugin(spec,details,edited)) r = w->apply_plugin_spec(id,edited);
|
||||||
|
if (r != XTC_OK) w->report_error("Plugin editor failed; configuration retained. Check module identity and control ranges",r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};
|
xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};
|
||||||
double rate = 48000; xtc_backend_status status{};
|
double rate = 48000; xtc_backend_status status{};
|
||||||
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||||
|
|
@ -1487,12 +1560,6 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
||||||
if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r);
|
if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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);
|
if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ class MainWindow : public Fl_Double_Window {
|
||||||
/* Control-thread plugin commands, transactional model adoption. */
|
/* Control-thread plugin commands, transactional model adoption. */
|
||||||
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
|
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
|
||||||
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
|
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
|
||||||
|
xtc_result replace_plugin(size_t id, const char *path, unsigned int index);
|
||||||
|
xtc_result reset_plugin(size_t id, unsigned int port = UINT32_MAX);
|
||||||
|
xtc_result insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id = nullptr);
|
||||||
xtc_result set_plugin_control(size_t id, unsigned int port, float value);
|
xtc_result set_plugin_control(size_t id, unsigned int port, float value);
|
||||||
/**
|
/**
|
||||||
* @brief Constructs the window, its widgets, and the initial project.
|
* @brief Constructs the window, its widgets, and the initial project.
|
||||||
|
|
@ -290,7 +293,7 @@ class MainWindow : public Fl_Double_Window {
|
||||||
* @param context MainWindow * owning the project and file path.
|
* @param context MainWindow * owning the project and file path.
|
||||||
*/
|
*/
|
||||||
static void plugin_callback(Fl_Widget *widget, void *context);
|
static void plugin_callback(Fl_Widget *widget, void *context);
|
||||||
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec);
|
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false);
|
||||||
xtc_result install_plugin_graph(xtc_graph *graph);
|
xtc_result install_plugin_graph(xtc_graph *graph);
|
||||||
static void file_callback(Fl_Widget * widget, void *context);
|
static void file_callback(Fl_Widget * widget, void *context);
|
||||||
GraphWidget *graph_widget_ = nullptr;
|
GraphWidget *graph_widget_ = nullptr;
|
||||||
|
|
|
||||||
103
src/gui/plugin_dialog.cpp
Normal file
103
src/gui/plugin_dialog.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#include "plugin_dialog.hpp"
|
||||||
|
#include <FL/Fl.H>
|
||||||
|
#include <FL/Fl_Double_Window.H>
|
||||||
|
#include <FL/Fl_Hold_Browser.H>
|
||||||
|
#include <FL/Fl_Box.H>
|
||||||
|
#include <FL/Fl_Button.H>
|
||||||
|
#include <FL/Fl_Return_Button.H>
|
||||||
|
#include <FL/Fl_Value_Input.H>
|
||||||
|
#include <FL/Fl_Scroll.H>
|
||||||
|
#include <FL/fl_ask.H>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct Dialog : Fl_Double_Window {
|
||||||
|
bool accepted = false;
|
||||||
|
Dialog(int width, int height, const char *title) : Fl_Double_Window(width,height,title) { set_modal(); }
|
||||||
|
static void accept(Fl_Widget *, void *data) { auto *d = static_cast<Dialog *>(data); d->accepted = true; d->hide(); }
|
||||||
|
static void cancel(Fl_Widget *, void *data) { static_cast<Dialog *>(data)->hide(); }
|
||||||
|
bool run() { callback(cancel,this); end(); show(); while (shown()) Fl::wait(); return accepted; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index)
|
||||||
|
{
|
||||||
|
Dialog dialog(760,450,"LADSPA descriptors");
|
||||||
|
auto *heading = new Fl_Box(15,10,730,40);
|
||||||
|
heading->copy_label(path); heading->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_CLIP);
|
||||||
|
auto *list = new Fl_Hold_Browser(15,55,730,325);
|
||||||
|
list->format_char(0); // Plugin names are data, never browser formatting.
|
||||||
|
std::vector<unsigned int> indices;
|
||||||
|
size_t rejected = 0;
|
||||||
|
for (unsigned int i = 0; i < 1024; ++i) {
|
||||||
|
xtc_plugin_spec spec{}; xtc_plugin_details details{};
|
||||||
|
xtc_result r = xtc_plugin_probe(path,i,rate,&spec);
|
||||||
|
if (r == XTC_ERR_NOT_FOUND) break;
|
||||||
|
if (r != XTC_OK) { ++rejected; continue; }
|
||||||
|
if (xtc_plugin_describe(&spec,rate,&details) != XTC_OK) { ++rejected; continue; }
|
||||||
|
std::string row = std::to_string(i) + ": " + details.name + " (" + spec.label + ") — " + details.maker +
|
||||||
|
(spec.inputs ? " [effect]" : " [source]");
|
||||||
|
list->add(row.c_str()); indices.push_back(i);
|
||||||
|
}
|
||||||
|
auto *status = new Fl_Box(15,385,520,50);
|
||||||
|
std::string summary = std::to_string(indices.size()) + " compatible; " + std::to_string(rejected) +
|
||||||
|
" unsupported/unavailable. Select a descriptor.";
|
||||||
|
status->copy_label(summary.c_str()); status->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
|
||||||
|
auto *cancel = new Fl_Button(540,400,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog);
|
||||||
|
auto *choose = new Fl_Return_Button(645,400,100,30,"Choose"); choose->callback(Dialog::accept,&dialog);
|
||||||
|
if (indices.empty()) choose->deactivate(); else list->select(1);
|
||||||
|
if (!dialog.run() || list->value() < 1) return false;
|
||||||
|
index = indices[static_cast<size_t>(list->value()-1)]; return true;
|
||||||
|
}
|
||||||
|
bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, xtc_plugin_spec &edited)
|
||||||
|
{
|
||||||
|
Dialog dialog(800,600,"Plugin identity and controls");
|
||||||
|
auto *identity = new Fl_Box(15,10,770,125);
|
||||||
|
std::string text = std::string(details.name) + " — " + details.maker + "\n" + spec.label +
|
||||||
|
" | LADSPA ID " + std::to_string(spec.unique_id) + " | descriptor " + std::to_string(spec.index) +
|
||||||
|
"\n" + spec.path + "\n" + details.copyright +
|
||||||
|
"\nMetadata verified; Apply validates initialization. Units unspecified by LADSPA.";
|
||||||
|
identity->copy_label(text.c_str()); identity->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
|
||||||
|
auto *scroll = new Fl_Scroll(10,140,780,370);
|
||||||
|
std::array<Fl_Value_Input *,XTC_PLUGIN_CONTROLS> inputs{};
|
||||||
|
for (unsigned int i = 0; i < spec.control_count; ++i) {
|
||||||
|
int y = 150 + static_cast<int>(i)*65;
|
||||||
|
const auto &p = details.parameters[i];
|
||||||
|
std::string name = std::to_string(p.id) + ": " + p.name;
|
||||||
|
auto *label = new Fl_Box(25,y,425,25); label->copy_label(name.c_str()); label->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||||
|
inputs[i] = new Fl_Value_Input(460,y,150,25); inputs[i]->value(spec.values[i]);
|
||||||
|
inputs[i]->step((details.flags[i] & (XTC_PLUGIN_INTEGER|XTC_PLUGIN_TOGGLED)) ? 1 : 0.001);
|
||||||
|
std::string hint = "Range " + std::string(details.flags[i] & XTC_PLUGIN_BOUNDED_BELOW ? std::to_string(p.minimum) : "unbounded") +
|
||||||
|
" .. " + (details.flags[i] & XTC_PLUGIN_BOUNDED_ABOVE ? std::to_string(p.maximum) : "unbounded") +
|
||||||
|
"; default " + std::to_string(p.default_value) +
|
||||||
|
((details.flags[i] & XTC_PLUGIN_TOGGLED) ? "; toggle 0/1" : (details.flags[i] & XTC_PLUGIN_INTEGER) ? "; integer" : "; continuous") +
|
||||||
|
((details.flags[i] & XTC_PLUGIN_LOGARITHMIC) ? "; logarithmic hint" : "");
|
||||||
|
auto *range = new Fl_Box(25,y+27,715,25); range->copy_label(hint.c_str()); range->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||||
|
}
|
||||||
|
scroll->end();
|
||||||
|
auto *note = new Fl_Box(15,515,770,30,"Automation overrides static controls. Reset defaults also clears all plugin automation.");
|
||||||
|
note->labelsize(12);
|
||||||
|
struct Reset { const xtc_plugin_details *details; std::array<Fl_Value_Input *,XTC_PLUGIN_CONTROLS> *inputs; unsigned int count; bool used; } reset{&details,&inputs,spec.control_count,false};
|
||||||
|
auto *defaults = new Fl_Button(20,555,170,30,"Reset defaults");
|
||||||
|
defaults->callback([](Fl_Widget *, void *data) {
|
||||||
|
auto *r = static_cast<Reset *>(data); r->used = true;
|
||||||
|
for (unsigned int i = 0; i < r->count; ++i) (*r->inputs)[i]->value(r->details->parameters[i].default_value);
|
||||||
|
},&reset);
|
||||||
|
auto *cancel = new Fl_Button(570,555,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog);
|
||||||
|
auto *apply = new Fl_Return_Button(675,555,105,30,"Apply"); apply->callback(Dialog::accept,&dialog);
|
||||||
|
if (!dialog.run()) return false;
|
||||||
|
xtc_plugin_spec candidate = spec;
|
||||||
|
for (unsigned int i = 0; i < spec.control_count; ++i) {
|
||||||
|
double value = inputs[i]->value(); const auto &p = details.parameters[i]; unsigned int flags = details.flags[i];
|
||||||
|
if (!std::isfinite(value) || value < p.minimum || value > p.maximum ||
|
||||||
|
((flags & XTC_PLUGIN_INTEGER) && value != std::floor(value)) ||
|
||||||
|
((flags & XTC_PLUGIN_TOGGLED) && value != 0 && value != 1)) {
|
||||||
|
fl_alert("Invalid value for port %u (%s); no controls were changed.",p.id,p.name); return false;
|
||||||
|
}
|
||||||
|
candidate.values[i] = static_cast<float>(value);
|
||||||
|
}
|
||||||
|
if (reset.used) candidate.point_count = 0;
|
||||||
|
edited = candidate; return true;
|
||||||
|
}
|
||||||
9
src/gui/plugin_dialog.hpp
Normal file
9
src/gui/plugin_dialog.hpp
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#ifndef XTC_PLUGIN_DIALOG_HPP
|
||||||
|
#define XTC_PLUGIN_DIALOG_HPP
|
||||||
|
#include "xtc/xtc_plugin.h"
|
||||||
|
/* Modal, control-thread-only views. They own copied metadata and widget state,
|
||||||
|
* never engine/module handles. Cancel/close leaves caller configuration intact. */
|
||||||
|
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index);
|
||||||
|
bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details,
|
||||||
|
xtc_plugin_spec &edited);
|
||||||
|
#endif
|
||||||
|
|
@ -116,6 +116,9 @@ static xtc_result inspect(const LADSPA_Descriptor *d, double rate, xtc_plugin_sp
|
||||||
const LADSPA_PortRangeHint *h = d->PortRangeHints+i;
|
const LADSPA_PortRangeHint *h = d->PortRangeHints+i;
|
||||||
if ((LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && !isfinite(h->LowerBound)) ||
|
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_ABOVE(h->HintDescriptor) && !isfinite(h->UpperBound)) ||
|
||||||
|
(LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) &&
|
||||||
|
((LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && fabs((double)h->LowerBound*rate) > FLT_MAX) ||
|
||||||
|
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && fabs((double)h->UpperBound*rate) > FLT_MAX))) ||
|
||||||
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) &&
|
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) &&
|
||||||
h->LowerBound > h->UpperBound)) return XTC_ERR_UNSUPPORTED;
|
h->LowerBound > h->UpperBound)) return XTC_ERR_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
@ -135,10 +138,10 @@ xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, x
|
||||||
if (r == XTC_OK) { strcpy(s.path,path); s.index = index; *out = s; }
|
if (r == XTC_OK) { strcpy(s.path,path); s.index = index; *out = s; }
|
||||||
dlclose(module); return r;
|
dlclose(module); return r;
|
||||||
}
|
}
|
||||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin_details *out)
|
||||||
{
|
{
|
||||||
void *module = NULL; const LADSPA_Descriptor *d; xtc_plugin_spec actual = {0};
|
void *module = NULL; const LADSPA_Descriptor *d; xtc_plugin_spec actual = {0};
|
||||||
xtc_result r; size_t i;
|
xtc_result r; size_t i; xtc_plugin_details details = {0};
|
||||||
if (!out || xtc_plugin_spec_validate(s) != XTC_OK || !isfinite(rate) || rate < 1 || rate > 768000)
|
if (!out || xtc_plugin_spec_validate(s) != XTC_OK || !isfinite(rate) || rate < 1 || rate > 768000)
|
||||||
return XTC_ERR_INVALID_ARGUMENT;
|
return XTC_ERR_INVALID_ARGUMENT;
|
||||||
r = load(s->path,s->index,&module,&d); if (r != XTC_OK) return r;
|
r = load(s->path,s->index,&module,&d); if (r != XTC_OK) return r;
|
||||||
|
|
@ -153,7 +156,20 @@ xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_para
|
||||||
snprintf(p.name,sizeof(p.name),"%s",d->PortNames && d->PortNames[p.id] ? d->PortNames[p.id] : "Control");
|
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.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.maximum = LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? (float)(h->UpperBound*scale) : FLT_MAX;
|
||||||
p.default_value = actual.values[i]; out[i] = p;
|
p.default_value = actual.values[i]; details.parameters[i] = p;
|
||||||
|
details.flags[i] = (LADSPA_IS_HINT_INTEGER(h->HintDescriptor) ? XTC_PLUGIN_INTEGER : 0U) |
|
||||||
|
(LADSPA_IS_HINT_TOGGLED(h->HintDescriptor) ? XTC_PLUGIN_TOGGLED : 0U) |
|
||||||
|
(LADSPA_IS_HINT_LOGARITHMIC(h->HintDescriptor) ? XTC_PLUGIN_LOGARITHMIC : 0U) |
|
||||||
|
(LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) ? XTC_PLUGIN_SAMPLE_RATE : 0U) |
|
||||||
|
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) ? XTC_PLUGIN_BOUNDED_BELOW : 0U) |
|
||||||
|
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? XTC_PLUGIN_BOUNDED_ABOVE : 0U);
|
||||||
|
}
|
||||||
|
if (r == XTC_OK) {
|
||||||
|
snprintf(details.name,sizeof(details.name),"%s",d->Name ? d->Name : d->Label);
|
||||||
|
snprintf(details.maker,sizeof(details.maker),"%s",d->Maker ? d->Maker : "Unknown");
|
||||||
|
snprintf(details.copyright,sizeof(details.copyright),"%s",d->Copyright ? d->Copyright : "Unspecified");
|
||||||
|
details.hard_realtime = LADSPA_IS_HARD_RT_CAPABLE(d->Properties) ? 1U : 0U;
|
||||||
|
*out = details;
|
||||||
}
|
}
|
||||||
dlclose(module); return r;
|
dlclose(module); return r;
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +270,7 @@ xtc_result xtc_plugin_process(xtc_plugin *p, const float *input, float *output,
|
||||||
{ return xtc_plugin_process_at(p,input,output,frames,p ? p->next : 0); }
|
{ return xtc_plugin_process_at(p,input,output,frames,p ? p->next : 0); }
|
||||||
|
|
||||||
#else
|
#else
|
||||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double r, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double r, xtc_plugin_details *out)
|
||||||
{ (void)s; (void)r; (void)out; return XTC_ERR_UNSUPPORTED; }
|
{ (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)
|
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; }
|
{ (void)p; (void)i; (void)r; (void)s; return XTC_ERR_UNSUPPORTED; }
|
||||||
|
|
@ -266,3 +282,33 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *i, float *o, size_t
|
||||||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *i, float *o, size_t f)
|
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; }
|
{ (void)p; (void)i; (void)o; (void)f; return XTC_ERR_UNSUPPORTED; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||||
|
{
|
||||||
|
xtc_plugin_details details;
|
||||||
|
xtc_result r;
|
||||||
|
if (!out) return XTC_ERR_INVALID_ARGUMENT;
|
||||||
|
r = xtc_plugin_describe(s,rate,&details);
|
||||||
|
if (r == XTC_OK) memcpy(out,details.parameters,sizeof(details.parameters));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
xtc_result xtc_plugin_reset(xtc_plugin_spec *s, double rate, unsigned int port)
|
||||||
|
{
|
||||||
|
xtc_plugin_details details;
|
||||||
|
xtc_plugin_spec candidate;
|
||||||
|
size_t i, count = 0;
|
||||||
|
int found = port == UINT32_MAX;
|
||||||
|
xtc_result r = xtc_plugin_describe(s,rate,&details);
|
||||||
|
if (r != XTC_OK) return r;
|
||||||
|
candidate = *s;
|
||||||
|
for (i = 0; i < s->control_count; ++i) if (port == UINT32_MAX || s->ports[i] == port) {
|
||||||
|
candidate.values[i] = details.parameters[i].default_value;
|
||||||
|
found = 1;
|
||||||
|
}
|
||||||
|
if (!found) return XTC_ERR_NOT_FOUND;
|
||||||
|
for (i = 0; i < s->point_count; ++i)
|
||||||
|
if (port != UINT32_MAX && s->points[i].port != port) candidate.points[count++] = s->points[i];
|
||||||
|
candidate.point_count = (unsigned int)count;
|
||||||
|
*s = candidate;
|
||||||
|
return XTC_OK;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include "../../src/gui/sequencer_widget.hpp"
|
#include "../../src/gui/sequencer_widget.hpp"
|
||||||
#include "../../src/gui/piano_roll.hpp"
|
#include "../../src/gui/piano_roll.hpp"
|
||||||
#include "../../src/gui/mixer_widget.hpp"
|
#include "../../src/gui/mixer_widget.hpp"
|
||||||
|
#include "../../src/gui/plugin_dialog.hpp"
|
||||||
|
#include <FL/Fl_Hold_Browser.H>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <FL/Fl.H>
|
#include <FL/Fl.H>
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
|
|
@ -635,6 +637,37 @@ int main(int argc, char **argv)
|
||||||
size_t id = SIZE_MAX;
|
size_t id = SIZE_MAX;
|
||||||
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
|
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
|
||||||
xtc_plugin_spec spec{};
|
xtc_plugin_spec spec{};
|
||||||
|
// Exercise the actual modal chooser/editor with copied metadata only.
|
||||||
|
Fl::add_timeout(.05,[](void *) {
|
||||||
|
auto *dialog = Fl::first_window();
|
||||||
|
CHECK(dialog && dialog->label() && std::strcmp(dialog->label(),"LADSPA descriptors") == 0);
|
||||||
|
if (!dialog) return;
|
||||||
|
for (int i = 0; i < dialog->children(); ++i)
|
||||||
|
if (auto *list = dynamic_cast<Fl_Hold_Browser *>(dialog->child(i))) {
|
||||||
|
CHECK(list->size() == 7); list->select(2);
|
||||||
|
}
|
||||||
|
auto *choose = find(dialog,"Choose"); CHECK(choose); if (choose) choose->do_callback();
|
||||||
|
});
|
||||||
|
unsigned int chosen = UINT32_MAX;
|
||||||
|
CHECK(xtc_choose_plugin(module,48000,chosen) && chosen == 1);
|
||||||
|
xtc_plugin_details details{}; xtc_plugin_spec edited{};
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
|
||||||
|
CHECK(xtc_plugin_describe(&spec,48000,&details) == XTC_OK);
|
||||||
|
spec.values[0] = .25f; spec.point_count = 1; spec.points[0] = {0,spec.ports[0],0,.5f};
|
||||||
|
Fl::add_timeout(.05,[](void *) {
|
||||||
|
auto *dialog = Fl::first_window(); CHECK(dialog);
|
||||||
|
CHECK(contains(dialog,"Test gain"));
|
||||||
|
auto *reset = find(dialog,"Reset defaults"); CHECK(reset); if (reset) reset->do_callback();
|
||||||
|
auto *apply = find(dialog,"Apply"); CHECK(apply); if (apply) apply->do_callback();
|
||||||
|
});
|
||||||
|
CHECK(xtc_edit_plugin(spec,details,edited) && edited.values[0] == 1 && !edited.point_count);
|
||||||
|
edited.values[0] = .75f;
|
||||||
|
Fl::add_timeout(.05,[](void *) {
|
||||||
|
auto *dialog = Fl::first_window(); auto *cancel = find(dialog,"Cancel");
|
||||||
|
CHECK(cancel); if (cancel) cancel->do_callback();
|
||||||
|
});
|
||||||
|
CHECK(!xtc_edit_plugin(spec,details,edited) && edited.values[0] == .75f);
|
||||||
|
|
||||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
|
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(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(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
||||||
|
|
@ -656,6 +689,33 @@ int main(int argc, char **argv)
|
||||||
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == 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);
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
||||||
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
|
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
|
||||||
|
CHECK(window.reset_plugin(id) == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1 && !spec.point_count);
|
||||||
|
CHECK(window.undo(false) == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f && spec.point_count == 1);
|
||||||
|
// Replace an effect while preserving node ID and wires; undo restores state.
|
||||||
|
auto *canvas = graph_canvas(&window);
|
||||||
|
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
|
||||||
|
const size_t source_id = canvas->selected_node();
|
||||||
|
CHECK(canvas->connect_ports({source_id,1,id,0}) == XTC_OK);
|
||||||
|
CHECK(window.replace_plugin(id,module,4) == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 4 && spec.values[0] == 2 && !spec.point_count);
|
||||||
|
before = sequence->project();
|
||||||
|
CHECK(window.replace_plugin(id,module,1) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
|
||||||
|
CHECK(window.replace_plugin(id,module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
|
||||||
|
CHECK(window.replace_plugin(id,"/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
||||||
|
CHECK(window.undo(false) == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 0 && spec.values[0] == .5f && spec.point_count == 1);
|
||||||
|
const xtc_connection_info wire{source_id,1,id,0}; size_t inserted = SIZE_MAX;
|
||||||
|
CHECK(window.insert_plugin(wire,module,0,&inserted) == XTC_OK && inserted != id);
|
||||||
|
xtc_routing_info routing{};
|
||||||
|
CHECK(canvas->routing(inserted,routing) && routing.inputs == 1 && routing.outputs == 1);
|
||||||
|
before = sequence->project();
|
||||||
|
CHECK(window.insert_plugin(wire,module,0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
||||||
|
CHECK(canvas->select_node(inserted) && canvas->delete_selected() == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_ERR_NOT_FOUND);
|
||||||
|
CHECK(window.undo(false) == XTC_OK);
|
||||||
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_OK);
|
||||||
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
|
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
|
||||||
CHECK(xtc_project_create(&missing) == XTC_OK);
|
CHECK(xtc_project_create(&missing) == XTC_OK);
|
||||||
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
|
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,58 @@ int main(int argc, char **argv)
|
||||||
assert(xtc_plugin_probe(argv[1],2,48000,&s) == XTC_ERR_UNSUPPORTED);
|
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_probe(argv[1],3,48000,&s) == XTC_OK);
|
||||||
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_INVALID_STATE && !p);
|
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);
|
{
|
||||||
|
xtc_plugin_details details;
|
||||||
|
xtc_plugin_spec saved;
|
||||||
|
assert(xtc_plugin_probe(argv[1],0,48000,&s) == XTC_OK);
|
||||||
|
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK);
|
||||||
|
assert(!strcmp(details.name,"Test gain") && !strcmp(details.maker,"xtc tests") &&
|
||||||
|
!strcmp(details.copyright,"MIT") && !details.parameters[0].unit[0]);
|
||||||
|
assert(details.parameters[0].minimum == 0 && details.parameters[0].maximum == 2);
|
||||||
|
assert(details.flags[0] == (XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE));
|
||||||
|
s.values[0] = .25f;
|
||||||
|
s.point_count = 1; s.points[0] = (xtc_plugin_point){0,s.ports[0],0,.5f};
|
||||||
|
saved = s;
|
||||||
|
assert(xtc_plugin_reset(&s,48000,63) == XTC_ERR_NOT_FOUND && !memcmp(&s,&saved,sizeof(s)));
|
||||||
|
assert(xtc_plugin_reset(&s,48000,s.ports[0]) == XTC_OK && s.values[0] == 1 && !s.point_count);
|
||||||
|
assert(xtc_plugin_probe(argv[1],4,48000,&s) == XTC_OK && s.values[0] == 2);
|
||||||
|
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK && (details.flags[0] & XTC_PLUGIN_INTEGER));
|
||||||
|
s.values[0] = .5f; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
|
||||||
|
assert(xtc_plugin_reset(&s,48000,UINT32_MAX) == XTC_OK && s.values[0] == 2);
|
||||||
|
assert(xtc_plugin_probe(argv[1],5,48000,&s) == XTC_OK && s.values[0] == 1);
|
||||||
|
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK && (details.flags[0] & XTC_PLUGIN_TOGGLED));
|
||||||
|
s.values[0] = .5f; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
|
||||||
|
assert(xtc_plugin_probe(argv[1],6,48000,&s) == XTC_OK);
|
||||||
|
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK &&
|
||||||
|
(details.flags[0] & XTC_PLUGIN_SAMPLE_RATE) && (details.flags[0] & XTC_PLUGIN_LOGARITHMIC));
|
||||||
|
assert(fabsf(details.parameters[0].minimum-48) < .001f && fabsf(details.parameters[0].maximum-4800) < .01f);
|
||||||
|
assert(xtc_plugin_probe(argv[1],7,48000,&s) == XTC_ERR_UNSUPPORTED);
|
||||||
|
assert(xtc_plugin_probe(argv[1],9,48000,&s) == XTC_ERR_NOT_FOUND);
|
||||||
|
// Noncommutative effects demonstrate that wires define DSP order,
|
||||||
|
// independent of node creation order; persisted graphs retain it.
|
||||||
|
{
|
||||||
|
xtc_graph *g = NULL; xtc_plan *plan = NULL;
|
||||||
|
xtc_plugin_spec gain, offset; size_t first, second, output;
|
||||||
|
float input[1] = {.5f}, out[1];
|
||||||
|
assert(xtc_plugin_probe(argv[1],0,48000,&gain) == XTC_OK);
|
||||||
|
assert(xtc_plugin_probe(argv[1],8,48000,&offset) == XTC_OK);
|
||||||
|
gain.values[0] = 2;
|
||||||
|
assert(xtc_graph_create(8,&g) == XTC_OK);
|
||||||
|
assert(xtc_graph_add_plugin(g,&gain,&first) == XTC_OK);
|
||||||
|
assert(xtc_graph_add_plugin(g,&offset,&second) == XTC_OK);
|
||||||
|
assert(xtc_graph_add_output(g,&output) == XTC_OK);
|
||||||
|
assert(xtc_graph_connect(g,first,second) == XTC_OK && xtc_graph_connect(g,second,output) == XTC_OK);
|
||||||
|
assert(xtc_graph_compile(g,48000,1,&plan) == XTC_OK);
|
||||||
|
audio = 1; assert(xtc_plan_process(plan,input,out,1) == XTC_OK && out[0] == 2); audio = 0;
|
||||||
|
xtc_plan_destroy(plan); plan = NULL;
|
||||||
|
// Swap identities at stable IDs: gain -> offset becomes offset -> gain.
|
||||||
|
assert(xtc_graph_replace_plugin(g,first,&offset) == XTC_OK);
|
||||||
|
assert(xtc_graph_replace_plugin(g,second,&gain) == XTC_OK);
|
||||||
|
assert(xtc_graph_compile(g,48000,1,&plan) == XTC_OK);
|
||||||
|
audio = 1; assert(xtc_plan_process(plan,input,out,1) == XTC_OK && out[0] == 3); audio = 0;
|
||||||
|
xtc_plan_destroy(plan); xtc_graph_destroy(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
puts("Plugin lifecycle, identity, layouts, processing and persisted graph checks passed.");
|
puts("Plugin lifecycle, identity, layouts, processing and persisted graph checks passed.");
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* MIT test fixture; not a redistributed third-party plugin. */
|
/* MIT test fixture; not a redistributed third-party plugin. */
|
||||||
#include <ladspa.h>
|
#include <ladspa.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <float.h>
|
||||||
typedef struct { float *in, *out, *gain; } instance;
|
typedef struct { float *in, *out, *gain; } instance;
|
||||||
static unsigned alive, runs;
|
static unsigned alive, runs;
|
||||||
unsigned xtc_fixture_alive(void) { return alive; }
|
unsigned xtc_fixture_alive(void) { return alive; }
|
||||||
|
|
@ -24,6 +25,8 @@ static void run(LADSPA_Handle h, unsigned long frames)
|
||||||
instance *p = h; unsigned long i; ++runs;
|
instance *p = h; unsigned long i; ++runs;
|
||||||
for (i = 0; i < frames; ++i) p->out[i] = p->in[i] * *p->gain;
|
for (i = 0; i < frames; ++i) p->out[i] = p->in[i] * *p->gain;
|
||||||
}
|
}
|
||||||
|
static void offset(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)
|
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; }
|
{ 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 void destroy(LADSPA_Handle h) { if (h) { --alive; free(h); } }
|
||||||
|
|
@ -37,15 +40,33 @@ static const LADSPA_PortDescriptor stereo_ports[] = {
|
||||||
static const char * const names[] = {"Input","Output","Gain","Right output"};
|
static const char * const names[] = {"Input","Output","Gain","Right output"};
|
||||||
static const LADSPA_PortRangeHint hints[4] = {{0,0,0},{0,0,0},
|
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}};
|
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1,0,2},{0,0,0}};
|
||||||
|
static const LADSPA_PortRangeHint discrete_hints[3] = {{0,0,0},{0,0,0},
|
||||||
|
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_INTEGER|LADSPA_HINT_DEFAULT_MIDDLE,0,3}};
|
||||||
|
static const LADSPA_PortRangeHint toggle_hints[3] = {{0,0,0},{0,0,0},
|
||||||
|
{LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1,0,0}};
|
||||||
|
static const LADSPA_PortRangeHint scaled_hints[3] = {{0,0,0},{0,0,0},
|
||||||
|
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_SAMPLE_RATE|LADSPA_HINT_LOGARITHMIC|LADSPA_HINT_DEFAULT_MIDDLE,.001f,.1f}};
|
||||||
|
static const LADSPA_PortRangeHint overflow_hints[3] = {{0,0,0},{0,0,0},
|
||||||
|
{LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_SAMPLE_RATE,0,FLT_MAX}};
|
||||||
static const LADSPA_Descriptor descriptors[] = {
|
static const LADSPA_Descriptor descriptors[] = {
|
||||||
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.PortCount=3,.PortDescriptors=ports,
|
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.Maker="xtc tests",.Copyright="MIT",.PortCount=3,.PortDescriptors=ports,
|
||||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
.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,
|
{.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},
|
.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,
|
{.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},
|
.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,
|
{.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}
|
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||||
|
{.UniqueID=900004,.Label="xtc_test_integer",.Name="Integer gain",.PortCount=3,.PortDescriptors=ports,
|
||||||
|
.PortNames=names,.PortRangeHints=discrete_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||||
|
{.UniqueID=900005,.Label="xtc_test_toggle",.Name="Toggle gain",.PortCount=3,.PortDescriptors=ports,
|
||||||
|
.PortNames=names,.PortRangeHints=toggle_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||||
|
{.UniqueID=900006,.Label="xtc_test_scaled",.Name="Scaled gain",.PortCount=3,.PortDescriptors=ports,
|
||||||
|
.PortNames=names,.PortRangeHints=scaled_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||||
|
{.UniqueID=900007,.Label="xtc_test_overflow",.Name="Overflow hint",.PortCount=3,.PortDescriptors=ports,
|
||||||
|
.PortNames=names,.PortRangeHints=overflow_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||||
|
{.UniqueID=900008,.Label="xtc_test_offset",.Name="Offset effect",.PortCount=3,.PortDescriptors=ports,
|
||||||
|
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=offset,.cleanup=destroy}
|
||||||
};
|
};
|
||||||
const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
|
const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
|
||||||
{ return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }
|
{ return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue