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 }