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 }