Mercurial > hg > pub > prymula > com
diff DPF-Prymula-audioplugins/dpf/distrho/src/DistrhoPluginLV2export.cpp @ 3:84e66ea83026
DPF-Prymula-audioplugins-0.231015-2
author | prymula <prymula76@outlook.com> |
---|---|
date | Mon, 16 Oct 2023 21:53:34 +0200 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/DPF-Prymula-audioplugins/dpf/distrho/src/DistrhoPluginLV2export.cpp Mon Oct 16 21:53:34 2023 +0200 @@ -0,0 +1,1708 @@ +/* + * DISTRHO Plugin Framework (DPF) + * Copyright (C) 2012-2023 Filipe Coelho <falktx@falktx.com> + * + * Permission to use, copy, modify, and/or distribute this software for any purpose with + * or without fee is hereby granted, provided that the above copyright notice and this + * permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "DistrhoPluginInternal.hpp" +#include "../DistrhoPluginUtils.hpp" + +#include "lv2/atom.h" +#include "lv2/buf-size.h" +#include "lv2/data-access.h" +#include "lv2/instance-access.h" +#include "lv2/midi.h" +#include "lv2/options.h" +#include "lv2/parameters.h" +#include "lv2/patch.h" +#include "lv2/port-groups.h" +#include "lv2/port-props.h" +#include "lv2/presets.h" +#include "lv2/resize-port.h" +#include "lv2/state.h" +#include "lv2/time.h" +#include "lv2/ui.h" +#include "lv2/units.h" +#include "lv2/urid.h" +#include "lv2/worker.h" +#include "lv2/lv2_kxstudio_properties.h" +#include "lv2/lv2_programs.h" + +#ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD +# include "mod-license.h" +#endif + +#ifdef DISTRHO_OS_WINDOWS +# include <direct.h> +#else +# include <sys/stat.h> +# include <sys/types.h> +# include <unistd.h> +#endif + +#include <fstream> +#include <iostream> + +#ifndef DISTRHO_PLUGIN_URI +# error DISTRHO_PLUGIN_URI undefined! +#endif + +#ifndef DISTRHO_PLUGIN_LV2_STATE_PREFIX +# define DISTRHO_PLUGIN_LV2_STATE_PREFIX "urn:distrho:" +#endif + +#ifndef DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE +# define DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE 2048 +#endif + +#ifndef DISTRHO_PLUGIN_USES_MODGUI +# define DISTRHO_PLUGIN_USES_MODGUI 0 +#endif + +#ifndef DISTRHO_PLUGIN_USES_CUSTOM_MODGUI +# define DISTRHO_PLUGIN_USES_CUSTOM_MODGUI 0 +#endif + +#if DISTRHO_PLUGIN_HAS_EMBED_UI +# if DISTRHO_OS_HAIKU +# define DISTRHO_LV2_UI_TYPE "BeUI" +# elif DISTRHO_OS_MAC +# define DISTRHO_LV2_UI_TYPE "CocoaUI" +# elif DISTRHO_OS_WINDOWS +# define DISTRHO_LV2_UI_TYPE "WindowsUI" +# else +# define DISTRHO_LV2_UI_TYPE "X11UI" +# endif +#else +# define DISTRHO_LV2_UI_TYPE "UI" +#endif + +#define DISTRHO_LV2_USE_EVENTS_IN (DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_TIMEPOS || DISTRHO_PLUGIN_WANT_STATE) +#define DISTRHO_LV2_USE_EVENTS_OUT (DISTRHO_PLUGIN_WANT_MIDI_OUTPUT || DISTRHO_PLUGIN_WANT_STATE) + +#define DISTRHO_BYPASS_PARAMETER_NAME "lv2_enabled" + +// ----------------------------------------------------------------------- +static const char* const lv2ManifestPluginExtensionData[] = +{ + "opts:interface", +#if DISTRHO_PLUGIN_WANT_STATE + LV2_STATE__interface, + LV2_WORKER__interface, +#endif +#if DISTRHO_PLUGIN_WANT_PROGRAMS + LV2_PROGRAMS__Interface, +#endif +#ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD + MOD_LICENSE__interface, +#endif + nullptr +}; + +static const char* const lv2ManifestPluginOptionalFeatures[] = +{ +#if DISTRHO_PLUGIN_IS_RT_SAFE + LV2_CORE__hardRTCapable, +#endif + LV2_BUF_SIZE__boundedBlockLength, + nullptr +}; + +static const char* const lv2ManifestPluginRequiredFeatures[] = +{ + "opts:options", + LV2_URID__map, +#if DISTRHO_PLUGIN_WANT_STATE + LV2_WORKER__schedule, +#endif +#ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD + MOD_LICENSE__feature, +#endif + nullptr +}; + +static const char* const lv2ManifestPluginSupportedOptions[] = +{ + LV2_BUF_SIZE__nominalBlockLength, + LV2_BUF_SIZE__maxBlockLength, + LV2_PARAMETERS__sampleRate, + nullptr +}; + +#if DISTRHO_PLUGIN_HAS_UI +static const char* const lv2ManifestUiExtensionData[] = +{ + "opts:interface", + "ui:idleInterface", + "ui:showInterface", +#if DISTRHO_PLUGIN_WANT_PROGRAMS + LV2_PROGRAMS__UIInterface, +#endif + nullptr +}; + +static const char* const lv2ManifestUiOptionalFeatures[] = +{ +#if DISTRHO_PLUGIN_HAS_EMBED_UI +# if !DISTRHO_UI_USER_RESIZABLE + "ui:noUserResize", +# endif + "ui:parent", + "ui:touch", +#endif + "ui:requestValue", + nullptr +}; + +static const char* const lv2ManifestUiRequiredFeatures[] = +{ + "opts:options", + "ui:idleInterface", +#if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + LV2_DATA_ACCESS_URI, + LV2_INSTANCE_ACCESS_URI, +#endif + LV2_URID__map, + nullptr +}; + +static const char* const lv2ManifestUiSupportedOptions[] = +{ + LV2_PARAMETERS__sampleRate, + nullptr +}; +#endif // DISTRHO_PLUGIN_HAS_UI + +static void addAttribute(DISTRHO_NAMESPACE::String& text, + const char* const attribute, + const char* const values[], + const uint indent, + const bool endInDot = false) +{ + if (values[0] == nullptr) + { + if (endInDot) + { + bool found; + const size_t index = text.rfind(';', &found); + if (found) text[index] = '.'; + } + return; + } + + const size_t attributeLength = std::strlen(attribute); + + for (uint i = 0; values[i] != nullptr; ++i) + { + for (uint j = 0; j < indent; ++j) + text += " "; + + if (i == 0) + { + text += attribute; + } + else + { + for (uint j = 0; j < attributeLength; ++j) + text += " "; + } + + text += " "; + + const bool isUrl = std::strstr(values[i], "://") != nullptr || std::strncmp(values[i], "urn:", 4) == 0; + if (isUrl) text += "<"; + text += values[i]; + if (isUrl) text += ">"; + text += values[i + 1] ? " ,\n" : (endInDot ? " .\n\n" : " ;\n\n"); + } +} + +// ----------------------------------------------------------------------- + +DISTRHO_PLUGIN_EXPORT +void lv2_generate_ttl(const char* const basename) +{ + USE_NAMESPACE_DISTRHO + + String bundlePath(getBinaryFilename()); + if (bundlePath.isNotEmpty()) + { + bundlePath.truncate(bundlePath.rfind(DISTRHO_OS_SEP)); + } +#ifndef DISTRHO_OS_WINDOWS + else if (char* const cwd = ::getcwd(nullptr, 0)) + { + bundlePath = cwd; + std::free(cwd); + } +#endif + d_nextBundlePath = bundlePath.buffer(); + + // Dummy plugin to get data from + d_nextBufferSize = 512; + d_nextSampleRate = 44100.0; + d_nextPluginIsDummy = true; + PluginExporter plugin(nullptr, nullptr, nullptr, nullptr); + d_nextBufferSize = 0; + d_nextSampleRate = 0.0; + d_nextPluginIsDummy = false; + + const String pluginDLL(basename); + const String pluginTTL(pluginDLL + ".ttl"); + +#if DISTRHO_PLUGIN_HAS_UI + String pluginUI(pluginDLL); +# if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + pluginUI.truncate(pluginDLL.rfind("_dsp")); + pluginUI += "_ui"; + const String uiTTL(pluginUI + ".ttl"); +# endif +#endif + + // --------------------------------------------- + + { + std::cout << "Writing manifest.ttl..."; std::cout.flush(); + std::fstream manifestFile("manifest.ttl", std::ios::out); + + String manifestString; + manifestString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n"; + manifestString += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"; +#if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + manifestString += "@prefix opts: <" LV2_OPTIONS_PREFIX "> .\n"; +#endif +#if DISTRHO_PLUGIN_WANT_PROGRAMS + manifestString += "@prefix pset: <" LV2_PRESETS_PREFIX "> .\n"; +#endif +#if DISTRHO_PLUGIN_HAS_UI + manifestString += "@prefix ui: <" LV2_UI_PREFIX "> .\n"; +#endif + manifestString += "\n"; + + manifestString += "<" DISTRHO_PLUGIN_URI ">\n"; + manifestString += " a lv2:Plugin ;\n"; + manifestString += " lv2:binary <" + pluginDLL + "." DISTRHO_DLL_EXTENSION "> ;\n"; +#if DISTRHO_PLUGIN_USES_MODGUI + manifestString += " rdfs:seeAlso <" + pluginTTL + "> ,\n"; + manifestString += " <modgui.ttl> .\n"; +#else + manifestString += " rdfs:seeAlso <" + pluginTTL + "> .\n"; +#endif + manifestString += "\n"; + +#if DISTRHO_PLUGIN_HAS_UI + manifestString += "<" DISTRHO_UI_URI ">\n"; + manifestString += " a ui:" DISTRHO_LV2_UI_TYPE " ;\n"; + manifestString += " ui:binary <" + pluginUI + "." DISTRHO_DLL_EXTENSION "> ;\n"; +# if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + addAttribute(manifestString, "lv2:extensionData", lv2ManifestUiExtensionData, 4); + addAttribute(manifestString, "lv2:optionalFeature", lv2ManifestUiOptionalFeatures, 4); + addAttribute(manifestString, "lv2:requiredFeature", lv2ManifestUiRequiredFeatures, 4); + addAttribute(manifestString, "opts:supportedOption", lv2ManifestUiSupportedOptions, 4, true); +# else // DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + manifestString += " rdfs:seeAlso <" + uiTTL + "> .\n"; +# endif // DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + manifestString += "\n"; +#endif + +#if DISTRHO_PLUGIN_WANT_PROGRAMS + const String presetSeparator(std::strstr(DISTRHO_PLUGIN_URI, "#") != nullptr ? ":" : "#"); + + char strBuf[0xff+1]; + strBuf[0xff] = '\0'; + + String presetString; + + // Presets + for (uint32_t i = 0; i < plugin.getProgramCount(); ++i) + { + std::snprintf(strBuf, 0xff, "%03i", i+1); + + presetString = "<" DISTRHO_PLUGIN_URI + presetSeparator + "preset" + strBuf + ">\n"; + presetString += " a pset:Preset ;\n"; + presetString += " lv2:appliesTo <" DISTRHO_PLUGIN_URI "> ;\n"; + presetString += " rdfs:label \"" + plugin.getProgramName(i) + "\" ;\n"; + presetString += " rdfs:seeAlso <presets.ttl> .\n"; + presetString += "\n"; + + manifestString += presetString; + } +#endif + + manifestFile << manifestString; + manifestFile.close(); + std::cout << " done!" << std::endl; + } + + // --------------------------------------------- + + { + std::cout << "Writing " << pluginTTL << "..."; std::cout.flush(); + std::fstream pluginFile(pluginTTL, std::ios::out); + + String pluginString; + + // header +#if DISTRHO_LV2_USE_EVENTS_IN || DISTRHO_LV2_USE_EVENTS_OUT + pluginString += "@prefix atom: <" LV2_ATOM_PREFIX "> .\n"; +#endif + pluginString += "@prefix doap: <http://usefulinc.com/ns/doap#> .\n"; + pluginString += "@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n"; + pluginString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n"; + pluginString += "@prefix midi: <" LV2_MIDI_PREFIX "> .\n"; + pluginString += "@prefix mod: <http://moddevices.com/ns/mod#> .\n"; + pluginString += "@prefix opts: <" LV2_OPTIONS_PREFIX "> .\n"; + pluginString += "@prefix pg: <" LV2_PORT_GROUPS_PREFIX "> .\n"; + pluginString += "@prefix patch: <" LV2_PATCH_PREFIX "> .\n"; + pluginString += "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n"; + pluginString += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"; +#if DISTRHO_LV2_USE_EVENTS_IN || DISTRHO_LV2_USE_EVENTS_OUT + pluginString += "@prefix rsz: <" LV2_RESIZE_PORT_PREFIX "> .\n"; +#endif + pluginString += "@prefix spdx: <http://spdx.org/rdf/terms#> .\n"; +#if DISTRHO_PLUGIN_HAS_UI + pluginString += "@prefix ui: <" LV2_UI_PREFIX "> .\n"; +#endif + pluginString += "@prefix unit: <" LV2_UNITS_PREFIX "> .\n"; + pluginString += "\n"; + +#if DISTRHO_PLUGIN_WANT_STATE + bool hasHostVisibleState = false; + + for (uint32_t i=0, count=plugin.getStateCount(); i < count; ++i) + { + const uint32_t hints = plugin.getStateHints(i); + + if ((hints & kStateIsHostReadable) == 0x0) + continue; + + pluginString += "<" DISTRHO_PLUGIN_URI "#" + plugin.getStateKey(i) + ">\n"; + pluginString += " a lv2:Parameter ;\n"; + pluginString += " rdfs:label \"" + plugin.getStateLabel(i) + "\" ;\n"; + + const String& comment(plugin.getStateDescription(i)); + + if (comment.isNotEmpty()) + { + if (comment.contains('"') || comment.contains('\n')) + pluginString += " rdfs:comment \"\"\"" + comment + "\"\"\" ;\n"; + else + pluginString += " rdfs:comment \"" + comment + "\" ;\n"; + } + + if ((hints & kStateIsFilenamePath) == kStateIsFilenamePath) + { + #ifdef __MOD_DEVICES__ + const String& fileTypes(plugin.getStateFileTypes(i)); + if (fileTypes.isNotEmpty()) + pluginString += " mod:fileTypes \"" + fileTypes + "\" ; \n"; + #endif + pluginString += " rdfs:range atom:Path .\n\n"; + } + else + { + pluginString += " rdfs:range atom:String .\n\n"; + } + + hasHostVisibleState = true; + } +#endif + + // plugin + pluginString += "<" DISTRHO_PLUGIN_URI ">\n"; +#ifdef DISTRHO_PLUGIN_LV2_CATEGORY + pluginString += " a " DISTRHO_PLUGIN_LV2_CATEGORY ", lv2:Plugin, doap:Project ;\n"; +#elif DISTRHO_PLUGIN_IS_SYNTH + pluginString += " a lv2:InstrumentPlugin, lv2:Plugin, doap:Project ;\n"; +#else + pluginString += " a lv2:Plugin, doap:Project ;\n"; +#endif + pluginString += "\n"; + + addAttribute(pluginString, "lv2:extensionData", lv2ManifestPluginExtensionData, 4); + addAttribute(pluginString, "lv2:optionalFeature", lv2ManifestPluginOptionalFeatures, 4); + addAttribute(pluginString, "lv2:requiredFeature", lv2ManifestPluginRequiredFeatures, 4); + addAttribute(pluginString, "opts:supportedOption", lv2ManifestPluginSupportedOptions, 4); + +#if DISTRHO_PLUGIN_WANT_STATE + if (hasHostVisibleState) + { + for (uint32_t i=0, count=plugin.getStateCount(); i < count; ++i) + { + const uint32_t hints = plugin.getStateHints(i); + + if ((hints & kStateIsHostReadable) == 0x0) + continue; + + const String& key(plugin.getStateKey(i)); + + if ((hints & kStateIsHostWritable) == kStateIsHostWritable) + pluginString += " patch:writable <" DISTRHO_PLUGIN_URI "#" + key + "> ;\n"; + else + pluginString += " patch:readable <" DISTRHO_PLUGIN_URI "#" + key + "> ;\n"; + } + pluginString += "\n"; + } +#endif + + // UI +#if DISTRHO_PLUGIN_HAS_UI + pluginString += " ui:ui <" DISTRHO_UI_URI "> ;\n"; + pluginString += "\n"; +#endif + + { + uint32_t portIndex = 0; + +#if DISTRHO_PLUGIN_NUM_INPUTS > 0 + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i, ++portIndex) + { + const AudioPort& port(plugin.getAudioPort(true, i)); + const bool cvPortScaled = port.hints & kCVPortHasScaledRange; + + if (i == 0) + pluginString += " lv2:port [\n"; + else + pluginString += " [\n"; + + if (cvPortScaled) + pluginString += " a lv2:InputPort, lv2:CVPort, mod:CVPort ;\n"; + else if (port.hints & kAudioPortIsCV) + pluginString += " a lv2:InputPort, lv2:CVPort ;\n"; + else + pluginString += " a lv2:InputPort, lv2:AudioPort ;\n"; + + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + pluginString += " lv2:symbol \"lv2_" + port.symbol + "\" ;\n"; + pluginString += " lv2:name \"" + port.name + "\" ;\n"; + + if (port.hints & kAudioPortIsSidechain) + pluginString += " lv2:portProperty lv2:isSideChain;\n"; + + if (port.groupId != kPortGroupNone) + { + pluginString += " pg:group <" DISTRHO_PLUGIN_URI "#portGroup_" + + plugin.getPortGroupSymbolForId(port.groupId) + "> ;\n"; + + switch (port.groupId) + { + case kPortGroupMono: + pluginString += " lv2:designation pg:center ;\n"; + break; + case kPortGroupStereo: + if (i == 1) + pluginString += " lv2:designation pg:right ;\n"; + else + pluginString += " lv2:designation pg:left ;\n"; + break; + } + } + + // set ranges + if (port.hints & kCVPortHasBipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum -5.0 ;\n"; + pluginString += " lv2:maximum 5.0 ;\n"; + } + else + { + pluginString += " lv2:minimum -1.0 ;\n"; + pluginString += " lv2:maximum 1.0 ;\n"; + } + } + else if (port.hints & kCVPortHasNegativeUnipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum -10.0 ;\n"; + pluginString += " lv2:maximum 0.0 ;\n"; + } + else + { + pluginString += " lv2:minimum -1.0 ;\n"; + pluginString += " lv2:maximum 0.0 ;\n"; + } + } + else if (port.hints & kCVPortHasPositiveUnipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum 0.0 ;\n"; + pluginString += " lv2:maximum 10.0 ;\n"; + } + else + { + pluginString += " lv2:minimum 0.0 ;\n"; + pluginString += " lv2:maximum 1.0 ;\n"; + } + } + + if ((port.hints & (kAudioPortIsCV|kCVPortIsOptional)) == (kAudioPortIsCV|kCVPortIsOptional)) + pluginString += " lv2:portProperty lv2:connectionOptional;\n"; + + if (i+1 == DISTRHO_PLUGIN_NUM_INPUTS) + pluginString += " ] ;\n"; + else + pluginString += " ] ,\n"; + } + pluginString += "\n"; +#endif + +#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0 + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i, ++portIndex) + { + const AudioPort& port(plugin.getAudioPort(false, i)); + const bool cvPortScaled = port.hints & kCVPortHasScaledRange; + + if (i == 0) + pluginString += " lv2:port [\n"; + else + pluginString += " [\n"; + + if (cvPortScaled) + pluginString += " a lv2:OutputPort, lv2:CVPort, mod:CVPort ;\n"; + else if (port.hints & kAudioPortIsCV) + pluginString += " a lv2:OutputPort, lv2:CVPort ;\n"; + else + pluginString += " a lv2:OutputPort, lv2:AudioPort ;\n"; + + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + pluginString += " lv2:symbol \"lv2_" + port.symbol + "\" ;\n"; + pluginString += " lv2:name \"" + port.name + "\" ;\n"; + + if (port.hints & kAudioPortIsSidechain) + pluginString += " lv2:portProperty lv2:isSideChain;\n"; + + if (port.groupId != kPortGroupNone) + { + pluginString += " pg:group <" DISTRHO_PLUGIN_URI "#portGroup_" + + plugin.getPortGroupSymbolForId(port.groupId) + "> ;\n"; + + switch (port.groupId) + { + case kPortGroupMono: + pluginString += " lv2:designation pg:center ;\n"; + break; + case kPortGroupStereo: + if (i == 1) + pluginString += " lv2:designation pg:right ;\n"; + else + pluginString += " lv2:designation pg:left ;\n"; + break; + } + } + + // set ranges + if (port.hints & kCVPortHasBipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum -5.0 ;\n"; + pluginString += " lv2:maximum 5.0 ;\n"; + } + else + { + pluginString += " lv2:minimum -1.0 ;\n"; + pluginString += " lv2:maximum 1.0 ;\n"; + } + } + else if (port.hints & kCVPortHasNegativeUnipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum -10.0 ;\n"; + pluginString += " lv2:maximum 0.0 ;\n"; + } + else + { + pluginString += " lv2:minimum -1.0 ;\n"; + pluginString += " lv2:maximum 0.0 ;\n"; + } + } + else if (port.hints & kCVPortHasPositiveUnipolarRange) + { + if (cvPortScaled) + { + pluginString += " lv2:minimum 0.0 ;\n"; + pluginString += " lv2:maximum 10.0 ;\n"; + } + else + { + pluginString += " lv2:minimum 0.0 ;\n"; + pluginString += " lv2:maximum 1.0 ;\n"; + } + } + + if (i+1 == DISTRHO_PLUGIN_NUM_OUTPUTS) + pluginString += " ] ;\n"; + else + pluginString += " ] ,\n"; + } + pluginString += "\n"; +#endif + +#if DISTRHO_LV2_USE_EVENTS_IN + pluginString += " lv2:port [\n"; + pluginString += " a lv2:InputPort, atom:AtomPort ;\n"; + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + pluginString += " lv2:name \"Events Input\" ;\n"; + pluginString += " lv2:symbol \"lv2_events_in\" ;\n"; + pluginString += " rsz:minimumSize " + String(DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE) + " ;\n"; + pluginString += " atom:bufferType atom:Sequence ;\n"; +# if (DISTRHO_PLUGIN_WANT_STATE && DISTRHO_PLUGIN_HAS_UI) + pluginString += " atom:supports atom:String ;\n"; +# endif +# if DISTRHO_PLUGIN_WANT_MIDI_INPUT + pluginString += " atom:supports midi:MidiEvent ;\n"; +# endif +# if DISTRHO_PLUGIN_WANT_TIMEPOS + pluginString += " atom:supports <" LV2_TIME__Position "> ;\n"; +# endif +# if DISTRHO_PLUGIN_WANT_STATE + if (hasHostVisibleState) + { + pluginString += " atom:supports <" LV2_PATCH__Message "> ;\n"; + pluginString += " lv2:designation lv2:control ;\n"; + } +# endif + pluginString += " ] ;\n\n"; + ++portIndex; +#endif + +#if DISTRHO_LV2_USE_EVENTS_OUT + pluginString += " lv2:port [\n"; + pluginString += " a lv2:OutputPort, atom:AtomPort ;\n"; + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + pluginString += " lv2:name \"Events Output\" ;\n"; + pluginString += " lv2:symbol \"lv2_events_out\" ;\n"; + pluginString += " rsz:minimumSize " + String(DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE) + " ;\n"; + pluginString += " atom:bufferType atom:Sequence ;\n"; +# if (DISTRHO_PLUGIN_WANT_STATE && DISTRHO_PLUGIN_HAS_UI) + pluginString += " atom:supports atom:String ;\n"; +# endif +# if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT + pluginString += " atom:supports midi:MidiEvent ;\n"; +# endif +# if DISTRHO_PLUGIN_WANT_STATE + if (hasHostVisibleState) + { + pluginString += " atom:supports <" LV2_PATCH__Message "> ;\n"; + pluginString += " lv2:designation lv2:control ;\n"; + } +# endif + pluginString += " ] ;\n\n"; + ++portIndex; +#endif + +#if DISTRHO_PLUGIN_WANT_LATENCY + pluginString += " lv2:port [\n"; + pluginString += " a lv2:OutputPort, lv2:ControlPort ;\n"; + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + pluginString += " lv2:name \"Latency\" ;\n"; + pluginString += " lv2:symbol \"lv2_latency\" ;\n"; + pluginString += " lv2:designation lv2:latency ;\n"; + pluginString += " lv2:portProperty lv2:reportsLatency, lv2:integer, <" LV2_PORT_PROPS__notOnGUI "> ;\n"; + pluginString += " ] ;\n\n"; + ++portIndex; +#endif + + for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i, ++portIndex) + { + if (i == 0) + pluginString += " lv2:port [\n"; + else + pluginString += " [\n"; + + if (plugin.isParameterOutput(i)) + pluginString += " a lv2:OutputPort, lv2:ControlPort ;\n"; + else + pluginString += " a lv2:InputPort, lv2:ControlPort ;\n"; + + pluginString += " lv2:index " + String(portIndex) + " ;\n"; + + bool designated = false; + + // designation + if (plugin.isParameterInput(i)) + { + switch (plugin.getParameterDesignation(i)) + { + case kParameterDesignationNull: + break; + case kParameterDesignationBypass: + designated = true; + pluginString += " lv2:name \"Enabled\" ;\n"; + pluginString += " lv2:symbol \"" DISTRHO_BYPASS_PARAMETER_NAME "\" ;\n"; + pluginString += " lv2:default 1 ;\n"; + pluginString += " lv2:minimum 0 ;\n"; + pluginString += " lv2:maximum 1 ;\n"; + pluginString += " lv2:portProperty lv2:toggled , lv2:integer ;\n"; + pluginString += " lv2:designation lv2:enabled ;\n"; + break; + } + } + + if (! designated) + { + const uint32_t hints = plugin.getParameterHints(i); + + // name and symbol + const String& paramName(plugin.getParameterName(i)); + + if (paramName.contains('"')) + pluginString += " lv2:name \"\"\"" + paramName + "\"\"\" ;\n"; + else + pluginString += " lv2:name \"" + paramName + "\" ;\n"; + + String symbol(plugin.getParameterSymbol(i)); + + if (symbol.isEmpty()) + symbol = "lv2_port_" + String(portIndex-1); + + pluginString += " lv2:symbol \"" + symbol + "\" ;\n"; + + // short name + const String& shortName(plugin.getParameterShortName(i)); + + if (shortName.isNotEmpty()) + pluginString += " lv2:shortName \"\"\"" + shortName + "\"\"\" ;\n"; + + // ranges + const ParameterRanges& ranges(plugin.getParameterRanges(i)); + + if (hints & kParameterIsInteger) + { + if (plugin.isParameterInput(i)) + pluginString += " lv2:default " + String(int(ranges.def)) + " ;\n"; + pluginString += " lv2:minimum " + String(int(ranges.min)) + " ;\n"; + pluginString += " lv2:maximum " + String(int(ranges.max)) + " ;\n"; + } + else if (hints & kParameterIsLogarithmic) + { + if (plugin.isParameterInput(i)) + { + if (d_isNotZero(ranges.def)) + pluginString += " lv2:default " + String(ranges.def) + " ;\n"; + else if (d_isEqual(ranges.def, ranges.max)) + pluginString += " lv2:default -0.0001 ;\n"; + else + pluginString += " lv2:default 0.0001 ;\n"; + } + + if (d_isNotZero(ranges.min)) + pluginString += " lv2:minimum " + String(ranges.min) + " ;\n"; + else + pluginString += " lv2:minimum 0.0001 ;\n"; + + if (d_isNotZero(ranges.max)) + pluginString += " lv2:maximum " + String(ranges.max) + " ;\n"; + else + pluginString += " lv2:maximum -0.0001 ;\n"; + } + else + { + if (plugin.isParameterInput(i)) + pluginString += " lv2:default " + String(ranges.def) + " ;\n"; + pluginString += " lv2:minimum " + String(ranges.min) + " ;\n"; + pluginString += " lv2:maximum " + String(ranges.max) + " ;\n"; + } + + // enumeration + const ParameterEnumerationValues& enumValues(plugin.getParameterEnumValues(i)); + + if (enumValues.count > 0) + { + if (enumValues.count >= 2 && enumValues.restrictedMode) + pluginString += " lv2:portProperty lv2:enumeration ;\n"; + + for (uint8_t j=0; j < enumValues.count; ++j) + { + const ParameterEnumerationValue& enumValue(enumValues.values[j]); + + if (j == 0) + pluginString += " lv2:scalePoint [\n"; + else + pluginString += " [\n"; + + if (enumValue.label.contains('"')) + pluginString += " rdfs:label \"\"\"" + enumValue.label + "\"\"\" ;\n"; + else + pluginString += " rdfs:label \"" + enumValue.label + "\" ;\n"; + + if (hints & kParameterIsInteger) + { + const int rounded = (int)(enumValue.value + (enumValue.value < 0.0f ? -0.5f : 0.5f)); + pluginString += " rdf:value " + String(rounded) + " ;\n"; + } + else + { + pluginString += " rdf:value " + String(enumValue.value) + " ;\n"; + } + + if (j+1 == enumValues.count) + pluginString += " ] ;\n"; + else + pluginString += " ] ,\n"; + } + } + + // MIDI CC binding + if (const uint8_t midiCC = plugin.getParameterMidiCC(i)) + { + pluginString += " midi:binding [\n"; + pluginString += " a midi:Controller ;\n"; + pluginString += " midi:controllerNumber " + String(midiCC) + " ;\n"; + pluginString += " ] ;\n"; + } + + // unit + const String& unit(plugin.getParameterUnit(i)); + + if (unit.isNotEmpty() && ! unit.contains(' ')) + { + String lunit(unit); + lunit.toLower(); + + /**/ if (lunit == "db") + { + pluginString += " unit:unit unit:db ;\n"; + } + else if (lunit == "hz") + { + pluginString += " unit:unit unit:hz ;\n"; + } + else if (lunit == "khz") + { + pluginString += " unit:unit unit:khz ;\n"; + } + else if (lunit == "mhz") + { + pluginString += " unit:unit unit:mhz ;\n"; + } + else if (lunit == "ms") + { + pluginString += " unit:unit unit:ms ;\n"; + } + else if (lunit == "s") + { + pluginString += " unit:unit unit:s ;\n"; + } + else if (lunit == "%") + { + pluginString += " unit:unit unit:pc ;\n"; + } + else + { + pluginString += " unit:unit [\n"; + pluginString += " a unit:Unit ;\n"; + pluginString += " rdfs:label \"" + unit + "\" ;\n"; + pluginString += " unit:symbol \"" + unit + "\" ;\n"; + if (hints & kParameterIsInteger) + pluginString += " unit:render \"%d " + unit + "\" ;\n"; + else + pluginString += " unit:render \"%f " + unit + "\" ;\n"; + pluginString += " ] ;\n"; + } + } + + // comment + const String& comment(plugin.getParameterDescription(i)); + + if (comment.isNotEmpty()) + { + if (comment.contains('"') || comment.contains('\n')) + pluginString += " rdfs:comment \"\"\"" + comment + "\"\"\" ;\n"; + else + pluginString += " rdfs:comment \"" + comment + "\" ;\n"; + } + + // hints + if (hints & kParameterIsBoolean) + { + if ((hints & kParameterIsTrigger) == kParameterIsTrigger) + pluginString += " lv2:portProperty <" LV2_PORT_PROPS__trigger "> ;\n"; + pluginString += " lv2:portProperty lv2:toggled ;\n"; + } + if (hints & kParameterIsInteger) + pluginString += " lv2:portProperty lv2:integer ;\n"; + if (hints & kParameterIsLogarithmic) + pluginString += " lv2:portProperty <" LV2_PORT_PROPS__logarithmic "> ;\n"; + if (hints & kParameterIsHidden) + pluginString += " lv2:portProperty <" LV2_PORT_PROPS__notOnGUI "> ;\n"; + if ((hints & kParameterIsAutomatable) == 0 && plugin.isParameterInput(i)) + { + pluginString += " lv2:portProperty <" LV2_PORT_PROPS__expensive "> ,\n"; + pluginString += " <" LV2_KXSTUDIO_PROPERTIES__NonAutomatable "> ;\n"; + } + + // group + const uint32_t groupId = plugin.getParameterGroupId(i); + + if (groupId != kPortGroupNone) + pluginString += " pg:group <" DISTRHO_PLUGIN_URI "#portGroup_" + + plugin.getPortGroupSymbolForId(groupId) + "> ;\n"; + + } // ! designated + + if (i+1 == count) + pluginString += " ] ;\n\n"; + else + pluginString += " ] ,\n"; + } + } + + // comment + { + const String comment(plugin.getDescription()); + + if (comment.isNotEmpty()) + { + if (comment.contains('"') || comment.contains('\n')) + pluginString += " rdfs:comment \"\"\"" + comment + "\"\"\" ;\n\n"; + else + pluginString += " rdfs:comment \"" + comment + "\" ;\n\n"; + } + } + + #ifdef DISTRHO_PLUGIN_BRAND + // MOD + pluginString += " mod:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n"; + pluginString += " mod:label \"" DISTRHO_PLUGIN_NAME "\" ;\n\n"; + #endif + + // name + { + const String name(plugin.getName()); + + if (name.contains('"')) + pluginString += " doap:name \"\"\"" + name + "\"\"\" ;\n"; + else + pluginString += " doap:name \"" + name + "\" ;\n"; + } + + // license + { + const String license(plugin.getLicense()); + + // Using URL as license + if (license.contains("://")) + { + pluginString += " doap:license <" + license + "> ;\n\n"; + } + // String contaning quotes, use as-is + else if (license.contains('"')) + { + pluginString += " doap:license \"\"\"" + license + "\"\"\" ;\n\n"; + } + // Regular license string, convert to URL as much as we can + else + { + const String uplicense(license.asUpper()); + + // for reference, see https://spdx.org/licenses/ + + // common licenses + /**/ if (uplicense == "AGPL-1.0-ONLY" || + uplicense == "AGPL1" || + uplicense == "AGPLV1") + { + pluginString += " doap:license <http://spdx.org/licenses/AGPL-1.0-only.html> ;\n\n"; + } + else if (uplicense == "AGPL-1.0-OR-LATER" || + uplicense == "AGPL1+" || + uplicense == "AGPLV1+") + { + pluginString += " doap:license <http://spdx.org/licenses/AGPL-1.0-or-later.html> ;\n\n"; + } + else if (uplicense == "AGPL-3.0-ONLY" || + uplicense == "AGPL3" || + uplicense == "AGPLV3") + { + pluginString += " doap:license <http://spdx.org/licenses/AGPL-3.0-only.html> ;\n\n"; + } + else if (uplicense == "AGPL-3.0-OR-LATER" || + uplicense == "AGPL3+" || + uplicense == "AGPLV3+") + { + pluginString += " doap:license <http://spdx.org/licenses/AGPL-3.0-or-later.html> ;\n\n"; + } + else if (uplicense == "APACHE-2.0" || + uplicense == "APACHE2" || + uplicense == "APACHE-2") + { + pluginString += " doap:license <http://spdx.org/licenses/Apache-2.0.html> ;\n\n"; + } + else if (uplicense == "BSD-2-CLAUSE" || + uplicense == "BSD2" || + uplicense == "BSD-2") + { + pluginString += " doap:license <http://spdx.org/licenses/BSD-2-Clause.html> ;\n\n"; + } + else if (uplicense == "BSD-3-CLAUSE" || + uplicense == "BSD3" || + uplicense == "BSD-3") + { + pluginString += " doap:license <http://spdx.org/licenses/BSD-3-Clause.html> ;\n\n"; + } + else if (uplicense == "GPL-2.0-ONLY" || + uplicense == "GPL2" || + uplicense == "GPLV2") + { + pluginString += " doap:license <http://spdx.org/licenses/GPL-2.0-only.html> ;\n\n"; + } + else if (uplicense == "GPL-2.0-OR-LATER" || + uplicense == "GPL2+" || + uplicense == "GPLV2+" || + uplicense == "GPLV2.0+" || + uplicense == "GPL V2+") + { + pluginString += " doap:license <http://spdx.org/licenses/GPL-2.0-or-later.html> ;\n\n"; + } + else if (uplicense == "GPL-3.0-ONLY" || + uplicense == "GPL3" || + uplicense == "GPLV3") + { + pluginString += " doap:license <http://spdx.org/licenses/GPL-3.0-only.html> ;\n\n"; + } + else if (uplicense == "GPL-3.0-OR-LATER" || + uplicense == "GPL3+" || + uplicense == "GPLV3+" || + uplicense == "GPLV3.0+" || + uplicense == "GPL V3+") + { + pluginString += " doap:license <http://spdx.org/licenses/GPL-3.0-or-later.html> ;\n\n"; + } + else if (uplicense == "ISC") + { + pluginString += " doap:license <http://spdx.org/licenses/ISC.html> ;\n\n"; + } + else if (uplicense == "LGPL-2.0-ONLY" || + uplicense == "LGPL2" || + uplicense == "LGPLV2") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-2.0-only.html> ;\n\n"; + } + else if (uplicense == "LGPL-2.0-OR-LATER" || + uplicense == "LGPL2+" || + uplicense == "LGPLV2+") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-2.0-or-later.html> ;\n\n"; + } + else if (uplicense == "LGPL-2.1-ONLY" || + uplicense == "LGPL2.1" || + uplicense == "LGPLV2.1") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-2.1-only.html> ;\n\n"; + } + else if (uplicense == "LGPL-2.1-OR-LATER" || + uplicense == "LGPL2.1+" || + uplicense == "LGPLV2.1+") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-2.1-or-later.html> ;\n\n"; + } + else if (uplicense == "LGPL-3.0-ONLY" || + uplicense == "LGPL3" || + uplicense == "LGPLV3") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-2.0-only.html> ;\n\n"; + } + else if (uplicense == "LGPL-3.0-OR-LATER" || + uplicense == "LGPL3+" || + uplicense == "LGPLV3+") + { + pluginString += " doap:license <http://spdx.org/licenses/LGPL-3.0-or-later.html> ;\n\n"; + } + else if (uplicense == "MIT") + { + pluginString += " doap:license <http://spdx.org/licenses/MIT.html> ;\n\n"; + } + + // generic fallbacks + else if (uplicense.startsWith("GPL")) + { + pluginString += " doap:license <http://opensource.org/licenses/gpl-license> ;\n\n"; + } + else if (uplicense.startsWith("LGPL")) + { + pluginString += " doap:license <http://opensource.org/licenses/lgpl-license> ;\n\n"; + } + + // unknown or not handled yet, log a warning + else + { + d_stderr("Unknown license string '%s'", license.buffer()); + pluginString += " doap:license \"" + license + "\" ;\n\n"; + } + } + } + + // developer + { + const String homepage(plugin.getHomePage()); + const String maker(plugin.getMaker()); + + pluginString += " doap:maintainer [\n"; + + if (maker.contains('"')) + pluginString += " foaf:name \"\"\"" + maker + "\"\"\" ;\n"; + else + pluginString += " foaf:name \"" + maker + "\" ;\n"; + + if (homepage.isNotEmpty()) + pluginString += " foaf:homepage <" + homepage + "> ;\n"; + + pluginString += " ] ;\n\n"; + } + + { + const uint32_t version(plugin.getVersion()); + + const uint32_t majorVersion = (version & 0xFF0000) >> 16; + /* */ uint32_t minorVersion = (version & 0x00FF00) >> 8; + const uint32_t microVersion = (version & 0x0000FF) >> 0; + + // NOTE: LV2 ignores 'major' version and says 0 for minor is pre-release/unstable. + if (majorVersion > 0) + minorVersion += 2; + + pluginString += " lv2:microVersion " + String(microVersion) + " ;\n"; + pluginString += " lv2:minorVersion " + String(minorVersion) + " .\n"; + } + + // port groups + if (const uint32_t portGroupCount = plugin.getPortGroupCount()) + { + bool isInput, isOutput; + + for (uint32_t i = 0; i < portGroupCount; ++i) + { + const PortGroupWithId& portGroup(plugin.getPortGroupByIndex(i)); + DISTRHO_SAFE_ASSERT_CONTINUE(portGroup.groupId != kPortGroupNone); + DISTRHO_SAFE_ASSERT_CONTINUE(portGroup.symbol.isNotEmpty()); + + pluginString += "\n<" DISTRHO_PLUGIN_URI "#portGroup_" + portGroup.symbol + ">\n"; + isInput = isOutput = false; + +#if DISTRHO_PLUGIN_NUM_INPUTS > 0 + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS && !isInput; ++i) + isInput = plugin.getAudioPort(true, i).groupId == portGroup.groupId; +#endif + +#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0 + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS && !isOutput; ++i) + isOutput = plugin.getAudioPort(false, i).groupId == portGroup.groupId; +#endif + + for (uint32_t i=0, count=plugin.getParameterCount(); i < count && (!isInput || !isOutput); ++i) + { + if (plugin.getParameterGroupId(i) == portGroup.groupId) + { + isInput = isInput || plugin.isParameterInput(i); + isOutput = isOutput || plugin.isParameterOutput(i); + } + } + + pluginString += " a "; + + if (isInput && !isOutput) + pluginString += "pg:InputGroup"; + else if (isOutput && !isInput) + pluginString += "pg:OutputGroup"; + else + pluginString += "pg:Group"; + + switch (portGroup.groupId) + { + case kPortGroupMono: + pluginString += " , pg:MonoGroup"; + break; + case kPortGroupStereo: + pluginString += " , pg:StereoGroup"; + break; + } + + pluginString += " ;\n"; + + // pluginString += " rdfs:label \"" + portGroup.name + "\" ;\n"; + pluginString += " lv2:name \"" + portGroup.name + "\" ;\n"; + pluginString += " lv2:symbol \"" + portGroup.symbol + "\" .\n"; + } + } + + pluginFile << pluginString; + pluginFile.close(); + std::cout << " done!" << std::endl; + } + + #if DISTRHO_PLUGIN_USES_MODGUI && !DISTRHO_PLUGIN_USES_CUSTOM_MODGUI + { + std::cout << "Writing modgui.ttl..."; std::cout.flush(); + std::fstream modguiFile("modgui.ttl", std::ios::out); + + String modguiString; + modguiString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n"; + modguiString += "@prefix modgui: <http://moddevices.com/ns/modgui#> .\n"; + modguiString += "\n"; + + modguiString += "<" DISTRHO_PLUGIN_URI ">\n"; + modguiString += " modgui:gui [\n"; + #ifdef DISTRHO_PLUGIN_BRAND + modguiString += " modgui:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n"; + #endif + modguiString += " modgui:label \"" DISTRHO_PLUGIN_NAME "\" ;\n"; + modguiString += " modgui:resourcesDirectory <modgui> ;\n"; + modguiString += " modgui:iconTemplate <modgui/icon.html> ;\n"; + modguiString += " modgui:javascript <modgui/javascript.js> ;\n"; + modguiString += " modgui:stylesheet <modgui/stylesheet.css> ;\n"; + modguiString += " modgui:screenshot <modgui/screenshot.png> ;\n"; + modguiString += " modgui:thumbnail <modgui/thumbnail.png> ;\n"; + + uint32_t numParametersOutputs = 0; + for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i) + { + if (plugin.isParameterOutput(i)) + ++numParametersOutputs; + } + if (numParametersOutputs != 0) + { + modguiString += " modgui:monitoredOutputs [\n"; + for (uint32_t i=0, j=0, count=plugin.getParameterCount(); i < count; ++i) + { + if (!plugin.isParameterOutput(i)) + continue; + modguiString += " lv2:symbol \"" + plugin.getParameterSymbol(i) + "\" ;\n"; + if (++j != numParametersOutputs) + modguiString += " ] , [\n"; + } + modguiString += " ] ;\n"; + } + + modguiString += " ] .\n"; + + modguiFile << modguiString; + modguiFile.close(); + std::cout << " done!" << std::endl; + } + + #ifdef DISTRHO_OS_WINDOWS + ::_mkdir("modgui"); + #else + ::mkdir("modgui", 0755); + #endif + + { + std::cout << "Writing modgui/javascript.js..."; std::cout.flush(); + std::fstream jsFile("modgui/javascript.js", std::ios::out); + + String jsString; + jsString += "function(e,f){\n"; + jsString += "'use strict';\nvar ps=["; + + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i) + jsString += "'lv2_" + plugin.getAudioPort(false, i).symbol + "',"; + for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i) + jsString += "'lv2_" + plugin.getAudioPort(true, i).symbol + "',"; + #if DISTRHO_LV2_USE_EVENTS_IN + jsString += "'lv2_events_in',"; + #endif + #if DISTRHO_LV2_USE_EVENTS_OUT + jsString += "'lv2_events_out',"; + #endif + #if DISTRHO_PLUGIN_WANT_LATENCY + jsString += "'lv2_latency',"; + #endif + + int32_t enabledIndex = INT32_MAX; + for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i) + { + jsString += "'" + plugin.getParameterSymbol(i) + "',"; + if (plugin.getParameterDesignation(i) == kParameterDesignationBypass) + enabledIndex = i; + } + jsString += "];\n"; + jsString += "var ei=" + String(enabledIndex != INT32_MAX ? enabledIndex : -1) + ";\n\n"; + jsString += "if(e.type==='start'){\n"; + jsString += "e.data.p={p:{},c:{},};\n\n"; + jsString += "var err=[];\n"; + jsString += "if(typeof(WebAssembly)==='undefined'){err.push('WebAssembly unsupported');}\n"; + jsString += "else{\n"; + jsString += "if(!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])))"; + jsString += "err.push('Bulk Memory Operations unsupported');\n"; + jsString += "if(!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,2,8,1,1,97,1,98,3,127,1,6,6,1,127,1,65,0,11,7,5,1,1,97,3,1])))"; + jsString += "err.push('Importable/Exportable mutable globals unsupported');\n"; + jsString += "}\n"; + jsString += "if(err.length!==0){e.icon.find('.canvas_wrapper').html('<h2>'+err.join('<br>')+'</h2>');return;}\n\n"; + jsString += "var s=document.createElement('script');\n"; + jsString += "s.setAttribute('async',true);\n"; + jsString += "s.setAttribute('src',e.api_version>=3?f.get_custom_resource_filename('module.js'):('/resources/module.js?uri='+escape(\"" DISTRHO_PLUGIN_URI "\")+'&r='+VERSION));\n"; + jsString += "s.setAttribute('type','text/javascript');\n"; + jsString += "s.onload=function(){\n"; + jsString += " Module_" DISTRHO_PLUGIN_MODGUI_CLASS_NAME "({\n"; + jsString += " locateFile: function(p,_){return e.api_version>=3?f.get_custom_resource_filename(p):('/resources/'+p+'?uri='+escape(\"" DISTRHO_PLUGIN_URI "\")+'&r='+VERSION)},\n"; + jsString += " postRun:function(m){\n"; + jsString += " var cn=e.icon.attr('mod-instance').replaceAll('/','_');\n"; + jsString += " var cnl=m.lengthBytesUTF8(cn) + 1;\n"; + jsString += " var cna=m._malloc(cnl);\n"; + jsString += " m.stringToUTF8(cn, cna, cnl);\n"; + jsString += " e.icon.find('canvas')[0].id=cn;\n"; + jsString += " var a=m.addFunction(function(i,v){f.set_port_value(ps[i],v);},'vif');\n"; + jsString += " var b=m.addFunction(function(u,v){f.patch_set(m.UTF8ToString(u),'s',m.UTF8ToString(v));},'vpp');\n"; + jsString += " var h=m._modgui_init(cna,a,b);\n"; + jsString += " m._free(cna);\n"; + jsString += " e.data.h=h;\n"; + jsString += " e.data.m=m;\n"; + jsString += " for(var u in e.data.p.p){\n"; + jsString += " var ul=m.lengthBytesUTF8(u)+1,ua=m._malloc(ul),v=e.data.p.p[u],vl=m.lengthBytesUTF8(v)+1,va=m._malloc(vl);\n"; + jsString += " m.stringToUTF8(u,ua,ul);\n"; + jsString += " m.stringToUTF8(v,va,vl);\n"; + jsString += " m._modgui_patch_set(h, ua, va);\n"; + jsString += " m._free(ua);\n"; + jsString += " m._free(va);\n"; + jsString += " }\n"; + jsString += " for(var symbol in e.data.p.c){m._modgui_param_set(h,ps.indexOf(symbol),e.data.p.c[symbol]);}\n"; + jsString += " delete e.data.p;\n"; + jsString += " window.dispatchEvent(new Event('resize'));\n"; + jsString += " },\n"; + jsString += " canvas:(function(){var c=e.icon.find('canvas')[0];c.addEventListener('webglcontextlost',function(e2){alert('WebGL context lost. You will need to reload the page.');e2.preventDefault();},false);return c;})(),\n"; + jsString += " });\n"; + jsString += "};\n"; + jsString += "document.head.appendChild(s);\n\n"; + jsString += "}else if(e.type==='change'){\n\n"; + jsString += "if(e.data.h && e.data.m){\n"; + jsString += " var m=e.data.m;\n"; + jsString += " if(e.uri){\n"; + jsString += " var ul=m.lengthBytesUTF8(e.uri)+1,ua=m._malloc(ul),vl=m.lengthBytesUTF8(e.value)+1,va=m._malloc(vl);\n"; + jsString += " m.stringToUTF8(e.uri,ua,ul);\n"; + jsString += " m.stringToUTF8(e.value,va,vl);\n"; + jsString += " m._modgui_patch_set(e.data.h,ua,va);\n"; + jsString += " m._free(ua);\n"; + jsString += " m._free(va);\n"; + jsString += " }else if(e.symbol===':bypass'){return;\n"; + jsString += " }else{m._modgui_param_set(e.data.h,ps.indexOf(e.symbol),e.value);}\n"; + jsString += "}else{\n"; + jsString += " if(e.symbol===':bypass')return;\n"; + jsString += " if(e.uri){e.data.p.p[e.uri]=e.value;}else{e.data.p.c[e.symbol]=e.value;}\n"; + jsString += "}\n\n"; + jsString += "}else if(e.type==='end'){\n"; + jsString += " if(e.data.h && e.data.m){\n"; + jsString += " var h = e.data.h;\n"; + jsString += " var m = e.data.m;\n"; + jsString += " e.data.h = e.data.m = null;\n"; + jsString += " m._modgui_cleanup(h);\n"; + jsString += "}\n\n"; + jsString += "}\n}\n"; + jsFile << jsString; + jsFile.close(); + std::cout << " done!" << std::endl; + } + + { + std::cout << "Writing modgui/icon.html..."; std::cout.flush(); + std::fstream iconFile("modgui/icon.html", std::ios::out); + + iconFile << "<div class='" DISTRHO_PLUGIN_MODGUI_CLASS_NAME " mod-pedal'>" << std::endl; + iconFile << " <div mod-role='drag-handle' class='mod-drag-handle'></div>" << std::endl; + iconFile << " <div class='mod-plugin-title'><h1>{{#brand}}{{brand}} | {{/brand}}{{label}}</h1></div>" << std::endl; + iconFile << " <div class='mod-light on' mod-role='bypass-light'></div>" << std::endl; + iconFile << " <div class='mod-control-group mod-switch'>" << std::endl; + iconFile << " <div class='mod-control-group mod-switch-image mod-port transport' mod-role='bypass' mod-widget='film'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " <div class='canvas_wrapper'>" << std::endl; + iconFile << " <canvas oncontextmenu='event.preventDefault()' tabindex=-1></canvas>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " <div class='mod-pedal-input'>" << std::endl; + iconFile << " {{#effect.ports.audio.input}}" << std::endl; + iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.audio.input}}" << std::endl; + iconFile << " {{#effect.ports.midi.input}}" << std::endl; + iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.midi.input}}" << std::endl; + iconFile << " {{#effect.ports.cv.input}}" << std::endl; + iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.cv.input}}" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " <div class='mod-pedal-output'>" << std::endl; + iconFile << " {{#effect.ports.audio.output}}" << std::endl; + iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.audio.output}}" << std::endl; + iconFile << " {{#effect.ports.midi.output}}" << std::endl; + iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.midi.output}}" << std::endl; + iconFile << " {{#effect.ports.cv.output}}" << std::endl; + iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl; + iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << " {{/effect.ports.cv.output}}" << std::endl; + iconFile << " </div>" << std::endl; + iconFile << "</div>" << std::endl; + + iconFile.close(); + std::cout << " done!" << std::endl; + } + + { + std::cout << "Writing modgui/stylesheet.css..."; std::cout.flush(); + std::fstream stylesheetFile("modgui/stylesheet.css", std::ios::out); + + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal{" << std::endl; + stylesheetFile << " padding:0;" << std::endl; + stylesheetFile << " margin:0;" << std::endl; + stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl; + stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT + 50) + "px;" << std::endl; + stylesheetFile << " background:#2a2e32;" << std::endl; + stylesheetFile << " border-radius:20px 20px 0 0;" << std::endl; + stylesheetFile << " color:#fff;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper{" << std::endl; + stylesheetFile << " --device-pixel-ratio:1;" << std::endl; + stylesheetFile << " /*image-rendering:pixelated;*/" << std::endl; + stylesheetFile << " /*image-rendering:crisp-edges;*/" << std::endl; + stylesheetFile << " background:#000;" << std::endl; + stylesheetFile << " position:absolute;" << std::endl; + stylesheetFile << " top:50px;" << std::endl; + stylesheetFile << " transform-origin:0 0 0;" << std::endl; + stylesheetFile << " transform:scale(calc(1/var(--device-pixel-ratio)));" << std::endl; + stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl; + stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT) + "px;" << std::endl; + stylesheetFile << " text-align:center;" << std::endl; + stylesheetFile << " z-index:21;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "/*" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper:focus-within{" << std::endl; + stylesheetFile << " z-index:21;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "*/" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-plugin-title{" << std::endl; + stylesheetFile << " position:absolute;" << std::endl; + stylesheetFile << " text-align:center;" << std::endl; + stylesheetFile << " width:100%;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal h1{" << std::endl; + stylesheetFile << " font-size:20px;" << std::endl; + stylesheetFile << " font-weight:bold;" << std::endl; + stylesheetFile << " line-height:50px;" << std::endl; + stylesheetFile << " margin:0;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-control-group{" << std::endl; + stylesheetFile << " position:absolute;" << std::endl; + stylesheetFile << " left:5px;" << std::endl; + stylesheetFile << " z-index:35;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-input," << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-output{" << std::endl; + stylesheetFile << " top:75px;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-input," << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-output{" << std::endl; + stylesheetFile << " margin-bottom:25px;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .jack-disconnected{" << std::endl; + stylesheetFile << " top:0px!important;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image{" << std::endl; + stylesheetFile << " background-image: url(/img/switch.png);" << std::endl; + stylesheetFile << " background-position: left center;" << std::endl; + stylesheetFile << " background-repeat: no-repeat;" << std::endl; + stylesheetFile << " background-size: auto 50px;" << std::endl; + stylesheetFile << " font-weight: bold;" << std::endl; + stylesheetFile << " width: 100px;" << std::endl; + stylesheetFile << " height: 50px;" << std::endl; + stylesheetFile << " cursor: pointer;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.off{" << std::endl; + stylesheetFile << " background-position: right center !important;" << std::endl; + stylesheetFile << "}" << std::endl; + stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.on{" << std::endl; + stylesheetFile << " background-position: left center !important;" << std::endl; + stylesheetFile << "}" << std::endl; + + stylesheetFile.close(); + std::cout << " done!" << std::endl; + } + #endif // DISTRHO_PLUGIN_USES_MODGUI && !DISTRHO_PLUGIN_USES_CUSTOM_MODGUI + + // --------------------------------------------- + +#if DISTRHO_PLUGIN_HAS_UI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS + { + std::cout << "Writing " << uiTTL << "..."; std::cout.flush(); + std::fstream uiFile(uiTTL, std::ios::out); + + String uiString; + uiString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n"; + uiString += "@prefix ui: <" LV2_UI_PREFIX "> .\n"; + uiString += "@prefix opts: <" LV2_OPTIONS_PREFIX "> .\n"; + uiString += "\n"; + + uiString += "<" DISTRHO_UI_URI ">\n"; + + addAttribute(uiString, "lv2:extensionData", lv2ManifestUiExtensionData, 4); + addAttribute(uiString, "lv2:optionalFeature", lv2ManifestUiOptionalFeatures, 4); + addAttribute(uiString, "lv2:requiredFeature", lv2ManifestUiRequiredFeatures, 4); + addAttribute(uiString, "opts:supportedOption", lv2ManifestUiSupportedOptions, 4, true); + + uiFile << uiString; + uiFile.close(); + std::cout << " done!" << std::endl; + } +#endif + + // --------------------------------------------- + +#if DISTRHO_PLUGIN_WANT_PROGRAMS + { + std::cout << "Writing presets.ttl..."; std::cout.flush(); + std::fstream presetsFile("presets.ttl", std::ios::out); + + String presetsString; + presetsString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n"; + presetsString += "@prefix pset: <" LV2_PRESETS_PREFIX "> .\n"; +# if DISTRHO_PLUGIN_WANT_STATE + presetsString += "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n"; + presetsString += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"; + presetsString += "@prefix state: <" LV2_STATE_PREFIX "> .\n"; + presetsString += "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n"; +# endif + presetsString += "\n"; + + const uint32_t numParameters = plugin.getParameterCount(); + const uint32_t numPrograms = plugin.getProgramCount(); +# if DISTRHO_PLUGIN_WANT_FULL_STATE + const uint32_t numStates = plugin.getStateCount(); + const bool valid = numParameters != 0 || numStates != 0; +# else + const bool valid = numParameters != 0; +# endif + + DISTRHO_CUSTOM_SAFE_ASSERT_RETURN("Programs require parameters or full state", valid, presetsFile.close()); + + const String presetSeparator(std::strstr(DISTRHO_PLUGIN_URI, "#") != nullptr ? ":" : "#"); + + char strBuf[0xff+1]; + strBuf[0xff] = '\0'; + + String presetString; + +# if DISTRHO_PLUGIN_WANT_FULL_STATE + for (uint32_t i=0; i<numStates; ++i) + { + if (plugin.getStateHints(i) & kStateIsHostReadable) + continue; + + // readable states are defined as lv2 parameters. + // non-readable states have no definition, but one is needed for presets and ttl validation. + presetString = "<" DISTRHO_PLUGIN_LV2_STATE_PREFIX + plugin.getStateKey(i) + ">\n"; + presetString += " a owl:DatatypeProperty ;\n"; + presetString += " rdfs:label \"Plugin state key-value string pair\" ;\n"; + presetString += " rdfs:domain state:State ;\n"; + presetString += " rdfs:range xsd:string .\n\n"; + presetsString += presetString; + } +# endif + + for (uint32_t i=0; i<numPrograms; ++i) + { + std::snprintf(strBuf, 0xff, "%03i", i+1); + + plugin.loadProgram(i); + + presetString = "<" DISTRHO_PLUGIN_URI + presetSeparator + "preset" + strBuf + ">\n"; + +# if DISTRHO_PLUGIN_WANT_FULL_STATE + presetString += " state:state [\n"; + for (uint32_t j=0; j<numStates; ++j) + { + const String key = plugin.getStateKey(j); + const String value = plugin.getStateValue(key); + + presetString += " <"; + + if (plugin.getStateHints(j) & kStateIsHostReadable) + presetString += DISTRHO_PLUGIN_URI "#"; + else + presetString += DISTRHO_PLUGIN_LV2_STATE_PREFIX; + + presetString += key + ">"; + + if (value.length() < 10) + presetString += " \"" + value + "\" ;\n"; + else + presetString += "\n\"\"\"" + value + "\"\"\" ;\n"; + } + + if (numParameters > 0) + presetString += " ] ;\n\n"; + else + presetString += " ] .\n\n"; +# endif + + bool firstParameter = true; + + for (uint32_t j=0; j <numParameters; ++j) + { + if (plugin.isParameterOutput(j)) + continue; + + if (firstParameter) + { + presetString += " lv2:port [\n"; + firstParameter = false; + } + else + { + presetString += " [\n"; + } + + String parameterSymbol = plugin.getParameterSymbol(j); + float parameterValue = plugin.getParameterValue(j); + + if (plugin.getParameterDesignation(j) == kParameterDesignationBypass) + { + parameterSymbol = DISTRHO_BYPASS_PARAMETER_NAME; + parameterValue = 1.0f - parameterValue; + } + + presetString += " lv2:symbol \"" + parameterSymbol + "\" ;\n"; + + if (plugin.getParameterHints(j) & kParameterIsInteger) + presetString += " pset:value " + String(int(parameterValue)) + " ;\n"; + else + presetString += " pset:value " + String(parameterValue) + " ;\n"; + + if (j+1 == numParameters || plugin.isParameterOutput(j+1)) + presetString += " ] .\n\n"; + else + presetString += " ] ,\n"; + } + + presetsString += presetString; + } + + presetsFile << presetsString; + presetsFile.close(); + std::cout << " done!" << std::endl; + } +#endif +}