1
0
Fork 0
forked from hanez/xtc

Phase 15: host trusted LADSPA processors with persisted graph state

This commit is contained in:
Johannes Findeisen 2026-10-27 04:00:27 +01:00
commit f72314b5a3
24 changed files with 1046 additions and 70 deletions

View file

@ -15,6 +15,7 @@
#include <cstring>
#include <cmath>
#include <initializer_list>
#include <cstdlib>
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
* timers run under Xvfb; no server/device is required or touched. */
@ -456,6 +457,45 @@ int main(int argc, char **argv)
window.do_callback();
}
CHECK(alive == 0U);
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);
auto *sequence = timeline(&window);
size_t id = SIZE_MAX;
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
xtc_plugin_spec spec{};
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
CHECK(window.set_plugin_control(id,spec.ports[0],.5f) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
CHECK(window.set_plugin_control(id,spec.ports[0],3) == XTC_ERR_UNSUPPORTED);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
const xtc_project *before = sequence->project();
CHECK(window.add_plugin("/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(window.add_plugin(module,2) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
CHECK(window.save_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK);
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
CHECK(xtc_project_create(&missing) == XTC_OK);
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
std::strcpy(spec.path,"/absent/xtc-test-plugin.so");
CHECK(xtc_graph_add_plugin(graph,&spec,&absent) == XTC_OK);
CHECK(xtc_project_set_graph(missing,graph) == XTC_OK);
CHECK(xtc_project_save(missing,"/tmp/xtc-gui-missing-plugin.xtc") == XTC_OK);
before = sequence->project();
CHECK(window.open_project("/tmp/xtc-gui-missing-plugin.xtc") == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(contains(&window,"/absent/xtc-test-plugin.so"));
CHECK(window.add_plugin(module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
xtc_project_destroy(missing); xtc_graph_destroy(graph);
unlink("/tmp/xtc-gui-missing-plugin.xtc");
unlink("/tmp/xtc-gui-plugin-test.xtc");
window.do_callback();
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -0,0 +1,146 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_project.h"
#include "xtc/xtc_realtime.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
static int audio;
void *__real_malloc(size_t);
void *__real_calloc(size_t,size_t);
void *__real_realloc(void *,size_t);
void __real_free(void *);
void *__real_dlopen(const char *,int);
int __real_dlclose(void *);
void *__wrap_malloc(size_t n) { assert(!audio); return __real_malloc(n); }
void *__wrap_calloc(size_t n,size_t z) { assert(!audio); return __real_calloc(n,z); }
void *__wrap_realloc(void *p,size_t n) { assert(!audio); return __real_realloc(p,n); }
void __wrap_free(void *p) { assert(!audio); __real_free(p); }
void *__wrap_dlopen(const char *p,int flags) { assert(!audio); return __real_dlopen(p,flags); }
int __wrap_dlclose(void *p) { assert(!audio); return __real_dlclose(p); }
static void graph_roundtrip(const xtc_plugin_spec *spec)
{
xtc_graph *g = NULL; xtc_plan *p = NULL, *again = NULL;
xtc_project *project = NULL, *loaded = NULL;
xtc_plugin_spec copy;
char path[] = "/tmp/xtc-plugin-project-XXXXXX";
float input[31], out[31], expected[31]; size_t i, plugin, output;
int fd = mkstemp(path); assert(fd >= 0); close(fd);
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,spec,&plugin) == XTC_OK);
assert(xtc_graph_add_output(g,&output) == XTC_OK);
assert(xtc_graph_connect(g,plugin,output) == XTC_OK);
assert(xtc_graph_compile(g,48000,31,&p) == XTC_OK);
for (i = 0; i < 31; ++i) input[i] = (float)i / 100;
audio = 1;
assert(xtc_plan_process(p,input,expected,31) == XTC_OK);
audio = 0;
{ int nonzero = 0; for (i = 0; i < 31; ++i) if (expected[i] != 0) nonzero = 1; assert(nonzero); }
assert(xtc_project_create(&project) == XTC_OK);
assert(xtc_project_set_graph(project,g) == XTC_OK);
assert(xtc_project_save(project,path) == XTC_OK);
assert(xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),plugin,&copy) == XTC_OK);
assert(!strcmp(copy.path,spec->path) && !strcmp(copy.label,spec->label));
assert(xtc_graph_compile(xtc_project_graph(loaded),48000,31,&again) == XTC_OK);
audio = 1;
assert(xtc_plan_process(again,input,out,31) == XTC_OK);
audio = 0;
for (i = 0; i < 31; ++i) assert(out[i] == expected[i] && isfinite(out[i]));
xtc_plan_destroy(again); again = NULL;
if (spec->control_count) {
char malformed[] = "/tmp/xtc-plugin-malformed-XXXXXX", record[1536];
int descriptor = mkstemp(malformed);
FILE *source = fopen(path,"r"), *dest = fdopen(descriptor,"w");
assert(descriptor >= 0 && source && dest);
while (fgets(record,sizeof(record),source)) {
if (!strncmp(record,"CONTROL ",8)) fputs("CONTROL 64 3f800000\n",dest);
else fputs(record,dest);
}
assert(!ferror(source)); fclose(source); assert(fclose(dest) == 0);
assert(xtc_project_load(loaded,malformed) == XTC_ERR_FORMAT);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),plugin,&copy) == XTC_OK);
assert(!strcmp(copy.path,spec->path));
unlink(malformed);
}
/* Metadata-only loading/saving preserves absent modules. */
strcpy(copy.path,"/nonexistent/xtc-preserved-plugin.so");
xtc_graph_destroy(g); g = NULL;
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,&copy,&plugin) == XTC_OK);
assert(xtc_project_set_graph(project,g) == XTC_OK);
assert(xtc_project_save(project,path) == XTC_OK);
xtc_project_destroy(loaded); loaded = NULL;
assert(xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_load(loaded,path) == XTC_OK);
assert(xtc_graph_compile(xtc_project_graph(loaded),48000,31,&again) == XTC_ERR_NOT_FOUND && !again);
assert(xtc_project_save(loaded,path) == XTC_OK);
xtc_graph_destroy(g); xtc_project_destroy(project); xtc_project_destroy(loaded);
xtc_plan_destroy(p); unlink(path);
/* Repeated publication, audio adoption and control retirement. */
{
xtc_realtime *rt = NULL;
assert(xtc_graph_create(8,&g) == XTC_OK);
assert(xtc_graph_add_plugin(g,spec,&plugin) == XTC_OK);
assert(xtc_realtime_create(48000,31,&rt) == XTC_OK);
for (i = 0; i < 100; ++i) {
assert(xtc_realtime_submit(rt,g) == XTC_OK);
audio = 1;
assert(xtc_realtime_process(rt,input,out,31) == XTC_OK);
audio = 0;
xtc_realtime_collect(rt);
}
xtc_realtime_destroy(rt); xtc_graph_destroy(g);
}
}
int main(int argc, char **argv)
{
xtc_plugin_spec s, bad; xtc_plugin *p = NULL; float data[17]; size_t i;
assert(argc == 2 || argc == 3);
assert(xtc_plugin_probe(argv[1],argc == 3 && strcmp(argv[2],"fixture") ? (unsigned int)strtoul(argv[2],NULL,10) : 0,48000,&s) == XTC_OK);
printf("Testing %s (%u controls, %u inputs)\n",s.label,s.control_count,s.inputs);
graph_roundtrip(&s);
{
xtc_parameter_info info[XTC_PLUGIN_CONTROLS];
assert(xtc_plugin_parameters(&s,48000,info) == XTC_OK);
for (i = 0; i < s.control_count; ++i) assert(info[i].id == s.ports[i] && info[i].name[0]);
}
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_OK);
for (i = 0; i < 17; ++i) data[i] = .5f;
audio = 1;
assert(xtc_plugin_process(p,data,data,17) == XTC_OK);
audio = 0;
for (i = 0; i < 17; ++i) assert(isfinite(data[i]));
xtc_plugin_destroy(p); p = NULL;
bad = s; ++bad.unique_id;
assert(xtc_plugin_create(&bad,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
bad = s; if (bad.control_count) {
bad.values[0] = NAN; assert(xtc_plugin_create(&bad,48000,17,&p) == XTC_ERR_INVALID_ARGUMENT);
}
if (argc == 3 && !strcmp(argv[2],"fixture")) {
assert(s.control_count == 1 && s.values[0] == 1);
s.values[0] = 2;
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_OK);
for (i = 0; i < 17; ++i) data[i] = .5f;
audio = 1;
assert(xtc_plugin_process(p,data,data,17) == XTC_OK);
audio = 0;
for (i = 0; i < 17; ++i) assert(data[i] == 1);
xtc_plugin_destroy(p); p = NULL;
graph_roundtrip(&s);
s.values[0] = 3; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
assert(xtc_plugin_probe(argv[1],1,48000,&s) == XTC_OK && !s.inputs);
graph_roundtrip(&s);
assert(xtc_plugin_probe(argv[1],2,48000,&s) == XTC_ERR_UNSUPPORTED);
assert(xtc_plugin_probe(argv[1],3,48000,&s) == XTC_OK);
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_INVALID_STATE && !p);
assert(xtc_plugin_probe(argv[1],4,48000,&s) == XTC_ERR_NOT_FOUND);
}
puts("Plugin lifecycle, identity, layouts, processing and persisted graph checks passed.");
return 0;
}

