From 355cc690fbb9ae3a346790c3b33513bb043b9ae7 Mon Sep 17 00:00:00 2001 From: Stefano D'Angelo Date: Fri, 17 Jan 2025 17:13:26 +0100 Subject: [PATCH] lv2 mailto and better uris (no need for ui uri) + expertimental lv2-next --- templates/lv2-next/README.md | 1 + templates/lv2-next/data/manifest.ttl.in | 95 +++++ templates/lv2-next/src/data.h | 78 ++++ templates/lv2-next/src/lv2.c | 495 ++++++++++++++++++++++++ templates/lv2-next/tibia-index.js | 136 +++++++ templates/lv2/data/manifest.ttl.in | 10 +- templates/lv2/src/data.h | 4 +- templates/lv2/tibia-index.js | 9 +- test/lv2.json | 5 +- 9 files changed, 821 insertions(+), 12 deletions(-) create mode 100644 templates/lv2-next/README.md create mode 100644 templates/lv2-next/data/manifest.ttl.in create mode 100644 templates/lv2-next/src/data.h create mode 100644 templates/lv2-next/src/lv2.c create mode 100644 templates/lv2-next/tibia-index.js diff --git a/templates/lv2-next/README.md b/templates/lv2-next/README.md new file mode 100644 index 0000000..e8c5011 --- /dev/null +++ b/templates/lv2-next/README.md @@ -0,0 +1 @@ +This is experimental and broken, do not use this. diff --git a/templates/lv2-next/data/manifest.ttl.in b/templates/lv2-next/data/manifest.ttl.in new file mode 100644 index 0000000..a9998c8 --- /dev/null +++ b/templates/lv2-next/data/manifest.ttl.in @@ -0,0 +1,95 @@ +{{~it.tibia.lv2.prefixes :p}} +@prefix {{=p.id}}: <{{=p.uri}}> . +{{~}} + +{{~it.product.parameters :p}} +plugin:{{=p.id}} + a lv2:Parameter ; + rdfs:label "{{=p.name}}" ; + lv2:name "{{=p.name}}" ; +{{?"shortName" in p}} + lv2:shortName "{{=p.shortName.substring(0, 16)}}" ; +{{?}} + lv2:symbol "{{=p.id}}" ; + rdfs:range atom:Float . +{{~}} + +{{=it.tibia.lv2.ttlURI(it.lv2.uri)}} + a lv2:Plugin ; +{{~it.lv2.types :t}} + a {{=it.tibia.lv2.ttlURI(t)}} ; +{{~}} +{{?it.lv2.project}} + lv2:project {{=it.tibia.lv2.ttlURI(it.lv2.project)}} ; +{{?}} + lv2:binary <{{=it.product.bundleName}}@DLL_SUFFIX@> ; + doap:name "{{=it.product.name}}" ; + doap:maintainer [ + a foaf:Organization ; + foaf:name "{{=it.company.name}}" ; + foaf:mbox ; + rdfs:seeAlso {{=it.tibia.lv2.ttlURI(it.company.url)}} + ] ; + lv2:minorVersion {{=/^([0-9]+)\./.exec(it.lv2.version)[1]}} ; + lv2:microVersion {{=/^[0-9]+\.([0-9]+)/.exec(it.lv2.version)[1]}} ; +{{?it.tibia.lv2.ports.find(p => p.type == "midi" || p.type == "param")}} + lv2:requiredFeature urid:map ; + lv2:optionalFeature log:log ; +{{?}} +{{?it.product.parameters.find(p => p.direction == "input")}} + patch:writable {{=it.product.parameters.filter(p => p.direction == "input").map(p => "plugin:" + p.id).join(" , ")}} ; +{{?}} +{{?it.product.parameters.find(p => p.direction == "output")}} + patch:readable {{=it.product.parameters.filter(p => p.direction == "output").map(p => "plugin:" + p.id).join(" , ")}} ; +{{?}} + lv2:optionalFeature lv2:hardRTCapable ; +{{?it.product.ui}} + ui:ui plugin:ui ; +{{?}} + lv2:port [ +{{~it.tibia.lv2.ports :p:i}} + a {{?p.type == "control"}}lv2:ControlPort{{??(p.type == "midi" || p.type == "param")}}atom:AtomPort{{??}}{{?p.cv}}lv2:CVPort{{??}}lv2:AudioPort{{?}}{{?}} , + {{?p.direction == "input"}}lv2:InputPort{{??}}lv2:OutputPort{{?}} ; + lv2:name "{{=p.name}}" ; +{{?"shortName" in p}} + lv2:shortName "{{=p.shortName.substring(0, 16)}}" ; +{{?}} + lv2:symbol "{{=p.id}}" ; +{{?p.type == "param"}} + atom:bufferType atom:Sequence ; + atom:supports atom:Object ; + atom:supports patch:Message ; +{{??p.type == "midi"}} + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; +{{?}} +{{?p.sidechain}} + lv2:portProperty lv2:isSideChain ; +{{?}} +{{?p.control}} + lv2:designation lv2:control ; +{{?}} +{{?p.optional}} + lv2:portProperty lv2:connectionOptional ; +{{?}} + lv2:index {{=i}} +{{?i < it.tibia.lv2.ports.length - 1}} + ] , [ +{{??}} + ] . +{{?}} +{{~}} + +{{?it.product.ui}} +plugin:ui + a ui:@UI_TYPE@ ; + ui:binary <{{=it.product.bundleName}}@DLL_SUFFIX@> ; +{{?!it.product.ui.userResizable}} + lv2:optionalFeature ui:noUserResize ; # doesn't work as lv2:requiredFeature, don't ask me why +{{?!it.product.ui.selfResizable}} + lv2:optionalFeature ui:fixedSize ; +{{?}} +{{?}} + lv2:requiredFeature ui:idleInterface ; + lv2:extensionData ui:idleInterface . +{{?}} diff --git a/templates/lv2-next/src/data.h b/templates/lv2-next/src/data.h new file mode 100644 index 0000000..cf5e8b5 --- /dev/null +++ b/templates/lv2-next/src/data.h @@ -0,0 +1,78 @@ +/* + * Tibia + * + * Copyright (C) 2024 Orastron Srl unipersonale + * + * Tibia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * Tibia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tibia. If not, see . + * + * File author: Stefano D'Angelo + */ + +#define DATA_LV2_URI "{{=it.tibia.CGetUTF8StringLiteral(it.tibia.lv2.expandURI(it.lv2.uri))}}" + +#define DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "input").reduce((s, x) => s += x.channels == "mono" ? 1 : 2, 0)}} +#define DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "output").reduce((s, x) => s += x.channels == "mono" ? 1 : 2, 0)}} +#define DATA_PRODUCT_MIDI_INPUTS_N {{=it.product.buses.filter(x => x.type == "midi" && x.direction == "input").length}} +#define DATA_PRODUCT_MIDI_OUTPUTS_N {{=it.product.buses.filter(x => x.type == "midi" && x.direction == "output").length}} +#define DATA_PRODUCT_CONTROL_INPUTS_N {{=it.product.parameters.filter(x => x.direction == "input").length}} +#define DATA_PRODUCT_CONTROL_OUTPUTS_N {{=it.product.parameters.filter(x => x.direction == "output").length}} + +#if DATA_PRODUCT_MIDI_INPUTS_N > 0 +static uint32_t midi_in_index[DATA_PRODUCT_MIDI_INPUTS_N] = { + {{~it.tibia.lv2.ports.filter(x => x.type == "midi" && x.direction == "input") :p}}{{=p.busIndex}}, {{~}} +}; +#endif + +#if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + +# define DATA_PARAM_BYPASS 1 +# define DATA_PARAM_TOGGLED (1<<1) +# define DATA_PARAM_INTEGER (1<<2) + +static struct { + uint32_t index; + float min; + float max; + float def; + uint32_t flags; +} param_data[DATA_PRODUCT_CONTROL_INPUTS_N] = { + {{~it.tibia.lv2.ports.filter(x => x.type == "control" && x.direction == "input") :p}} + { + /* .index = */ {{=p.paramIndex}}, + /* .min = */ {{=p.minimum.toExponential()}}f, + /* .max = */ {{=p.maximum.toExponential()}}f, + /* .def = */ {{=p.defaultValue.toExponential()}}f, + /* .flags = */ {{?p.isBypass}}DATA_PARAM_BYPASS{{??p.isLatency}}DATA_PARAM_INTEGER{{??}}0{{?p.toggled}} | DATA_PARAM_TOGGLED{{?}}{{?p.integer}} | DATA_PARAM_INTEGER{{?}}{{?}} + }, + {{~}} +}; + +#endif + +#if DATA_PRODUCT_CONTROL_OUTPUTS_N > 0 +static uint32_t param_out_index[DATA_PRODUCT_CONTROL_OUTPUTS_N] = { + {{~it.tibia.lv2.ports.filter(x => x.type == "control" && x.direction == "output") :p}}{{=p.paramIndex}}, {{~}} +}; +#endif + +{{?it.lv2.ui}} +#define DATA_UI +#define DATA_LV2_UI_URI "{{=it.tibia.CGetUTF8StringLiteral(it.tibia.lv2.expandURI(it.lv2.ui.uri))}}" +#define DATA_UI_USER_RESIZABLE {{=it.product.ui.userResizable ? 1 : 0}} + +#if DATA_PRODUCT_CONTROL_INPUTS_N > 0 +static uint32_t index_to_param[DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N] = { + {{~it.tibia.lv2.ports.filter(x => x.type == "control").map((e, i) => ({ i: i, pi: e.paramIndex })).sort((a, b) => a.pi - b.pi) :p}}{{=p.i + it.tibia.lv2.ports.filter(x => x.type != "control").length}}, {{~}} +}; +#endif +{{?}} diff --git a/templates/lv2-next/src/lv2.c b/templates/lv2-next/src/lv2.c new file mode 100644 index 0000000..39fd934 --- /dev/null +++ b/templates/lv2-next/src/lv2.c @@ -0,0 +1,495 @@ +/* + * Tibia + * + * Copyright (C) 2024 Orastron Srl unipersonale + * + * Tibia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * Tibia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tibia. If not, see . + * + * File author: Stefano D'Angelo + */ + +#include +#include + +#include "data.h" +#include "plugin_api.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#include "plugin.h" +#ifdef DATA_UI +# include "plugin_ui.h" +#endif +#pragma GCC diagnostic pop + +#include "lv2/core/lv2.h" +#include "lv2/core/lv2_util.h" +#include "lv2/log/log.h" +#include "lv2/log/logger.h" +#include "lv2/urid/urid.h" +#if DATA_PRODUCT_MIDI_INPUTS_N + DATA_PRODUCT_MIDI_OUTPUTS_N > 0 +# include "lv2/atom/util.h" +# include "lv2/atom/atom.h" +# include "lv2/midi/midi.h" +#endif +#ifdef DATA_UI +# include "lv2/ui/ui.h" +#endif + +#include + +#if defined(__i386__) || defined(__x86_64__) +# include +# include +#endif + +static inline float clampf(float x, float m, float M) { + return x < m ? m : (x > M ? M : x); +} + +static float adjust_param(size_t index, float value) { + if (param_data[index].flags & DATA_PARAM_BYPASS) + value = value > 0.f ? 0.f : 1.f; + else if (param_data[index].flags & DATA_PARAM_TOGGLED) + value = value > 0.f ? 1.f : 0.f; + else if (param_data[index].flags & DATA_PARAM_INTEGER) + value = (int32_t)(value + (value >= 0.f ? 0.5f : -0.5f)); + return clampf(value, param_data[index].min, param_data[index].max); +} + +typedef struct { + plugin p; +#if DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N > 0 + const float * x[DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N]; +#endif +#if DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N > 0 + float * y[DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N]; +#endif +#if DATA_PRODUCT_MIDI_INPUTS_N > 0 + const LV2_Atom_Sequence * x_midi[DATA_PRODUCT_MIDI_INPUTS_N]; +#endif +#if DATA_PRODUCT_MIDI_OUTPUTS_N > 0 + LV2_Atom_Sequence * y_midi[DATA_PRODUCT_MIDI_OUTPUTS_N]; +#endif +#if (DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N) > 0 + float * c[DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N]; +#endif +#if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + float params[DATA_PRODUCT_CONTROL_INPUTS_N]; +#endif + void * mem; + char * bundle_path; + LV2_Log_Logger logger; + LV2_URID_Map * map; +#if DATA_PRODUCT_MIDI_INPUTS_N + DATA_PRODUCT_MIDI_OUTPUTS_N > 0 + LV2_URID uri_midi_MidiEvent; +#endif +} plugin_instance; + +static const char * get_bundle_path_cb(void *handle) { + plugin_instance *instance = (plugin_instance *)handle; + return instance->bundle_path; +} + +static LV2_Handle instantiate(const struct LV2_Descriptor * descriptor, double sample_rate, const char * bundle_path, const LV2_Feature * const * features) { + (void)descriptor; + (void)bundle_path; + + plugin_instance *instance = malloc(sizeof(plugin_instance)); + if (instance == NULL) + goto err_instance; + + instance->bundle_path = strdup(bundle_path); + if (instance->bundle_path == NULL) + goto err_bundle_path; + + // from https://lv2plug.in/book + const char* missing = lv2_features_query(features, + LV2_LOG__log, &instance->logger.log, false, + LV2_URID__map, &instance->map, true, + NULL); + + lv2_log_logger_set_map(&instance->logger, instance->map); + if (missing) { + lv2_log_error(&instance->logger, "Missing feature <%s>\n", missing); + goto err_urid; + } + +#if DATA_PRODUCT_MIDI_INPUTS_N + DATA_PRODUCT_MIDI_OUTPUTS_N > 0 + instance->uri_midi_MidiEvent = instance->map->map(instance->map->handle, LV2_MIDI__MidiEvent); +#endif + + plugin_callbacks cbs = { + /* .handle = */ (void *)instance, + /* .format = */ "lv2", + /* .get_bindir = */ get_bundle_path_cb, + /* .get_datadir = */ get_bundle_path_cb + }; + plugin_init(&instance->p, &cbs); + + plugin_set_sample_rate(&instance->p, sample_rate); + size_t req = plugin_mem_req(&instance->p); + if (req != 0) { + instance->mem = malloc(req); + if (instance->mem == NULL) { + lv2_log_error(&instance->logger, "Not enough memory\n"); + goto err_mem; + } + plugin_mem_set(&instance->p, instance->mem); + } else + instance->mem = NULL; + +#if DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N > 0 + for (uint32_t i = 0; i < DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N; i++) + instance->x[i] = NULL; +#endif +#if DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N > 0 + for (uint32_t i = 0; i < DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N; i++) + instance->y[i] = NULL; +#endif +#if DATA_PRODUCT_MIDI_INPUTS_N > 0 + for (uint32_t i = 0; i < DATA_PRODUCT_MIDI_INPUTS_N; i++) + instance->x_midi[i] = NULL; +#endif +#if DATA_PRODUCT_MIDI_OUTPUTS_N > 0 + for (uint32_t i = 0; i < DATA_PRODUCT_MIDI_OUTPUTS_N; i++) + instance->y_midi[i] = NULL; +#endif +#if (DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N) > 0 + for (uint32_t i = 0; i < DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N; i++) + instance->c[i] = NULL; +#endif + + return instance; + +err_mem: + plugin_fini(&instance->p); +err_urid: + free(instance->bundle_path); +err_bundle_path: + free(instance); +err_instance: + return NULL; +} + +static void connect_port(LV2_Handle instance, uint32_t port, void * data_location) { + plugin_instance * i = (plugin_instance *)instance; +#if DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N > 0 + if (port < DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N) { + i->x[port] = data_location; + return; + } + port -= DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N; +#endif +#if DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N > 0 + if (port < DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N) { + i->y[port] = data_location; + return; + } + port -= DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N; +#endif +#if DATA_PRODUCT_MIDI_INPUTS_N > 0 + if (port < DATA_PRODUCT_MIDI_INPUTS_N) { + i->x_midi[port] = data_location; + return; + } + port -= DATA_PRODUCT_MIDI_INPUTS_N; +#endif +#if DATA_PRODUCT_MIDI_OUTPUTS_N > 0 + if (port < DATA_PRODUCT_MIDI_OUTPUTS_N) { + i->y_midi[port] = data_location; + return; + } + port -= DATA_PRODUCT_MIDI_OUTPUTS_N; +#endif +#if (DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N) > 0 + i->c[port] = data_location; +#endif +} + +static void activate(LV2_Handle instance) { + plugin_instance * i = (plugin_instance *)instance; +#if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + for (uint32_t j = 0; j < DATA_PRODUCT_CONTROL_INPUTS_N; j++) { + i->params[j] = i->c[j] != NULL ? *i->c[j] : param_data[j].def; + plugin_set_parameter(&i->p, param_data[j].index, i->params[j]); + } +#endif + plugin_reset(&i->p); +} + +static void run(LV2_Handle instance, uint32_t sample_count) { + plugin_instance * i = (plugin_instance *)instance; + +#if defined(__aarch64__) + uint64_t fpcr; + __asm__ __volatile__ ("mrs %0, fpcr" : "=r"(fpcr)); + __asm__ __volatile__ ("msr fpcr, %0" :: "r"(fpcr | 0x1000000)); // enable FZ +#elif defined(__i386__) || defined(__x86_64__) + const unsigned int flush_zero_mode = _MM_GET_FLUSH_ZERO_MODE(); + const unsigned int denormals_zero_mode = _MM_GET_DENORMALS_ZERO_MODE(); + + _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); + _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); +#endif + +#if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + for (uint32_t j = 0; j < DATA_PRODUCT_CONTROL_INPUTS_N; j++) { + if (i->c[j] == NULL) + continue; + float v = adjust_param(j, *i->c[j]); + if (v != i->params[j]) { + i->params[j] = v; + plugin_set_parameter(&i->p, param_data[j].index, v); + } + } +#endif + + // from https://lv2plug.in/book +#if DATA_PRODUCT_MIDI_INPUTS_N > 0 + for (size_t j = 0; j < DATA_PRODUCT_MIDI_INPUTS_N; j++) { + if (i->x_midi[j] == NULL) + continue; + LV2_ATOM_SEQUENCE_FOREACH(i->x_midi[j], ev) { + if (ev->body.type == i->uri_midi_MidiEvent) { + const uint8_t * data = (const uint8_t *)(ev + 1); + if ((data[0] & 0xf0) != 0xf0) + plugin_midi_msg_in(&i->p, midi_in_index[j], data); + } + } + } +#endif + +#if DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N > 0 + const float ** x = i->x; +#else + const float ** x = NULL; +#endif +#if DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N > 0 + float ** y = i-> y; +#else + float ** y = NULL; +#endif + plugin_process(&i->p, x, y, sample_count); + +#if DATA_PRODUCT_CONTROL_OUTPUTS_N > 0 + for (uint32_t j = 0; j < DATA_PRODUCT_CONTROL_OUTPUTS_N; j++) { + uint32_t k = param_out_index[j]; + if (i->c[k] != NULL) + *i->c[k] = plugin_get_parameter(&i->p, k); + } +#else + (void)plugin_get_parameter; +#endif + +#if defined(__aarch64__) + __asm__ __volatile__ ("msr fpcr, %0" : : "r"(fpcr)); +#elif defined(__i386__) || defined(__x86_64__) + _MM_SET_FLUSH_ZERO_MODE(flush_zero_mode); + _MM_SET_DENORMALS_ZERO_MODE(denormals_zero_mode); +#endif +} + +static void cleanup(LV2_Handle instance) { + plugin_instance * i = (plugin_instance *)instance; + plugin_fini(&i->p); + if (i->mem) + free(i->mem); + free(i->bundle_path); + free(instance); +} + +static const LV2_Descriptor descriptor = { + /* .URI = */ DATA_LV2_URI, + /* .instantiate = */ instantiate, + /* .connect_port = */ connect_port, + /* .activate = */ activate, + /* .run = */ run, + /* .deactivate = */ NULL, + /* .cleanup = */ cleanup, + /* .extension_data = */ NULL +}; + +LV2_SYMBOL_EXPORT const LV2_Descriptor * lv2_descriptor(uint32_t index) { + return index == 0 ? &descriptor : NULL; +} + +#ifdef DATA_UI +typedef struct { + plugin_ui * ui; + char * bundle_path; +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + LV2UI_Write_Function write; + LV2UI_Controller controller; + char has_touch; + LV2UI_Touch touch; +# endif +} ui_instance; + +# define CONTROL_INPUT_INDEX_OFFSET ( \ + DATA_PRODUCT_AUDIO_INPUT_CHANNELS_N \ + + DATA_PRODUCT_AUDIO_OUTPUT_CHANNELS_N \ + + DATA_PRODUCT_MIDI_INPUTS_N \ + + DATA_PRODUCT_MIDI_OUTPUTS_N ) +# define CONTROL_OUTPUT_INDEX_OFFSET (CONTROL_INPUT_INDEX_OFFSET + DATA_PRODUCT_CONTROL_INPUTS_N) + +static const char * ui_get_bundle_path_cb(void *handle) { + ui_instance *instance = (ui_instance *)handle; + return instance->bundle_path; +} + +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 +static void ui_set_parameter_begin_cb(void *handle, size_t index) { + ui_instance *instance = (ui_instance *)handle; + if (instance->has_touch) { + index = index_to_param[index]; + instance->touch.touch(instance->touch.handle, index, true); + } +} + +static void ui_set_parameter_cb(void *handle, size_t index, float value) { + ui_instance *instance = (ui_instance *)handle; + index = index_to_param[index]; + value = adjust_param(index - CONTROL_INPUT_INDEX_OFFSET, value); + instance->write(instance->controller, index, sizeof(float), 0, &value); +} + +static void ui_set_parameter_end_cb(void *handle, size_t index) { + ui_instance *instance = (ui_instance *)handle; + if (instance->has_touch) { + index = index_to_param[index]; + instance->touch.touch(instance->touch.handle, index, false); + } +} +# endif + +static LV2UI_Handle ui_instantiate(const LV2UI_Descriptor * descriptor, const char * plugin_uri, const char * bundle_path, LV2UI_Write_Function write_function, LV2UI_Controller controller, LV2UI_Widget * widget, const LV2_Feature * const * features) { + (void)descriptor; + (void)plugin_uri; + + ui_instance *instance = malloc(sizeof(ui_instance)); + if (instance == NULL) + goto err_instance; + + instance->bundle_path = strdup(bundle_path); + if (instance->bundle_path == NULL) + goto err_bundle_path; + + char has_parent = 0; + void *parent = NULL; + instance->has_touch = 0; + for (size_t i = 0; features[i] != NULL; i++) { + if (!strcmp(features[i]->URI, LV2_UI__parent)) { + has_parent = 1; + parent = features[i]->data; + } + if (!strcmp(features[i]->URI, LV2_UI__touch)) { + instance->has_touch = 1; + instance->touch = *((LV2UI_Touch *)features[i]->data); + } + } + + plugin_ui_callbacks cbs = { + /* .handle = */ (void *)instance, + /* .format = */ "lv2", + /* .get_bindir = */ ui_get_bundle_path_cb, + /* .get_datadir = */ ui_get_bundle_path_cb, +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + /* .set_parameter_begin = */ ui_set_parameter_begin_cb, + /* .set_parameter = */ ui_set_parameter_cb, + /* .set_parameter_end = */ ui_set_parameter_end_cb +# else + /* .set_parameter_begin = */ NULL, + /* .set_parameter = */ NULL, + /* .set_parameter_end = */ NULL +# endif + }; +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + instance->write = write_function; + instance->controller = controller; +# else + (void)write_function; + (void)controller; +# endif + instance->ui = plugin_ui_create(has_parent, parent, &cbs); + if (instance->ui == NULL) + goto err_create; + + *widget = instance->ui->widget; + return instance; + +err_create: + free(instance->bundle_path); +err_bundle_path: + free(instance); +err_instance: + *widget = NULL; + return NULL; +} + +static void ui_cleanup(LV2UI_Handle handle) { + ui_instance *instance = (ui_instance *)handle; + plugin_ui_free(instance->ui); + free(instance->bundle_path); + free(instance); +} + +# if DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N > 0 +static void ui_port_event(LV2UI_Handle handle, uint32_t port_index, uint32_t buffer_size, uint32_t format, const void * buffer) { + (void)buffer_size; + (void)format; + + ui_instance *instance = (ui_instance *)handle; +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + if (port_index < CONTROL_OUTPUT_INDEX_OFFSET) { + size_t index = port_index - CONTROL_INPUT_INDEX_OFFSET; + plugin_ui_set_parameter(instance->ui, param_data[index].index, adjust_param(index, *((float *)buffer))); + } +# endif +# if DATA_PRODUCT_CONTROL_OUTPUTS_N > 0 +# if DATA_PRODUCT_CONTROL_INPUTS_N > 0 + else +# endif + plugin_ui_set_parameter(instance->ui, param_out_index[port_index - CONTROL_OUTPUT_INDEX_OFFSET], *((float *)buffer)); +# endif +} +# endif + +static int ui_idle(LV2UI_Handle handle) { + ui_instance *instance = (ui_instance *)handle; + plugin_ui_idle(instance->ui); + return 0; +} + +static const void * ui_extension_data(const char * uri) { + static const LV2UI_Idle_Interface idle = { ui_idle }; + if (!strcmp(uri, LV2_UI__idleInterface)) + return &idle; + return NULL; +} + +static const LV2UI_Descriptor ui_descriptor = { + /* .URI = */ DATA_LV2_UI_URI, + /* .instantiate = */ ui_instantiate, + /* .cleanup = */ ui_cleanup, +# if DATA_PRODUCT_CONTROL_INPUTS_N + DATA_PRODUCT_CONTROL_OUTPUTS_N > 0 + /* .port_event = */ ui_port_event, +# else + /* .port_event = */ NULL, +# endif + /* .extension_data = */ ui_extension_data +}; + +LV2_SYMBOL_EXPORT const LV2UI_Descriptor * lv2ui_descriptor(uint32_t index) { + return index == 0 ? &ui_descriptor : NULL; +} +#endif diff --git a/templates/lv2-next/tibia-index.js b/templates/lv2-next/tibia-index.js new file mode 100644 index 0000000..f8b96af --- /dev/null +++ b/templates/lv2-next/tibia-index.js @@ -0,0 +1,136 @@ +/* + * Tibia + * + * Copyright (C) 2024 Orastron Srl unipersonale + * + * Tibia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * Tibia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tibia. If not, see . + * + * File author: Stefano D'Angelo + */ + +var path = require("path"); +var sep = path.sep; + +module.exports = function (data, api, outputCommon, outputData) { + if (outputData) { + data.tibia.lv2 = { + prefixes: [ + { id: "atom", uri: "http://lv2plug.in/ns/ext/atom#" }, + { id: "doap", uri: "http://usefulinc.com/ns/doap#" }, + { id: "foaf", uri: "http://xmlns.com/foaf/0.1/" }, + { id: "log", uri: "http://lv2plug.in/ns/ext/log#" }, + { id: "lv2", uri: "http://lv2plug.in/ns/lv2core#" }, + { id: "midi", uri: "http://lv2plug.in/ns/ext/midi#" }, + { id: "patch", uri: "http://lv2plug.in/ns/ext/patch#" }, + { id: "pprops", uri: "http://lv2plug.in/ns/ext/port-props#" }, + { id: "rdf", uri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }, + { id: "rdfs", uri: "http://www.w3.org/2000/01/rdf-schema#" }, + { id: "ui", uri: "http://lv2plug.in/ns/extensions/ui#" }, + { id: "units", uri: "http://lv2plug.in/ns/extensions/units#" }, + { id: "urid", uri: "http://lv2plug.in/ns/ext/urid#" } + ], + units: { + "bar": "@units:bar", + "beat": "@units:beat", + "bpm": "@units:bpm", + "cent": "@units:cent", + "cm": "@units:cm", + "coef": "@units:coef", + "db": "@units:db", + "degree": "@units:degree", + "frame": "@units:frame", + "hz": "@units:hz", + "inch": "@units:inch", + "khz": "@units:khz", + "km": "@units:km", + "m": "@units:m", + "mhz": "@units:mhz", + "midiNote": "@units:midiNote", + "mile": "@units:mile", + "min": "@units:min", + "mm": "@units:mm", + "ms": "@units:ms", + "oct": "@units:oct", + "pc": "@units:pc", + "s": "@units:s", + "semitone12TET": "@units:semitone12TET" + }, + ports: [], + + ttlURI: function (uri) { + if (uri.charAt(0) == "@") { + if (uri.indexOf("#") == -1) + return uri.substring(1); + uri = data.tibia.lv2.expandURI(uri); + } + return "<" + uri + ">"; + }, + + expandURI: function (uri) { + if (uri.charAt(0) != "@") + return uri; + var i = uri.indexOf(":"); + var p = uri.substring(1, uri.indexOf(":")); + return data.tibia.lv2.prefixes.find(x => x.id == p).uri + uri.substring(i + 1); + } + }; + + for (var id in data.lv2.prefixes) + data.tibia.lv2.prefixes.push({ id: id, uri: data.lv2.prefixes[id] }); + + data.tibia.lv2.prefixes.push({ id: "plugin", uri: data.tibia.lv2.expandURI(data.lv2.uri) + "#" }); + + var buses = data.product.buses; + var audioPorts = []; + var midiPorts = []; + for (var bi = 0; bi < buses.length; bi++) { + var b = buses[bi]; + if (b.type == "audio") { + if (b.channels == "mono") { + var e = { type: "audio", direction: b.direction, name: b.name, sidechain: b.sidechain, cv: b.cv, optional: b.optional, busIndex: bi, id: b.id }; + audioPorts.push(e); + } else { + var e = { type: "audio", direction: b.direction, name: b.name + " Left", shortName: b.shortName + " L", sidechain: b.sidechain, cv: b.cv, busIndex: bi, id: b.id + "_l" }; + audioPorts.push(e); + var e = { type: "audio", direction: b.direction, name: b.name + " Right", shortName: b.shortName + " R", sidechain: b.sidechain, cv: b.cv, busIndex: bi, id: b.id + "_r" }; + audioPorts.push(e); + } + } else { + var e = { type: "midi", direction: b.direction, name: b.name, sidechain: b.sidechain, control: b.control, optional: b.optional, busIndex: bi, id: b.id }; + midiPorts.push(e); + } + } + audioPorts.sort((a, b) => a.direction != b.direction ? (a.direction == "input" ? -1 : 1) : 0); + midiPorts.sort((a, b) => a.direction != b.direction ? (a.direction == "input" ? -1 : 1) : 0); + data.tibia.lv2.ports.push.apply(data.tibia.lv2.ports, audioPorts); + data.tibia.lv2.ports.push.apply(data.tibia.lv2.ports, midiPorts); + + var hasCtrlIn = false; + var hasCtrlOut = false; + for (var i = 0; i < data.product.parameters.length && !(hasCtrlIn && hasCtrlOut); i++) { + var p = data.product.parameters[i]; + if (p.direction == "input") + hasCtrlIn = true; + else + hasCtrlOut = true; + } + if (hasCtrlIn) + data.tibia.lv2.ports.push({ type: "param", direction: "input", name: "Control in", id: "control_in", control: true }); + if (hasCtrlOut) + data.tibia.lv2.ports.push({ type: "param", direction: "output", name: "Control out", id: "control_out", control: true }); + } + + api.generateFileFromTemplateFile(`data${sep}manifest.ttl.in`, `data${sep}manifest.ttl.in`, data); + api.copyFile(`src${sep}lv2.c`, `src${sep}lv2.c`); + api.generateFileFromTemplateFile(`src${sep}data.h`, `src${sep}data.h`, data); +}; diff --git a/templates/lv2/data/manifest.ttl.in b/templates/lv2/data/manifest.ttl.in index 131f00b..7fdb587 100644 --- a/templates/lv2/data/manifest.ttl.in +++ b/templates/lv2/data/manifest.ttl.in @@ -15,7 +15,7 @@ doap:maintainer [ a foaf:Organization ; foaf:name "{{=it.company.name}}" ; - foaf:mbox "{{=it.company.email}}" ; + foaf:mbox ; rdfs:seeAlso {{=it.tibia.lv2.ttlURI(it.company.url)}} ] ; lv2:minorVersion {{=/^([0-9]+)\./.exec(it.lv2.version)[1]}} ; @@ -25,8 +25,8 @@ lv2:optionalFeature log:log ; {{?}} lv2:optionalFeature lv2:hardRTCapable ; -{{?it.lv2.ui}} - ui:ui {{=it.tibia.lv2.ttlURI(it.lv2.ui.uri)}} ; +{{?it.product.ui}} + ui:ui plugin:ui ; {{?}} lv2:port [ {{~it.tibia.lv2.ports :p:i}} @@ -121,8 +121,8 @@ {{?}} {{~}} -{{?it.lv2.ui}} -{{=it.tibia.lv2.ttlURI(it.lv2.ui.uri)}} +{{?it.product.ui}} +plugin:ui a ui:@UI_TYPE@ ; ui:binary <{{=it.product.bundleName}}@DLL_SUFFIX@> ; {{?!it.product.ui.userResizable}} diff --git a/templates/lv2/src/data.h b/templates/lv2/src/data.h index cf5e8b5..666a1fb 100644 --- a/templates/lv2/src/data.h +++ b/templates/lv2/src/data.h @@ -65,9 +65,9 @@ static uint32_t param_out_index[DATA_PRODUCT_CONTROL_OUTPUTS_N] = { }; #endif -{{?it.lv2.ui}} +{{?it.product.ui}} #define DATA_UI -#define DATA_LV2_UI_URI "{{=it.tibia.CGetUTF8StringLiteral(it.tibia.lv2.expandURI(it.lv2.ui.uri))}}" +#define DATA_LV2_UI_URI "{{=it.tibia.CGetUTF8StringLiteral(it.tibia.lv2.expandURI(it.lv2.uri + '#ui'))}}" #define DATA_UI_USER_RESIZABLE {{=it.product.ui.userResizable ? 1 : 0}} #if DATA_PRODUCT_CONTROL_INPUTS_N > 0 diff --git a/templates/lv2/tibia-index.js b/templates/lv2/tibia-index.js index 5608d5d..4992bb7 100644 --- a/templates/lv2/tibia-index.js +++ b/templates/lv2/tibia-index.js @@ -67,7 +67,12 @@ module.exports = function (data, api, outputCommon, outputData) { ports: [], ttlURI: function (uri) { - return uri.charAt(0) == "@" ? uri.substring(1) : "<" + uri + ">"; + if (uri.charAt(0) == "@") { + if (uri.indexOf("#") == -1) + return uri.substring(1); + uri = data.tibia.lv2.expandURI(uri); + } + return "<" + uri + ">"; }, expandURI: function (uri) { @@ -82,6 +87,8 @@ module.exports = function (data, api, outputCommon, outputData) { for (var id in data.lv2.prefixes) data.tibia.lv2.prefixes.push({ id: id, uri: data.lv2.prefixes[id] }); + data.tibia.lv2.prefixes.push({ id: "plugin", uri: data.tibia.lv2.expandURI(data.lv2.uri) + "#" }); + var buses = data.product.buses; var audioPorts = []; var midiPorts = []; diff --git a/test/lv2.json b/test/lv2.json index 728cfef..098c70f 100644 --- a/test/lv2.json +++ b/test/lv2.json @@ -6,9 +6,6 @@ "uri": "@example:tibia_test", "project": "@example:project", "types": [ "@lv2:AmplifierPlugin" ], - "version": "1.0", - "ui": { - "uri": "@example:tibia_test_ui" - } + "version": "1.0" } }