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

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