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 }