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