1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005 - 2015 Cockos Incorporated 5 Copyright (C) 2015 and later 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. 19 20 module dplug.client.client; 21 22 import core.atomic; 23 import core.stdc.string; 24 import core.stdc.stdio; 25 26 import std.container; 27 28 import dplug.core.nogc; 29 import dplug.core.math; 30 import dplug.core.alignedbuffer; 31 32 import dplug.client.params; 33 import dplug.client.preset; 34 import dplug.client.midi; 35 import dplug.client.graphics; 36 import dplug.client.daw; 37 38 39 version = lazyGraphicsCreation; 40 41 /// A plugin client can send commands to the host. 42 /// This interface is injected after the client creation though. 43 interface IHostCommand 44 { 45 nothrow @nogc: 46 void beginParamEdit(int paramIndex); 47 void paramAutomate(int paramIndex, float value); 48 void endParamEdit(int paramIndex); 49 bool requestResize(int width, int height); 50 DAW getDAW(); 51 } 52 53 // Plugin version in major.minor.patch form. 54 struct PluginVersion 55 { 56 int major; 57 int minor; 58 int patch; 59 60 int toVSTVersion() pure const nothrow @nogc 61 { 62 assert(major < 10 && minor < 10 && patch < 10); 63 return major * 1000 + minor * 100 + patch*10; 64 } 65 66 int toAUVersion() pure const nothrow @nogc 67 { 68 assert(major < 256 && minor < 256 && patch < 256); 69 return (major << 16) | (minor << 8) | patch; 70 } 71 } 72 73 // Statically known features of the plugin. 74 // There is some default for explanation purpose, but you really ought to override them all. 75 // Most of it is redundant with plugin.json, in the future the JSON will be parsed instead. 76 struct PluginInfo 77 { 78 string vendorName = "Witty Audio"; 79 80 /// Used in AU only. 81 char[4] vendorUniqueID = "Wity"; 82 83 string pluginName = "Destructatorizer"; 84 85 /// Used for both VST and AU. 86 /// In AU it is namespaced by the manufacturer. In VST it 87 /// should be unique. While it seems no VST host use this 88 /// ID as a unique way to identify a plugin, common wisdom 89 /// is to try to get a sufficiently random one. 90 char[4] pluginUniqueID = "WiDi"; 91 92 // For AU, 0.x.y means "do not cache", useful in development 93 // Though caching rarely makes problem, if ever? 94 deprecated("Use publicVersion instead") alias pluginVersion = publicVersion; 95 PluginVersion publicVersion = PluginVersion(0, 0, 0); 96 97 /// True if the plugin has a graphical UI. Easy way to disable it. 98 bool hasGUI = false; 99 100 /// True if the plugin "is a synth". This has only a semantic effect. 101 bool isSynth = false; 102 103 /// True if the plugin should receive MIDI events. 104 /// Warning: receiving MIDI forces you to call `getNextMidiMessages` 105 /// with the right number of `frames`, every buffer. 106 bool receivesMIDI = false; 107 } 108 109 /// This allows to write things life tempo-synced LFO. 110 struct TimeInfo 111 { 112 /// BPM 113 double tempo = 120; 114 115 /// Current time from the beginning of the song in samples. 116 long timeInSamples = 0; 117 118 /// Whether the host sequencer is currently playing 119 bool hostIsPlaying; 120 } 121 122 /// Describe a combination of input channels count and output channels count 123 struct LegalIO 124 { 125 int numInputChannels; 126 int numOutputChannels; 127 } 128 129 /// Plugin interface, from the client point of view. 130 /// This client has no knowledge of thread-safety, it must be handled externally. 131 /// User plugins derivate from this class. 132 /// Plugin formats wrappers owns one dplug.plugin.Client as a member. 133 class Client 134 { 135 public: 136 nothrow: 137 @nogc: 138 139 this() 140 { 141 _info = buildPluginInfo(); 142 143 // Create legal I/O combinations 144 _legalIOs = buildLegalIO(); 145 146 // Create parameters. 147 _params = buildParameters(); 148 149 // Check parameter consistency 150 // This avoid mistake when adding/reordering parameters in a plugin. 151 foreach(int i, Parameter param; _params) 152 { 153 // If you fail here, this means your buildParameter() override is incorrect. 154 // Check the values of the index you're giving. 155 // They should be 0, 1, 2, ..., N-1 156 // Maybe you have duplicated a line or misordered them. 157 assert(param.index() == i); 158 159 // Sets owner reference. 160 param.setClientReference(this); 161 } 162 163 // Create presets 164 _presetBank = mallocEmplace!PresetBank(this, buildPresets()); 165 166 167 _maxFramesInProcess = maxFramesInProcess(); 168 169 _maxInputs = 0; 170 _maxOutputs = 0; 171 foreach(legalIO; _legalIOs) 172 { 173 if (_maxInputs < legalIO.numInputChannels) 174 _maxInputs = legalIO.numInputChannels; 175 if (_maxOutputs < legalIO.numOutputChannels) 176 _maxOutputs = legalIO.numOutputChannels; 177 } 178 179 version (lazyGraphicsCreation) {} 180 else 181 { 182 createGraphicsLazily(); 183 } 184 185 _midiQueue = makeMidiQueue(); 186 } 187 188 ~this() 189 { 190 // Destroy graphics 191 if (_graphics !is null) 192 { 193 // Acquire _graphicsIsAvailable forever 194 // so that it's the last time the audio uses it, 195 // and we can wait for its exit in _graphics destructor 196 while(!cas(&_graphicsIsAvailable, true, false)) 197 { 198 // MAYDO: relax CPU 199 } 200 _graphics.destroyFree(); 201 } 202 203 // Destroy presets 204 _presetBank.destroyFree(); 205 206 // Destroy parameters 207 foreach(p; _params) 208 p.destroyFree(); 209 _params.freeSlice(); 210 _legalIOs.freeSlice(); 211 } 212 213 final int maxInputs() pure const nothrow @nogc 214 { 215 return _maxInputs; 216 } 217 218 final int maxOutputs() pure const nothrow @nogc 219 { 220 return _maxInputs; 221 } 222 223 /// Returns: Array of parameters. 224 final Parameter[] params() nothrow @nogc 225 { 226 return _params; 227 } 228 229 /// Returns: Array of legal I/O combinations. 230 final LegalIO[] legalIOs() nothrow @nogc 231 { 232 return _legalIOs; 233 } 234 235 /// Returns: true if the following I/O combination is a legal one. 236 /// < 0 means "do not check" 237 final bool isLegalIO(int numInputChannels, int numOutputChannels) pure const nothrow @nogc 238 { 239 foreach(io; _legalIOs) 240 if ( ( (numInputChannels < 0) 241 || 242 (io.numInputChannels == numInputChannels) ) 243 && 244 ( (numOutputChannels < 0) 245 || 246 (io.numOutputChannels == numOutputChannels) ) 247 ) 248 return true; 249 250 return false; 251 } 252 253 /// Returns: Array of presets. 254 final PresetBank presetBank() nothrow @nogc 255 { 256 return _presetBank; 257 } 258 259 /// Returns: The parameter indexed by index. 260 final Parameter param(int index) nothrow @nogc 261 { 262 return _params.ptr[index]; 263 } 264 265 /// Returns: true if index is a valid parameter index. 266 final bool isValidParamIndex(int index) nothrow @nogc 267 { 268 return index >= 0 && index < _params.length; 269 } 270 271 /// Returns: true if index is a valid input index. 272 final bool isValidInputIndex(int index) nothrow @nogc 273 { 274 return index >= 0 && index < maxInputs(); 275 } 276 277 /// Returns: true if index is a valid output index. 278 final bool isValidOutputIndex(int index) nothrow @nogc 279 { 280 return index >= 0 && index < maxOutputs(); 281 } 282 283 // Note: openGUI, getGUISize and closeGUI are guaranteed 284 // synchronized by the client implementation 285 final void* openGUI(void* parentInfo, void* controlInfo, GraphicsBackend backend) nothrow @nogc 286 { 287 createGraphicsLazily(); 288 return (cast(IGraphics)_graphics).openUI(parentInfo, controlInfo, _hostCommand.getDAW(), backend); 289 } 290 291 final bool getGUISize(int* width, int* height) nothrow @nogc 292 { 293 createGraphicsLazily(); 294 auto graphics = (cast(IGraphics)_graphics); 295 if (graphics) 296 { 297 graphics.getGUISize(width, height); 298 return true; 299 } 300 else 301 return false; 302 } 303 304 /// ditto 305 final void closeGUI() nothrow @nogc 306 { 307 (cast(IGraphics)_graphics).closeUI(); 308 } 309 310 // This should be called only by a client implementation. 311 void setParameterFromHost(int index, float value) nothrow @nogc 312 { 313 param(index).setFromHost(value); 314 } 315 316 /// Override if you create a plugin with UI. 317 /// The returned IGraphics must be allocated with `mallocEmplace`. 318 IGraphics createGraphics() nothrow @nogc 319 { 320 return null; 321 } 322 323 /// Getter for the IGraphics interface 324 /// This is intended for the audio thread and has acquire semantics. 325 /// Not reentrant! You can't call this twice without a graphicsRelease first. 326 /// Returns: null if feedback from audio thread is not welcome. 327 final IGraphics graphicsAcquire() nothrow @nogc 328 { 329 if (cas(&_graphicsIsAvailable, true, false)) 330 return _graphics; 331 else 332 return null; 333 } 334 335 /// Mirror function to release the IGraphics from the audio-thread. 336 /// Do not call if graphicsAcquire() returned `null`. 337 final void graphicsRelease() nothrow @nogc 338 { 339 // graphicsAcquire should have been called before 340 // MAYDO: which memory order here? Don't looks like we need a barrier. 341 atomicStore(_graphicsIsAvailable, true); 342 } 343 344 // Getter for the IHostCommand interface 345 final IHostCommand hostCommand() nothrow @nogc 346 { 347 return _hostCommand; 348 } 349 350 /// Override to clear state (eg: resize and clear delay lines) and allocate buffers. 351 /// Important: This will be called by the audio thread. 352 /// So you should not use the GC in this callback. 353 abstract void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc; 354 355 /// Override to set the plugin latency in samples. 356 /// Unfortunately most of the time latency is dependent on the sampling rate and frequency, 357 /// but most hosts don't support latency changes. 358 /// Returns: Plugin latency in samples. 359 int latencySamples() pure const nothrow @nogc 360 { 361 return 0; 362 } 363 364 /// Override to set the plugin tail length in seconds. 365 /// This is the amount of time before silence is reached with a silent input. 366 /// Returns: Plugin tail size in seconds. 367 float tailSizeInSeconds() pure const nothrow @nogc 368 { 369 return 0.100f; // default: 100ms 370 } 371 372 /// Override to declare the maximum number of samples to accept 373 /// If greater, the audio buffers will be splitted up. 374 /// This splitting have several benefits: 375 /// - help allocating temporary audio buffers on the stack 376 /// - keeps memory usage low and reuse it 377 /// - allow faster-than-buffer-size parameter changes 378 /// Returns: Maximum number of samples 379 int maxFramesInProcess() pure const nothrow @nogc 380 { 381 return 0; // default returns 0 which means "do not split" 382 } 383 384 /// Process some audio. 385 /// Override to make some noise. 386 /// In processAudio you are always guaranteed to get valid pointers 387 /// to all the channels the plugin requested. 388 /// Unconnected input pins are zeroed. 389 /// This callback is the only place you may call `getNextMidiMessages()` (it is 390 /// even required for plugins receiving MIDI). 391 /// 392 /// Number of frames are guaranteed to be less or equal to what the last reset() call said. 393 /// Number of inputs and outputs are guaranteed to be exactly what the last reset() call said. 394 /// Warning: Do not modify the pointers! 395 abstract void processAudio(const(float*)[] inputs, // array of input channels 396 float*[] outputs, // array of output channels 397 int frames, // number of sample in each input & output channel 398 TimeInfo timeInfo // time information associated with this signal frame 399 ) nothrow @nogc; 400 401 /// Should only be called in `processAudio`. 402 /// This return a slice of MIDI messages corresponding to the next `frames` samples. 403 /// Useful if you don't want to process messages every samples, or every split buffer. 404 final const(MidiMessage)[] getNextMidiMessages(int frames) nothrow @nogc 405 { 406 return _midiQueue.getNextMidiMessages(frames); 407 } 408 409 /// Returns a new default preset. 410 final Preset makeDefaultPreset() nothrow @nogc 411 { 412 // MAYDO: use mallocSlice for perf 413 auto values = makeAlignedBuffer!float(); 414 foreach(param; _params) 415 values.pushBack(param.getNormalizedDefault()); 416 return mallocEmplace!Preset("Default", values.releaseData); 417 } 418 419 // Getters for fields in _info 420 421 final bool hasGUI() pure const nothrow @nogc 422 { 423 return _info.hasGUI; 424 } 425 426 final bool isSynth() pure const nothrow @nogc 427 { 428 return _info.isSynth; 429 } 430 431 final bool receivesMIDI() pure const nothrow @nogc 432 { 433 return _info.receivesMIDI; 434 } 435 436 final string vendorName() pure const nothrow @nogc 437 { 438 return _info.vendorName; 439 } 440 441 final char[4] getVendorUniqueID() pure const nothrow @nogc 442 { 443 return _info.vendorUniqueID; 444 } 445 446 final string pluginName() pure const nothrow @nogc 447 { 448 return _info.pluginName; 449 } 450 451 /// Returns: Plugin "unique" ID. 452 final char[4] getPluginUniqueID() pure const nothrow @nogc 453 { 454 return _info.pluginUniqueID; 455 } 456 457 /// Returns: Plugin full name "$VENDOR $PRODUCT" 458 final void getPluginFullName(char* p, int bufLength) const nothrow @nogc 459 { 460 snprintf(p, bufLength, "%.*s %.*s", 461 _info.vendorName.length, _info.vendorName.ptr, 462 _info.pluginName.length, _info.pluginName.ptr); 463 } 464 465 /// Returns: Plugin version in x.x.x.x decimal form. 466 deprecated("Use getPublicVersion instead") alias getPluginVersion = getPublicVersion; 467 final PluginVersion getPublicVersion() pure const nothrow @nogc 468 { 469 return _info.publicVersion; 470 } 471 472 /// Boilerplate function to get the value of a `FloatParameter`, for use in `processAudio`. 473 final float readFloatParamValue(int paramIndex) nothrow @nogc 474 { 475 auto p = param(paramIndex); 476 assert(cast(FloatParameter)p !is null); // check it's a FloatParameter 477 return unsafeObjectCast!FloatParameter(p).valueAtomic(); 478 } 479 480 /// Boilerplate function to get the value of an `IntParameter`, for use in `processAudio`. 481 final int readIntegerParamValue(int paramIndex) nothrow @nogc 482 { 483 auto p = param(paramIndex); 484 assert(cast(IntegerParameter)p !is null); // check it's an IntParameter 485 return unsafeObjectCast!IntegerParameter(p).valueAtomic(); 486 } 487 488 final int readEnumParamValue(int paramIndex) nothrow @nogc 489 { 490 auto p = param(paramIndex); 491 assert(cast(EnumParameter)p !is null); // check it's an EnumParameter 492 return unsafeObjectCast!EnumParameter(p).valueAtomic(); 493 } 494 495 /// Boilerplate function to get the value of a `BoolParameter`,for use in `processAudio`. 496 final bool readBoolParamValue(int paramIndex) nothrow @nogc 497 { 498 auto p = param(paramIndex); 499 assert(cast(BoolParameter)p !is null); // check it's a BoolParameter 500 return unsafeObjectCast!BoolParameter(p).valueAtomic(); 501 } 502 503 /// For plugin format clients only. 504 final void setHostCommand(IHostCommand hostCommand) nothrow @nogc 505 { 506 _hostCommand = hostCommand; 507 } 508 509 /// For plugin format clients only. 510 /// Enqueues an incoming MIDI message. 511 void enqueueMIDIFromHost(MidiMessage message) 512 { 513 _midiQueue.enqueue(message); 514 } 515 516 /// For plugin format clients only. 517 /// Calls processAudio repeatedly, splitting the buffers. 518 /// Splitting allow to decouple memory requirements from the actual host buffer size. 519 /// There is few performance penalty above 512 samples. 520 void processAudioFromHost(float*[] inputs, 521 float*[] outputs, 522 int frames, 523 TimeInfo timeInfo 524 ) nothrow @nogc 525 { 526 527 if (_maxFramesInProcess == 0) 528 { 529 processAudio(inputs, outputs, frames, timeInfo); 530 } 531 else 532 { 533 // Slice audio in smaller parts 534 while (frames > 0) 535 { 536 // Note: the last slice will be smaller than the others 537 int sliceLength = frames; 538 if (sliceLength > _maxFramesInProcess) 539 sliceLength = _maxFramesInProcess; 540 541 processAudio(inputs, outputs, sliceLength, timeInfo); 542 543 // offset all input buffer pointers 544 for (int i = 0; i < cast(int)inputs.length; ++i) 545 inputs[i] = inputs[i] + sliceLength; 546 547 // offset all output buffer pointers 548 for (int i = 0; i < cast(int)outputs.length; ++i) 549 outputs[i] = outputs[i] + sliceLength; 550 551 frames -= sliceLength; 552 553 // timeInfo must be updated 554 timeInfo.timeInSamples += sliceLength; 555 } 556 assert(frames == 0); 557 } 558 } 559 560 /// For plugin format clients only. 561 /// Calls `reset()`. 562 /// Muzt be called by the audio thread. 563 void resetFromHost(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc 564 { 565 // Clear outstanding MIDI messages (now invalid) 566 _midiQueue.initialize(); 567 568 // We potentially give to the client implementation a lower value 569 // for the maximum number of frames 570 if (_maxFramesInProcess != 0 && _maxFramesInProcess < maxFrames) 571 maxFrames = _maxFramesInProcess; 572 573 // Calls the reset virtual call 574 reset(sampleRate, maxFrames, numInputs, numOutputs); 575 } 576 577 protected: 578 579 /// Override this method to implement parameter creation. 580 /// This is an optional overload, default implementation declare no parameters. 581 /// The returned slice must be allocated with `malloc`/`mallocSlice` and contains 582 /// `Parameter` objects created with `mallocEmplace`. 583 Parameter[] buildParameters() 584 { 585 return []; 586 } 587 588 /// Override this methods to load/fill presets. 589 /// This function must return a slice allocated with `malloc`, 590 /// that contains presets crteated with `mallocEmplace`. 591 Preset[] buildPresets() nothrow @nogc 592 { 593 auto presets = makeAlignedBuffer!Preset(); 594 presets.pushBack( makeDefaultPreset() ); 595 return presets.releaseData(); 596 } 597 598 /// Override this method to tell what plugin you are. 599 /// Mandatory override, fill the fields with care. 600 abstract PluginInfo buildPluginInfo(); 601 602 /// Override this method to tell which I/O are legal. 603 /// The returned slice must be allocated with `malloc`/`mallocSlice`. 604 abstract LegalIO[] buildLegalIO(); 605 606 IGraphics _graphics; 607 608 // Used as a flag that _graphics can be used (by audio thread or for destruction) 609 shared(bool) _graphicsIsAvailable = false; 610 611 IHostCommand _hostCommand; 612 613 PluginInfo _info; 614 615 private: 616 Parameter[] _params; 617 618 PresetBank _presetBank; 619 620 LegalIO[] _legalIOs; 621 622 int _maxInputs, _maxOutputs; // maximum number of input/outputs 623 624 // Cache result of maxFramesInProcess(), maximum frame length 625 int _maxFramesInProcess; 626 627 // Container for awaiting MIDI messages. 628 MidiQueue _midiQueue; 629 630 final void createGraphicsLazily() nothrow @nogc 631 { 632 // First GUI opening create the graphics object 633 // no need to protect _graphics here since the audio thread 634 // does not write to it 635 if ( (_graphics is null) && hasGUI()) 636 { 637 // Why is the IGraphics created lazily? This allows to load a plugin very quickly, 638 // without opening its logical UI 639 IGraphics graphics = createGraphics(); 640 641 // Don't forget to override the createGraphics method! 642 assert(graphics !is null); 643 644 _graphics = graphics; 645 646 // Now that the UI is fully created, we enable the audio thread to use it 647 atomicStore(_graphicsIsAvailable, true); 648 } 649 } 650 } 651 652 /// Should be called in Client class during compile time 653 /// to parse a `PluginInfo` from a supplied json file. 654 PluginInfo parsePluginInfo(string json) 655 { 656 import std.json; 657 import std.string; 658 import std.conv; 659 660 JSONValue j = parseJSON(json); 661 662 static bool toBoolean(JSONValue value) 663 { 664 if (value.type == JSON_TYPE.TRUE) 665 return true; 666 if (value.type == JSON_TYPE.FALSE) 667 return false; 668 throw new Exception(format("Expected a boolean, got %s instead", value)); 669 } 670 671 // Check that a string is "x.y.z" 672 // FUTURE: support larger integers than 0 to 9 in the string 673 static PluginVersion parsePluginVersion(string value) 674 { 675 bool isDigit(char ch) 676 { 677 return ch >= '0' && ch <= '9'; 678 } 679 680 if ( value.length != 5 || 681 !isDigit(value[0]) || 682 value[1] != '.' || 683 !isDigit(value[2]) || 684 value[3] != '.' || 685 !isDigit(value[4])) 686 { 687 throw new Exception("\"publicVersion\" should follow the form x.y.z (eg: \"1.0.0\")"); 688 } 689 690 PluginVersion ver; 691 ver.major = value[0] - '0'; 692 ver.minor = value[2] - '0'; 693 ver.patch = value[4] - '0'; 694 return ver; 695 } 696 697 PluginInfo info; 698 info.vendorName = j["vendorName"].str; 699 info.vendorUniqueID = j["vendorUniqueID"].str; 700 info.pluginName = j["pluginName"].str; 701 info.pluginUniqueID = j["pluginUniqueID"].str; 702 info.isSynth = toBoolean(j["isSynth"]); 703 info.hasGUI = toBoolean(j["hasGUI"]); 704 info.receivesMIDI = toBoolean(j["receivesMIDI"]); 705 info.publicVersion = parsePluginVersion(j["publicVersion"].str); 706 707 return info; 708 }