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