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(legacyBinState)
81     {}
82     else
83     {
84         manifest ~= "@prefix owl: <http://www.w3.org/2002/07/owl#>.\n";
85         manifest ~= "@prefix state: <http://lv2plug.in/ns/ext/state#>.\n";
86         manifest ~= "@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\n";
87     }
88 
89     if (client.sendsMIDI)
90     {
91         manifest ~= "@prefix rsz: <http://lv2plug.in/ns/ext/resize-port#>.\n";
92     }
93     manifest ~= "@prefix pprops: <http://lv2plug.in/ns/ext/port-props#>.\n";
94     manifest ~= "@prefix vendor: "; // this prefix abbreviate the ttl with our own URL base
95     manifest ~= strUriVendor;
96     manifest ~= ".\n\n";
97 
98     String strCategory;
99     lv2PluginCategory(client.pluginCategory, strCategory);
100 
101     String strBinaryFile;
102     escapeRDF_IRI2(binaryFileName, strBinaryFile);
103 
104     String strPluginName;
105     escapeRDFString(client.pluginName, strPluginName);
106 
107     String strVendorName;
108     escapeRDFString(client.vendorName, strVendorName);
109 
110     String paramString;
111 
112     version(legacyBinState)
113     {}
114     else
115     {
116 
117     manifest ~=
118 `
119 vendor:stateBinary
120     a owl:DatatypeProperty ;
121     rdfs:label "Dplug plugin state as base64-encoded string" ;
122     rdfs:domain state:State ;
123     rdfs:range xsd:base64Binary .
124 
125 `;
126 
127     }
128 
129     foreach(legalIO; legalIOs)
130     {
131         // Make an URI for this I/O configuration
132         sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
133 
134         manifest.appendZeroTerminatedString(uriBuf.ptr);
135         manifest ~= "\n";
136         manifest ~= "    a lv2:Plugin";
137         manifest ~= strCategory;
138         manifest ~= " ;\n";
139         manifest ~= "    lv2:binary ";
140         manifest ~= strBinaryFile;
141         manifest ~= " ;\n";
142         manifest ~= "    doap:name ";
143         manifest ~= strPluginName;
144         manifest ~= " ;\n";
145         manifest ~= "    doap:maintainer [ foaf:name ";
146         manifest ~= strVendorName;
147         manifest ~= " ] ;\n";
148         manifest ~= "    lv2:requiredFeature opts:options ,\n";
149     /*    version(legacyBinState)
150         {}
151         else
152         {
153             manifest ~= "    state:loadDefaultState ,\n";
154         } */
155         manifest ~= "                        urid:map ;\n";
156 
157         // We do not provide such an interface
158         //manifest ~= "    lv2:extensionData <" ~ LV2_OPTIONS__interface ~ "> ; \n";
159 
160         version(legacyBinState)
161         {}
162         else
163         {
164             manifest ~= "    lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ;\n";
165         }
166 
167         if(client.hasGUI)
168         {
169             manifest ~= "    ui:ui vendor:ui;\n";
170         }
171 
172         buildParamPortConfiguration(client.params(), legalIO, client.receivesMIDI, client.sendsMIDI, paramString);
173         manifest ~= paramString;
174     }
175 
176     // add presets information
177 
178     auto presetBank = client.presetBank();
179     String strPresetName;
180 
181     Vec!ubyte stateBuf;
182 
183     for(int presetIndex = 0; presetIndex < presetBank.numPresets(); ++presetIndex)
184     {
185         // Make an URI for this preset
186         sprintPluginURI_preset_short(uriBuf.ptr, 256, presetIndex);
187         Preset preset = presetBank.preset(presetIndex);
188         manifest ~= "\n";
189         manifest.appendZeroTerminatedString(uriBuf.ptr);
190         manifest ~= "\n"; 
191         manifest ~= "        a pset:Preset ;\n";
192         manifest ~= "        rdfs:label ";
193         escapeRDFString(preset.name, strPresetName);
194         manifest ~= strPresetName;
195         manifest ~= " ;\n";
196 
197         version(legacyBinState)
198         {}
199         else
200         {{
201             // Encode state buffer to base64.
202             const(ubyte)[] stateData = preset.getStateData();
203             if (stateData !is null) // there is some state associated with the preset
204             {
205                 stateData.encodeBase64(stateBuf);
206                 manifest ~= "        state:state [\n";
207                 manifest ~= "            vendor:stateBinary \"\"\"";
208                 manifest ~= cast(const(char)[])(stateBuf[]);
209                 manifest ~= "\"\"\"^^xsd:base64Binary ;\n";
210                 manifest ~= "        ] ;\n";
211             }
212         }}
213 
214         manifest ~= "        lv2:port [\n";
215 
216         const(float)[] paramValues = preset.getNormalizedParamValues();
217 
218         char[32] paramSymbol;
219         char[32] paramValue;
220 
221         for (int p = 0; p < paramValues.length; ++p)
222         {
223             snprintf(paramSymbol.ptr, 32, "p%d", p);
224             snprintf(paramValue.ptr, 32, "%g", paramValues[p]);
225 
226             manifest ~= "            lv2:symbol \"";
227             manifest.appendZeroTerminatedString( paramSymbol.ptr );
228             manifest ~= "\"; pset:value ";
229             manifest.appendZeroTerminatedString( paramValue.ptr );
230             manifest ~= " \n";
231             if (p + 1 == paramValues.length)
232                 manifest ~= "        ] ;\n";
233             else
234                 manifest ~= "        ] , [\n";
235         }
236 
237         // Each preset applies to every plugin I/O configuration
238         manifest ~= "        lv2:appliesTo ";
239         foreach(size_t n, legalIO; legalIOs)
240         {
241             // Make an URI for this I/O configuration
242             sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
243             manifest.appendZeroTerminatedString(uriBuf.ptr);
244             if (n + 1 == legalIOs.length)
245                 manifest ~= " . \n";
246             else
247                 manifest ~= " , ";
248         }
249     }
250 
251     // describe UI
252     if(client.hasGUI)
253     {
254         manifest ~= "\nvendor:ui\n";
255 
256         version(OSX)
257             manifest ~= "    a ui:CocoaUI;\n";
258         else version(Windows)
259             manifest ~= "    a ui:WindowsUI;\n";
260         else version(linux)
261             manifest ~= "    a ui:X11UI;\n";
262         else
263             static assert("unsupported OS");
264 
265         manifest ~= "    lv2:optionalFeature ui:noUserResize ,\n";
266         manifest ~= "                        ui:resize ,\n";
267         manifest ~= "                        ui:touch ;\n";
268         manifest ~= "    lv2:requiredFeature opts:options ,\n";
269         manifest ~= "                        urid:map ,\n";
270 
271         // No DSP separated from UI for us
272         manifest ~= "                        <http://lv2plug.in/ns/ext/instance-access> ;\n";
273 
274         manifest ~= "    ui:binary ";
275         manifest ~= strBinaryFile;
276         manifest ~= " .\n";
277     }
278 
279     assert(manifest.length < int.max); // now that would be a very big .ttl
280 
281     const int manifestFinalLength = cast(int) manifest.length;
282 
283     if (outputBuffer !is null)
284     {
285         outputBuffer[0..manifestFinalLength] = manifest[0..manifestFinalLength];
286     }
287 
288     return manifestFinalLength; // Always return manifest length, but you can pass null to get the needed size.
289 }
290 
291 package:
292 
293 void sprintVendorPrefix(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
294 {
295     CString pluginHomepageZ = CString(pluginHomepage);
296     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
297 }
298 
299 void sprintPluginURI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
300 {
301     CString pluginHomepageZ = CString(pluginHomepage);
302     snprintf(buf, maxChars, "%s%2X%2X%2X%2X", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
303 }
304 
305 void sprintPluginURI_UI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
306 {
307     CString pluginHomepageZ = CString(pluginHomepage);
308     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#ui", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
309 }
310 
311 void sprintPluginURI_preset_short(char* buf, size_t maxChars, int presetIndex) nothrow @nogc
312 {
313     snprintf(buf, maxChars, "vendor:preset%d", presetIndex);
314 }
315 
316 void sprintPluginURI_IO_short(char* buf, size_t maxChars, LegalIO io) nothrow @nogc
317 {
318     int ins = io.numInputChannels;
319     int outs = io.numOutputChannels;
320 
321     // give user-friendly names
322     if (ins == 1 && outs == 1)
323     {
324         snprintf(buf, maxChars, "vendor:mono");
325     }
326     else if (ins == 2 && outs == 2)
327     {
328         snprintf(buf, maxChars, "vendor:stereo");
329     }
330     else
331     {
332         snprintf(buf, maxChars, "vendor:in%dout%d", ins, outs);
333     }
334 }
335 
336 void sprintPluginURI_IO(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID, LegalIO io) nothrow @nogc
337 {
338     CString pluginHomepageZ = CString(pluginHomepage);
339     int ins = io.numInputChannels;
340     int outs = io.numOutputChannels;
341 
342     // give user-friendly names
343     if (ins == 1 && outs == 1)
344     {
345         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#mono", pluginHomepageZ.storage,
346                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
347     }
348     else if (ins == 2 && outs == 2)
349     {
350         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#stereo", pluginHomepageZ.storage,
351                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
352     }
353     else
354     {
355         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#in%dout%d", pluginHomepageZ.storage,
356                                                             pluginID[0], pluginID[1], pluginID[2], pluginID[3],
357                                                             ins, outs);
358     }
359 }
360 
361 void lv2PluginCategory(PluginCategory category, ref String lv2Category) nothrow @nogc
362 {
363     lv2Category.makeEmpty();
364     lv2Category ~= ", lv2:";
365     with(PluginCategory)
366     {
367         switch(category)
368         {
369             case effectAnalysisAndMetering:
370                 lv2Category ~= "AnalyserPlugin";
371                 break;
372             case effectDelay:
373                 lv2Category ~= "DelayPlugin";
374                 break;
375             case effectDistortion:
376                 lv2Category ~= "DistortionPlugin";
377                 break;
378             case effectDynamics:
379                 lv2Category ~= "DynamicsPlugin";
380                 break;
381             case effectEQ:
382                 lv2Category ~= "EQPlugin";
383                 break;
384             case effectImaging:
385                 lv2Category ~= "SpatialPlugin";
386                 break;
387             case effectModulation:
388                 lv2Category ~= "ModulatorPlugin";
389                 break;
390             case effectPitch:
391                 lv2Category ~= "PitchPlugin";
392                 break;
393             case effectReverb:
394                 lv2Category ~= "ReverbPlugin";
395                 break;
396             case effectOther:
397                 lv2Category ~= "UtilityPlugin";
398                 break;
399             case instrumentDrums:
400             case instrumentSampler:
401             case instrumentSynthesizer:
402             case instrumentOther:
403                 lv2Category ~= "InstrumentPlugin";
404                 break;
405             case invalid:
406             default:
407                 lv2Category.makeEmpty();
408         }
409     }
410 }
411 
412 /// escape a UTF-8 string for UTF-8 RDF
413 /// See_also: https://www.w3.org/TR/turtle/
414 void escapeRDFString(const(char)[] s, ref String r) nothrow @nogc
415 {   
416     r = '\"';
417 
418     int index = 1;
419 
420     foreach(char ch; s)
421     {
422         switch(ch)
423         {
424            // Escape some whitespace chars
425            case '\t': r ~= '\\'; r ~= 't'; break;
426            case '\b': r ~= '\\'; r ~= 'b'; break;
427            case '\n': r ~= '\\'; r ~= 'n'; break;
428            case '\r': r ~= '\\'; r ~= 'r'; break;
429            case '\f': r ~= '\\'; r ~= 'f'; break;
430            case '\"': r ~= '\\'; r ~= '\"'; break;
431            case '\'': r ~= '\\'; r ~= '\''; break;
432            case '\\': r ~= '\\'; r ~= '\\'; break;
433            default:
434                r ~= ch;
435         }
436     }
437     r ~= '\"';
438 }
439 unittest
440 {
441     String r;
442     escapeRDFString("Stereo Link", r);
443     assert(r == "\"Stereo Link\"");
444 }
445 
446 /// Escape a UTF-8 string for UTF-8 IRI literal
447 /// See_also: https://www.w3.org/TR/turtle/
448 void escapeRDF_IRI2(const(char)[] s, ref String outString) nothrow @nogc
449 {
450     outString.makeEmpty();
451     outString ~= '<';
452 
453     // We actually remove all special characters, because it seems not all hosts properly decode escape sequences
454     foreach(char ch; s)
455     {
456         switch(ch)
457         {
458             // escape some whitespace chars
459             case '\0': .. case ' ':
460             case '<':
461             case '>':
462             case '"':
463             case '{':
464             case '}':
465             case '|':
466             case '^':
467             case '`':
468             case '\\':
469                 break; // skip that character
470             default:
471                 outString ~= ch;
472         }
473     }
474     outString ~= '>';
475 }
476 
477 void buildParamPortConfiguration(Parameter[] params, 
478                                  LegalIO legalIO, 
479                                  bool hasMIDIInput, 
480                                  bool hasMIDIOutput,
481                                  ref String paramString) nothrow @nogc
482 {
483     int portIndex = 0;
484 
485     paramString = "";
486 
487     // Note: parameters symbols should be consistent across versions
488     // Can't change them without issuing a major version change.
489     // We choose to have symbol "p<n>" for parameter n (Dplug assume we can append parameters in minor versions)
490     // We choose to have symbol "input_<n>" for input channel n
491     // We choose to have symbol "output_<n>" for output channel n
492 
493     {
494         char[256] indexString;
495         char[256] paramSymbol;
496 
497         String strParamName;
498 
499         paramString ~= "    lv2:port\n";
500         foreach(paramIndex, param; params)
501         {
502             sprintf(indexString.ptr, "%d", portIndex);
503             sprintf(paramSymbol.ptr, "p%d", cast(int)paramIndex);
504             paramString ~= "    [\n";
505             paramString ~= "        a lv2:InputPort , lv2:ControlPort ;\n";
506             paramString ~= "        lv2:index ";
507             paramString.appendZeroTerminatedString(indexString.ptr);
508             paramString ~= " ;\n";
509             paramString ~= "        lv2:symbol \"";
510             paramString.appendZeroTerminatedString(paramSymbol.ptr);
511             paramString ~= "\" ;\n";
512 
513             paramString ~= "        lv2:name ";
514             escapeRDFString(param.name, strParamName);
515             paramString ~= strParamName;
516 
517             paramString ~= " ;\n";
518             paramString ~= "        lv2:default ";
519 
520             char[10] paramNormalized;
521             snprintf(paramNormalized.ptr, 10, "%g", param.getNormalized());
522 
523             paramString.appendZeroTerminatedString(paramNormalized.ptr);
524 
525             paramString ~= " ;\n";
526             paramString ~= "        lv2:minimum 0.0 ;\n";
527             paramString ~= "        lv2:maximum 1.0 ;\n";
528             if (!param.isAutomatable) {
529                 paramString ~= "        lv2:portProperty <http://kxstudio.sf.net/ns/lv2ext/props#NonAutomable> ;\n";
530             }
531             paramString ~= "    ] ,\n";
532             ++portIndex;
533         }
534     }
535 
536     {
537         char[256] indexString;
538         char[256] inputString;
539         foreach(input; 0..legalIO.numInputChannels)
540         {
541             sprintf(indexString.ptr, "%d", portIndex);
542         
543             static if (false)
544                 sprintf(inputString.ptr, "%d", input);
545             else
546             {
547                 // kept for backward compatibility; however this breaks if the
548                 // number of parameters change in the future.
549                 sprintf(inputString.ptr, "%d", cast(int)(input + params.length));
550             }
551 
552             paramString ~= "    [\n";
553             paramString ~= "        a lv2:AudioPort , lv2:InputPort ;\n";
554             paramString ~= "        lv2:index ";
555             paramString.appendZeroTerminatedString(indexString.ptr);
556             paramString ~= ";\n";
557             paramString ~= "        lv2:symbol \"input_";
558             paramString.appendZeroTerminatedString(inputString.ptr);
559             paramString ~= "\" ;\n";
560             paramString ~= "        lv2:name \"Input";
561             paramString.appendZeroTerminatedString(inputString.ptr);
562             paramString ~= "\" ;\n";
563             paramString ~= "    ] ,\n";
564             ++portIndex;
565         }
566     }
567 
568     {
569         char[256] indexString;
570         char[256] outputString;
571         foreach(output; 0..legalIO.numOutputChannels)
572         {
573             sprintf(indexString.ptr, "%d", portIndex);
574             sprintf(outputString.ptr, "%d", output);
575 
576             paramString ~= "    [\n";
577             paramString ~= "        a lv2:AudioPort , lv2:OutputPort ;\n";
578             paramString ~= "        lv2:index ";
579             paramString.appendZeroTerminatedString(indexString.ptr);
580             paramString ~= ";\n";
581             paramString ~= "        lv2:symbol \"output_";
582             paramString.appendZeroTerminatedString(outputString.ptr);
583             paramString ~= "\" ;\n";
584             paramString ~= "        lv2:name \"Output";
585             paramString.appendZeroTerminatedString(outputString.ptr);
586             paramString ~= "\" ;\n";
587             paramString ~= "    ] ,\n";
588 
589             if(output == legalIO.numOutputChannels - 1)
590             {
591                 ++portIndex;
592                 sprintf(indexString.ptr, "%d", portIndex);
593                 paramString ~= "    [\n";
594                 paramString ~= "        a lv2:ControlPort , lv2:OutputPort ;\n";
595                 paramString ~= "        lv2:index ";
596                 paramString.appendZeroTerminatedString(indexString.ptr);
597                 paramString ~= ";\n";
598                 paramString ~= "        lv2:designation lv2:latency ;\n";
599                 paramString ~= "        lv2:symbol \"latency\" ;\n";
600                 paramString ~= "        lv2:name \"Latency\" ;\n";
601                 paramString ~= "        lv2:portProperty lv2:reportsLatency, lv2:connectionOptional, pprops:notOnGUI ;\n";
602                 paramString ~= "    ] ,\n";
603             }
604             ++portIndex;
605         }
606     }
607 
608     paramString ~= "    [\n";
609     paramString ~= "        a lv2:InputPort, atom:AtomPort ;\n";
610     paramString ~= "        atom:bufferType atom:Sequence ;\n";
611     paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
612 
613     if(hasMIDIInput)
614         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
615 
616     char[16] indexBuf;
617     snprintf(indexBuf.ptr, 16, "%d", portIndex);
618 
619     paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/time#Position> ;\n";
620     paramString ~= "        lv2:designation lv2:control ;\n";
621     paramString ~= "        lv2:index ";
622     paramString.appendZeroTerminatedString(indexBuf.ptr);
623     paramString ~= ";\n";
624     paramString ~= "        lv2:symbol \"lv2_events_in\" ;\n";
625     paramString ~= "        lv2:name \"Events Input\"\n";
626     paramString ~= "    ]";
627     ++portIndex;
628 
629     if (hasMIDIOutput)
630     {
631         paramString ~= " ,\n    [\n";
632         paramString ~= "        a lv2:OutputPort, atom:AtomPort ;\n";
633         paramString ~= "        atom:bufferType atom:Sequence ;\n";
634         paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
635         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
636         paramString ~= "        lv2:designation lv2:control ;\n";
637         snprintf(indexBuf.ptr, 16, "%d", portIndex);
638         paramString ~= "        lv2:index ";
639         paramString.appendZeroTerminatedString(indexBuf.ptr);
640         paramString ~= ";\n";
641         paramString ~= "        lv2:symbol \"lv2_events_out\" ;\n";
642         paramString ~= "        lv2:name \"Events Output\" ;\n";
643         paramString ~= "        rsz:minimumSize 2048 ;\n";
644         paramString ~= "    ]";
645     }
646     ++portIndex;
647 
648     paramString ~= " .\n";
649 }