1
0
Fork 0
forked from hanez/xtc

Phase 25: improve LADSPA discovery, parameter editing and plugin lifecycle

This commit is contained in:
Johannes Findeisen 2026-10-27 16:24:21 +01:00
commit 7bff7b535d
18 changed files with 591 additions and 42 deletions

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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}};

View file

@ -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;

View file

@ -3,6 +3,7 @@
#include "sequencer_widget.hpp"
#include "piano_roll.hpp"
#include "mixer_widget.hpp"
#include "plugin_dialog.hpp"
#include <FL/Fl_Native_File_Chooser.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("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<Fl_Menu_Bar *>(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<unsigned int>(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);

View file

@ -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;

103
src/gui/plugin_dialog.cpp Normal file
View 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;
}

View 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

View file

@ -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;
}

View file

@ -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 <FL/Fl_Hold_Browser.H>
#include <unistd.h>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
@ -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<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(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);

View file

@ -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;

View file

@ -1,6 +1,7 @@
/* MIT test fixture; not a redistributed third-party plugin. */
#include <ladspa.h>
#include <stdlib.h>
#include <float.h>
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; }