diff --git a/Makefile b/Makefile index d4cb617..fd668e8 100644 --- a/Makefile +++ b/Makefile @@ -139,14 +139,14 @@ endif 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 +$(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 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 +$(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 test-gui-jack: $(BUILD_DIR)/test_gui_jack @@ -157,7 +157,7 @@ test-gui-jack: @exit 1 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 else test-gui-jack test-gui: @@ -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 +$(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 test-workflow: $(BUILD_DIR)/test_workflow @@ -407,3 +407,5 @@ test-buses: $(BUILD_DIR)/test_buses ./$(BUILD_DIR)/test_buses test: test-buses -include $(BUILD_DIR)/tests/unit/test_buses.d + +-include $(BUILD_DIR)/src/gui/plugin_dialog.d diff --git a/README.md b/README.md index 5d7054f..bf0125f 100644 --- a/README.md +++ b/README.md @@ -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 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. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index 4db333a..6c111b3 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -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 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. + +- 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. diff --git a/docs/PLUGIN_HOSTING.md b/docs/PLUGIN_HOSTING.md index 85dd2db..d7bef92 100644 --- a/docs/PLUGIN_HOSTING.md +++ b/docs/PLUGIN_HOSTING.md @@ -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 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 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. +4. Select its node and use **Edit → Selected plugin control**. The editor shows + name, maker, copyright, module path, descriptor index and unique ID, then + 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. 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 processors define the initial compatible set; other plugins remain unqualified. 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. diff --git a/docs/RECOVERY.md b/docs/RECOVERY.md index 2747f30..9da97cf 100644 --- a/docs/RECOVERY.md +++ b/docs/RECOVERY.md @@ -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 supersedes the earlier lack of a WAV recovery tool described above. See 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 07449aa..a47a312 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -625,10 +625,35 @@ Make instruments and effects practical to use, manage, and diagnose in real proj - Document plugin API versioning and compatibility expectations. ### Acceptance criteria -- 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. -- 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] Users can identify and manage loaded instruments and effects without editing configuration files. +- [x] Missing or incompatible plugins produce actionable diagnostics and do not silently corrupt projects. +- [x] Plugin state restoration is tested for supported plugin types. +- [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 diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index baece1b..b4e0f39 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -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 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. + +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. diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index a6b636c..3d7f56e 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -21,6 +21,28 @@ typedef struct { unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */ xtc_plugin_point points[XTC_PLUGIN_POINTS]; } 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; /* Control thread only. Probe explicitly enumerates a file by descriptor index; * NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading diff --git a/src/graph/graph.c b/src/graph/graph.c index e8d2aa2..5043a85 100644 --- a/src/graph/graph.c +++ b/src/graph/graph.c @@ -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; } +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_routing_info info[XTC_MAX_NODES] = {{0}}; diff --git a/src/gui/graph_widget.hpp b/src/gui/graph_widget.hpp index 036e327..b8d2af4 100644 --- a/src/gui/graph_widget.hpp +++ b/src/gui/graph_widget.hpp @@ -81,6 +81,7 @@ class GraphWidget : public Fl_Widget { */ xtc_result add_node(xtc_node_type type); 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 routing(size_t id, xtc_routing_info &info) const; diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index cf7f26a..443b166 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -3,6 +3,7 @@ #include "sequencer_widget.hpp" #include "piano_roll.hpp" #include "mixer_widget.hpp" +#include "plugin_dialog.hpp" #include #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("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/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 control", 0, plugin_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; 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); + 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 (r == XTC_OK && id) *id = added; 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) { 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); } -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_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. */ xtc_plan *check = nullptr; xtc_backend_status status{}; double rate = 48000; @@ -1440,30 +1491,52 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context) auto *menu = static_cast(widget); const char *label = menu->mvalue()->label(); 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; - 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; 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"; + unsigned int index = 0; + xtc_plugin_spec probe{}; + r = xtc_plugin_probe(chooser.filename(),0,rate,&probe); + if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) { + w->report_error("Cannot discover LADSPA module; check path, dependencies and ladspa_descriptor export",r); return; } - choices += "Enter descriptor index:"; - const char *answer = fl_input("%s","0",choices.c_str()); - if (!answer) return; - char *end = nullptr; errno = 0; unsigned long index = std::strtoul(answer,&end,10); - if (errno || !answer[0] || *end || index >= 1024) r = XTC_ERR_INVALID_ARGUMENT; - else r = w->add_plugin(chooser.filename(),static_cast(index)); + if (!xtc_choose_plugin(chooser.filename(),rate,index)) return; + if (replace) r = w->replace_plugin(selected,chooser.filename(),index); + else if (insert) r = w->insert_plugin(wire,chooser.filename(),index); + else r = w->add_plugin(chooser.filename(),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"; + 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]{}; double rate = 48000; xtc_backend_status status{}; 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); 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); diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index e64a3e0..68454ae 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -22,6 +22,9 @@ class MainWindow : public Fl_Double_Window { /* 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_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); /** * @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. */ 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); static void file_callback(Fl_Widget * widget, void *context); GraphWidget *graph_widget_ = nullptr; diff --git a/src/gui/plugin_dialog.cpp b/src/gui/plugin_dialog.cpp new file mode 100644 index 0000000..c6695ac --- /dev/null +++ b/src/gui/plugin_dialog.cpp @@ -0,0 +1,103 @@ +#include "plugin_dialog.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(data); d->accepted = true; d->hide(); } + static void cancel(Fl_Widget *, void *data) { static_cast(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 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(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 inputs{}; + for (unsigned int i = 0; i < spec.control_count; ++i) { + int y = 150 + static_cast(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 *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(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(value); + } + if (reset.used) candidate.point_count = 0; + edited = candidate; return true; +} diff --git a/src/gui/plugin_dialog.hpp b/src/gui/plugin_dialog.hpp new file mode 100644 index 0000000..dcc21f9 --- /dev/null +++ b/src/gui/plugin_dialog.hpp @@ -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 diff --git a/src/plugin/ladspa.c b/src/plugin/ladspa.c index d05fc5a..664a4e2 100644 --- a/src/plugin/ladspa.c +++ b/src/plugin/ladspa.c @@ -116,6 +116,9 @@ static xtc_result inspect(const LADSPA_Descriptor *d, double rate, xtc_plugin_sp 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_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) && 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; } 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}; - 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) return XTC_ERR_INVALID_ARGUMENT; 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"); 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; + 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; } @@ -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); } #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; } 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; } @@ -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) { (void)p; (void)i; (void)o; (void)f; return XTC_ERR_UNSUPPORTED; } #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; +} diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 3ca5933..7b7210d 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -3,6 +3,8 @@ #include "../../src/gui/sequencer_widget.hpp" #include "../../src/gui/piano_roll.hpp" #include "../../src/gui/mixer_widget.hpp" +#include "../../src/gui/plugin_dialog.hpp" +#include #include #include #include @@ -635,6 +637,37 @@ int main(int argc, char **argv) size_t id = SIZE_MAX; CHECK(window.add_plugin(module,0,&id) == XTC_OK); 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(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(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); @@ -656,6 +689,33 @@ int main(int argc, char **argv) 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(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; CHECK(xtc_project_create(&missing) == XTC_OK); CHECK(xtc_graph_create(8,&graph) == XTC_OK); diff --git a/tests/integration/test_plugins.c b/tests/integration/test_plugins.c index d2cae3e..b3fedd1 100644 --- a/tests/integration/test_plugins.c +++ b/tests/integration/test_plugins.c @@ -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],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); + { + 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."); return 0; diff --git a/tests/plugins/fixture.c b/tests/plugins/fixture.c index 2667b84..2087ed1 100644 --- a/tests/plugins/fixture.c +++ b/tests/plugins/fixture.c @@ -1,6 +1,7 @@ /* MIT test fixture; not a redistributed third-party plugin. */ #include #include +#include typedef struct { float *in, *out, *gain; } instance; static unsigned alive, runs; 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; 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) { 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); } } @@ -37,15 +40,33 @@ static const LADSPA_PortDescriptor stereo_ports[] = { 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_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[] = { - {.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}, {.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} + .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) { return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }