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