View file

@ -40,7 +40,7 @@ def main():
make = shlex.split(env.get('MAKE', 'make'))
with log.open('w') as output:
try:
subprocess.run(make + ['release'], cwd=project, env=env, stdout=output,
subprocess.run(make + ['BUILD_DIR=build', 'release'], cwd=project, env=env, stdout=output,
stderr=subprocess.STDOUT, check=True)
except subprocess.CalledProcessError:
print(log.read_text()[-12000:])

51
tests/plugins/fixture.c Normal file
View file

@ -0,0 +1,51 @@
/* MIT test fixture; not a redistributed third-party plugin. */
#include <ladspa.h>
#include <stdlib.h>
typedef struct { float *in, *out, *gain; } instance;
static unsigned alive, runs;
unsigned xtc_fixture_alive(void) { return alive; }
unsigned xtc_fixture_runs(void) { return runs; }
static LADSPA_Handle create(const LADSPA_Descriptor *d, unsigned long rate)
{
instance *p;
(void)rate;
if (d->UniqueID == 900003) return NULL;
p = calloc(1,sizeof(*p)); if (p) ++alive; return p;
}
static void connect(LADSPA_Handle h, unsigned long port, LADSPA_Data *data)
{
instance *p = h;
if (port == 0) p->in = data;
else if (port == 1) p->out = data;
else p->gain = data;
}
static void run(LADSPA_Handle h, unsigned long frames)
{
instance *p = h; unsigned long i; ++runs;
for (i = 0; i < frames; ++i) p->out[i] = p->in[i] * *p->gain;
}
static void source(LADSPA_Handle h, unsigned long frames)
{ instance *p = h; unsigned long i; ++runs; for (i = 0; i < frames; ++i) p->in[i] = .25f; }
static void destroy(LADSPA_Handle h) { if (h) { --alive; free(h); } }
static const LADSPA_PortDescriptor ports[] = {
LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO,
LADSPA_PORT_INPUT|LADSPA_PORT_CONTROL};
static const LADSPA_PortDescriptor source_ports[] = {LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO};
static const LADSPA_PortDescriptor stereo_ports[] = {
LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_INPUT|LADSPA_PORT_AUDIO,
LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO, LADSPA_PORT_OUTPUT|LADSPA_PORT_AUDIO};
static const char * const names[] = {"Input","Output","Gain","Right output"};
static const LADSPA_PortRangeHint hints[4] = {{0,0,0},{0,0,0},
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1,0,2},{0,0,0}};
static const LADSPA_Descriptor descriptors[] = {
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.PortCount=3,.PortDescriptors=ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
{.UniqueID=900001,.Label="xtc_test_source",.Name="Test source",.PortCount=1,.PortDescriptors=source_ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=source,.cleanup=destroy},
{.UniqueID=900002,.Label="xtc_test_stereo",.Name="Unsupported stereo",.PortCount=4,.PortDescriptors=stereo_ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
{.UniqueID=900003,.Label="xtc_test_fail",.Name="Initialization failure",.PortCount=3,.PortDescriptors=ports,
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy}
};
const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
{ return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }