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 }