1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005 - 2015 Cockos Incorporated 5 Copyright (C) 2015 - 2017 Auburn Sounds 6 7 Portions copyright other contributors, see each source file for more information 8 9 This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. 10 11 Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 14 1. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 15 1. This notice may not be removed or altered from any source distribution. 16 */ 17 18 /// Base client implementation. Every plugin format implementation hold a `Client` member. 19 module dplug.client.client; 20 21 import core.atomic; 22 import core.stdc.string; 23 import core.stdc.stdio; 24 import core.stdc.stdlib: free; 25 26 import dplug.core.nogc; 27 import dplug.core.math; 28 import dplug.core.vec; 29 import dplug.core.sync; 30 31 import dplug.client.params; 32 import dplug.client.preset; 33 import dplug.client.midi; 34 import dplug.client.graphics; 35 import dplug.client.daw; 36 37 38 enum PluginFormat 39 { 40 vst2, // Virtual Studio Technology v2 41 vst3, // Virtual Studio Technology v3 42 aax, // Avid Audio eXtension 43 auv2, // Audio Unit v2 44 lv2, // LADSPA Version 2 45 clap, // CLever Audio Plug-in 46 flp, // Fruity Loops Plug-in, aka FP, aka FL 47 } 48 49 50 /// A plugin client can send commands to the host. 51 /// This interface is injected after the client creation though. 52 interface IHostCommand 53 { 54 nothrow @nogc: 55 56 /// Notifies the host that editing of a parameter has begun from UI side. 57 void beginParamEdit(int paramIndex); 58 59 /// Notifies the host that a parameter was edited from the UI side. 60 /// This enables the host to record automation. 61 /// It is illegal to call `paramAutomate` outside of a `beginParamEdit`/`endParamEdit` pair. 62 /// 63 /// Params: 64 /// value Normalized parameter value. 65 void paramAutomate(int paramIndex, float value); 66 67 /// Notifies the host that editing of a parameter has finished from UI side. 68 void endParamEdit(int paramIndex); 69 70 /// Requests to the host a resize of the plugin window's PARENT window, given logical pixels of plugin window. 71 /// 72 /// Note: UI widgets and plugin format clients have different coordinate systems. 73 /// 74 /// Params: 75 /// width New width of the plugin, in logical pixels. 76 /// height New height of the plugin, in logical pixels. 77 /// Returns: `true` if the host parent window has been resized. 78 bool requestResize(int widthLogicalPixels, int heightLogicalPixels); 79 80 /// Tells the host that the plugin window HAS resized already, and the parent need to update. 81 /// Only useful for FL Studio own plugin format right now. 82 /// To be useful, the `requestResize` must return false for this format, so that manual resize is performed. 83 /// Returns: `true` is the host will act on it, `false` if not supported in this format. 84 bool notifyResized(); 85 86 /// Report the identied host name (DAW). 87 /// MAYDO: not available for LV2. 88 DAW getDAW(); 89 90 /// Gets the plugin format used at runtime. Version identifier may not be enough in the future, in case of 91 /// unity builds. 92 PluginFormat getPluginFormat(); 93 } 94 95 // Plugin version in major.minor.patch form. 96 struct PluginVersion 97 { 98 nothrow: 99 @nogc: 100 int major; 101 int minor; 102 int patch; 103 104 int toVSTVersion() pure const 105 { 106 assert(major < 10 && minor < 10 && patch < 10); 107 return major * 1000 + minor * 100 + patch*10; 108 } 109 110 int toAUVersion() pure const 111 { 112 assert(major < 256 && minor < 256 && patch < 256); 113 return (major << 16) | (minor << 8) | patch; 114 } 115 116 int toAAXPackageVersion() pure const 117 { 118 // For AAX, considered binary-compatible unless major version change 119 return major; 120 } 121 122 void toVST3VersionString(char* outBuffer, int bufLength) const 123 { 124 snprintf(outBuffer, bufLength, "%d.%d.%d", major, minor, patch); 125 126 // DigitalMars's snprintf doesn't always add a terminal zero 127 if (bufLength > 0) 128 outBuffer[bufLength-1] = '\0'; 129 } 130 131 alias toCLAPVersionString = toVST3VersionString; 132 } 133 134 135 // Statically known features of the plugin. 136 // There is some default for explanation purpose, but you really ought to override them all. 137 // Most of it is redundant with plugin.json, in the future the JSON will be parsed instead. 138 struct PluginInfo 139 { 140 string vendorName = "Witty Audio"; 141 142 /// A four char vendor "unique" ID 143 char[4] vendorUniqueID = "Wity"; 144 145 /// The vendor email adress for support. Can be null. 146 string vendorSupportEmail = null; 147 148 /// Plugin name. 149 string pluginName = "Destructatorizer"; 150 151 /// Plugin web page. Can be null. 152 string pluginHomepage = null; 153 154 /// Used for both VST and AU. 155 /// In AU it is namespaced by the manufacturer. In VST it 156 /// should be unique. While it seems no VST host use this 157 /// ID as a unique way to identify a plugin, common wisdom 158 /// is to try to get a sufficiently random one. 159 char[4] pluginUniqueID = "WiDi"; 160 161 // Plugin version information. 162 // It's important that the version you fill at runtime is identical to the 163 // one in `plugin.json` else you won't pass AU validation. 164 // 165 // Note: For AU, 0.x.y is supposed to mean "do not cache", however it is 166 // unknown what it actually changes. AU caching hasn't caused any problem 167 // and can probably be ignored. 168 PluginVersion publicVersion = PluginVersion(0, 0, 0); 169 170 /// True if the plugin has a graphical UI. Easy way to disable it. 171 bool hasGUI = false; 172 173 /// True if the plugin "is a synth". This has only a semantic effect. 174 bool isSynth = false; 175 176 /// True if the plugin should receive MIDI events. 177 /// Warning: receiving MIDI forces you to call `getNextMidiMessages` 178 /// with the right number of `frames`, every buffer. 179 bool receivesMIDI = false; 180 181 /// True if the plugin sends MIDI events. 182 bool sendsMIDI = false; 183 184 /// Used for being at the right place in list of plug-ins. 185 PluginCategory category; 186 187 /// Used as name of the bundle in VST. 188 string VSTBundleIdentifier; 189 190 /// Used as name of the bundle in AU. 191 string AUBundleIdentifier; 192 193 /// Used as name of the bundle in AAX. 194 string AAXBundleIdentifier; 195 196 /// Used as CLAP identifier ("com.wittyaudio.Destructatorizer") 197 string CLAPIdentifier; 198 199 /// Used as CLAP identifier for factory preset provider ("com.wittyaudio.Destructatorizer.factory") 200 string CLAPIdentifierFactory; 201 } 202 203 /// This allows to write things life tempo-synced LFO. 204 struct TimeInfo 205 { 206 /// BPM 207 double tempo = 120; 208 209 /// Current time from the beginning of the song in samples. 210 /// This time can be negative, which is normal at beginning of a song. 211 long timeInSamples = 0; 212 213 /// Whether the host sequencer is currently playing 214 bool hostIsPlaying; 215 } 216 217 /// Describe a combination of input channels count and output channels count 218 /// FUTURE: this should look more like clap_audio_ports_config_t 219 struct LegalIO 220 { 221 int numInputChannels; 222 int numOutputChannels; 223 } 224 225 /// This is the interface used by the GUI, to reduce coupling and avoid exposing the whole of `Client` to it. 226 /// It should eventually allows to supersede/hide IHostCommand. 227 interface IClient 228 { 229 nothrow: 230 @nogc: 231 /// Requests a resize of the plugin window, notifying the host. 232 /// Returns: `true` if succeeded. 233 /// 234 /// Params: 235 /// width New width of the plugin, in logical pixels. 236 /// height New height of the plugin, in logical pixels. 237 bool requestResize(int widthLogicalPixels, int heightLogicalPixels); 238 239 /// Notify AFTER a manual resize of the plugin, so that the host updates its window. 240 /// Returns `true` if succeeded. Not needed if `requestResize` returned true. 241 bool notifyResized(); 242 243 /// Report the identied host name (DAW). 244 DAW getDAW(); 245 246 /// Gets the plugin format used at runtime. Version identifier may not be enough in the future, in case of 247 /// unity builds. 248 PluginFormat getPluginFormat(); 249 } 250 251 /// Plugin interface, from the client point of view. 252 /// This client has no knowledge of thread-safety, it must be handled externally. 253 /// User plugins derivate from this class. 254 /// Plugin formats wrappers owns one dplug.plugin.Client as a member. 255 /// 256 /// Note: this is an architecture failure since there are 3 users of that interface: 257 /// 1. the plugin "client" implementation (= product), 258 /// 2. the format client 259 /// 3. the UI, directly 260 /// Those should be splitted cleanly. 261 class Client : IClient 262 { 263 public: 264 nothrow: 265 @nogc: 266 267 this() 268 { 269 _info = buildPluginInfo(); 270 271 // Create legal I/O combinations 272 _legalIOs = buildLegalIO(); 273 274 // Create parameters. 275 _params = buildParameters(); 276 277 // Check parameter consistency 278 // This avoid mistake when adding/reordering parameters in a plugin. 279 foreach(size_t i, Parameter param; _params) 280 { 281 // If you fail here, this means your buildParameter() override is incorrect. 282 // Check the values of the index you're giving. 283 // They should be 0, 1, 2, ..., N-1 284 // Maybe you have duplicated a line or misordered them. 285 assert(param.index() == i); 286 287 // Sets owner reference. 288 param.setClientReference(this); 289 } 290 291 _maxFramesInProcess = maxFramesInProcess(); 292 293 _maxInputs = 0; 294 _maxOutputs = 0; 295 foreach(legalIO; _legalIOs) 296 { 297 if (_maxInputs < legalIO.numInputChannels) 298 _maxInputs = legalIO.numInputChannels; 299 if (_maxOutputs < legalIO.numOutputChannels) 300 _maxOutputs = legalIO.numOutputChannels; 301 } 302 303 _inputMidiQueue = makeMidiQueue(); // PERF: only init those for plugins that need it? 304 _outputMidiQueue = makeMidiQueue(); 305 306 if (sendsMIDI) 307 { 308 _midiOutFromUIMutex = makeMutex(); 309 } 310 311 version(legacyBinState) 312 {} 313 else 314 { 315 // Snapshot default extra state here, before any preset is created, so that `makeDefaultPreset` 316 // can work even if called multiple times. 317 saveState(_defaultStateData); 318 } 319 320 // Create presets last, so that we enjoy the presence of built Parameters, 321 // and default I/O configuration. 322 _presetBank = mallocNew!PresetBank(this, buildPresets()); 323 } 324 325 ~this() 326 { 327 // Destroy graphics 328 if (_graphics !is null) 329 { 330 // Acquire _graphicsIsAvailable forever 331 // so that it's the last time the audio uses it, 332 // and we can wait for its exit in _graphics destructor 333 while(!cas(&_graphicsIsAvailable, true, false)) 334 { 335 // MAYDO: relax CPU 336 } 337 _graphics.destroyFree(); 338 } 339 340 // Destroy presets 341 _presetBank.destroyFree(); 342 343 // Destroy parameters 344 foreach(p; _params) 345 p.destroyFree(); 346 _params.freeSlice(); 347 _legalIOs.freeSlice(); 348 } 349 350 final int maxInputs() pure const nothrow @nogc 351 { 352 return _maxInputs; 353 } 354 355 final int maxOutputs() pure const nothrow @nogc 356 { 357 return _maxOutputs; 358 } 359 360 /// Returns: Array of parameters. 361 final inout(Parameter[]) params() inout nothrow @nogc 362 { 363 return _params; 364 } 365 366 /// Returns: Array of legal I/O combinations. 367 final LegalIO[] legalIOs() nothrow @nogc 368 { 369 return _legalIOs; 370 } 371 372 /// Returns: true if the following I/O combination is a legal one. 373 /// < 0 means "do not check" 374 final bool isLegalIO(int numInputChannels, int numOutputChannels) pure const nothrow @nogc 375 { 376 foreach(io; _legalIOs) 377 if ( ( (numInputChannels < 0) 378 || 379 (io.numInputChannels == numInputChannels) ) 380 && 381 ( (numOutputChannels < 0) 382 || 383 (io.numOutputChannels == numOutputChannels) ) 384 ) 385 return true; 386 387 return false; 388 } 389 390 /// Returns: Array of presets. 391 final PresetBank presetBank() nothrow @nogc 392 { 393 return _presetBank; 394 } 395 396 /// Returns: The parameter indexed by index. 397 final inout(Parameter) param(int index) inout nothrow @nogc 398 { 399 return _params.ptr[index]; 400 } 401 402 /// Returns: true if index is a valid parameter index. 403 final bool isValidParamIndex(int index) const nothrow @nogc 404 { 405 return index >= 0 && index < _params.length; 406 } 407 408 /// Returns: true if index is a valid input index. 409 final bool isValidInputIndex(int index) nothrow @nogc 410 { 411 return index >= 0 && index < maxInputs(); 412 } 413 414 /// Returns: true if index is a valid output index. 415 final bool isValidOutputIndex(int index) nothrow @nogc 416 { 417 return index >= 0 && index < maxOutputs(); 418 } 419 420 /// Note: openGUI, getGUISize, getGraphics and closeGUI are guaranteed 421 /// synchronized by the client implementation 422 /// Only allowed for client implementation. 423 final void* openGUI(void* parentInfo, void* controlInfo, GraphicsBackend backend) nothrow @nogc 424 { 425 createGraphicsLazily(); 426 assert(_hostCommand !is null); 427 return (cast(IGraphics)_graphics).openUI(parentInfo, controlInfo, this, backend); 428 } 429 430 /// Only allowed for client implementation. 431 final bool getGUISize(int* widthLogicalPixels, int* heightLogicalPixels) nothrow @nogc 432 { 433 createGraphicsLazily(); 434 auto graphics = (cast(IGraphics)_graphics); 435 if (graphics) 436 { 437 graphics.getGUISize(widthLogicalPixels, heightLogicalPixels); 438 return true; 439 } 440 else 441 return false; 442 } 443 444 /// Only allowed for client implementation. 445 /// This is helpful for some hosts, like OBS and Cubase, but not others like FL Studio. 446 final bool getDesiredGUISize(int* widthLogicalPixels, int* heightLogicalPixels) nothrow @nogc 447 { 448 createGraphicsLazily(); 449 auto graphics = (cast(IGraphics)_graphics); 450 if (graphics) 451 { 452 graphics.getDesiredGUISize(widthLogicalPixels, heightLogicalPixels); 453 return true; 454 } 455 else 456 return false; 457 } 458 459 /// Close the plugin UI if one was opened. 460 /// Note: OBS Studio will happily call effEditClose without having called effEditOpen. 461 /// Only allowed for client implementation. 462 final void closeGUI() nothrow @nogc 463 { 464 auto graphics = (cast(IGraphics)_graphics); 465 if (graphics) 466 { 467 graphics.closeUI(); 468 } 469 } 470 471 /// This creates the GUIGraphics object lazily, and return it without synchronization. 472 /// Only allowed for client implementation. 473 final IGraphics getGraphics() 474 { 475 createGraphicsLazily(); 476 return (cast(IGraphics)_graphics); 477 } 478 479 // This should be called only by a client implementation. 480 void setParameterFromHost(int index, float value) nothrow @nogc 481 { 482 param(index).setFromHost(value); 483 } 484 485 /// Override if you create a plugin with UI. 486 /// The returned IGraphics must be allocated with `mallocNew`. 487 /// `plugin.json` needs to have a "hasGUI" key equal to true, else this callback is never called. 488 IGraphics createGraphics() nothrow @nogc 489 { 490 return null; 491 } 492 493 /// Intended from inside the audio thread, in `process`. 494 /// Enqueue one MIDI message on the output MIDI priority queue, so that it is 495 /// eventually sent. 496 /// Its offset is relative to the current buffer, and you can send messages arbitrarily 497 /// in the future too. 498 void sendMIDIMessage(MidiMessage message) nothrow @nogc 499 { 500 _outputMidiQueue.enqueue(message); 501 } 502 503 /// Send MIDI from inside the UI. 504 /// Intended to be called from inside an UI event callback. 505 /// 506 /// Enqueue several MIDI messages in a synchronized manner, so that they are sent all at once, 507 /// as early as possible as "live" MIDI messages. 508 /// No guarantee of any timing for these messages, for example this can be in response to 509 /// a key press on a virtual keyboard. 510 /// The messages don't have to be ordered if they are spaced, but have to be if they 511 /// have the same `offset`. 512 /// 513 /// Note: It is guaranteed that all messages passed this way will keep their offset 514 /// relationship in MIDI output. (Typically such a messages would all have a zero 515 /// timestamp). 516 /// Though they are sent as soon as possible in a best effort manner, their relative 517 /// offset is preserved. 518 /// Its offset is relative to the current buffer, and you can send messages arbitrarily 519 /// in the future too. 520 void sendMIDIMessagesFromUI(const(MidiMessage)[] messages) nothrow @nogc 521 { 522 _midiOutFromUIMutex.lock(); 523 524 foreach(msg; messages) 525 _outputMidiFromUI.pushBack(msg); 526 527 _midiOutFromUIMutex.unlock(); 528 } 529 530 /// Getter for the IGraphics interface 531 /// This is intended ONLY for the audio thread inside processing and has acquire semantics. 532 /// Not reentrant! You can't call this twice without a graphicsRelease first. 533 /// THIS CAN RETURN NULL EVEN AFTER HAVING RETURNED NON-NULL AT ONE POINT. 534 /// Returns: null if feedback from audio thread is not welcome. 535 final IGraphics graphicsAcquire() nothrow @nogc 536 { 537 if (cas(&_graphicsIsAvailable, true, false)) // exclusive, since there is only one audio thread normally 538 return _graphics; 539 else 540 return null; 541 } 542 543 /// Mirror function to release the IGraphics from the audio-thread. 544 /// Do not call if graphicsAcquire() returned `null`. 545 final void graphicsRelease() nothrow @nogc 546 { 547 // graphicsAcquire should have been called before 548 // MAYDO: which memory order here? Don't looks like we need a barrier. 549 atomicStore(_graphicsIsAvailable, true); 550 } 551 552 // Getter for the IHostCommand interface 553 final IHostCommand hostCommand() nothrow @nogc 554 { 555 return _hostCommand; 556 } 557 558 /// Override to clear state (eg: resize and clear delay lines) and allocate buffers. 559 /// Note: `reset` should not be called directly by plug-in format implementations. Use 560 /// `resetFromHost` if you write a new plug-in format client. 561 abstract void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc; 562 563 /// Override to set the plugin latency in samples. 564 /// Plugin latency can depend on `sampleRate` but no other value. 565 /// If you want your latency to depend on a `Parameter` your only choice is to 566 /// pessimize the needed latency and compensate in the process callback. 567 /// 568 /// Dynamic latency changes are not possible in Dplug yet. 569 /// See Issue #442 => https://github.com/AuburnSounds/Dplug/issues/442 570 /// 571 /// Returns: Plugin latency in samples. 572 /// Note: this will absolutely be called before `reset` is called, so be prepared. 573 int latencySamples(double sampleRate) nothrow @nogc 574 { 575 return 0; // By default, no latency introduced by plugin 576 } 577 578 /// Override to set the plugin tail length in seconds. 579 /// 580 /// This is the amount of time before silence is reached with a silent input, on the worst 581 /// possible settings. 582 /// 583 /// Returns: Plugin tail size in seconds. 584 /// - Returning 0.0f means that as soon as your input is silent, the output will be silent. 585 /// It isn't a special value. 586 /// - Returning `float.infinity` means that the host should not optimize calls to `processAudio`. 587 /// If your plugin is a synth, or an effect generating sound, you MUST return `float.infinity`. 588 /// - Otherwise, returning a particular tail size is the regular meaning. 589 /// 590 float tailSizeInSeconds() nothrow @nogc 591 { 592 // Default: always call `processAudio`. This is safest. 593 // 594 // It is recommended to setup this override at one point in developemnt, especially for an effect plugin. 595 // This allows VST3 and AU hosts to optimize things. 596 // 597 // For an effect 2 secs is a good starting point. Which should be safe for most effects plugins except delay or reverb 598 // Warning: plugins have often MUCH more tail size than expected! 599 // Don't reduce to a shorter time unless you know for sure what you are doing. 600 // Synths, and effects generating audio from MIDI should return `float.infinity`. 601 602 return float.infinity; 603 } 604 605 /// Override to declare the maximum number of samples to accept 606 /// If greater, the audio buffers will be splitted up. 607 /// This splitting have several benefits: 608 /// - help allocating temporary audio buffers on the stack 609 /// - keeps memory usage low and reuse it 610 /// - allow faster-than-buffer-size parameter changes (VST3) 611 /// Returns: Maximum number of samples 612 /// Warning: Some buffersize-related bugs might be hidden by having sub-buffers. 613 /// If you are looking for a buffersize bug, maybe try to disable sub-buffers 614 /// by returning the default 0. 615 int maxFramesInProcess() nothrow @nogc 616 { 617 return 0; // default returns 0 which means "do not split buffers" 618 } 619 620 /// Process `frames` audio frames. 621 /// 622 /// Read audio input from `inputs` channels, and write it to `outputs` channels. 623 /// 624 /// Params: 625 /// 626 /// inputs Input channels pointers, each pointing to `frames` audio samples. 627 /// Number of pointer is equal to `numInputs` given in last `reset()` callback. 628 /// Unconnected input pins (if any) point to zeroes. 629 /// 630 /// outputs Output channels pointers, each pointing to `frames` audio samples. 631 /// Number of pointer is equal to `numOutputs` given in last `reset()` callback. 632 /// The output space can be used as temporary storage. 633 /// (do not modify these non-const pointers). 634 /// 635 /// frames Number of audio samples for this callback. 636 /// This number is always <= `maxFrames`, given in last `reset()` callback. 637 /// To force this number to arbitrarily, you may override `maxFramesInProcess`, 638 /// which will split host buffers in smaller chunks. 639 /// 640 /// timeInfo Timing information for the first sample (0th) of this buffer. 641 /// 642 /// Warning: Using MIDI input? 643 /// If your plug-in has "receivesMidi" set to `true` in its `plugin.json`, this 644 /// callback is the one place where you MUST call `getNextMidiMessages()`. 645 /// See poly-alias or simple-mono-synth examples for how. 646 /// 647 abstract void processAudio(const(float*)[] inputs, // array of input channels 648 float*[] outputs, // array of output channels 649 int frames, // number of sample in each input & output channel 650 TimeInfo timeInfo // time information associated with this signal frame 651 ) nothrow @nogc; 652 653 /// Should only be called in `processAudio`. 654 /// This return a slice of MIDI messages corresponding to the next `frames` samples. 655 /// Useful if you don't want to process messages every samples, or every split buffer. 656 final const(MidiMessage)[] getNextMidiMessages(int frames) nothrow @nogc 657 { 658 return _inputMidiQueue.getNextMidiMessages(frames); 659 } 660 661 /// Return default state data, to be used in constructing a programmatic preset. 662 /// Note: It is recommended to use .fbx instead of constructing presets with code. 663 /// This is intended to be used in `buildPresets` callback. 664 final const(ubyte)[] defaultStateData() nothrow @nogc 665 { 666 // Should this preset have extra state data? 667 const(ubyte)[] stateData = null; 668 version(legacyBinState) 669 {} 670 else 671 { 672 // Note: the default state data is called early, so if you plan to call 673 // `defaultStateData` outside of buildPresets, it will have odd restrictions. 674 stateData = _defaultStateData[]; 675 } 676 return stateData; 677 } 678 679 /// Returns a new default preset. 680 /// This is intended to be used in `buildPresets` callback. 681 final Preset makeDefaultPreset() nothrow @nogc 682 { 683 // MAYDO: use mallocSlice for perf 684 auto values = makeVec!float(); 685 foreach(param; _params) 686 values.pushBack(param.getNormalizedDefault()); 687 688 // Perf: one could avoid malloc to copy those arrays again there 689 float[] valuesSlice = values.releaseData; 690 Preset result = mallocNew!Preset("Default", valuesSlice, defaultStateData()); 691 free(valuesSlice.ptr); // PERF: could disown this instead of copy, with another Preset constructor 692 return result; 693 } 694 695 // Getters for fields in _info 696 697 final bool hasGUI() pure const nothrow @nogc 698 { 699 return _info.hasGUI; 700 } 701 702 final bool isSynth() pure const nothrow @nogc 703 { 704 return _info.isSynth; 705 } 706 707 final bool receivesMIDI() pure const nothrow @nogc 708 { 709 return _info.receivesMIDI; 710 } 711 712 final bool sendsMIDI() pure const nothrow @nogc 713 { 714 return _info.sendsMIDI; 715 } 716 717 final string vendorName() pure const nothrow @nogc 718 { 719 return _info.vendorName; 720 } 721 722 final char[4] getVendorUniqueID() pure const nothrow @nogc 723 { 724 return _info.vendorUniqueID; 725 } 726 727 final string getVendorSupportEmail() pure const nothrow @nogc 728 { 729 return _info.vendorSupportEmail; 730 } 731 732 final string pluginName() pure const nothrow @nogc 733 { 734 return _info.pluginName; 735 } 736 737 final string pluginHomepage() pure const nothrow @nogc 738 { 739 return _info.pluginHomepage; 740 } 741 742 final PluginCategory pluginCategory() pure const nothrow @nogc 743 { 744 return _info.category; 745 } 746 747 final string VSTBundleIdentifier() pure const nothrow @nogc 748 { 749 return _info.VSTBundleIdentifier; 750 } 751 752 final string AUBundleIdentifier() pure const nothrow @nogc 753 { 754 return _info.AUBundleIdentifier; 755 } 756 757 final string AAXBundleIdentifier() pure const nothrow @nogc 758 { 759 return _info.AAXBundleIdentifier; 760 } 761 762 final string CLAPIdentifier() pure const nothrow @nogc 763 { 764 return _info.CLAPIdentifier; 765 } 766 767 final string CLAPIdentifierFactory() pure const nothrow @nogc 768 { 769 return _info.CLAPIdentifierFactory; 770 } 771 772 /// Returns: Plugin "unique" ID. 773 final char[4] getPluginUniqueID() pure const nothrow @nogc 774 { 775 return _info.pluginUniqueID; 776 } 777 778 /// Returns: Plugin full name "$VENDOR $PRODUCT" 779 final void getPluginFullName(char* p, int bufLength) const nothrow @nogc 780 { 781 snprintf(p, bufLength, "%.*s %.*s", 782 cast(int)(_info.vendorName.length), _info.vendorName.ptr, 783 cast(int)(_info.pluginName.length), _info.pluginName.ptr); 784 785 // DigitalMars's snprintf doesn't always add a terminal zero 786 if (bufLength > 0) 787 { 788 p[bufLength-1] = '\0'; 789 } 790 } 791 792 /// Returns: Plugin name "$PRODUCT" 793 final void getPluginName(char* p, int bufLength) const nothrow @nogc 794 { 795 snprintf(p, bufLength, "%.*s", 796 cast(int)(_info.pluginName.length), _info.pluginName.ptr); 797 798 // DigitalMars's snprintf doesn't always add a terminal zero 799 if (bufLength > 0) 800 { 801 p[bufLength-1] = '\0'; 802 } 803 } 804 805 /// Returns: Plugin version in x.x.x.x decimal form. 806 final PluginVersion getPublicVersion() pure const nothrow @nogc 807 { 808 return _info.publicVersion; 809 } 810 811 /// Boilerplate function to get the value of a `FloatParameter`, for use in `processAudio`. 812 final T readParam(T)(int paramIndex) nothrow @nogc 813 if (is(T == float) || is(T == double)) 814 { 815 auto p = param(paramIndex); 816 assert(cast(FloatParameter)p !is null); // check it's a FloatParameter 817 return unsafeObjectCast!FloatParameter(p).valueAtomic(); 818 } 819 820 /// Boilerplate function to get the value of an `IntParameter`, for use in `processAudio`. 821 final T readParam(T)(int paramIndex) nothrow @nogc 822 if (is(T == int) && !is(T == enum)) 823 { 824 auto p = param(paramIndex); 825 assert(cast(IntegerParameter)p !is null); // check it's an IntParameter 826 return unsafeObjectCast!IntegerParameter(p).valueAtomic(); 827 } 828 829 /// Boilerplate function to get the value of an `EnumParameter`, for use in `processAudio`. 830 final T readParam(T)(int paramIndex) nothrow @nogc 831 if (is(T == enum)) 832 { 833 auto p = param(paramIndex); 834 assert(cast(EnumParameter)p !is null); // check it's an EnumParameter 835 return cast(T)(unsafeObjectCast!EnumParameter(p).valueAtomic()); 836 } 837 838 /// Boilerplate function to get the value of a `BoolParameter`,for use in `processAudio`. 839 final T readParam(T)(int paramIndex) nothrow @nogc 840 if (is(T == bool)) 841 { 842 auto p = param(paramIndex); 843 assert(cast(BoolParameter)p !is null); // check it's a BoolParameter 844 return unsafeObjectCast!BoolParameter(p).valueAtomic(); 845 } 846 847 /// For plugin format clients only. 848 final void setHostCommand(IHostCommand hostCommand) nothrow @nogc 849 { 850 _hostCommand = hostCommand; 851 852 // In VST3, for accuracy of parameter automation we choose to split buffers in chunks of maximum 512. 853 // This avoids painful situations where parameters could higher precision. 854 // See: https://github.com/AuburnSounds/Dplug/issues/368 855 if (hostCommand.getPluginFormat() == PluginFormat.vst3) 856 { 857 if (_maxFramesInProcess == 0 || _maxFramesInProcess > 512) 858 _maxFramesInProcess = 512; 859 } 860 } 861 862 /// For plugin format clients only. 863 /// Enqueues an incoming MIDI message. 864 void enqueueMIDIFromHost(MidiMessage message) 865 { 866 _inputMidiQueue.enqueue(message); 867 } 868 869 /// For plugin format clients only. 870 /// This return a slice of MIDI messages to be sent for this (whole unsplit) buffer. 871 /// Internally, you need to either use split-buffering from this file, or if the format does 872 /// its own buffer split it needs to call `accumulateOutputMIDI` itself. 873 final const(MidiMessage)[] getAccumulatedOutputMidiMessages() nothrow @nogc 874 { 875 return _outputMidiMessages[]; 876 } 877 /// For plugin format clients only. 878 /// Clear MIDI output buffer. Call it before `processAudioFromHost` or `accumulateOutputMIDI`. 879 /// What it also does it get all MIDI message from the UI, and add them to the priority queue, 880 /// so that they may be accumulated like normal MIDI sent from the process callback. 881 final void clearAccumulatedOutputMidiMessages() nothrow @nogc 882 { 883 assert(sendsMIDI()); 884 885 _outputMidiMessages.clearContents(); 886 887 // Enqueue all messages from UI in the priority queue. 888 _midiOutFromUIMutex.lock(); 889 foreach(msg; _outputMidiFromUI[]) 890 _outputMidiQueue.enqueue(msg); 891 _outputMidiFromUI.clearContents(); 892 _midiOutFromUIMutex.unlock(); 893 } 894 895 /// For plugin format clients only. 896 /// Calls processAudio repeatedly, splitting the buffers. 897 /// Splitting allow to decouple memory requirements from the actual host buffer size. 898 /// There is few performance penalty above 512 samples. 899 /// TODO: unclear when using this if inputs.ptr can/should be null in case of zero channels... 900 /// 901 /// CAUTION: channel such as inputs[0]/outputs[0] are MODIFIED by this function. 902 void processAudioFromHost(float*[] inputs, 903 float*[] outputs, 904 int frames, 905 TimeInfo timeInfo, 906 bool doNotSplit = false, // flag that exist in case the plugin client want to split itself 907 ) nothrow @nogc 908 { 909 // In debug mode, fill all output audio buffers with `float.nan`. 910 // This avoids a plug-in forgetting to fill output buffers, which can happen if you 911 // implement silence detection badly. 912 // CAUTION: this assumes inputs and outputs buffers do not point into the same memory areas 913 debug 914 { 915 for (int k = 0; k < outputs.length; ++k) 916 { 917 float* pOut = outputs[k]; 918 pOut[0..frames] = float.nan; 919 } 920 } 921 922 if (_maxFramesInProcess == 0 || doNotSplit) 923 { 924 processAudio(inputs, outputs, frames, timeInfo); 925 if (sendsMIDI) accumulateOutputMIDI(frames); 926 } 927 else 928 { 929 // Slice audio in smaller parts 930 while (frames > 0) 931 { 932 // Note: the last slice will be smaller than the others 933 int sliceLength = frames; 934 if (sliceLength > _maxFramesInProcess) 935 sliceLength = _maxFramesInProcess; 936 937 processAudio(inputs, outputs, sliceLength, timeInfo); 938 if (sendsMIDI) accumulateOutputMIDI(sliceLength); 939 940 // offset all input buffer pointers 941 for (int i = 0; i < cast(int)inputs.length; ++i) 942 inputs[i] = inputs[i] + sliceLength; 943 944 // offset all output buffer pointers 945 for (int i = 0; i < cast(int)outputs.length; ++i) 946 outputs[i] = outputs[i] + sliceLength; 947 948 frames -= sliceLength; 949 950 // timeInfo must be updated 951 timeInfo.timeInSamples += sliceLength; 952 } 953 assert(frames == 0); 954 } 955 } 956 957 /// For VST3 client only. Format clients that split the buffers themselves (for automation precision) 958 /// Need as well to accumulate MIDI output themselves. 959 /// See_also: `getAccumulatedOutputMidiMessages` for how to get those accumulated messages for the whole buffer. 960 final void accumulateOutputMIDI(int frames) 961 { 962 _outputMidiQueue.accumNextMidiMessages(_outputMidiMessages, frames); 963 } 964 965 /// For plugin format clients only. 966 /// Calls `reset()`. 967 /// Must be called by the audio thread. 968 void resetFromHost(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc 969 { 970 // Clear outstanding MIDI messages (now invalid) 971 _inputMidiQueue.initialize(); // MAYDO: should it push a MIDI message to mute all voices? 972 973 _outputMidiQueue.initialize(); // TODO: that sounds fishy, what if we have send a note on but not the note off? 974 975 // We potentially give to the client implementation a lower value 976 // for the maximum number of frames 977 if (_maxFramesInProcess != 0 && _maxFramesInProcess < maxFrames) 978 maxFrames = _maxFramesInProcess; 979 980 // Calls the reset virtual call 981 reset(sampleRate, maxFrames, numInputs, numOutputs); 982 } 983 984 /// For use by plugin format clients. This gives the buffer split size to use. 985 /// (0 == no split). 986 /// This is useful in the cast the format client wants to split buffers by itself. 987 /// 988 /// Note: in VST3, no split or a split larger than 512 samples, is replaced by a split by 512. 989 final int getBufferSplitMaxFrames() 990 { 991 return _maxFramesInProcess; 992 } 993 994 // <IClient> 995 override bool requestResize(int widthLogicalPixels, int heightLogicalPixels) 996 { 997 if (_hostCommand is null) 998 return false; 999 1000 return _hostCommand.requestResize(widthLogicalPixels, heightLogicalPixels); 1001 } 1002 1003 bool notifyResized() 1004 { 1005 if (_hostCommand is null) 1006 return false; 1007 1008 return _hostCommand.notifyResized(); 1009 } 1010 1011 override DAW getDAW() 1012 { 1013 assert(_hostCommand !is null); 1014 return _hostCommand.getDAW(); 1015 } 1016 1017 override PluginFormat getPluginFormat() 1018 { 1019 assert(_hostCommand !is null); 1020 return _hostCommand.getPluginFormat(); 1021 } 1022 1023 version(legacyBinState) 1024 {} 1025 else 1026 { 1027 /** 1028 Write the extra state of plugin in a chunk, so that the host can restore that later. 1029 You would typically serialize arbitrary stuff with `dplug.core.binrange`. 1030 This is called quite frequently. 1031 1032 What should go here: 1033 * your own chunk format with hopefully your plugin major version. 1034 * user-defined structures, like opened .wav, strings, wavetables, file paths... 1035 You can finally make plugins with arbitrary data in presets! 1036 * Typically stuff used to render sound identically. 1037 * Do not put host-exposed plug-in Parameters, they are saved by other means. 1038 * Do not put stuff that depends on I/O settings, such as: 1039 - current sample rate 1040 - current I/O count and layout 1041 - maxFrames and buffering 1042 What you put in an extra state chunk must be parameter-like, 1043 just not those a DAW allows. 1044 1045 Contrarily, this is a disappointing solution for: 1046 * Storing UI size, dark mode, and all kind of editor preferences. 1047 Indeed, when `loadState` is called, the UI might not exist at all. 1048 1049 Note: Using state chunks comes with a BIG challenge of making your own synchronization 1050 with the UI. You can expect any thread to call `saveState` and `loadState`. 1051 A proper design would probably have you represent state in the editor and the 1052 audio client separately, with a clean interchange. 1053 1054 Important: This is called at the instantiating of a plug-in to get the "default state", 1055 so that `makeDefaultPreset()` can work. At this point, the preset bank isn't 1056 yet constructed, so you cannot rely on it. 1057 1058 Warning: Just append new content to the `Vec!ubyte`, do not modify its existing content 1059 if any exist. 1060 1061 See_also: `loadState`. 1062 */ 1063 void saveState(ref Vec!ubyte chunk) nothrow @nogc 1064 { 1065 } 1066 1067 /** 1068 Read the extra state of your plugin from a chunk, to restore a former save. 1069 You would typically deserialize arbitrary stuff with `dplug.core.binrange`. 1070 1071 This is called on session load or on preset load (IF the preset had a state chunk), 1072 but this isn't called on plugin instantiation. 1073 1074 Note: Using state chunks comes with a BIG challenge of making your own synchronization 1075 with the UI. You can expect any thread to call `saveState` and `loadState`. 1076 A proper design would probably have you represent state in the editor and the 1077 audio client separately, with a clean interchange. 1078 1079 Important: This should successfully parse whatever the "default state" is 1080 so that `makeDefaultPreset()` can work. 1081 1082 Returns: `true` on successful parse, return false to indicate a parsing error. 1083 1084 See_also: `loadState`. 1085 */ 1086 bool loadState(const(ubyte)[] chunk) nothrow @nogc 1087 { 1088 return true; 1089 } 1090 } 1091 1092 // </IClient> 1093 1094 protected: 1095 1096 /// Override this method to implement parameter creation. 1097 /// This is an optional overload, default implementation declare no parameters. 1098 /// The returned slice must be allocated with `malloc`/`mallocSlice` and contains 1099 /// `Parameter` objects created with `mallocEmplace`. 1100 Parameter[] buildParameters() 1101 { 1102 return []; 1103 } 1104 1105 /// Override this methods to load/fill presets. 1106 /// This function must return a slice allocated with `malloc`, 1107 /// that contains presets crteated with `mallocEmplace`. 1108 Preset[] buildPresets() nothrow @nogc 1109 { 1110 auto presets = makeVec!Preset(); 1111 presets.pushBack( makeDefaultPreset() ); 1112 return presets.releaseData(); 1113 } 1114 1115 /// Override this method to tell what plugin you are. 1116 /// Mandatory override, fill the fields with care. 1117 /// Note: this should not be called by a plugin client implementation directly. 1118 /// Access the content of PluginInfo through the various accessors. 1119 abstract PluginInfo buildPluginInfo(); 1120 1121 /// Override this method to tell which I/O are legal. 1122 /// The returned slice must be allocated with `malloc`/`mallocSlice`. 1123 abstract LegalIO[] buildLegalIO(); 1124 1125 IGraphics _graphics; 1126 1127 // Used as a flag that _graphics can be used (by audio thread or for destruction) 1128 shared(bool) _graphicsIsAvailable = false; 1129 1130 // Note: when implementing a new plug-in format, the format wrapper has to call 1131 // `setHostCommand` and implement `IHostCommand`. 1132 IHostCommand _hostCommand = null; 1133 1134 PluginInfo _info; 1135 1136 private: 1137 Parameter[] _params; 1138 1139 PresetBank _presetBank; 1140 1141 LegalIO[] _legalIOs; 1142 1143 int _maxInputs, _maxOutputs; // maximum number of input/outputs 1144 1145 // Cache result of maxFramesInProcess(), maximum frame length 1146 int _maxFramesInProcess; 1147 1148 // Container for awaiting MIDI messages. 1149 MidiQueue _inputMidiQueue; 1150 1151 // Priority queue for sending MIDI messages. 1152 MidiQueue _outputMidiQueue; 1153 1154 // Protects MIDI out from UI. 1155 UncheckedMutex _midiOutFromUIMutex; 1156 1157 // Additional, unsorted messages to be sent, courtesy of the UI. 1158 Vec!MidiMessage _outputMidiFromUI; 1159 1160 // Accumulated output MIDI messages, for one unsplit buffer. 1161 // Output MIDI messages, if any, are accumulated there. 1162 Vec!MidiMessage _outputMidiMessages; 1163 1164 version(legacyBinState) 1165 {} 1166 else 1167 { 1168 /// Stores the extra state data (from a `saveState` call) from when the plugin was newly 1169 /// instantiated. This is helpful, in order to synthesize presets, and also because some 1170 /// hosts don't restore default state when instantiating. 1171 Vec!ubyte _defaultStateData; 1172 } 1173 1174 final void createGraphicsLazily() 1175 { 1176 // First GUI opening create the graphics object 1177 // no need to protect _graphics here since the audio thread 1178 // does not write to it. 1179 if ( (_graphics is null) && hasGUI()) 1180 { 1181 // Why is the IGraphics created lazily? This allows to load a plugin very quickly, 1182 // without opening its logical UI 1183 IGraphics graphics = createGraphics(); 1184 1185 // Don't forget to override the createGraphics method! 1186 assert(graphics !is null); 1187 1188 _graphics = graphics; 1189 1190 // Now that the UI is fully created, we enable the audio thread to use it 1191 atomicStore(_graphicsIsAvailable, true); 1192 } 1193 } 1194 } 1195 1196 /// Should be called in Client class during compile time 1197 /// to parse a `PluginInfo` from a supplied json file. 1198 PluginInfo parsePluginInfo(string json) 1199 { 1200 import std.json; 1201 import std.string; 1202 import std.conv; 1203 1204 JSONValue j = parseJSON(json); 1205 1206 static bool toBoolean(JSONValue value) 1207 { 1208 static if (__VERSION__ >= 2087) 1209 { 1210 if (value.type == JSONType.true_) 1211 return true; 1212 if (value.type == JSONType.false_) 1213 return false; 1214 } 1215 else 1216 { 1217 if (value.type == JSON_TYPE.TRUE) 1218 return true; 1219 if (value.type == JSON_TYPE.FALSE) 1220 return false; 1221 } 1222 throw new Exception(format("Expected a boolean, got %s instead", value)); 1223 } 1224 1225 // Check that a string is "x.y.z" 1226 // FUTURE: support larger integers than 0 to 9 in the string 1227 static PluginVersion parsePluginVersion(string value) 1228 { 1229 bool isDigit(char ch) 1230 { 1231 return ch >= '0' && ch <= '9'; 1232 } 1233 1234 if ( value.length != 5 || 1235 !isDigit(value[0]) || 1236 value[1] != '.' || 1237 !isDigit(value[2]) || 1238 value[3] != '.' || 1239 !isDigit(value[4])) 1240 { 1241 throw new Exception("\"publicVersion\" should follow the form x.y.z (eg: \"1.0.0\")"); 1242 } 1243 1244 PluginVersion ver; 1245 ver.major = value[0] - '0'; 1246 ver.minor = value[2] - '0'; 1247 ver.patch = value[4] - '0'; 1248 return ver; 1249 } 1250 1251 PluginInfo info; 1252 info.vendorName = j["vendorName"].str; 1253 info.vendorUniqueID = j["vendorUniqueID"].str; 1254 info.pluginName = j["pluginName"].str; 1255 info.pluginUniqueID = j["pluginUniqueID"].str; 1256 1257 if ("vendorSupportEmail" in j) 1258 info.vendorSupportEmail= j["vendorSupportEmail"].str; 1259 1260 if ("pluginHomepage" in j) 1261 info.pluginHomepage = j["pluginHomepage"].str; 1262 1263 if ("isSynth" in j) 1264 info.isSynth = toBoolean(j["isSynth"]); 1265 info.hasGUI = toBoolean(j["hasGUI"]); 1266 if ("receivesMIDI" in j) 1267 info.receivesMIDI = toBoolean(j["receivesMIDI"]); 1268 if ("sendsMIDI" in j) 1269 info.sendsMIDI = toBoolean(j["sendsMIDI"]); 1270 1271 // Plugins that sends MIDI must also receives MIDI. 1272 if (info.sendsMIDI && !info.receivesMIDI) 1273 throw new Exception("A plugin that sends MIDI must also receives MIDI. Caution: a plugin that receives MIDI must call getNextMidiMessages() in the audio callback"); 1274 1275 info.publicVersion = parsePluginVersion(j["publicVersion"].str); 1276 1277 string CFBundleIdentifierPrefix = j["CFBundleIdentifierPrefix"].str; 1278 1279 string sanitizedName = sanitizeBundleString(info.pluginName); 1280 info.VSTBundleIdentifier = CFBundleIdentifierPrefix ~ ".vst." ~ sanitizedName; 1281 info.AUBundleIdentifier = CFBundleIdentifierPrefix ~ ".audiounit." ~ sanitizedName; 1282 info.AAXBundleIdentifier = CFBundleIdentifierPrefix ~ ".aax." ~ sanitizedName; 1283 info.CLAPIdentifier = CFBundleIdentifierPrefix ~ "." ~ sanitizedName; 1284 info.CLAPIdentifierFactory = CFBundleIdentifierPrefix ~ "." ~ sanitizedName ~ ".factory"; 1285 1286 PluginCategory category = parsePluginCategory(j["category"].str); 1287 if (category == PluginCategory.invalid) 1288 throw new Exception("Invalid \"category\" in plugin.json. Check out dplug.client.daw for valid values (eg: \"effectDynamics\")."); 1289 info.category = category; 1290 1291 // See Issue #581. 1292 // Check that we aren't leaking secrets in this build, through `import("plugin.json")`. 1293 void checkNotLeakingPassword(string key) 1294 { 1295 if (key in j) 1296 { 1297 string pwd = j[key].str; 1298 if (pwd == "!PROMPT") 1299 return; 1300 1301 if (pwd.length > 0 && pwd[0] == '$') 1302 return; // using an envvar 1303 1304 throw new Exception( 1305 "\n*************************** WARNING ***************************\n\n" 1306 ~ " This build is using a plain text password in plugin.json\n" 1307 ~ " This will leak through `import(\"plugin.json\")`\n\n" 1308 ~ " Solutions:\n" 1309 ~ " 1. Use environment variables, such as:\n" 1310 ~ " \"iLokPassword\": \"$ILOK_PASSWORD\"\n" 1311 ~ " 2. Use the special value \"!PROMPT\", such as:\n" 1312 ~ " \"keyPassword-windows\": \"!PROMPT\"\n\n" 1313 ~ "***************************************************************\n"); 1314 } 1315 } 1316 checkNotLeakingPassword("keyPassword-windows"); 1317 checkNotLeakingPassword("iLokPassword"); 1318 1319 void checkNotLeakingNoPrompt(string key) 1320 { 1321 if (key in j) 1322 { 1323 string pwd = j[key].str; 1324 if (pwd.length > 0 && pwd[0] == '$') 1325 return; // using an envvar 1326 1327 throw new Exception( 1328 "\n*************************** WARNING ***************************\n\n" 1329 ~ " This build is using a plain text password in plugin.json\n" 1330 ~ " This will leak through `import(\"plugin.json\")`\n\n" 1331 ~ " Solution:\n" 1332 ~ " Use environment variables, such as:\n" 1333 ~ " \"appSpecificPassword-altool\": \"$APP_SPECIFIC_PASSWORD\"\n\n" 1334 ~ "***************************************************************\n"); 1335 } 1336 } 1337 checkNotLeakingNoPrompt("appSpecificPassword-altool"); 1338 checkNotLeakingNoPrompt("appSpecificPassword-stapler"); 1339 1340 return info; 1341 } 1342 1343 private string sanitizeBundleString(string s) pure 1344 { 1345 string r = ""; 1346 foreach(dchar ch; s) 1347 { 1348 if (ch >= 'A' && ch <= 'Z') 1349 r ~= ch; 1350 else if (ch >= 'a' && ch <= 'z') 1351 r ~= ch; 1352 else if (ch == '.') 1353 r ~= ch; 1354 else 1355 r ~= "-"; 1356 } 1357 return r; 1358 }