DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

DistrhoPluginLV2export.cpp (73989B)


      1 /*
      2  * DISTRHO Plugin Framework (DPF)
      3  * Copyright (C) 2012-2023 Filipe Coelho <falktx@falktx.com>
      4  *
      5  * Permission to use, copy, modify, and/or distribute this software for any purpose with
      6  * or without fee is hereby granted, provided that the above copyright notice and this
      7  * permission notice appear in all copies.
      8  *
      9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
     10  * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
     11  * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
     12  * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
     13  * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
     14  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 #include "DistrhoPluginInternal.hpp"
     18 #include "../DistrhoPluginUtils.hpp"
     19 
     20 #include "lv2/atom.h"
     21 #include "lv2/buf-size.h"
     22 #include "lv2/data-access.h"
     23 #include "lv2/instance-access.h"
     24 #include "lv2/midi.h"
     25 #include "lv2/options.h"
     26 #include "lv2/parameters.h"
     27 #include "lv2/patch.h"
     28 #include "lv2/port-groups.h"
     29 #include "lv2/port-props.h"
     30 #include "lv2/presets.h"
     31 #include "lv2/resize-port.h"
     32 #include "lv2/state.h"
     33 #include "lv2/time.h"
     34 #include "lv2/ui.h"
     35 #include "lv2/units.h"
     36 #include "lv2/urid.h"
     37 #include "lv2/worker.h"
     38 #include "lv2/lv2_kxstudio_properties.h"
     39 #include "lv2/lv2_programs.h"
     40 #include "lv2/control-input-port-change-request.h"
     41 
     42 #ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD
     43 # include "mod-license.h"
     44 #endif
     45 
     46 #ifdef DISTRHO_OS_WINDOWS
     47 # include <direct.h>
     48 #else
     49 # include <sys/stat.h>
     50 # include <sys/types.h>
     51 # include <unistd.h>
     52 #endif
     53 
     54 #include <fstream>
     55 #include <iostream>
     56 
     57 #ifndef DISTRHO_PLUGIN_URI
     58 # error DISTRHO_PLUGIN_URI undefined!
     59 #endif
     60 
     61 #ifndef DISTRHO_PLUGIN_LV2_STATE_PREFIX
     62 # define DISTRHO_PLUGIN_LV2_STATE_PREFIX "urn:distrho:"
     63 #endif
     64 
     65 #ifndef DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE
     66 # define DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE 2048
     67 #endif
     68 
     69 #ifndef DISTRHO_PLUGIN_USES_MODGUI
     70 # define DISTRHO_PLUGIN_USES_MODGUI 0
     71 #endif
     72 
     73 #ifndef DISTRHO_PLUGIN_USES_CUSTOM_MODGUI
     74 # define DISTRHO_PLUGIN_USES_CUSTOM_MODGUI 0
     75 #endif
     76 
     77 #if DISTRHO_PLUGIN_HAS_EMBED_UI
     78 # if defined(DISTRHO_OS_HAIKU)
     79 #  define DISTRHO_LV2_UI_TYPE "BeUI"
     80 # elif defined(DISTRHO_OS_MAC)
     81 #  define DISTRHO_LV2_UI_TYPE "CocoaUI"
     82 # elif defined(DISTRHO_OS_WINDOWS)
     83 #  define DISTRHO_LV2_UI_TYPE "WindowsUI"
     84 # else
     85 #  define DISTRHO_LV2_UI_TYPE "X11UI"
     86 # endif
     87 #else
     88 # define DISTRHO_LV2_UI_TYPE "UI"
     89 #endif
     90 
     91 #define DISTRHO_LV2_USE_EVENTS_IN  (DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_TIMEPOS || DISTRHO_PLUGIN_WANT_STATE)
     92 #define DISTRHO_LV2_USE_EVENTS_OUT (DISTRHO_PLUGIN_WANT_MIDI_OUTPUT || DISTRHO_PLUGIN_WANT_STATE)
     93 
     94 // --------------------------------------------------------------------------------------------------------------------
     95 
     96 static constexpr const char* const lv2ManifestPluginExtensionData[] = {
     97     "opts:interface",
     98    #if DISTRHO_PLUGIN_WANT_STATE
     99     LV2_STATE__interface,
    100     LV2_WORKER__interface,
    101    #endif
    102    #if DISTRHO_PLUGIN_WANT_PROGRAMS
    103     LV2_PROGRAMS__Interface,
    104    #endif
    105    #ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD
    106     MOD_LICENSE__interface,
    107    #endif
    108     nullptr
    109 };
    110 
    111 static constexpr const char* const lv2ManifestPluginOptionalFeatures[] = {
    112    #if DISTRHO_PLUGIN_IS_RT_SAFE
    113     LV2_CORE__hardRTCapable,
    114    #endif
    115     LV2_BUF_SIZE__boundedBlockLength,
    116    #if DISTRHO_PLUGIN_WANT_STATE
    117     LV2_STATE__mapPath,
    118     LV2_STATE__freePath,
    119    #endif
    120    #if DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
    121     LV2_CONTROL_INPUT_PORT_CHANGE_REQUEST_URI,
    122    #endif
    123     nullptr
    124 };
    125 
    126 static constexpr const char* const lv2ManifestPluginRequiredFeatures[] = {
    127     "opts:options",
    128     LV2_URID__map,
    129    #if DISTRHO_PLUGIN_WANT_STATE
    130     LV2_WORKER__schedule,
    131    #endif
    132    #ifdef DISTRHO_PLUGIN_LICENSED_FOR_MOD
    133     MOD_LICENSE__feature,
    134    #endif
    135     nullptr
    136 };
    137 
    138 static constexpr const char* const lv2ManifestPluginSupportedOptions[] =
    139 {
    140     LV2_BUF_SIZE__nominalBlockLength,
    141     LV2_BUF_SIZE__maxBlockLength,
    142     LV2_PARAMETERS__sampleRate,
    143     nullptr
    144 };
    145 
    146 #if DISTRHO_PLUGIN_HAS_UI
    147 static constexpr const char* const lv2ManifestUiExtensionData[] = {
    148     "opts:interface",
    149     "ui:idleInterface",
    150     "ui:showInterface",
    151    #if DISTRHO_PLUGIN_WANT_PROGRAMS
    152     LV2_PROGRAMS__UIInterface,
    153    #endif
    154     nullptr
    155 };
    156 
    157 static constexpr const char* const lv2ManifestUiOptionalFeatures[] = {
    158   #if DISTRHO_PLUGIN_HAS_EMBED_UI
    159    #if !DISTRHO_UI_USER_RESIZABLE
    160     "ui:noUserResize",
    161    #endif
    162     "ui:parent",
    163     "ui:touch",
    164   #endif
    165     "ui:requestValue",
    166     nullptr
    167 };
    168 
    169 static constexpr const char* const lv2ManifestUiRequiredFeatures[] = {
    170     "opts:options",
    171     "ui:idleInterface",
    172    #if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    173     LV2_DATA_ACCESS_URI,
    174     LV2_INSTANCE_ACCESS_URI,
    175    #endif
    176     LV2_URID__map,
    177     nullptr
    178 };
    179 
    180 static constexpr const char* const lv2ManifestUiSupportedOptions[] = {
    181     LV2_PARAMETERS__sampleRate,
    182     nullptr
    183 };
    184 #endif // DISTRHO_PLUGIN_HAS_UI
    185 
    186 static void addAttribute(DISTRHO_NAMESPACE::String& text,
    187                          const char* const attribute,
    188                          const char* const values[],
    189                          const uint indent,
    190                          const bool endInDot = false)
    191 {
    192     if (values[0] == nullptr)
    193     {
    194         if (endInDot)
    195         {
    196             bool found;
    197             const size_t index = text.rfind(';', &found);
    198             if (found) text[index] = '.';
    199         }
    200         return;
    201     }
    202 
    203     const size_t attributeLength = std::strlen(attribute);
    204 
    205     for (uint i = 0; values[i] != nullptr; ++i)
    206     {
    207         for (uint j = 0; j < indent; ++j)
    208             text += " ";
    209 
    210         if (i == 0)
    211         {
    212             text += attribute;
    213         }
    214         else
    215         {
    216             for (uint j = 0; j < attributeLength; ++j)
    217                 text += " ";
    218         }
    219 
    220         text += " ";
    221 
    222         const bool isUrl = std::strstr(values[i], "://") != nullptr || std::strncmp(values[i], "urn:", 4) == 0;
    223         if (isUrl) text += "<";
    224         text += values[i];
    225         if (isUrl) text += ">";
    226         text += values[i + 1] ? " ,\n" : (endInDot ? " .\n\n" : " ;\n\n");
    227     }
    228 }
    229 
    230 // --------------------------------------------------------------------------------------------------------------------
    231 
    232 DISTRHO_PLUGIN_EXPORT
    233 void lv2_generate_ttl(const char* const basename)
    234 {
    235     USE_NAMESPACE_DISTRHO
    236 
    237     String bundlePath(getBinaryFilename());
    238     if (bundlePath.isNotEmpty())
    239     {
    240         bundlePath.truncate(bundlePath.rfind(DISTRHO_OS_SEP));
    241     }
    242 #ifndef DISTRHO_OS_WINDOWS
    243     else if (char* const cwd = ::getcwd(nullptr, 0))
    244     {
    245         bundlePath = cwd;
    246         std::free(cwd);
    247     }
    248 #endif
    249     d_nextBundlePath = bundlePath.buffer();
    250 
    251     // Dummy plugin to get data from
    252     d_nextBufferSize = 512;
    253     d_nextSampleRate = 44100.0;
    254     d_nextPluginIsDummy = true;
    255     PluginExporter plugin(nullptr, nullptr, nullptr, nullptr);
    256     d_nextBufferSize = 0;
    257     d_nextSampleRate = 0.0;
    258     d_nextPluginIsDummy = false;
    259 
    260     const String pluginDLL(basename);
    261     const String pluginTTL(pluginDLL + ".ttl");
    262 
    263 #if DISTRHO_PLUGIN_HAS_UI
    264     String pluginUI(pluginDLL);
    265 # if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    266     pluginUI.truncate(pluginDLL.rfind("_dsp"));
    267     pluginUI += "_ui";
    268     const String uiTTL(pluginUI + ".ttl");
    269 # endif
    270 #endif
    271 
    272     // ---------------------------------------------
    273 
    274     {
    275         std::cout << "Writing manifest.ttl..."; std::cout.flush();
    276         std::fstream manifestFile("manifest.ttl", std::ios::out);
    277 
    278         String manifestString;
    279         manifestString += "@prefix lv2:  <" LV2_CORE_PREFIX "> .\n";
    280         manifestString += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n";
    281 #if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    282         manifestString += "@prefix opts: <" LV2_OPTIONS_PREFIX "> .\n";
    283 #endif
    284 #if DISTRHO_PLUGIN_WANT_PROGRAMS
    285         manifestString += "@prefix pset: <" LV2_PRESETS_PREFIX "> .\n";
    286 #endif
    287 #if DISTRHO_PLUGIN_HAS_UI
    288         manifestString += "@prefix ui:   <" LV2_UI_PREFIX "> .\n";
    289 #endif
    290         manifestString += "\n";
    291 
    292         manifestString += "<" DISTRHO_PLUGIN_URI ">\n";
    293         manifestString += "    a lv2:Plugin ;\n";
    294         manifestString += "    lv2:binary <" + pluginDLL + "." DISTRHO_DLL_EXTENSION "> ;\n";
    295 #if DISTRHO_PLUGIN_USES_MODGUI
    296         manifestString += "    rdfs:seeAlso <" + pluginTTL + "> ,\n";
    297         manifestString += "                 <modgui.ttl> .\n";
    298 #else
    299         manifestString += "    rdfs:seeAlso <" + pluginTTL + "> .\n";
    300 #endif
    301         manifestString += "\n";
    302 
    303 #if DISTRHO_PLUGIN_HAS_UI
    304         manifestString += "<" DISTRHO_UI_URI ">\n";
    305         manifestString += "    a ui:" DISTRHO_LV2_UI_TYPE " ;\n";
    306         manifestString += "    ui:binary <" + pluginUI + "." DISTRHO_DLL_EXTENSION "> ;\n";
    307 # if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    308         addAttribute(manifestString, "lv2:extensionData", lv2ManifestUiExtensionData, 4);
    309         addAttribute(manifestString, "lv2:optionalFeature", lv2ManifestUiOptionalFeatures, 4);
    310         addAttribute(manifestString, "lv2:requiredFeature", lv2ManifestUiRequiredFeatures, 4);
    311         addAttribute(manifestString, "opts:supportedOption", lv2ManifestUiSupportedOptions, 4, true);
    312 # else // DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    313         manifestString += "    rdfs:seeAlso <" + uiTTL + "> .\n";
    314 # endif // DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    315         manifestString += "\n";
    316 #endif
    317 
    318 #if DISTRHO_PLUGIN_WANT_PROGRAMS
    319         const String presetSeparator(std::strstr(DISTRHO_PLUGIN_URI, "#") != nullptr ? ":" : "#");
    320 
    321         char strBuf[0xff+1];
    322         strBuf[0xff] = '\0';
    323 
    324         String presetString;
    325 
    326         // Presets
    327         for (uint32_t i = 0; i < plugin.getProgramCount(); ++i)
    328         {
    329             std::snprintf(strBuf, 0xff, "%03i", i+1);
    330 
    331             const String& programName(plugin.getProgramName(i));
    332 
    333             presetString  = "<" DISTRHO_PLUGIN_URI + presetSeparator + "preset" + strBuf + ">\n";
    334             presetString += "    a pset:Preset ;\n";
    335             presetString += "    lv2:appliesTo <" DISTRHO_PLUGIN_URI "> ;\n";
    336 
    337             if (programName.contains('"'))
    338                 presetString += "    rdfs:label\"\"\"" + programName + "\"\"\" ;\n";
    339             else
    340                 presetString += "    rdfs:label \"" + programName + "\" ;\n";
    341 
    342             presetString += "    rdfs:seeAlso <presets.ttl> .\n";
    343             presetString += "\n";
    344 
    345             manifestString += presetString;
    346         }
    347 #endif
    348 
    349         manifestFile << manifestString;
    350         manifestFile.close();
    351         std::cout << " done!" << std::endl;
    352     }
    353 
    354     // ---------------------------------------------
    355 
    356     {
    357         std::cout << "Writing " << pluginTTL << "..."; std::cout.flush();
    358         std::fstream pluginFile(pluginTTL, std::ios::out);
    359 
    360         String pluginString;
    361 
    362         // header
    363 #if DISTRHO_LV2_USE_EVENTS_IN || DISTRHO_LV2_USE_EVENTS_OUT
    364         pluginString += "@prefix atom:  <" LV2_ATOM_PREFIX "> .\n";
    365 #endif
    366         pluginString += "@prefix doap:  <http://usefulinc.com/ns/doap#> .\n";
    367         pluginString += "@prefix foaf:  <http://xmlns.com/foaf/0.1/> .\n";
    368         pluginString += "@prefix lv2:   <" LV2_CORE_PREFIX "> .\n";
    369         pluginString += "@prefix midi:  <" LV2_MIDI_PREFIX "> .\n";
    370         pluginString += "@prefix mod:   <http://moddevices.com/ns/mod#> .\n";
    371         pluginString += "@prefix opts:  <" LV2_OPTIONS_PREFIX "> .\n";
    372         pluginString += "@prefix pg:    <" LV2_PORT_GROUPS_PREFIX "> .\n";
    373         pluginString += "@prefix patch: <" LV2_PATCH_PREFIX "> .\n";
    374         pluginString += "@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n";
    375         pluginString += "@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .\n";
    376 #if DISTRHO_LV2_USE_EVENTS_IN || DISTRHO_LV2_USE_EVENTS_OUT
    377         pluginString += "@prefix rsz:   <" LV2_RESIZE_PORT_PREFIX "> .\n";
    378 #endif
    379         pluginString += "@prefix spdx:  <http://spdx.org/rdf/terms#> .\n";
    380 #if DISTRHO_PLUGIN_HAS_UI
    381         pluginString += "@prefix ui:    <" LV2_UI_PREFIX "> .\n";
    382 #endif
    383         pluginString += "@prefix unit:  <" LV2_UNITS_PREFIX "> .\n";
    384         pluginString += "\n";
    385 
    386 #if DISTRHO_PLUGIN_WANT_STATE
    387         bool hasHostVisibleState = false;
    388 
    389         for (uint32_t i=0, count=plugin.getStateCount(); i < count; ++i)
    390         {
    391             const uint32_t hints = plugin.getStateHints(i);
    392 
    393             if ((hints & kStateIsHostReadable) == 0x0)
    394                 continue;
    395 
    396             pluginString += "<" DISTRHO_PLUGIN_URI "#" + plugin.getStateKey(i) + ">\n";
    397             pluginString += "    a lv2:Parameter ;\n";
    398             pluginString += "    rdfs:label \"" + plugin.getStateLabel(i) + "\" ;\n";
    399 
    400             const String& comment(plugin.getStateDescription(i));
    401 
    402             if (comment.isNotEmpty())
    403             {
    404                 if (comment.contains('"') || comment.contains('\n'))
    405                     pluginString += "    rdfs:comment \"\"\"" + comment + "\"\"\" ;\n";
    406                 else
    407                     pluginString += "    rdfs:comment \"" + comment + "\" ;\n";
    408             }
    409 
    410             if ((hints & kStateIsFilenamePath) == kStateIsFilenamePath)
    411             {
    412                #ifdef __MOD_DEVICES__
    413                 const String& fileTypes(plugin.getStateFileTypes(i));
    414                 if (fileTypes.isNotEmpty())
    415                     pluginString += "    mod:fileTypes \"" + fileTypes + "\" ; \n";
    416                #endif
    417                 pluginString += "    rdfs:range atom:Path .\n\n";
    418             }
    419             else
    420             {
    421                 pluginString += "    rdfs:range atom:String .\n\n";
    422             }
    423 
    424             hasHostVisibleState = true;
    425         }
    426 #endif
    427 
    428         // plugin
    429         pluginString += "<" DISTRHO_PLUGIN_URI ">\n";
    430 #ifdef DISTRHO_PLUGIN_LV2_CATEGORY
    431         pluginString += "    a " DISTRHO_PLUGIN_LV2_CATEGORY ", lv2:Plugin, doap:Project ;\n";
    432 #elif DISTRHO_PLUGIN_IS_SYNTH
    433         pluginString += "    a lv2:InstrumentPlugin, lv2:Plugin, doap:Project ;\n";
    434 #else
    435         pluginString += "    a lv2:Plugin, doap:Project ;\n";
    436 #endif
    437         pluginString += "\n";
    438 
    439         addAttribute(pluginString, "lv2:extensionData", lv2ManifestPluginExtensionData, 4);
    440         addAttribute(pluginString, "lv2:optionalFeature", lv2ManifestPluginOptionalFeatures, 4);
    441         addAttribute(pluginString, "lv2:requiredFeature", lv2ManifestPluginRequiredFeatures, 4);
    442         addAttribute(pluginString, "opts:supportedOption", lv2ManifestPluginSupportedOptions, 4);
    443 
    444 #if DISTRHO_PLUGIN_WANT_STATE
    445         if (hasHostVisibleState)
    446         {
    447             for (uint32_t i=0, count=plugin.getStateCount(); i < count; ++i)
    448             {
    449                 const uint32_t hints = plugin.getStateHints(i);
    450 
    451                 if ((hints & kStateIsHostReadable) == 0x0)
    452                     continue;
    453 
    454                 const String& key(plugin.getStateKey(i));
    455 
    456                 if ((hints & kStateIsHostWritable) == kStateIsHostWritable)
    457                     pluginString += "    patch:writable <" DISTRHO_PLUGIN_URI "#" + key + "> ;\n";
    458                 else
    459                     pluginString += "    patch:readable <" DISTRHO_PLUGIN_URI "#" + key + "> ;\n";
    460             }
    461             pluginString += "\n";
    462         }
    463 #endif
    464 
    465         // UI
    466 #if DISTRHO_PLUGIN_HAS_UI
    467         pluginString += "    ui:ui <" DISTRHO_UI_URI "> ;\n";
    468         pluginString += "\n";
    469 #endif
    470 
    471         {
    472             uint32_t portIndex = 0;
    473 
    474 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
    475             for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i, ++portIndex)
    476             {
    477                 const AudioPort& port(plugin.getAudioPort(true, i));
    478                 const bool cvPortScaled = port.hints & kCVPortHasScaledRange;
    479 
    480                 if (i == 0)
    481                     pluginString += "    lv2:port [\n";
    482                 else
    483                     pluginString += "    [\n";
    484 
    485                 if (cvPortScaled)
    486                     pluginString += "        a lv2:InputPort, lv2:CVPort, mod:CVPort ;\n";
    487                 else if (port.hints & kAudioPortIsCV)
    488                     pluginString += "        a lv2:InputPort, lv2:CVPort ;\n";
    489                 else
    490                     pluginString += "        a lv2:InputPort, lv2:AudioPort ;\n";
    491 
    492                 pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    493                 pluginString += "        lv2:symbol \"lv2_" + port.symbol + "\" ;\n";
    494                 pluginString += "        lv2:name \"" + port.name + "\" ;\n";
    495 
    496                 if (port.hints & kAudioPortIsSidechain)
    497                     pluginString += "        lv2:portProperty lv2:isSideChain;\n";
    498 
    499                 if (port.groupId != kPortGroupNone)
    500                 {
    501                     pluginString += "        pg:group <" DISTRHO_PLUGIN_URI "#portGroup_"
    502                                     + plugin.getPortGroupSymbolForId(port.groupId) + "> ;\n";
    503 
    504                     switch (port.groupId)
    505                     {
    506                     case kPortGroupMono:
    507                         pluginString += "        lv2:designation pg:center ;\n";
    508                         break;
    509                     case kPortGroupStereo:
    510                         if (i == 1)
    511                             pluginString += "        lv2:designation pg:right ;\n";
    512                         else
    513                             pluginString += "        lv2:designation pg:left ;\n";
    514                         break;
    515                     }
    516                 }
    517 
    518                 // set ranges
    519                 if (port.hints & kCVPortHasBipolarRange)
    520                 {
    521                     if (cvPortScaled)
    522                     {
    523                         pluginString += "        lv2:minimum -5.0 ;\n";
    524                         pluginString += "        lv2:maximum 5.0 ;\n";
    525                     }
    526                     else
    527                     {
    528                         pluginString += "        lv2:minimum -1.0 ;\n";
    529                         pluginString += "        lv2:maximum 1.0 ;\n";
    530                     }
    531                 }
    532                 else if (port.hints & kCVPortHasNegativeUnipolarRange)
    533                 {
    534                     if (cvPortScaled)
    535                     {
    536                         pluginString += "        lv2:minimum -10.0 ;\n";
    537                         pluginString += "        lv2:maximum 0.0 ;\n";
    538                     }
    539                     else
    540                     {
    541                         pluginString += "        lv2:minimum -1.0 ;\n";
    542                         pluginString += "        lv2:maximum 0.0 ;\n";
    543                     }
    544                 }
    545                 else if (port.hints & kCVPortHasPositiveUnipolarRange)
    546                 {
    547                     if (cvPortScaled)
    548                     {
    549                         pluginString += "        lv2:minimum 0.0 ;\n";
    550                         pluginString += "        lv2:maximum 10.0 ;\n";
    551                     }
    552                     else
    553                     {
    554                         pluginString += "        lv2:minimum 0.0 ;\n";
    555                         pluginString += "        lv2:maximum 1.0 ;\n";
    556                     }
    557                 }
    558 
    559                 if ((port.hints & (kAudioPortIsCV|kCVPortIsOptional)) == (kAudioPortIsCV|kCVPortIsOptional))
    560                     pluginString += "        lv2:portProperty lv2:connectionOptional;\n";
    561 
    562                 if (i+1 == DISTRHO_PLUGIN_NUM_INPUTS)
    563                     pluginString += "    ] ;\n";
    564                 else
    565                     pluginString += "    ] ,\n";
    566             }
    567             pluginString += "\n";
    568 #endif
    569 
    570 #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
    571             for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i, ++portIndex)
    572             {
    573                 const AudioPort& port(plugin.getAudioPort(false, i));
    574                 const bool cvPortScaled = port.hints & kCVPortHasScaledRange;
    575 
    576                 if (i == 0)
    577                     pluginString += "    lv2:port [\n";
    578                 else
    579                     pluginString += "    [\n";
    580 
    581                 if (cvPortScaled)
    582                     pluginString += "        a lv2:OutputPort, lv2:CVPort, mod:CVPort ;\n";
    583                 else if (port.hints & kAudioPortIsCV)
    584                     pluginString += "        a lv2:OutputPort, lv2:CVPort ;\n";
    585                 else
    586                     pluginString += "        a lv2:OutputPort, lv2:AudioPort ;\n";
    587 
    588                 pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    589                 pluginString += "        lv2:symbol \"lv2_" + port.symbol + "\" ;\n";
    590                 pluginString += "        lv2:name \"" + port.name + "\" ;\n";
    591 
    592                 if (port.hints & kAudioPortIsSidechain)
    593                     pluginString += "        lv2:portProperty lv2:isSideChain;\n";
    594 
    595                 if (port.groupId != kPortGroupNone)
    596                 {
    597                     pluginString += "        pg:group <" DISTRHO_PLUGIN_URI "#portGroup_"
    598                                     + plugin.getPortGroupSymbolForId(port.groupId) + "> ;\n";
    599 
    600                     switch (port.groupId)
    601                     {
    602                     case kPortGroupMono:
    603                         pluginString += "        lv2:designation pg:center ;\n";
    604                         break;
    605                     case kPortGroupStereo:
    606                         if (i == 1)
    607                             pluginString += "        lv2:designation pg:right ;\n";
    608                         else
    609                             pluginString += "        lv2:designation pg:left ;\n";
    610                         break;
    611                     }
    612                 }
    613 
    614                 // set ranges
    615                 if (port.hints & kCVPortHasBipolarRange)
    616                 {
    617                     if (cvPortScaled)
    618                     {
    619                         pluginString += "        lv2:minimum -5.0 ;\n";
    620                         pluginString += "        lv2:maximum 5.0 ;\n";
    621                     }
    622                     else
    623                     {
    624                         pluginString += "        lv2:minimum -1.0 ;\n";
    625                         pluginString += "        lv2:maximum 1.0 ;\n";
    626                     }
    627                 }
    628                 else if (port.hints & kCVPortHasNegativeUnipolarRange)
    629                 {
    630                     if (cvPortScaled)
    631                     {
    632                         pluginString += "        lv2:minimum -10.0 ;\n";
    633                         pluginString += "        lv2:maximum 0.0 ;\n";
    634                     }
    635                     else
    636                     {
    637                         pluginString += "        lv2:minimum -1.0 ;\n";
    638                         pluginString += "        lv2:maximum 0.0 ;\n";
    639                     }
    640                 }
    641                 else if (port.hints & kCVPortHasPositiveUnipolarRange)
    642                 {
    643                     if (cvPortScaled)
    644                     {
    645                         pluginString += "        lv2:minimum 0.0 ;\n";
    646                         pluginString += "        lv2:maximum 10.0 ;\n";
    647                     }
    648                     else
    649                     {
    650                         pluginString += "        lv2:minimum 0.0 ;\n";
    651                         pluginString += "        lv2:maximum 1.0 ;\n";
    652                     }
    653                 }
    654 
    655                 if (i+1 == DISTRHO_PLUGIN_NUM_OUTPUTS)
    656                     pluginString += "    ] ;\n";
    657                 else
    658                     pluginString += "    ] ,\n";
    659             }
    660             pluginString += "\n";
    661 #endif
    662 
    663 #if DISTRHO_LV2_USE_EVENTS_IN
    664             pluginString += "    lv2:port [\n";
    665             pluginString += "        a lv2:InputPort, atom:AtomPort ;\n";
    666             pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    667             pluginString += "        lv2:name \"Events Input\" ;\n";
    668             pluginString += "        lv2:symbol \"lv2_events_in\" ;\n";
    669             pluginString += "        rsz:minimumSize " + String(DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE) + " ;\n";
    670             pluginString += "        atom:bufferType atom:Sequence ;\n";
    671 # if (DISTRHO_PLUGIN_WANT_STATE && DISTRHO_PLUGIN_HAS_UI)
    672             pluginString += "        atom:supports atom:String ;\n";
    673 # endif
    674 # if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    675             pluginString += "        atom:supports midi:MidiEvent ;\n";
    676 # endif
    677 # if DISTRHO_PLUGIN_WANT_TIMEPOS
    678             pluginString += "        atom:supports <" LV2_TIME__Position "> ;\n";
    679 # endif
    680 # if DISTRHO_PLUGIN_WANT_STATE
    681             if (hasHostVisibleState)
    682             {
    683                 pluginString += "        atom:supports <" LV2_PATCH__Message "> ;\n";
    684                 pluginString += "        lv2:designation lv2:control ;\n";
    685             }
    686 # endif
    687             pluginString += "    ] ;\n\n";
    688             ++portIndex;
    689 #endif
    690 
    691 #if DISTRHO_LV2_USE_EVENTS_OUT
    692             pluginString += "    lv2:port [\n";
    693             pluginString += "        a lv2:OutputPort, atom:AtomPort ;\n";
    694             pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    695             pluginString += "        lv2:name \"Events Output\" ;\n";
    696             pluginString += "        lv2:symbol \"lv2_events_out\" ;\n";
    697             pluginString += "        rsz:minimumSize " + String(DISTRHO_PLUGIN_MINIMUM_BUFFER_SIZE) + " ;\n";
    698             pluginString += "        atom:bufferType atom:Sequence ;\n";
    699 # if (DISTRHO_PLUGIN_WANT_STATE && DISTRHO_PLUGIN_HAS_UI)
    700             pluginString += "        atom:supports atom:String ;\n";
    701 # endif
    702 # if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    703             pluginString += "        atom:supports midi:MidiEvent ;\n";
    704 # endif
    705 # if DISTRHO_PLUGIN_WANT_STATE
    706             if (hasHostVisibleState)
    707             {
    708                 pluginString += "        atom:supports <" LV2_PATCH__Message "> ;\n";
    709                 pluginString += "        lv2:designation lv2:control ;\n";
    710             }
    711 # endif
    712             pluginString += "    ] ;\n\n";
    713             ++portIndex;
    714 #endif
    715 
    716 #if DISTRHO_PLUGIN_WANT_LATENCY
    717             pluginString += "    lv2:port [\n";
    718             pluginString += "        a lv2:OutputPort, lv2:ControlPort ;\n";
    719             pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    720             pluginString += "        lv2:name \"Latency\" ;\n";
    721             pluginString += "        lv2:symbol \"lv2_latency\" ;\n";
    722             pluginString += "        lv2:designation lv2:latency ;\n";
    723             pluginString += "        lv2:portProperty lv2:reportsLatency, lv2:integer, <" LV2_PORT_PROPS__notOnGUI "> ;\n";
    724             pluginString += "    ] ;\n\n";
    725             ++portIndex;
    726 #endif
    727 
    728             for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i, ++portIndex)
    729             {
    730                 if (i == 0)
    731                     pluginString += "    lv2:port [\n";
    732                 else
    733                     pluginString += "    [\n";
    734 
    735                 if (plugin.isParameterOutput(i))
    736                     pluginString += "        a lv2:OutputPort, lv2:ControlPort ;\n";
    737                 else
    738                     pluginString += "        a lv2:InputPort, lv2:ControlPort ;\n";
    739 
    740                 pluginString += "        lv2:index " + String(portIndex) + " ;\n";
    741 
    742                 bool designated = false;
    743 
    744                 // designation
    745                 if (plugin.isParameterInput(i))
    746                 {
    747                     switch (plugin.getParameterDesignation(i))
    748                     {
    749                     case kParameterDesignationNull:
    750                         break;
    751                     case kParameterDesignationBypass:
    752                         designated = true;
    753                         pluginString += "        lv2:name \"Enabled\" ;\n";
    754                         pluginString += "        lv2:symbol \"" + String(ParameterDesignationSymbols::bypass_lv2) + "\" ;\n";
    755                         pluginString += "        lv2:default 1 ;\n";
    756                         pluginString += "        lv2:minimum 0 ;\n";
    757                         pluginString += "        lv2:maximum 1 ;\n";
    758                         pluginString += "        lv2:portProperty lv2:toggled , lv2:integer ;\n";
    759                         pluginString += "        lv2:designation lv2:enabled ;\n";
    760                         break;
    761                     }
    762                 }
    763 
    764                 if (! designated)
    765                 {
    766                     const uint32_t hints = plugin.getParameterHints(i);
    767 
    768                     // name and symbol
    769                     const String& paramName(plugin.getParameterName(i));
    770 
    771                     if (paramName.contains('"'))
    772                         pluginString += "        lv2:name \"\"\"" + paramName + "\"\"\" ;\n";
    773                     else
    774                         pluginString += "        lv2:name \"" + paramName + "\" ;\n";
    775 
    776                     String symbol(plugin.getParameterSymbol(i));
    777 
    778                     if (symbol.isEmpty())
    779                         symbol = "lv2_port_" + String(portIndex-1);
    780 
    781                     pluginString += "        lv2:symbol \"" + symbol + "\" ;\n";
    782 
    783                     // short name
    784                     const String& shortName(plugin.getParameterShortName(i));
    785 
    786                     if (shortName.isNotEmpty())
    787                         pluginString += "        lv2:shortName \"\"\"" + shortName + "\"\"\" ;\n";
    788 
    789                     // ranges
    790                     const ParameterRanges& ranges(plugin.getParameterRanges(i));
    791 
    792                     if (hints & kParameterIsInteger)
    793                     {
    794                         if (plugin.isParameterInput(i))
    795                             pluginString += "        lv2:default " + String(int(ranges.def)) + " ;\n";
    796                         pluginString += "        lv2:minimum " + String(int(ranges.min)) + " ;\n";
    797                         pluginString += "        lv2:maximum " + String(int(ranges.max)) + " ;\n";
    798                     }
    799                     else if (hints & kParameterIsLogarithmic)
    800                     {
    801                         if (plugin.isParameterInput(i))
    802                         {
    803                             if (d_isNotZero(ranges.def))
    804                                 pluginString += "        lv2:default " + String(ranges.def) + " ;\n";
    805                             else if (d_isEqual(ranges.def, ranges.max))
    806                                 pluginString += "        lv2:default -0.0001 ;\n";
    807                             else
    808                                 pluginString += "        lv2:default 0.0001 ;\n";
    809                         }
    810 
    811                         if (d_isNotZero(ranges.min))
    812                             pluginString += "        lv2:minimum " + String(ranges.min) + " ;\n";
    813                         else
    814                             pluginString += "        lv2:minimum 0.0001 ;\n";
    815 
    816                         if (d_isNotZero(ranges.max))
    817                             pluginString += "        lv2:maximum " + String(ranges.max) + " ;\n";
    818                         else
    819                             pluginString += "        lv2:maximum -0.0001 ;\n";
    820                     }
    821                     else
    822                     {
    823                         if (plugin.isParameterInput(i))
    824                             pluginString += "        lv2:default " + String(ranges.def) + " ;\n";
    825                         pluginString += "        lv2:minimum " + String(ranges.min) + " ;\n";
    826                         pluginString += "        lv2:maximum " + String(ranges.max) + " ;\n";
    827                     }
    828 
    829                     // enumeration
    830                     const ParameterEnumerationValues& enumValues(plugin.getParameterEnumValues(i));
    831 
    832                     if (enumValues.count > 0)
    833                     {
    834                         if (enumValues.count >= 2 && enumValues.restrictedMode)
    835                             pluginString += "        lv2:portProperty lv2:enumeration ;\n";
    836 
    837                         for (uint8_t j=0; j < enumValues.count; ++j)
    838                         {
    839                             const ParameterEnumerationValue& enumValue(enumValues.values[j]);
    840 
    841                             if (j == 0)
    842                                 pluginString += "        lv2:scalePoint [\n";
    843                             else
    844                                 pluginString += "        [\n";
    845 
    846                             if (enumValue.label.contains('"'))
    847                                 pluginString += "            rdfs:label  \"\"\"" + enumValue.label + "\"\"\" ;\n";
    848                             else
    849                                 pluginString += "            rdfs:label  \"" + enumValue.label + "\" ;\n";
    850 
    851                             if (hints & kParameterIsInteger)
    852                             {
    853                                 const int rounded = (int)(enumValue.value + (enumValue.value < 0.0f ? -0.5f : 0.5f));
    854                                 pluginString += "            rdf:value " + String(rounded) + " ;\n";
    855                             }
    856                             else
    857                             {
    858                                 pluginString += "            rdf:value " + String(enumValue.value) + " ;\n";
    859                             }
    860 
    861                             if (j+1 == enumValues.count)
    862                                 pluginString += "        ] ;\n";
    863                             else
    864                                 pluginString += "        ] ,\n";
    865                         }
    866                     }
    867 
    868                     // MIDI CC binding
    869                     if (const uint8_t midiCC = plugin.getParameterMidiCC(i))
    870                     {
    871                         pluginString += "        midi:binding [\n";
    872                         pluginString += "            a midi:Controller ;\n";
    873                         pluginString += "            midi:controllerNumber " + String(midiCC) + " ;\n";
    874                         pluginString += "        ] ;\n";
    875                     }
    876 
    877                     // unit
    878                     const String& unit(plugin.getParameterUnit(i));
    879 
    880                     if (unit.isNotEmpty() && ! unit.contains(' '))
    881                     {
    882                         String lunit(unit);
    883                         lunit.toLower();
    884 
    885                         /**/ if (lunit == "db")
    886                         {
    887                             pluginString += "        unit:unit unit:db ;\n";
    888                         }
    889                         else if (lunit == "hz")
    890                         {
    891                             pluginString += "        unit:unit unit:hz ;\n";
    892                         }
    893                         else if (lunit == "khz")
    894                         {
    895                             pluginString += "        unit:unit unit:khz ;\n";
    896                         }
    897                         else if (lunit == "mhz")
    898                         {
    899                             pluginString += "        unit:unit unit:mhz ;\n";
    900                         }
    901                         else if (lunit == "ms")
    902                         {
    903                             pluginString += "        unit:unit unit:ms ;\n";
    904                         }
    905                         else if (lunit == "s")
    906                         {
    907                             pluginString += "        unit:unit unit:s ;\n";
    908                         }
    909                         else if (lunit == "%")
    910                         {
    911                             pluginString += "        unit:unit unit:pc ;\n";
    912                         }
    913                         else
    914                         {
    915                             pluginString += "        unit:unit [\n";
    916                             pluginString += "            a unit:Unit ;\n";
    917                             pluginString += "            rdfs:label  \"" + unit + "\" ;\n";
    918                             pluginString += "            unit:symbol \"" + unit + "\" ;\n";
    919                             if (hints & kParameterIsInteger)
    920                                 pluginString += "            unit:render \"%d " + unit + "\" ;\n";
    921                             else
    922                                 pluginString += "            unit:render \"%f " + unit + "\" ;\n";
    923                             pluginString += "        ] ;\n";
    924                         }
    925                     }
    926 
    927                     // comment
    928                     const String& comment(plugin.getParameterDescription(i));
    929 
    930                     if (comment.isNotEmpty())
    931                     {
    932                         if (comment.contains('"') || comment.contains('\n'))
    933                             pluginString += "        rdfs:comment \"\"\"" + comment + "\"\"\" ;\n";
    934                         else
    935                             pluginString += "        rdfs:comment \"" + comment + "\" ;\n";
    936                     }
    937 
    938                     // hints
    939                     if (hints & kParameterIsBoolean)
    940                     {
    941                         if ((hints & kParameterIsTrigger) == kParameterIsTrigger)
    942                             pluginString += "        lv2:portProperty <" LV2_PORT_PROPS__trigger "> ;\n";
    943                         pluginString += "        lv2:portProperty lv2:toggled ;\n";
    944                     }
    945                     if (hints & kParameterIsInteger)
    946                         pluginString += "        lv2:portProperty lv2:integer ;\n";
    947                     if (hints & kParameterIsLogarithmic)
    948                         pluginString += "        lv2:portProperty <" LV2_PORT_PROPS__logarithmic "> ;\n";
    949                     if (hints & kParameterIsHidden)
    950                         pluginString += "        lv2:portProperty <" LV2_PORT_PROPS__notOnGUI "> ;\n";
    951                     if ((hints & kParameterIsAutomatable) == 0 && plugin.isParameterInput(i))
    952                     {
    953                         pluginString += "        lv2:portProperty <" LV2_PORT_PROPS__expensive "> ,\n";
    954                         pluginString += "                         <" LV2_KXSTUDIO_PROPERTIES__NonAutomatable "> ;\n";
    955                     }
    956 
    957                     // group
    958                     const uint32_t groupId = plugin.getParameterGroupId(i);
    959 
    960                     if (groupId != kPortGroupNone)
    961                         pluginString += "        pg:group <" DISTRHO_PLUGIN_URI "#portGroup_"
    962                                         + plugin.getPortGroupSymbolForId(groupId) + "> ;\n";
    963 
    964                 } // ! designated
    965 
    966                 if (i+1 == count)
    967                     pluginString += "    ] ;\n\n";
    968                 else
    969                     pluginString += "    ] ,\n";
    970             }
    971         }
    972 
    973         // comment
    974         {
    975             const String comment(plugin.getDescription());
    976 
    977             if (comment.isNotEmpty())
    978             {
    979                 if (comment.contains('"') || comment.contains('\n'))
    980                     pluginString += "    rdfs:comment \"\"\"" + comment + "\"\"\" ;\n\n";
    981                 else
    982                     pluginString += "    rdfs:comment \"" + comment + "\" ;\n\n";
    983             }
    984         }
    985 
    986        #ifdef DISTRHO_PLUGIN_BRAND
    987         // MOD
    988         pluginString += "    mod:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n";
    989         pluginString += "    mod:label \"" DISTRHO_PLUGIN_NAME "\" ;\n\n";
    990        #endif
    991 
    992         // name
    993         {
    994             const String name(plugin.getName());
    995 
    996             if (name.contains('"'))
    997                 pluginString += "    doap:name \"\"\"" + name + "\"\"\" ;\n";
    998             else
    999                 pluginString += "    doap:name \"" + name + "\" ;\n";
   1000         }
   1001 
   1002         // license
   1003         {
   1004             const String license(plugin.getLicense());
   1005 
   1006             if (license.isEmpty())
   1007             {}
   1008             // Using URL as license
   1009             else if (license.contains("://"))
   1010             {
   1011                 pluginString += "    doap:license <" +  license + "> ;\n\n";
   1012             }
   1013             // String contaning quotes, use as-is
   1014             else if (license.contains('"'))
   1015             {
   1016                 pluginString += "    doap:license \"\"\"" +  license + "\"\"\" ;\n\n";
   1017             }
   1018             // Regular license string, convert to URL as much as we can
   1019             else
   1020             {
   1021                 const String uplicense(license.asUpper());
   1022 
   1023                 // for reference, see https://spdx.org/licenses/
   1024 
   1025                 // common licenses
   1026                 /**/ if (uplicense == "AGPL-1.0-ONLY" ||
   1027                          uplicense == "AGPL1" ||
   1028                          uplicense == "AGPLV1")
   1029                 {
   1030                     pluginString += "    doap:license <http://spdx.org/licenses/AGPL-1.0-only.html> ;\n\n";
   1031                 }
   1032                 else if (uplicense == "AGPL-1.0-OR-LATER" ||
   1033                          uplicense == "AGPL1+" ||
   1034                          uplicense == "AGPLV1+")
   1035                 {
   1036                     pluginString += "    doap:license <http://spdx.org/licenses/AGPL-1.0-or-later.html> ;\n\n";
   1037                 }
   1038                 else if (uplicense == "AGPL-3.0-ONLY" ||
   1039                          uplicense == "AGPL3" ||
   1040                          uplicense == "AGPLV3")
   1041                 {
   1042                     pluginString += "    doap:license <http://spdx.org/licenses/AGPL-3.0-only.html> ;\n\n";
   1043                 }
   1044                 else if (uplicense == "AGPL-3.0-OR-LATER" ||
   1045                          uplicense == "AGPL3+" ||
   1046                          uplicense == "AGPLV3+")
   1047                 {
   1048                     pluginString += "    doap:license <http://spdx.org/licenses/AGPL-3.0-or-later.html> ;\n\n";
   1049                 }
   1050                 else if (uplicense == "APACHE-2.0" ||
   1051                          uplicense == "APACHE2" ||
   1052                          uplicense == "APACHE-2")
   1053                 {
   1054                     pluginString += "    doap:license <http://spdx.org/licenses/Apache-2.0.html> ;\n\n";
   1055                 }
   1056                 else if (uplicense == "BSD-2-CLAUSE" ||
   1057                          uplicense == "BSD2" ||
   1058                          uplicense == "BSD-2")
   1059                 {
   1060                     pluginString += "    doap:license <http://spdx.org/licenses/BSD-2-Clause.html> ;\n\n";
   1061                 }
   1062                 else if (uplicense == "BSD-3-CLAUSE" ||
   1063                          uplicense == "BSD3" ||
   1064                          uplicense == "BSD-3")
   1065                 {
   1066                     pluginString += "    doap:license <http://spdx.org/licenses/BSD-3-Clause.html> ;\n\n";
   1067                 }
   1068                 else if (uplicense == "GPL-2.0-ONLY" ||
   1069                          uplicense == "GPL2" ||
   1070                          uplicense == "GPLV2")
   1071                 {
   1072                     pluginString += "    doap:license <http://spdx.org/licenses/GPL-2.0-only.html> ;\n\n";
   1073                 }
   1074                 else if (uplicense == "GPL-2.0-OR-LATER" ||
   1075                          uplicense == "GPL2+" ||
   1076                          uplicense == "GPLV2+" ||
   1077                          uplicense == "GPLV2.0+" ||
   1078                          uplicense == "GPL V2+")
   1079                 {
   1080                     pluginString += "    doap:license <http://spdx.org/licenses/GPL-2.0-or-later.html> ;\n\n";
   1081                 }
   1082                 else if (uplicense == "GPL-3.0-ONLY" ||
   1083                          uplicense == "GPL3" ||
   1084                          uplicense == "GPLV3")
   1085                 {
   1086                     pluginString += "    doap:license <http://spdx.org/licenses/GPL-3.0-only.html> ;\n\n";
   1087                 }
   1088                 else if (uplicense == "GPL-3.0-OR-LATER" ||
   1089                          uplicense == "GPL3+" ||
   1090                          uplicense == "GPLV3+" ||
   1091                          uplicense == "GPLV3.0+" ||
   1092                          uplicense == "GPL V3+")
   1093                 {
   1094                     pluginString += "    doap:license <http://spdx.org/licenses/GPL-3.0-or-later.html> ;\n\n";
   1095                 }
   1096                 else if (uplicense == "ISC")
   1097                 {
   1098                     pluginString += "    doap:license <http://spdx.org/licenses/ISC.html> ;\n\n";
   1099                 }
   1100                 else if (uplicense == "LGPL-2.0-ONLY" ||
   1101                          uplicense == "LGPL2" ||
   1102                          uplicense == "LGPLV2")
   1103                 {
   1104                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-2.0-only.html> ;\n\n";
   1105                 }
   1106                 else if (uplicense == "LGPL-2.0-OR-LATER" ||
   1107                          uplicense == "LGPL2+" ||
   1108                          uplicense == "LGPLV2+")
   1109                 {
   1110                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-2.0-or-later.html> ;\n\n";
   1111                 }
   1112                 else if (uplicense == "LGPL-2.1-ONLY" ||
   1113                          uplicense == "LGPL2.1" ||
   1114                          uplicense == "LGPLV2.1")
   1115                 {
   1116                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-2.1-only.html> ;\n\n";
   1117                 }
   1118                 else if (uplicense == "LGPL-2.1-OR-LATER" ||
   1119                          uplicense == "LGPL2.1+" ||
   1120                          uplicense == "LGPLV2.1+")
   1121                 {
   1122                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-2.1-or-later.html> ;\n\n";
   1123                 }
   1124                 else if (uplicense == "LGPL-3.0-ONLY" ||
   1125                          uplicense == "LGPL3" ||
   1126                          uplicense == "LGPLV3")
   1127                 {
   1128                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-2.0-only.html> ;\n\n";
   1129                 }
   1130                 else if (uplicense == "LGPL-3.0-OR-LATER" ||
   1131                          uplicense == "LGPL3+" ||
   1132                          uplicense == "LGPLV3+")
   1133                 {
   1134                     pluginString += "    doap:license <http://spdx.org/licenses/LGPL-3.0-or-later.html> ;\n\n";
   1135                 }
   1136                 else if (uplicense == "MIT")
   1137                 {
   1138                     pluginString += "    doap:license <http://spdx.org/licenses/MIT.html> ;\n\n";
   1139                 }
   1140 
   1141                 // generic fallbacks
   1142                 else if (uplicense.startsWith("GPL"))
   1143                 {
   1144                     pluginString += "    doap:license <http://opensource.org/licenses/gpl-license> ;\n\n";
   1145                 }
   1146                 else if (uplicense.startsWith("LGPL"))
   1147                 {
   1148                     pluginString += "    doap:license <http://opensource.org/licenses/lgpl-license> ;\n\n";
   1149                 }
   1150 
   1151                 // unknown or not handled yet, log a warning
   1152                 else
   1153                 {
   1154                     d_stderr("Unknown license string '%s'", license.buffer());
   1155                     pluginString += "    doap:license \"" +  license + "\" ;\n\n";
   1156                 }
   1157             }
   1158         }
   1159 
   1160         // developer
   1161         {
   1162             const String homepage(plugin.getHomePage());
   1163             const String maker(plugin.getMaker());
   1164 
   1165             pluginString += "    doap:maintainer [\n";
   1166 
   1167             if (maker.contains('"'))
   1168                 pluginString += "        foaf:name \"\"\"" + maker + "\"\"\" ;\n";
   1169             else
   1170                 pluginString += "        foaf:name \"" + maker + "\" ;\n";
   1171 
   1172             if (homepage.isNotEmpty())
   1173                 pluginString += "        foaf:homepage <" + homepage + "> ;\n";
   1174 
   1175             pluginString += "    ] ;\n\n";
   1176         }
   1177 
   1178         {
   1179             const uint32_t version(plugin.getVersion());
   1180 
   1181             const uint32_t majorVersion = (version & 0xFF0000) >> 16;
   1182             /* */ uint32_t minorVersion = (version & 0x00FF00) >> 8;
   1183             const uint32_t microVersion = (version & 0x0000FF) >> 0;
   1184 
   1185             // NOTE: LV2 ignores 'major' version and says 0 for minor is pre-release/unstable.
   1186             if (majorVersion > 0)
   1187                 minorVersion += 2;
   1188 
   1189             pluginString += "    lv2:microVersion " + String(microVersion) + " ;\n";
   1190             pluginString += "    lv2:minorVersion " + String(minorVersion) + " .\n";
   1191         }
   1192 
   1193         // port groups
   1194         if (const uint32_t portGroupCount = plugin.getPortGroupCount())
   1195         {
   1196             bool isInput, isOutput;
   1197 
   1198             for (uint32_t i = 0; i < portGroupCount; ++i)
   1199             {
   1200                 const PortGroupWithId& portGroup(plugin.getPortGroupByIndex(i));
   1201                 DISTRHO_SAFE_ASSERT_CONTINUE(portGroup.groupId != kPortGroupNone);
   1202                 DISTRHO_SAFE_ASSERT_CONTINUE(portGroup.symbol.isNotEmpty());
   1203 
   1204                 pluginString += "\n<" DISTRHO_PLUGIN_URI "#portGroup_" + portGroup.symbol + ">\n";
   1205                 isInput = isOutput = false;
   1206 
   1207 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
   1208                 for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS && !isInput; ++i)
   1209                     isInput = plugin.getAudioPort(true, i).groupId == portGroup.groupId;
   1210 #endif
   1211 
   1212 #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
   1213                 for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS && !isOutput; ++i)
   1214                     isOutput = plugin.getAudioPort(false, i).groupId == portGroup.groupId;
   1215 #endif
   1216 
   1217                 for (uint32_t i=0, count=plugin.getParameterCount(); i < count && (!isInput || !isOutput); ++i)
   1218                 {
   1219                     if (plugin.getParameterGroupId(i) == portGroup.groupId)
   1220                     {
   1221                         isInput = isInput || plugin.isParameterInput(i);
   1222                         isOutput = isOutput || plugin.isParameterOutput(i);
   1223                     }
   1224                 }
   1225 
   1226                 pluginString += "    a ";
   1227 
   1228                 if (isInput && !isOutput)
   1229                     pluginString += "pg:InputGroup";
   1230                 else if (isOutput && !isInput)
   1231                     pluginString += "pg:OutputGroup";
   1232                 else
   1233                     pluginString += "pg:Group";
   1234 
   1235                 switch (portGroup.groupId)
   1236                 {
   1237                 case kPortGroupMono:
   1238                     pluginString += " , pg:MonoGroup";
   1239                     break;
   1240                 case kPortGroupStereo:
   1241                     pluginString += " , pg:StereoGroup";
   1242                     break;
   1243                 }
   1244 
   1245                 pluginString += " ;\n";
   1246 
   1247                 // pluginString += "    rdfs:label \"" + portGroup.name + "\" ;\n";
   1248                 pluginString += "    lv2:name \"" + portGroup.name + "\" ;\n";
   1249                 pluginString += "    lv2:symbol \"" + portGroup.symbol + "\" .\n";
   1250             }
   1251         }
   1252 
   1253         pluginFile << pluginString;
   1254         pluginFile.close();
   1255         std::cout << " done!" << std::endl;
   1256     }
   1257 
   1258    #if DISTRHO_PLUGIN_USES_MODGUI && !DISTRHO_PLUGIN_USES_CUSTOM_MODGUI
   1259     {
   1260         std::cout << "Writing modgui.ttl..."; std::cout.flush();
   1261         std::fstream modguiFile("modgui.ttl", std::ios::out);
   1262 
   1263         String modguiString;
   1264         modguiString += "@prefix lv2:    <" LV2_CORE_PREFIX "> .\n";
   1265         modguiString += "@prefix modgui: <http://moddevices.com/ns/modgui#> .\n";
   1266         modguiString += "\n";
   1267 
   1268         modguiString += "<" DISTRHO_PLUGIN_URI ">\n";
   1269         modguiString += "    modgui:gui [\n";
   1270        #ifdef DISTRHO_PLUGIN_BRAND
   1271         modguiString += "        modgui:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n";
   1272        #endif
   1273         modguiString += "        modgui:label \"" DISTRHO_PLUGIN_NAME "\" ;\n";
   1274         modguiString += "        modgui:resourcesDirectory <modgui> ;\n";
   1275         modguiString += "        modgui:iconTemplate <modgui/icon.html> ;\n";
   1276         modguiString += "        modgui:javascript <modgui/javascript.js> ;\n";
   1277         modguiString += "        modgui:stylesheet <modgui/stylesheet.css> ;\n";
   1278         modguiString += "        modgui:screenshot <modgui/screenshot.png> ;\n";
   1279         modguiString += "        modgui:thumbnail <modgui/thumbnail.png> ;\n";
   1280 
   1281         uint32_t numParametersOutputs = 0;
   1282         for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i)
   1283         {
   1284             if (plugin.isParameterOutput(i))
   1285                 ++numParametersOutputs;
   1286         }
   1287         if (numParametersOutputs != 0)
   1288         {
   1289             modguiString += "        modgui:monitoredOutputs [\n";
   1290             for (uint32_t i=0, j=0, count=plugin.getParameterCount(); i < count; ++i)
   1291             {
   1292                 if (!plugin.isParameterOutput(i))
   1293                     continue;
   1294                 modguiString += "            lv2:symbol \"" + plugin.getParameterSymbol(i) + "\" ;\n";
   1295                 if (++j != numParametersOutputs)
   1296                     modguiString += "        ] , [\n";
   1297             }
   1298             modguiString += "        ] ;\n";
   1299         }
   1300 
   1301         modguiString += "    ] .\n";
   1302 
   1303         modguiFile << modguiString;
   1304         modguiFile.close();
   1305         std::cout << " done!" << std::endl;
   1306     }
   1307 
   1308    #ifdef DISTRHO_OS_WINDOWS
   1309     ::_mkdir("modgui");
   1310    #else
   1311     ::mkdir("modgui", 0755);
   1312    #endif
   1313 
   1314     {
   1315         std::cout << "Writing modgui/javascript.js..."; std::cout.flush();
   1316         std::fstream jsFile("modgui/javascript.js", std::ios::out);
   1317 
   1318         String jsString;
   1319         jsString += "function(e,f){\n";
   1320         jsString += "'use strict';\nvar ps=[";
   1321 
   1322         for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i)
   1323             jsString += "'lv2_" + plugin.getAudioPort(false, i).symbol + "',";
   1324         for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
   1325             jsString += "'lv2_" + plugin.getAudioPort(true, i).symbol + "',";
   1326        #if DISTRHO_LV2_USE_EVENTS_IN
   1327         jsString += "'lv2_events_in',";
   1328        #endif
   1329        #if DISTRHO_LV2_USE_EVENTS_OUT
   1330         jsString += "'lv2_events_out',";
   1331        #endif
   1332        #if DISTRHO_PLUGIN_WANT_LATENCY
   1333         jsString += "'lv2_latency',";
   1334        #endif
   1335 
   1336         int32_t enabledIndex = INT32_MAX;
   1337         for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i)
   1338         {
   1339             jsString += "'" + plugin.getParameterSymbol(i) + "',";
   1340             if (plugin.getParameterDesignation(i) == kParameterDesignationBypass)
   1341                 enabledIndex = i;
   1342         }
   1343         jsString += "];\n";
   1344         jsString += "var ei=" + String(enabledIndex != INT32_MAX ? enabledIndex : -1) + ";\n\n";
   1345         jsString += "if(e.type==='start'){\n";
   1346         jsString += "e.data.p={p:{},c:{},};\n\n";
   1347         jsString += "var err=[];\n";
   1348         jsString += "if(typeof(WebAssembly)==='undefined'){err.push('WebAssembly unsupported');}\n";
   1349         jsString += "else{\n";
   1350         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])))";
   1351         jsString += "err.push('Bulk Memory Operations unsupported');\n";
   1352         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])))";
   1353         jsString += "err.push('Importable/Exportable mutable globals unsupported');\n";
   1354         jsString += "}\n";
   1355         jsString += "if(err.length!==0){e.icon.find('.canvas_wrapper').html('<h2>'+err.join('<br>')+'</h2>');return;}\n\n";
   1356         jsString += "var s=document.createElement('script');\n";
   1357         jsString += "s.setAttribute('async',true);\n";
   1358         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";
   1359         jsString += "s.setAttribute('type','text/javascript');\n";
   1360         jsString += "s.onload=function(){\n";
   1361         jsString += " Module_" DISTRHO_PLUGIN_MODGUI_CLASS_NAME "({\n";
   1362         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";
   1363         jsString += " postRun:function(m){\n";
   1364         jsString += " var cn=e.icon.attr('mod-instance').replaceAll('/','_');\n";
   1365         jsString += " var cnl=m.lengthBytesUTF8(cn) + 1;\n";
   1366         jsString += " var cna=m._malloc(cnl);\n";
   1367         jsString += " m.stringToUTF8(cn, cna, cnl);\n";
   1368         jsString += " e.icon.find('canvas')[0].id=cn;\n";
   1369         jsString += " var a=m.addFunction(function(i,v){f.set_port_value(ps[i],v);},'vif');\n";
   1370         jsString += " var b=m.addFunction(function(u,v){f.patch_set(m.UTF8ToString(u),'s',m.UTF8ToString(v));},'vpp');\n";
   1371         jsString += " var h=m._modgui_init(cna,a,b);\n";
   1372         jsString += " m._free(cna);\n";
   1373         jsString += " e.data.h=h;\n";
   1374         jsString += " e.data.m=m;\n";
   1375         jsString += " for(var u in e.data.p.p){\n";
   1376         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";
   1377         jsString += " m.stringToUTF8(u,ua,ul);\n";
   1378         jsString += " m.stringToUTF8(v,va,vl);\n";
   1379         jsString += " m._modgui_patch_set(h, ua, va);\n";
   1380         jsString += " m._free(ua);\n";
   1381         jsString += " m._free(va);\n";
   1382         jsString += " }\n";
   1383         jsString += " for(var symbol in e.data.p.c){m._modgui_param_set(h,ps.indexOf(symbol),e.data.p.c[symbol]);}\n";
   1384         jsString += " delete e.data.p;\n";
   1385         jsString += " window.dispatchEvent(new Event('resize'));\n";
   1386         jsString += " },\n";
   1387         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";
   1388         jsString += " });\n";
   1389         jsString += "};\n";
   1390         jsString += "document.head.appendChild(s);\n\n";
   1391         jsString += "}else if(e.type==='change'){\n\n";
   1392         jsString += "if(e.data.h && e.data.m){\n";
   1393         jsString += " var m=e.data.m;\n";
   1394         jsString += " if(e.uri){\n";
   1395         jsString += "  var ul=m.lengthBytesUTF8(e.uri)+1,ua=m._malloc(ul),vl=m.lengthBytesUTF8(e.value)+1,va=m._malloc(vl);\n";
   1396         jsString += "  m.stringToUTF8(e.uri,ua,ul);\n";
   1397         jsString += "  m.stringToUTF8(e.value,va,vl);\n";
   1398         jsString += "  m._modgui_patch_set(e.data.h,ua,va);\n";
   1399         jsString += "  m._free(ua);\n";
   1400         jsString += "  m._free(va);\n";
   1401         jsString += " }else if(e.symbol===':bypass'){return;\n";
   1402         jsString += " }else{m._modgui_param_set(e.data.h,ps.indexOf(e.symbol),e.value);}\n";
   1403         jsString += "}else{\n";
   1404         jsString += " if(e.symbol===':bypass')return;\n";
   1405         jsString += " if(e.uri){e.data.p.p[e.uri]=e.value;}else{e.data.p.c[e.symbol]=e.value;}\n";
   1406         jsString += "}\n\n";
   1407         jsString += "}else if(e.type==='end'){\n";
   1408         jsString += " if(e.data.h && e.data.m){\n";
   1409         jsString += "  var h = e.data.h;\n";
   1410         jsString += "  var m = e.data.m;\n";
   1411         jsString += "  e.data.h = e.data.m = null;\n";
   1412         jsString += "  m._modgui_cleanup(h);\n";
   1413         jsString += "}\n\n";
   1414         jsString += "}\n}\n";
   1415         jsFile << jsString;
   1416         jsFile.close();
   1417         std::cout << " done!" << std::endl;
   1418     }
   1419 
   1420     {
   1421         std::cout << "Writing modgui/icon.html..."; std::cout.flush();
   1422         std::fstream iconFile("modgui/icon.html", std::ios::out);
   1423 
   1424         iconFile << "<div class='" DISTRHO_PLUGIN_MODGUI_CLASS_NAME " mod-pedal'>" << std::endl;
   1425         iconFile << "    <div mod-role='drag-handle' class='mod-drag-handle'></div>" << std::endl;
   1426         iconFile << "    <div class='mod-plugin-title'><h1>{{#brand}}{{brand}} | {{/brand}}{{label}}</h1></div>" << std::endl;
   1427         iconFile << "    <div class='mod-light on' mod-role='bypass-light'></div>" << std::endl;
   1428         iconFile << "    <div class='mod-control-group mod-switch'>" << std::endl;
   1429         iconFile << "        <div class='mod-control-group mod-switch-image mod-port transport' mod-role='bypass' mod-widget='film'></div>" << std::endl;
   1430         iconFile << "    </div>" << std::endl;
   1431         iconFile << "    <div class='canvas_wrapper'>" << std::endl;
   1432         iconFile << "        <canvas oncontextmenu='event.preventDefault()' tabindex=-1></canvas>" << std::endl;
   1433         iconFile << "    </div>" << std::endl;
   1434         iconFile << "    <div class='mod-pedal-input'>" << std::endl;
   1435         iconFile << "        {{#effect.ports.audio.input}}" << std::endl;
   1436         iconFile << "        <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1437         iconFile << "            <div class='mod-pedal-input-image'></div>" << std::endl;
   1438         iconFile << "        </div>" << std::endl;
   1439         iconFile << "        {{/effect.ports.audio.input}}" << std::endl;
   1440         iconFile << "        {{#effect.ports.midi.input}}" << std::endl;
   1441         iconFile << "        <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1442         iconFile << "            <div class='mod-pedal-input-image'></div>" << std::endl;
   1443         iconFile << "        </div>" << std::endl;
   1444         iconFile << "        {{/effect.ports.midi.input}}" << std::endl;
   1445         iconFile << "        {{#effect.ports.cv.input}}" << std::endl;
   1446         iconFile << "        <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1447         iconFile << "            <div class='mod-pedal-input-image'></div>" << std::endl;
   1448         iconFile << "        </div>" << std::endl;
   1449         iconFile << "        {{/effect.ports.cv.input}}" << std::endl;
   1450         iconFile << "    </div>" << std::endl;
   1451         iconFile << "    <div class='mod-pedal-output'>" << std::endl;
   1452         iconFile << "        {{#effect.ports.audio.output}}" << std::endl;
   1453         iconFile << "        <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1454         iconFile << "            <div class='mod-pedal-output-image'></div>" << std::endl;
   1455         iconFile << "        </div>" << std::endl;
   1456         iconFile << "        {{/effect.ports.audio.output}}" << std::endl;
   1457         iconFile << "        {{#effect.ports.midi.output}}" << std::endl;
   1458         iconFile << "        <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1459         iconFile << "            <div class='mod-pedal-output-image'></div>" << std::endl;
   1460         iconFile << "        </div>" << std::endl;
   1461         iconFile << "        {{/effect.ports.midi.output}}" << std::endl;
   1462         iconFile << "        {{#effect.ports.cv.output}}" << std::endl;
   1463         iconFile << "        <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl;
   1464         iconFile << "            <div class='mod-pedal-output-image'></div>" << std::endl;
   1465         iconFile << "        </div>" << std::endl;
   1466         iconFile << "        {{/effect.ports.cv.output}}" << std::endl;
   1467         iconFile << "    </div>" << std::endl;
   1468         iconFile << "</div>" << std::endl;
   1469 
   1470         iconFile.close();
   1471         std::cout << " done!" << std::endl;
   1472     }
   1473 
   1474     {
   1475         std::cout << "Writing modgui/stylesheet.css..."; std::cout.flush();
   1476         std::fstream stylesheetFile("modgui/stylesheet.css", std::ios::out);
   1477 
   1478         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal{" << std::endl;
   1479         stylesheetFile << " padding:0;" << std::endl;
   1480         stylesheetFile << " margin:0;" << std::endl;
   1481         stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl;
   1482         stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT + 50) + "px;" << std::endl;
   1483         stylesheetFile << " background:#2a2e32;" << std::endl;
   1484         stylesheetFile << " border-radius:20px 20px 0 0;" << std::endl;
   1485         stylesheetFile << " color:#fff;" << std::endl;
   1486         stylesheetFile << "}" << std::endl;
   1487         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper{" << std::endl;
   1488         stylesheetFile << " --device-pixel-ratio:1;" << std::endl;
   1489         stylesheetFile << " /*image-rendering:pixelated;*/" << std::endl;
   1490         stylesheetFile << " /*image-rendering:crisp-edges;*/" << std::endl;
   1491         stylesheetFile << " background:#000;" << std::endl;
   1492         stylesheetFile << " position:absolute;" << std::endl;
   1493         stylesheetFile << " top:50px;" << std::endl;
   1494         stylesheetFile << " transform-origin:0 0 0;" << std::endl;
   1495         stylesheetFile << " transform:scale(calc(1/var(--device-pixel-ratio)));" << std::endl;
   1496         stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl;
   1497         stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT) + "px;" << std::endl;
   1498         stylesheetFile << " text-align:center;" << std::endl;
   1499         stylesheetFile << " z-index:21;" << std::endl;
   1500         stylesheetFile << "}" << std::endl;
   1501         stylesheetFile << "/*" << std::endl;
   1502         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper:focus-within{" << std::endl;
   1503         stylesheetFile << " z-index:21;" << std::endl;
   1504         stylesheetFile << "}" << std::endl;
   1505         stylesheetFile << "*/" << std::endl;
   1506         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-plugin-title{" << std::endl;
   1507         stylesheetFile << " position:absolute;" << std::endl;
   1508         stylesheetFile << " text-align:center;" << std::endl;
   1509         stylesheetFile << " width:100%;" << std::endl;
   1510         stylesheetFile << "}" << std::endl;
   1511         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal h1{" << std::endl;
   1512         stylesheetFile << " font-size:20px;" << std::endl;
   1513         stylesheetFile << " font-weight:bold;" << std::endl;
   1514         stylesheetFile << " line-height:50px;" << std::endl;
   1515         stylesheetFile << " margin:0;" << std::endl;
   1516         stylesheetFile << "}" << std::endl;
   1517         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-control-group{" << std::endl;
   1518         stylesheetFile << " position:absolute;" << std::endl;
   1519         stylesheetFile << " left:5px;" << std::endl;
   1520         stylesheetFile << " z-index:35;" << std::endl;
   1521         stylesheetFile << "}" << std::endl;
   1522         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-input," << std::endl;
   1523         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-output{" << std::endl;
   1524         stylesheetFile << " top:75px;" << std::endl;
   1525         stylesheetFile << "}" << std::endl;
   1526         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-input," << std::endl;
   1527         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-output{" << std::endl;
   1528         stylesheetFile << " margin-bottom:25px;" << std::endl;
   1529         stylesheetFile << "}" << std::endl;
   1530         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .jack-disconnected{" << std::endl;
   1531         stylesheetFile << " top:0px!important;" << std::endl;
   1532         stylesheetFile << "}" << std::endl;
   1533         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image{" << std::endl;
   1534         stylesheetFile << " background-image: url(/img/switch.png);" << std::endl;
   1535         stylesheetFile << " background-position: left center;" << std::endl;
   1536         stylesheetFile << " background-repeat: no-repeat;" << std::endl;
   1537         stylesheetFile << " background-size: auto 50px;" << std::endl;
   1538         stylesheetFile << " font-weight: bold;" << std::endl;
   1539         stylesheetFile << " width: 100px;" << std::endl;
   1540         stylesheetFile << " height: 50px;" << std::endl;
   1541         stylesheetFile << " cursor: pointer;" << std::endl;
   1542         stylesheetFile << "}" << std::endl;
   1543         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.off{" << std::endl;
   1544         stylesheetFile << " background-position: right center !important;" << std::endl;
   1545         stylesheetFile << "}" << std::endl;
   1546         stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.on{" << std::endl;
   1547         stylesheetFile << " background-position: left center !important;" << std::endl;
   1548         stylesheetFile << "}" << std::endl;
   1549 
   1550         stylesheetFile.close();
   1551         std::cout << " done!" << std::endl;
   1552     }
   1553    #endif // DISTRHO_PLUGIN_USES_MODGUI && DISTRHO_PLUGIN_HAS_EMBED_UI && !DISTRHO_PLUGIN_USES_CUSTOM_MODGUI
   1554 
   1555     // ---------------------------------------------
   1556 
   1557 #if DISTRHO_PLUGIN_HAS_UI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
   1558     {
   1559         std::cout << "Writing " << uiTTL << "..."; std::cout.flush();
   1560         std::fstream uiFile(uiTTL, std::ios::out);
   1561 
   1562         String uiString;
   1563         uiString += "@prefix lv2:  <" LV2_CORE_PREFIX "> .\n";
   1564         uiString += "@prefix ui:   <" LV2_UI_PREFIX "> .\n";
   1565         uiString += "@prefix opts: <" LV2_OPTIONS_PREFIX "> .\n";
   1566         uiString += "\n";
   1567 
   1568         uiString += "<" DISTRHO_UI_URI ">\n";
   1569 
   1570         addAttribute(uiString, "lv2:extensionData", lv2ManifestUiExtensionData, 4);
   1571         addAttribute(uiString, "lv2:optionalFeature", lv2ManifestUiOptionalFeatures, 4);
   1572         addAttribute(uiString, "lv2:requiredFeature", lv2ManifestUiRequiredFeatures, 4);
   1573         addAttribute(uiString, "opts:supportedOption", lv2ManifestUiSupportedOptions, 4, true);
   1574 
   1575         uiFile << uiString;
   1576         uiFile.close();
   1577         std::cout << " done!" << std::endl;
   1578     }
   1579 #endif
   1580 
   1581     // ---------------------------------------------
   1582 
   1583 #if DISTRHO_PLUGIN_WANT_PROGRAMS
   1584     {
   1585         std::cout << "Writing presets.ttl..."; std::cout.flush();
   1586         std::fstream presetsFile("presets.ttl", std::ios::out);
   1587 
   1588         String presetsString;
   1589         presetsString += "@prefix lv2:   <" LV2_CORE_PREFIX "> .\n";
   1590         presetsString += "@prefix pset:  <" LV2_PRESETS_PREFIX "> .\n";
   1591 # if DISTRHO_PLUGIN_WANT_STATE
   1592         presetsString += "@prefix owl:   <http://www.w3.org/2002/07/owl#> .\n";
   1593         presetsString += "@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .\n";
   1594         presetsString += "@prefix state: <" LV2_STATE_PREFIX "> .\n";
   1595         presetsString += "@prefix xsd:   <http://www.w3.org/2001/XMLSchema#> .\n";
   1596 # endif
   1597         presetsString += "\n";
   1598 
   1599         const uint32_t numParameters = plugin.getParameterCount();
   1600         const uint32_t numPrograms   = plugin.getProgramCount();
   1601 # if DISTRHO_PLUGIN_WANT_FULL_STATE
   1602         const uint32_t numStates     = plugin.getStateCount();
   1603         const bool     valid         = numParameters != 0 || numStates != 0;
   1604 # else
   1605         const bool     valid         = numParameters != 0;
   1606 # endif
   1607 
   1608         DISTRHO_CUSTOM_SAFE_ASSERT_RETURN("Programs require parameters or full state", valid, presetsFile.close());
   1609 
   1610         const String presetSeparator(std::strstr(DISTRHO_PLUGIN_URI, "#") != nullptr ? ":" : "#");
   1611 
   1612         char strBuf[0xff+1];
   1613         strBuf[0xff] = '\0';
   1614 
   1615         String presetString;
   1616 
   1617 # if DISTRHO_PLUGIN_WANT_FULL_STATE
   1618         for (uint32_t i=0; i<numStates; ++i)
   1619         {
   1620             if (plugin.getStateHints(i) & kStateIsHostReadable)
   1621                 continue;
   1622 
   1623             // readable states are defined as lv2 parameters.
   1624             // non-readable states have no definition, but one is needed for presets and ttl validation.
   1625             presetString  = "<" DISTRHO_PLUGIN_LV2_STATE_PREFIX + plugin.getStateKey(i) + ">\n";
   1626             presetString += "    a owl:DatatypeProperty ;\n";
   1627             presetString += "    rdfs:label \"Plugin state key-value string pair\" ;\n";
   1628             presetString += "    rdfs:domain state:State ;\n";
   1629             presetString += "    rdfs:range xsd:string .\n\n";
   1630             presetsString += presetString;
   1631         }
   1632 # endif
   1633 
   1634         for (uint32_t i=0; i<numPrograms; ++i)
   1635         {
   1636             std::snprintf(strBuf, 0xff, "%03i", i+1);
   1637 
   1638             plugin.loadProgram(i);
   1639 
   1640             presetString = "<" DISTRHO_PLUGIN_URI + presetSeparator + "preset" + strBuf + ">\n";
   1641 
   1642 # if DISTRHO_PLUGIN_WANT_FULL_STATE
   1643             presetString += "    state:state [\n";
   1644             for (uint32_t j=0; j<numStates; ++j)
   1645             {
   1646                 const String key   = plugin.getStateKey(j);
   1647                 const String value = plugin.getStateValue(key);
   1648 
   1649                 presetString += "        <";
   1650 
   1651                 if (plugin.getStateHints(j) & kStateIsHostReadable)
   1652                     presetString += DISTRHO_PLUGIN_URI "#";
   1653                 else
   1654                     presetString += DISTRHO_PLUGIN_LV2_STATE_PREFIX;
   1655 
   1656                 presetString += key + ">";
   1657 
   1658                 if (value.length() < 10)
   1659                     presetString += " \"" + value + "\" ;\n";
   1660                 else
   1661                     presetString += "\n\"\"\"" + value + "\"\"\" ;\n";
   1662             }
   1663 
   1664             if (numParameters > 0)
   1665                 presetString += "    ] ;\n\n";
   1666             else
   1667                 presetString += "    ] .\n\n";
   1668 # endif
   1669 
   1670             bool firstParameter = true;
   1671 
   1672             for (uint32_t j=0; j <numParameters; ++j)
   1673             {
   1674                 if (plugin.isParameterOutput(j))
   1675                     continue;
   1676 
   1677                 if (firstParameter)
   1678                 {
   1679                     presetString += "    lv2:port [\n";
   1680                     firstParameter = false;
   1681                 }
   1682                 else
   1683                 {
   1684                     presetString += "    [\n";
   1685                 }
   1686 
   1687                 String parameterSymbol = plugin.getParameterSymbol(j);
   1688                 float parameterValue = plugin.getParameterValue(j);
   1689 
   1690                 if (plugin.getParameterDesignation(j) == kParameterDesignationBypass)
   1691                 {
   1692                     parameterSymbol = ParameterDesignationSymbols::bypass_lv2;
   1693                     parameterValue = 1.0f - parameterValue;
   1694                 }
   1695 
   1696                 presetString += "        lv2:symbol \"" + parameterSymbol + "\" ;\n";
   1697 
   1698                 if (plugin.getParameterHints(j) & kParameterIsInteger)
   1699                     presetString += "        pset:value " + String(int(parameterValue)) + " ;\n";
   1700                 else
   1701                     presetString += "        pset:value " + String(parameterValue) + " ;\n";
   1702 
   1703                 if (j+1 == numParameters || plugin.isParameterOutput(j+1))
   1704                     presetString += "    ] .\n\n";
   1705                 else
   1706                     presetString += "    ] ,\n";
   1707             }
   1708 
   1709             presetsString += presetString;
   1710         }
   1711 
   1712         presetsFile << presetsString;
   1713         presetsFile.close();
   1714         std::cout << " done!" << std::endl;
   1715     }
   1716 #endif
   1717 }