Phase 31: add CLAP, VST2 and VST3 mono effect hosting

This commit is contained in:
Johannes Findeisen 2026-10-27 17:34:46 +01:00
commit 9e0e4562d7
114 changed files with 10369 additions and 75 deletions

View file

@ -3,7 +3,7 @@
Store small, redistributable fixtures here. Document their origin and license.
Do not commit large recordings or assets with unclear licensing.
`project-v1.xtc` through `project-v10.xtc` are hand-authored xtc metadata fixtures
`project-v1.xtc` through `project-v11.xtc` are hand-authored xtc metadata fixtures
created for this repository, under the project license. They contain no audio
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
missing module to exercise metadata migration without loading code.

39
tests/fixtures/project-v11.xtc vendored Normal file
View file

@ -0,0 +1,39 @@
XTC_PROJECT 11
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3f000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END

View file

@ -711,7 +711,7 @@ int main(int argc, char **argv)
// 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);
CHECK(dialog && dialog->label() && std::strcmp(dialog->label(),"Plugin descriptors") == 0);
if (!dialog) return;
for (int i = 0; i < dialog->children(); ++i)
if (auto *list = dynamic_cast<Fl_Hold_Browser *>(dialog->child(i))) {
@ -803,6 +803,30 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-gui-plugin-test.xtc");
window.do_callback();
}
for (const char *variable : {"XTC_TEST_CLAP","XTC_TEST_VST2","XTC_TEST_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false); auto *sequence = timeline(&window);
size_t id = SIZE_MAX; xtc_plugin_spec spec{};
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
CHECK(spec.format != XTC_PLUGIN_LADSPA && spec.inputs == 1 && !spec.control_count);
CHECK(window.undo(false) == XTC_OK && window.undo(true) == XTC_OK);
Fl::add_timeout(.05,[](void *) {
auto *dialog = Fl::first_window(); CHECK(dialog);
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 == 0);
CHECK(window.save_project("/tmp/xtc-gui-modern-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK);
CHECK(window.open_project("/tmp/xtc-gui-modern-test.xtc") == XTC_OK);
xtc_plugin_spec loaded{};
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&loaded) == XTC_OK && loaded.format == spec.format);
const xtc_project *before = sequence->project();
CHECK(window.replace_plugin(id,"/absent/modern.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
unlink("/tmp/xtc-gui-modern-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

@ -44,6 +44,8 @@ def main():
assert not (binaries / 'xtc').exists()
assert (resources / 'licenses/xtc/LICENSE').read_bytes() == (source / 'LICENSE').read_bytes()
assert (docs / 'PROJECT_FORMAT.md').is_file()
assert (resources / 'licenses/xtc/CLAP-LICENSE').is_file()
assert (resources / 'licenses/xtc/VST3-C-API-LICENSE').is_file()
assert os.access(binaries / 'xtc-check', os.X_OK)
run([str(binaries / 'xtc-check')])
for filename in ('performance-baseline.json', 'project-performance-baseline.json', 'graph-performance-phase29.json'):
@ -52,7 +54,7 @@ def main():
run([str(binaries / 'xtc-check'), '--inspect', str(example)])
assert (resources / 'xtc/examples/README.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..10' in diagnose and 'JACK compiled:' in diagnose
assert 'Project formats: 1..11' in diagnose and 'JACK compiled:' in diagnose
# Replacing a file symlink must not overwrite an unrelated target.
protected = stage / 'unrelated-target'
protected.write_text('keep unrelated target\n')

View file

@ -0,0 +1,116 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_project.h"
#include "xtc/xtc_realtime.h"
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.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 s) { assert(!audio); return __real_calloc(n,s); }
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 mode) { assert(!audio); return __real_dlopen(p,mode); }
int __wrap_dlclose(void *p) { assert(!audio); return __real_dlclose(p); }
static void render(xtc_realtime *rt)
{
float in[31],left[31],right[31]; size_t i;
for (i = 0; i < 31; ++i) in[i] = i % 2 ? -.5f : .25f;
audio = 1;
assert(xtc_realtime_process_stereo(rt,in,NULL,NULL,NULL,NULL,left,right,31) == XTC_OK);
audio = 0;
for (i = 0; i < 31; ++i) assert(left[i] == in[i]*.5f && right[i] == left[i]);
xtc_plugin_idle();
}
static void check(const char *path, xtc_plugin_format format)
{
xtc_plugin_spec spec, actual, bad;
xtc_plugin_details details;
xtc_plugin *plugin = NULL, *other = NULL;
xtc_project *p = NULL, *loaded = NULL;
xtc_graph *graph = NULL;
xtc_realtime *rt = NULL;
size_t node, output, i;
char document[] = "/tmp/xtc-modern-XXXXXX";
int fd = mkstemp(document); assert(fd >= 0); close(fd);
assert(xtc_plugin_probe(path,0,48000,&spec) == XTC_OK && spec.format == format);
assert(xtc_plugin_probe_format(format,path,0,48000,&actual) == XTC_OK);
assert(!memcmp(&spec,&actual,sizeof(spec)));
assert(xtc_plugin_describe(&spec,48000,&details) == XTC_OK && details.name[0]);
assert(xtc_plugin_create(&spec,48000,31,&plugin) == XTC_OK);
assert(xtc_plugin_create(&spec,48000,31,&other) == XTC_OK);
{ float data[31]; for (i = 0; i < 31; ++i) data[i] = 1;
audio = 1; assert(xtc_plugin_process(plugin,data,data,31) == XTC_OK); audio = 0;
for (i = 0; i < 31; ++i) assert(data[i] == .5f);
audio = 1; assert(xtc_plugin_process(other,NULL,data,31) == XTC_OK); audio = 0;
for (i = 0; i < 31; ++i) assert(data[i] == 0);
assert(xtc_plugin_process(plugin,data,data,32) == XTC_ERR_INVALID_ARGUMENT); }
xtc_plugin_idle(); xtc_plugin_destroy(plugin); xtc_plugin_destroy(other); plugin = NULL;
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&loaded) == XTC_OK);
assert(xtc_graph_create(2,&graph) == XTC_OK);
assert(xtc_graph_add_plugin(graph,&spec,&node) == XTC_OK && xtc_graph_add_output(graph,&output) == XTC_OK);
assert(xtc_graph_connect(graph,node,output) == XTC_OK);
assert(xtc_project_set_graph(p,graph) == XTC_OK);
assert(xtc_project_save(p,document) == XTC_OK && xtc_project_load(loaded,document) == XTC_OK);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),node,&actual) == XTC_OK);
assert(!memcmp(&actual,&spec,sizeof(spec)));
assert(xtc_realtime_create(48000,31,&rt) == XTC_OK);
assert(xtc_realtime_submit(rt,xtc_project_graph(loaded)) == XTC_OK); render(rt);
for (i = 0; i < 50; ++i) {
xtc_realtime_collect(rt);
assert(xtc_realtime_submit(rt,graph) == XTC_OK); render(rt);
}
if (format == XTC_PLUGIN_VST3) {
bad = spec; strcpy(bad.identifier,"not-a-class-id");
assert(xtc_plugin_spec_validate(&bad) == XTC_ERR_INVALID_ARGUMENT);
}
bad = spec; bad.control_count = 1;
assert(xtc_plugin_spec_validate(&bad) == XTC_ERR_UNSUPPORTED);
bad = spec; ++bad.unique_id;
assert(xtc_graph_replace_plugin(graph,node,&bad) == XTC_OK);
xtc_realtime_collect(rt);
assert(xtc_realtime_submit(rt,graph) == XTC_ERR_UNSUPPORTED); render(rt);
bad = spec; strcpy(bad.path,"/missing/xtc-modern-plugin.so");
assert(xtc_graph_replace_plugin(graph,node,&bad) == XTC_OK && xtc_project_set_graph(p,graph) == XTC_OK);
assert(xtc_project_save(p,document) == XTC_OK && xtc_project_load(loaded,document) == XTC_OK);
assert(xtc_graph_get_plugin(xtc_project_graph(loaded),node,&actual) == XTC_OK && !strcmp(actual.path,bad.path));
assert(xtc_plugin_create(&actual,48000,31,&plugin) == XTC_ERR_NOT_FOUND && !plugin);
xtc_realtime_destroy(rt); xtc_graph_destroy(graph); xtc_project_destroy(p); xtc_project_destroy(loaded); unlink(document);
printf("%s identity, buffers, stereo graph, retirement, reload and missing dependency passed.\n",xtc_plugin_format_name(format));
}
int main(int argc,char **argv)
{
unsigned int i; xtc_plugin_spec spec; xtc_plugin *p = NULL; float data[1] = {1};
assert(argc == 4);
check(argv[1],XTC_PLUGIN_CLAP); check(argv[2],XTC_PLUGIN_VST2); check(argv[3],XTC_PLUGIN_VST3);
for (i = 1; i < 4; ++i) assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],i,48000,&spec) != XTC_OK);
assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],4,48000,&spec) == XTC_OK);
assert(xtc_plugin_create(&spec,48000,1,&p) == XTC_OK);
assert(xtc_plugin_process(p,data,data,1) == XTC_ERR_INVALID_STATE && data[0] == 0); xtc_plugin_destroy(p); p = NULL;
assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],5,48000,&spec) == XTC_OK);
assert(xtc_plugin_create(&spec,48000,1,&p) == XTC_OK);
assert(xtc_plugin_process(p,data,data,1) == XTC_ERR_INVALID_STATE && data[0] == 0); xtc_plugin_destroy(p);
assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],6,48000,&spec) == XTC_OK);
p = NULL; data[0] = 1;
assert(xtc_plugin_create(&spec,48000,1,&p) == XTC_OK);
assert(xtc_plugin_process(p,data,data,1) == XTC_OK && data[0] == .5f);
xtc_plugin_idle();
assert(xtc_plugin_process(p,data,data,1) == XTC_ERR_INVALID_STATE && data[0] == 0); xtc_plugin_destroy(p);
assert(xtc_plugin_probe_format(XTC_PLUGIN_CLAP,argv[1],7,48000,&spec) == XTC_ERR_NOT_FOUND);
assert(setenv("XTC_VST2_FIXTURE_LAYOUT","1",1) == 0);
assert(xtc_plugin_probe_format(XTC_PLUGIN_VST2,argv[2],0,48000,&spec) == XTC_ERR_UNSUPPORTED);
unsetenv("XTC_VST2_FIXTURE_LAYOUT");
assert(setenv("XTC_VST3_FIXTURE_INIT","1",1) == 0);
assert(xtc_plugin_probe_format(XTC_PLUGIN_VST3,argv[3],0,48000,&spec) == XTC_ERR_INVALID_STATE);
unsetenv("XTC_VST3_FIXTURE_INIT");
puts("Modern plugin partial initialization/start/process failures passed."); return 0;
}

