1 /**
2 * LV2 Client implementation
3 *
4 * Copyright: Ethan Reker 2018-2019.
5 *            Guillaume Piolat 2019-2022.
6 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 /*
9 * DISTRHO Plugin Framework (DPF)
10 * Copyright (C) 2012-2018 Filipe Coelho <falktx@falktx.com>
11 *
12 * Permission to use, copy, modify, and/or distribute this software for any purpose with
13 * or without fee is hereby granted, provided that the above copyright notice and this
14 * permission notice appear in all copies.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
17 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
18 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
19 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
20 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 */
23 
24 /// TTL generation.
25 module dplug.lv2.ttl;
26 
27 version(LV2):
28 
29 
30 import core.stdc.stdio;
31 import core.stdc.stdlib;
32 import core.stdc.string;
33 import std.conv;
34 
35 import dplug.core.nogc;
36 import dplug.core.vec;
37 import dplug.core.string;
38 
39 import dplug.client.client;
40 import dplug.client.preset;
41 import dplug.client.params;
42 import dplug.client.daw;
43 
44 /// Generate a manifest. Used by dplug-build, for LV2 builds.
45 /// - to ask needed size in bytes, pass null as outputBuffer
46 /// - else, pass as much bytes or more than necessary. Result manifest in outputBuffer[0..returned-value]
47 /// outputBuffer can be null, in which case it makes no copy.
48 int GenerateManifestFromClient_templated(alias ClientClass)(char[] outputBuffer,
49                                                             const(char)[] binaryFileName) nothrow @nogc
50 {
51     // Create a temporary client just to know its properties.
52     ClientClass client = mallocNew!ClientClass();
53     scope(exit) client.destroyFree();    
54 
55     LegalIO[] legalIOs = client.legalIOs();
56     Parameter[] params = client.params();
57 
58     String manifest;
59 
60     // Make an URI for the GUI
61     char[256] uriBuf; // this one variable reused quite a lot
62     sprintVendorPrefix(uriBuf.ptr, 256, client.pluginHomepage(), client.getPluginUniqueID());
63     
64     String strUriVendor;
65     {
66         const(char)[] uriVendor = uriBuf[0..strlen(uriBuf.ptr)];
67         escapeRDF_IRI2(uriVendor, strUriVendor);
68     }
69 
70     manifest ~= "@prefix lv2: <http://lv2plug.in/ns/lv2core#>.\n";
71     manifest ~= "@prefix atom: <http://lv2plug.in/ns/ext/atom#>.\n";
72     manifest ~= "@prefix doap: <http://usefulinc.com/ns/doap#>.\n";
73     manifest ~= "@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n";
74     manifest ~= "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\n";
75     manifest ~= "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n";
76     manifest ~= "@prefix urid: <http://lv2plug.in/ns/ext/urid#>.\n";
77     manifest ~= "@prefix ui: <http://lv2plug.in/ns/extensions/ui#>.\n";
78     manifest ~= "@prefix pset: <http://lv2plug.in/ns/ext/presets#>.\n";
79     manifest ~= "@prefix opts: <http://lv2plug.in/ns/ext/options#>.\n";
80     version(futureBinState)
81     {
82         manifest ~= "@prefix owl: <http://www.w3.org/2002/07/owl#>.\n";
83         manifest ~= "@prefix state: <http://lv2plug.in/ns/ext/state#>.\n";
84         manifest ~= "@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\n";
85     }
86 
87     if (client.sendsMIDI)
88     {
89         manifest ~= "@prefix rsz: <http://lv2plug.in/ns/ext/resize-port#>.\n";
90     }
91     manifest ~= "@prefix pprops: <http://lv2plug.in/ns/ext/port-props#>.\n";
92     manifest ~= "@prefix vendor: "; // this prefix abbreviate the ttl with our own URL base
93     manifest ~= strUriVendor;
94     manifest ~= ".\n\n";
95 
96     String strCategory;
97     lv2PluginCategory(client.pluginCategory, strCategory);
98 
99     String strBinaryFile;
100     escapeRDF_IRI2(binaryFileName, strBinaryFile);
101 
102     String strPluginName;
103     escapeRDFString(client.pluginName, strPluginName);
104 
105     String strVendorName;
106     escapeRDFString(client.vendorName, strVendorName);
107 
108     String paramString;
109 
110     version(futureBinState)
111     {
112 
113     manifest ~=
114 `
115 vendor:stateBinary
116     a owl:DatatypeProperty ;
117     rdfs:label "Dplug plugin state as base64-encoded string" ;
118     rdfs:domain state:State ;
119     rdfs:range xsd:base64Binary .
120 
121 `;
122 
123     }
124 
125     foreach(legalIO; legalIOs)
126     {
127         // Make an URI for this I/O configuration
128         sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
129 
130         manifest.appendZeroTerminatedString(uriBuf.ptr);
131         manifest ~= "\n";
132         manifest ~= "    a lv2:Plugin";
133         manifest ~= strCategory;
134         manifest ~= " ;\n";
135         manifest ~= "    lv2:binary ";
136         manifest ~= strBinaryFile;
137         manifest ~= " ;\n";
138         manifest ~= "    doap:name ";
139         manifest ~= strPluginName;
140         manifest ~= " ;\n";
141         manifest ~= "    doap:maintainer [ foaf:name ";
142         manifest ~= strVendorName;
143         manifest ~= " ] ;\n";
144         manifest ~= "    lv2:requiredFeature opts:options ,\n";
145     /*    version(futureBinState)
146         {
147             manifest ~= "    state:loadDefaultState ,\n";
148         } */
149         manifest ~= "                        urid:map ;\n";
150 
151         // We do not provide such an interface
152         //manifest ~= "    lv2:extensionData <" ~ LV2_OPTIONS__interface ~ "> ; \n";
153 
154         version(futureBinState)
155         {
156             manifest ~= "    lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ;\n";
157         }
158 
159         if(client.hasGUI)
160         {
161             manifest ~= "    ui:ui vendor:ui;\n";
162         }
163 
164         buildParamPortConfiguration(client.params(), legalIO, client.receivesMIDI, client.sendsMIDI, paramString);
165         manifest ~= paramString;
166     }
167 
168     // add presets information
169 
170     auto presetBank = client.presetBank();
171     String strPresetName;
172 
173     Vec!ubyte stateBuf;
174 
175     for(int presetIndex = 0; presetIndex < presetBank.numPresets(); ++presetIndex)
176     {
177         // Make an URI for this preset
178         sprintPluginURI_preset_short(uriBuf.ptr, 256, presetIndex);
179         Preset preset = presetBank.preset(presetIndex);
180         manifest ~= "\n";
181         manifest.appendZeroTerminatedString(uriBuf.ptr);
182         manifest ~= "\n"; 
183         manifest ~= "        a pset:Preset ;\n";
184         manifest ~= "        rdfs:label ";
185         escapeRDFString(preset.name, strPresetName);
186         manifest ~= strPresetName;
187         manifest ~= " ;\n";
188 
189         version(futureBinState)
190         {{
191             // Encode state buffer to base64.
192             const(ubyte)[] stateData = preset.getStateData();
193             if (stateData !is null) // there is some state associated with the preset
194             {
195                 stateData.encodeBase64(stateBuf);
196                 manifest ~= "        state:state [\n";
197                 manifest ~= "            vendor:stateBinary \"\"\"";
198                 manifest ~= cast(const(char)[])(stateBuf[]);
199                 manifest ~= "\"\"\"^^xsd:base64Binary ;\n";
200                 manifest ~= "        ] ;\n";
201             }
202         }}
203 
204         manifest ~= "        lv2:port [\n";
205 
206         const(float)[] paramValues = preset.getNormalizedParamValues();
207 
208         char[32] paramSymbol;
209         char[32] paramValue;
210 
211         for (int p = 0; p < paramValues.length; ++p)
212         {
213             snprintf(paramSymbol.ptr, 32, "p%d", p);
214             snprintf(paramValue.ptr, 32, "%g", paramValues[p]);
215 
216             manifest ~= "            lv2:symbol \"";
217             manifest.appendZeroTerminatedString( paramSymbol.ptr );
218             manifest ~= "\"; pset:value ";
219             manifest.appendZeroTerminatedString( paramValue.ptr );
220             manifest ~= " \n";
221             if (p + 1 == paramValues.length)
222                 manifest ~= "        ] ;\n";
223             else
224                 manifest ~= "        ] , [\n";
225         }
226 
227         // Each preset applies to every plugin I/O configuration
228         manifest ~= "        lv2:appliesTo ";
229         foreach(size_t n, legalIO; legalIOs)
230         {
231             // Make an URI for this I/O configuration
232             sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
233             manifest.appendZeroTerminatedString(uriBuf.ptr);
234             if (n + 1 == legalIOs.length)
235                 manifest ~= " . \n";
236             else
237                 manifest ~= " , ";
238         }
239     }
240 
241     // describe UI
242     if(client.hasGUI)
243     {
244         manifest ~= "\nvendor:ui\n";
245 
246         version(OSX)
247             manifest ~= "    a ui:CocoaUI;\n";
248         else version(Windows)
249             manifest ~= "    a ui:WindowsUI;\n";
250         else version(linux)
251             manifest ~= "    a ui:X11UI;\n";
252         else
253             static assert("unsupported OS");
254 
255         manifest ~= "    lv2:optionalFeature ui:noUserResize ,\n";
256         manifest ~= "                        ui:resize ,\n";
257         manifest ~= "                        ui:touch ;\n";
258         manifest ~= "    lv2:requiredFeature opts:options ,\n";
259         manifest ~= "                        urid:map ,\n";
260 
261         // No DSP separated from UI for us
262         manifest ~= "                        <http://lv2plug.in/ns/ext/instance-access> ;\n";
263 
264         manifest ~= "    ui:binary ";
265         manifest ~= strBinaryFile;
266         manifest ~= " .\n";
267     }
268 
269     assert(manifest.length < int.max); // now that would be a very big .ttl
270 
271     const int manifestFinalLength = cast(int) manifest.length;
272 
273     if (outputBuffer !is null)
274     {
275         outputBuffer[0..manifestFinalLength] = manifest[0..manifestFinalLength];
276     }
277 
278     return manifestFinalLength; // Always return manifest length, but you can pass null to get the needed size.
279 }
280 
281 package:
282 
283 void sprintVendorPrefix(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
284 {
285     CString pluginHomepageZ = CString(pluginHomepage);
286     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
287 }
288 
289 void sprintPluginURI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
290 {
291     CString pluginHomepageZ = CString(pluginHomepage);
292     snprintf(buf, maxChars, "%s%2X%2X%2X%2X", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
293 }
294 
295 void sprintPluginURI_UI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
296 {
297     CString pluginHomepageZ = CString(pluginHomepage);
298     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#ui", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
299 }
300 
301 void sprintPluginURI_preset_short(char* buf, size_t maxChars, int presetIndex) nothrow @nogc
302 {
303     snprintf(buf, maxChars, "vendor:preset%d", presetIndex);
304 }
305 
306 void sprintPluginURI_IO_short(char* buf, size_t maxChars, LegalIO io) nothrow @nogc
307 {
308     int ins = io.numInputChannels;
309     int outs = io.numOutputChannels;
310 
311     // give user-friendly names
312     if (ins == 1 && outs == 1)
313     {
314         snprintf(buf, maxChars, "vendor:mono");
315     }
316     else if (ins == 2 && outs == 2)
317     {
318         snprintf(buf, maxChars, "vendor:stereo");
319     }
320     else
321     {
322         snprintf(buf, maxChars, "vendor:in%dout%d", ins, outs);
323     }
324 }
325 
326 void sprintPluginURI_IO(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID, LegalIO io) nothrow @nogc
327 {
328     CString pluginHomepageZ = CString(pluginHomepage);
329     int ins = io.numInputChannels;
330     int outs = io.numOutputChannels;
331 
332     // give user-friendly names
333     if (ins == 1 && outs == 1)
334     {
335         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#mono", pluginHomepageZ.storage,
336                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
337     }
338     else if (ins == 2 && outs == 2)
339     {
340         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#stereo", pluginHomepageZ.storage,
341                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
342     }
343     else
344     {
345         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#in%dout%d", pluginHomepageZ.storage,
346                                                             pluginID[0], pluginID[1], pluginID[2], pluginID[3],
347                                                             ins, outs);
348     }
349 }
350 
351 void lv2PluginCategory(PluginCategory category, ref String lv2Category) nothrow @nogc
352 {
353     lv2Category.makeEmpty();
354     lv2Category ~= ", lv2:";
355     with(PluginCategory)
356     {
357         switch(category)
358         {
359             case effectAnalysisAndMetering:
360                 lv2Category ~= "AnalyserPlugin";
361                 break;
362             case effectDelay:
363                 lv2Category ~= "DelayPlugin";
364                 break;
365             case effectDistortion:
366                 lv2Category ~= "DistortionPlugin";
367                 break;
368             case effectDynamics:
369                 lv2Category ~= "DynamicsPlugin";
370                 break;
371             case effectEQ:
372                 lv2Category ~= "EQPlugin";
373                 break;
374             case effectImaging:
375                 lv2Category ~= "SpatialPlugin";
376                 break;
377             case effectModulation:
378                 lv2Category ~= "ModulatorPlugin";
379                 break;
380             case effectPitch:
381                 lv2Category ~= "PitchPlugin";
382                 break;
383             case effectReverb:
384                 lv2Category ~= "ReverbPlugin";
385                 break;
386             case effectOther:
387                 lv2Category ~= "UtilityPlugin";
388                 break;
389             case instrumentDrums:
390             case instrumentSampler:
391             case instrumentSynthesizer:
392             case instrumentOther:
393                 lv2Category ~= "InstrumentPlugin";
394                 break;
395             case invalid:
396             default:
397                 lv2Category.makeEmpty();
398         }
399     }
400 }
401 
402 /// escape a UTF-8 string for UTF-8 RDF
403 /// See_also: https://www.w3.org/TR/turtle/
404 void escapeRDFString(const(char)[] s, ref String r) nothrow @nogc
405 {   
406     r = '\"';
407 
408     int index = 1;
409 
410     foreach(char ch; s)
411     {
412         switch(ch)
413         {
414            // Escape some whitespace chars
415            case '\t': r ~= '\\'; r ~= 't'; break;
416            case '\b': r ~= '\\'; r ~= 'b'; break;
417            case '\n': r ~= '\\'; r ~= 'n'; break;
418            case '\r': r ~= '\\'; r ~= 'r'; break;
419            case '\f': r ~= '\\'; r ~= 'f'; break;
420            case '\"': r ~= '\\'; r ~= '\"'; break;
421            case '\'': r ~= '\\'; r ~= '\''; break;
422            case '\\': r ~= '\\'; r ~= '\\'; break;
423            default:
424                r ~= ch;
425         }
426     }
427     r ~= '\"';
428 }
429 unittest
430 {
431     String r;
432     escapeRDFString("Stereo Link", r);
433     assert(r == "\"Stereo Link\"");
434 }
435 
436 /// Escape a UTF-8 string for UTF-8 IRI literal
437 /// See_also: https://www.w3.org/TR/turtle/
438 void escapeRDF_IRI2(const(char)[] s, ref String outString) nothrow @nogc
439 {
440     outString.makeEmpty();
441     outString ~= '<';
442 
443     // We actually remove all special characters, because it seems not all hosts properly decode escape sequences
444     foreach(char ch; s)
445     {
446         switch(ch)
447         {
448             // escape some whitespace chars
449             case '\0': .. case ' ':
450             case '<':
451             case '>':
452             case '"':
453             case '{':
454             case '}':
455             case '|':
456             case '^':
457             case '`':
458             case '\\':
459                 break; // skip that character
460             default:
461                 outString ~= ch;
462         }
463     }
464     outString ~= '>';
465 }
466 
467 void buildParamPortConfiguration(Parameter[] params, 
468                                  LegalIO legalIO, 
469                                  bool hasMIDIInput, 
470                                  bool hasMIDIOutput,
471                                  ref String paramString) nothrow @nogc
472 {
473     int portIndex = 0;
474 
475     paramString = "";
476 
477     // Note: parameters symbols should be consistent across versions
478     // Can't change them without issuing a major version change.
479     // We choose to have symbol "p<n>" for parameter n (Dplug assume we can append parameters in minor versions)
480     // We choose to have symbol "input_<n>" for input channel n
481     // We choose to have symbol "output_<n>" for output channel n
482 
483     {
484         char[256] indexString;
485         char[256] paramSymbol;
486 
487         String strParamName;
488 
489         paramString ~= "    lv2:port\n";
490         foreach(paramIndex, param; params)
491         {
492             sprintf(indexString.ptr, "%d", portIndex);
493             sprintf(paramSymbol.ptr, "p%d", cast(int)paramIndex);
494             paramString ~= "    [\n";
495             paramString ~= "        a lv2:InputPort , lv2:ControlPort ;\n";
496             paramString ~= "        lv2:index ";
497             paramString.appendZeroTerminatedString(indexString.ptr);
498             paramString ~= " ;\n";
499             paramString ~= "        lv2:symbol \"";
500             paramString.appendZeroTerminatedString(paramSymbol.ptr);
501             paramString ~= "\" ;\n";
502 
503             paramString ~= "        lv2:name ";
504             escapeRDFString(param.name, strParamName);
505             paramString ~= strParamName;
506 
507             paramString ~= " ;\n";
508             paramString ~= "        lv2:default ";
509 
510             char[10] paramNormalized;
511             snprintf(paramNormalized.ptr, 10, "%g", param.getNormalized());
512 
513             paramString.appendZeroTerminatedString(paramNormalized.ptr);
514 
515             paramString ~= " ;\n";
516             paramString ~= "        lv2:minimum 0.0 ;\n";
517             paramString ~= "        lv2:maximum 1.0 ;\n";
518             if (!param.isAutomatable) {
519                 paramString ~= "        lv2:portProperty <http://kxstudio.sf.net/ns/lv2ext/props#NonAutomable> ;\n";
520             }
521             paramString ~= "    ] ,\n";
522             ++portIndex;
523         }
524     }
525 
526     {
527         char[256] indexString;
528         char[256] inputString;
529         foreach(input; 0..legalIO.numInputChannels)
530         {
531             sprintf(indexString.ptr, "%d", portIndex);
532         
533             static if (false)
534                 sprintf(inputString.ptr, "%d", input);
535             else
536             {
537                 // kept for backward compatibility; however this breaks if the
538                 // number of parameters change in the future.
539                 sprintf(inputString.ptr, "%d", cast(int)(input + params.length));
540             }
541 
542             paramString ~= "    [\n";
543             paramString ~= "        a lv2:AudioPort , lv2:InputPort ;\n";
544             paramString ~= "        lv2:index ";
545             paramString.appendZeroTerminatedString(indexString.ptr);
546             paramString ~= ";\n";
547             paramString ~= "        lv2:symbol \"input_";
548             paramString.appendZeroTerminatedString(inputString.ptr);
549             paramString ~= "\" ;\n";
550             paramString ~= "        lv2:name \"Input";
551             paramString.appendZeroTerminatedString(inputString.ptr);
552             paramString ~= "\" ;\n";
553             paramString ~= "    ] ,\n";
554             ++portIndex;
555         }
556     }
557 
558     {
559         char[256] indexString;
560         char[256] outputString;
561         foreach(output; 0..legalIO.numOutputChannels)
562         {
563             sprintf(indexString.ptr, "%d", portIndex);
564             sprintf(outputString.ptr, "%d", output);
565 
566             paramString ~= "    [\n";
567             paramString ~= "        a lv2:AudioPort , lv2:OutputPort ;\n";
568             paramString ~= "        lv2:index ";
569             paramString.appendZeroTerminatedString(indexString.ptr);
570             paramString ~= ";\n";
571             paramString ~= "        lv2:symbol \"output_";
572             paramString.appendZeroTerminatedString(outputString.ptr);
573             paramString ~= "\" ;\n";
574             paramString ~= "        lv2:name \"Output";
575             paramString.appendZeroTerminatedString(outputString.ptr);
576             paramString ~= "\" ;\n";
577             paramString ~= "    ] ,\n";
578 
579             if(output == legalIO.numOutputChannels - 1)
580             {
581                 ++portIndex;
582                 sprintf(indexString.ptr, "%d", portIndex);
583                 paramString ~= "    [\n";
584                 paramString ~= "        a lv2:ControlPort , lv2:OutputPort ;\n";
585                 paramString ~= "        lv2:index ";
586                 paramString.appendZeroTerminatedString(indexString.ptr);
587                 paramString ~= ";\n";
588                 paramString ~= "        lv2:designation lv2:latency ;\n";
589                 paramString ~= "        lv2:symbol \"latency\" ;\n";
590                 paramString ~= "        lv2:name \"Latency\" ;\n";
591                 paramString ~= "        lv2:portProperty lv2:reportsLatency, lv2:connectionOptional, pprops:notOnGUI ;\n";
592                 paramString ~= "    ] ,\n";
593             }
594             ++portIndex;
595         }
596     }
597 
598     paramString ~= "    [\n";
599     paramString ~= "        a lv2:InputPort, atom:AtomPort ;\n";
600     paramString ~= "        atom:bufferType atom:Sequence ;\n";
601     paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
602 
603     if(hasMIDIInput)
604         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
605 
606     char[16] indexBuf;
607     snprintf(indexBuf.ptr, 16, "%d", portIndex);
608 
609     paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/time#Position> ;\n";
610     paramString ~= "        lv2:designation lv2:control ;\n";
611     paramString ~= "        lv2:index ";
612     paramString.appendZeroTerminatedString(indexBuf.ptr);
613     paramString ~= ";\n";
614     paramString ~= "        lv2:symbol \"lv2_events_in\" ;\n";
615     paramString ~= "        lv2:name \"Events Input\"\n";
616     paramString ~= "    ]";
617     ++portIndex;
618 
619     if (hasMIDIOutput)
620     {
621         paramString ~= " ,\n    [\n";
622         paramString ~= "        a lv2:OutputPort, atom:AtomPort ;\n";
623         paramString ~= "        atom:bufferType atom:Sequence ;\n";
624         paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
625         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
626         paramString ~= "        lv2:designation lv2:control ;\n";
627         snprintf(indexBuf.ptr, 16, "%d", portIndex);
628         paramString ~= "        lv2:index ";
629         paramString.appendZeroTerminatedString(indexBuf.ptr);
630         paramString ~= ";\n";
631         paramString ~= "        lv2:symbol \"lv2_events_out\" ;\n";
632         paramString ~= "        lv2:name \"Events Output\" ;\n";
633         paramString ~= "        rsz:minimumSize 2048 ;\n";
634         paramString ~= "    ]";
635     }
636     ++portIndex;
637 
638     paramString ~= " .\n";
639 }