View file

@ -19,7 +19,7 @@ int main(void)
assert(mkdtemp(root));
assert(xtc_project_create(&p) == XTC_OK);
snprintf(file,sizeof(file),"%s/migration.xtc",root);
for (version = 1; version <= 10; ++version) {
for (version = 1; version <= 11; ++version) {
xtc_track_info t; xtc_clip_info c; size_t notes, points;
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
assert(xtc_project_load(p,old) == XTC_OK);
@ -89,6 +89,6 @@ int main(void)
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
xtc_project_destroy(p); unlink(file); rmdir(moved);
puts("v1-v10 migration, current folder relocation, cwd independence and missing-media preservation passed.");
puts("v1-v11 migration, current folder relocation, cwd independence and missing-media preservation passed.");
return 0;
}

View file

@ -36,8 +36,37 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
# Inspection succeeds for readable files without trying dlopen or decoding.
module.write_text("Deliberately not an executable module")
check("--inspect", project, 0, "no plugin code executed")
# Current metadata-only inspection must not execute any modern plugin ABI.
maps = base[base.index("CONTROL_MAPS"):]
for number, name, identity in ((1, "CLAP", "org.xtc.fixture"), (2, "VST2", ""),
(3, "VST3", "0123456789abcdef0123456789abcdef")):
lines = []
for line in plugin.splitlines():
if line.startswith(("CONTROL ", "PARAM ")):
continue
if line.startswith("XTC_PROJECT "):
line = "XTC_PROJECT 11"
elif line.startswith("PLUGIN "):
fields = line.split()
fields[4] = "0"
line = " ".join(fields)
elif line.startswith("PLUGIN_AUTOMATION "):
line = "PLUGIN_AUTOMATION 0"
elif line == "END":
line = maps.rstrip()
lines.append(line)
if line.startswith("MODULE "):
lines.append(f"FORMAT {number} {identity.encode().hex() if identity else '-'}")
modern = "\n".join(lines) + "\n"
project.write_text(modern)
check("--inspect", project, 0, name + " node 0", "Project format 11")
project.write_text(modern.replace(f"FORMAT {number} ", "FORMAT 99 "))
check("--validate", project, 1, "Unsupported")
if number == 3:
project.write_text(modern.replace(identity.encode().hex(), b"bad-class-id".hex()))
check("--validate", project, 1)
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..10")),
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..11")),
("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),

View file

@ -0,0 +1,78 @@
#include <clap/clap.h>
#include <clap/ext/thread-check.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
static int loaded, count;
static const char *features[] = {CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,NULL};
static const clap_plugin_descriptor_t descriptors[] = {
{CLAP_VERSION,"xtc.fixture.gain","CLAP gain","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.stereo","Stereo","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.init","Init failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.activate","Activate failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.start","Start failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.process","Process failure","xtc","","","","1","",features},
{CLAP_VERSION,"xtc.fixture.restart","Restart request","xtc","","","","1","",features}
};
typedef struct { clap_plugin_t plugin; const clap_host_t *host; unsigned int index; int active, started; } instance;
static bool init(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(loaded); return p->index != 2; }
static void destroy(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(!p->active && !p->started); --count; free(p); }
static bool activate(const clap_plugin_t *plugin, double rate, uint32_t min, uint32_t max)
{ instance *p = plugin->plugin_data; assert(rate > 0 && min && max >= min && !p->active); if (p->index == 3) return false; p->active = 1; return true; }
static void deactivate(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->active && !p->started); p->active = 0; }
static bool start(const clap_plugin_t *plugin)
{
instance *p = plugin->plugin_data;
const clap_host_thread_check_t *check = p->host->get_extension(p->host,CLAP_EXT_THREAD_CHECK);
assert(p->active && !p->started && check->is_audio_thread(p->host));
if (p->index == 4) return false;
p->started = 1; return true;
}
static void stop(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->started); p->started = 0; }
static void reset(const clap_plugin_t *plugin) { (void)plugin; }
static clap_process_status process(const clap_plugin_t *plugin, const clap_process_t *block)
{
instance *p = plugin->plugin_data; uint32_t i;
assert(p->started && block->audio_inputs_count == 1 && block->audio_outputs_count == 1 && block->in_events->size(block->in_events) == 0);
if (p->index == 5) return CLAP_PROCESS_ERROR;
for (i = 0; i < block->frames_count; ++i) block->audio_outputs[0].data32[0][i] = block->audio_inputs[0].data32[0][i]*.5f;
p->host->request_callback(p->host);
if (p->index == 6) p->host->request_restart(p->host);
return CLAP_PROCESS_CONTINUE;
}
static uint32_t port_count(const clap_plugin_t *plugin, bool input) { (void)plugin; (void)input; return 1; }
static bool port_get(const clap_plugin_t *plugin, uint32_t index, bool input, clap_audio_port_info_t *info)
{ instance *p = plugin->plugin_data; (void)input; if (index) return false; memset(info,0,sizeof(*info)); info->channel_count = p->index == 1 ? 2 : 1; return true; }
static uint32_t latency(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; assert(p->active); return 0; }
static const void *extension(const clap_plugin_t *plugin, const char *id)
{
static const clap_plugin_audio_ports_t ports = {port_count,port_get};
static const clap_plugin_latency_t delay = {latency}; (void)plugin;
if (!strcmp(id,CLAP_EXT_AUDIO_PORTS)) return &ports;
return !strcmp(id,CLAP_EXT_LATENCY) ? &delay : NULL;
}
static void on_main(const clap_plugin_t *plugin)
{ instance *p = plugin->plugin_data; const clap_host_thread_check_t *check = p->host->get_extension(p->host,CLAP_EXT_THREAD_CHECK); assert(check->is_main_thread(p->host)); }
static uint32_t plugin_count(const clap_plugin_factory_t *factory) { (void)factory; return 7; }
static const clap_plugin_descriptor_t *descriptor(const clap_plugin_factory_t *factory, uint32_t index)
{ (void)factory; return index < 7 ? descriptors+index : NULL; }
static const clap_plugin_t *create(const clap_plugin_factory_t *factory, const clap_host_t *host, const char *id)
{
unsigned int i; instance *p; (void)factory;
for (i = 0; i < 7 && strcmp(id,descriptors[i].id); ++i) {}
if (i == 7) return NULL;
p = calloc(1,sizeof(*p)); if (!p) return NULL;
p->host = host; p->index = i;
p->plugin = (clap_plugin_t){descriptors+i,p,init,destroy,activate,deactivate,start,stop,reset,process,extension,on_main};
++count; return &p->plugin;
}
static bool entry_init(const char *path) { (void)path; assert(!loaded); loaded = 1; return true; }
static void entry_exit(void) { assert(loaded && !count); loaded = 0; }
static const void *factory(const char *id)
{ static const clap_plugin_factory_t f = {plugin_count,descriptor,create}; return !strcmp(id,CLAP_PLUGIN_FACTORY_ID) ? &f : NULL; }
CLAP_EXPORT const clap_plugin_entry_t clap_entry = {CLAP_VERSION,entry_init,entry_exit,factory};

View file

@ -0,0 +1,34 @@
/* Original MIT ABI fixture; not built from or linked to a proprietary SDK. */
#include "../../src/plugin/vst2_abi.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
typedef struct { xtc_v2_effect effect; int open, active, started; } instance;
static intptr_t dispatch(xtc_v2_effect *e, int32_t op, int32_t index, intptr_t value, void *ptr, float option)
{
instance *p = (instance *)e; (void)index; (void)option;
switch (op) {
case 0: assert(!p->open); p->open = 1; break;
case 1: assert(p->open && !p->active && !p->started); free(p); break;
case 12: p->active = value != 0; break;
case 45: strcpy(ptr,"VST2 gain"); return 1;
case 71: assert(p->active); p->started = 1; break;
case 72: p->started = 0; break;
default: break;
}
return 0;
}
static void process(xtc_v2_effect *e, float **input, float **output, int32_t frames)
{ instance *p = (instance *)e; int32_t i; assert(p->started); for (i = 0; i < frames; ++i) output[0][i] = input[0][i]*.5f; }
xtc_v2_effect *VSTPluginMain(xtc_v2_host host);
xtc_v2_effect *VSTPluginMain(xtc_v2_host host)
{
instance *p; assert(host(NULL,1,0,0,NULL,0) == 2400);
p = calloc(1,sizeof(*p)); if (!p) return NULL;
p->effect.magic = XTC_V2_MAGIC; p->effect.dispatch = dispatch;
p->effect.inputs = p->effect.outputs = 1;
if (getenv("XTC_VST2_FIXTURE_LAYOUT")) p->effect.inputs = 2;
p->effect.flags = 16;
p->effect.unique_id = 0x58544332; p->effect.replace = process;
return &p->effect;
}

View file

@ -0,0 +1,72 @@
#include <stdbool.h>
#include <vst3_c_api.h>
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
static int loaded, instances;
static const Steinberg_TUID cid = SMTG_INLINE_UID(0x58544333,0x12345678,0x12345678,0x12345678);
typedef struct { Steinberg_Vst_IComponent component; Steinberg_Vst_IAudioProcessor processor; unsigned int refs; int initialized, active, started; } instance;
static instance *from_processor(void *p) { return (instance *)((char *)p - offsetof(instance,processor)); }
static Steinberg_tresult cq(void *self, const Steinberg_TUID iid, void **out)
{
instance *p = self; *out = NULL;
if (!memcmp(iid,Steinberg_Vst_IAudioProcessor_iid,16)) *out = &p->processor;
else if (!memcmp(iid,Steinberg_Vst_IComponent_iid,16) || !memcmp(iid,Steinberg_FUnknown_iid,16)) *out = &p->component;
if (!*out) return -1;
++p->refs; return 0;
}
static Steinberg_uint32 add(void *self) { return ++((instance *)self)->refs; }
static Steinberg_uint32 release(void *self)
{ instance *p = self; unsigned int refs = --p->refs; if (!refs) { assert(!p->initialized); --instances; free(p); } return refs; }
static Steinberg_tresult pq(void *self, const Steinberg_TUID iid, void **out) { return cq(from_processor(self),iid,out); }
static Steinberg_uint32 pa(void *self) { return add(from_processor(self)); }
static Steinberg_uint32 pr(void *self) { return release(from_processor(self)); }
static Steinberg_tresult init(void *self, Steinberg_FUnknown *host)
{ instance *p = self; assert(loaded && host && !p->initialized); if (getenv("XTC_VST3_FIXTURE_INIT")) return -1; p->initialized = 1; return 0; }
static Steinberg_tresult terminate(void *self) { instance *p = self; assert(p->initialized && !p->active); p->initialized = 0; return 0; }
static Steinberg_tresult controller(void *self, Steinberg_TUID id) { (void)self; (void)id; return -1; }
static Steinberg_tresult mode(void *self, Steinberg_Vst_IoMode m) { (void)self; (void)m; return 0; }
static Steinberg_int32 buses(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir) { (void)self; (void)dir; return type == 0 ? 1 : 0; }
static Steinberg_tresult bus_info(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, struct Steinberg_Vst_BusInfo *info)
{ (void)self; (void)dir; assert(type == 0 && index == 0); memset(info,0,sizeof(*info)); info->channelCount = 1; return 0; }
static Steinberg_tresult routing(void *self, struct Steinberg_Vst_RoutingInfo *in, struct Steinberg_Vst_RoutingInfo *out)
{ (void)self; (void)in; (void)out; return -1; }
static Steinberg_tresult activate_bus(void *self, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_TBool state)
{ (void)self; (void)type; (void)dir; (void)index; (void)state; return 0; }
static Steinberg_tresult active(void *self, Steinberg_TBool state) { instance *p = self; assert(!p->started); p->active = state; return 0; }
static Steinberg_tresult state(void *self, Steinberg_IBStream *stream) { (void)self; (void)stream; return -1; }
static Steinberg_tresult arrangements(void *self, Steinberg_Vst_SpeakerArrangement *in, Steinberg_int32 ni, Steinberg_Vst_SpeakerArrangement *out, Steinberg_int32 no)
{ (void)self; return ni == 1 && no == 1 && in[0] == Steinberg_Vst_SpeakerArr_kMono && out[0] == in[0] ? 0 : -1; }
static Steinberg_tresult arrangement(void *self, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_Vst_SpeakerArrangement *out)
{ (void)self; (void)dir; (void)index; *out = Steinberg_Vst_SpeakerArr_kMono; return 0; }
static Steinberg_tresult sample_size(void *self, Steinberg_int32 size) { (void)self; return size == 0 ? 0 : -1; }
static Steinberg_uint32 latency(void *self) { (void)self; return 0; }
static Steinberg_tresult setup(void *self, struct Steinberg_Vst_ProcessSetup *data) { (void)self; return data->maxSamplesPerBlock > 0 && data->sampleRate > 0 ? 0 : -1; }
static Steinberg_tresult processing(void *self, Steinberg_TBool value) { instance *p = from_processor(self); assert(p->active); p->started = value; return 0; }
static Steinberg_tresult process(void *self, struct Steinberg_Vst_ProcessData *data)
{ instance *p = from_processor(self); int32_t i; assert(p->started); for (i = 0; i < data->numSamples; ++i) data->outputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i] = data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i]*.5f; return 0; }
static Steinberg_Vst_IComponentVtbl component_vtable = {cq,add,release,init,terminate,controller,mode,buses,bus_info,routing,activate_bus,active,state,state};
static Steinberg_Vst_IAudioProcessorVtbl processor_vtable = {pq,pa,pr,arrangements,arrangement,sample_size,latency,setup,processing,process,latency};
static Steinberg_tresult fq(void *self, const Steinberg_TUID iid, void **out) { (void)iid; *out = self; return 0; }
static Steinberg_uint32 fr(void *self) { (void)self; return 1; }
static Steinberg_tresult factory_info(void *self, struct Steinberg_PFactoryInfo *info) { (void)self; memset(info,0,sizeof(*info)); return 0; }
static Steinberg_int32 classes(void *self) { (void)self; return 1; }
static Steinberg_tresult class_info(void *self, Steinberg_int32 index, struct Steinberg_PClassInfo *info)
{ (void)self; if (index) return -1; memset(info,0,sizeof(*info)); memcpy(info->cid,cid,16); strcpy(info->category,"Audio Module Class"); strcpy(info->name,"VST3 gain"); return 0; }
static Steinberg_tresult create(void *self, Steinberg_FIDString id, Steinberg_FIDString iid, void **out)
{
instance *p; (void)self; *out = NULL;
if (memcmp(id,cid,16) || memcmp(iid,Steinberg_Vst_IComponent_iid,16)) return -1;
p = calloc(1,sizeof(*p)); if (!p) return -1;
p->component.lpVtbl = &component_vtable; p->processor.lpVtbl = &processor_vtable; p->refs = 1;
++instances; *out = p; return 0;
}
static Steinberg_IPluginFactoryVtbl factory_vtable = {fq,fr,fr,factory_info,classes,class_info,create};
static Steinberg_IPluginFactory factory = {&factory_vtable};
bool ModuleEntry(void *handle);
bool ModuleExit(void);
Steinberg_IPluginFactory *GetPluginFactory(void);
bool ModuleEntry(void *handle) { assert(handle && !loaded); loaded = 1; return true; }
bool ModuleExit(void) { assert(loaded && !instances); loaded = 0; return true; }
Steinberg_IPluginFactory *GetPluginFactory(void) { assert(loaded); return &factory; }

View file

@ -68,7 +68,7 @@ int main(void)
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
diagnostic.format_version == 10 && !diagnostic.line);
diagnostic.format_version == 11 && !diagnostic.line);
for (mode = 1; mode <= 5; ++mode) {
fault = mode;
assert(xtc_project_save(next,song) == XTC_ERR_IO);
@ -81,7 +81,7 @@ int main(void)
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..10"));
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..11"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));