1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005-2015 Cockos Incorporated 5 Copyright (C) 2015-2022 Guillaume Piolat 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 VST plug-in client implementation. 19 20 Copyright: Cockos Incorporated 2005-2015. 21 Copyright: Guillaume Piolat 2015-2022. 22 +/ 23 module dplug.vst2.client; 24 25 import std.string; 26 27 import core.stdc.stdlib, 28 core.stdc.string, 29 core.stdc.stdio; 30 31 import dplug.core.vec, 32 dplug.core.nogc, 33 dplug.core.math, 34 dplug.core.lockedqueue, 35 dplug.core.runtime, 36 dplug.core.fpcontrol, 37 dplug.core.thread, 38 dplug.core.sync; 39 40 import dplug.client.client, 41 dplug.client.daw, 42 dplug.client.preset, 43 dplug.client.graphics, 44 dplug.client.midi; 45 46 import dplug.vst2.translatesdk; 47 48 // Only does a semantic pass on this if the VST version identifier is defined. 49 // This allows building dplug:vst even without a VST2 SDK (though nothing will be defined in this case) 50 version(VST2): 51 52 template VST2EntryPoint(alias ClientClass) 53 { 54 enum entry_VSTPluginMain = 55 "export extern(C) nothrow AEffect* VSTPluginMain(HostCallbackFunction hostCallback) " ~ 56 "{" ~ 57 " return myVSTEntryPoint!" ~ ClientClass.stringof ~ "(hostCallback);" ~ 58 "}\n"; 59 enum entry_main_macho = 60 "export extern(C) nothrow AEffect* main_macho(HostCallbackFunction hostCallback) " ~ 61 "{" ~ 62 " return myVSTEntryPoint!" ~ ClientClass.stringof ~ "(hostCallback);" ~ 63 "}\n"; 64 65 // Workaround LDC #3505 66 // https://github.com/ldc-developers/ldc/issues/3505 67 // Not possible to return a pointer from a "main" function. 68 // Using pragma mangle, this symbol should be "main" in Win32 and "_main" on Linux. 69 static if (__VERSION__ >= 2079) 70 { 71 enum entry_main = 72 "pragma(mangle, \"main\") export extern(C) nothrow AEffect* main_renamed(HostCallbackFunction hostCallback) " ~ 73 "{" ~ 74 " return myVSTEntryPoint!" ~ ClientClass.stringof ~ "(hostCallback);" ~ 75 "}\n"; 76 } 77 else 78 { 79 enum entry_main = // earlier compilers didn't have a problem 80 "export extern(C) nothrow AEffect* main(HostCallbackFunction hostCallback) " ~ 81 "{" ~ 82 " return myVSTEntryPoint!" ~ ClientClass.stringof ~ "(hostCallback);" ~ 83 "}\n"; 84 } 85 86 version(unittest) 87 enum entry_main_if_no_unittest = ""; 88 else 89 enum entry_main_if_no_unittest = entry_main; 90 91 version(OSX) 92 // OSX has two VST entry points 93 const char[] VST2EntryPoint = entry_VSTPluginMain ~ entry_main_macho; 94 else version(Windows) 95 { 96 static if (size_t.sizeof == int.sizeof) 97 // 32-bit Windows needs a legacy "main" entry point 98 const char[] VST2EntryPoint = entry_VSTPluginMain ~ entry_main_if_no_unittest; 99 else 100 // 64-bit Windows does not 101 const char[] VST2EntryPoint = entry_VSTPluginMain; 102 103 } 104 else version(linux) 105 const char[] VST2EntryPoint = entry_VSTPluginMain ~ entry_main_if_no_unittest; 106 else 107 static assert(false); 108 } 109 110 // This is the main VST entrypoint 111 nothrow AEffect* myVSTEntryPoint(alias ClientClass)(HostCallbackFunction hostCallback) 112 { 113 if (hostCallback is null) 114 return null; 115 116 ScopedForeignCallback!(false, true) scopedCallback; 117 scopedCallback.enter(); 118 auto client = mallocNew!ClientClass(); 119 120 // malloc'd else the GC would not register roots for some reason! 121 VST2Client plugin = mallocNew!VST2Client(client, hostCallback); 122 return &plugin._effect; 123 }; 124 125 126 //version = logVSTDispatcher; 127 128 /// VST client wrapper 129 class VST2Client 130 { 131 public: 132 nothrow: 133 @nogc: 134 135 AEffect _effect; 136 137 this(Client client, HostCallbackFunction hostCallback) 138 { 139 int queueSize = 2048; 140 _messageQueue = makeLockedQueue!AudioThreadMessage(queueSize); 141 142 _client = client; 143 _effect = _effect.init; 144 _effect.magic = CCONST('V', 's', 't', 'P'); 145 146 int flags = effFlagsCanReplacing | effFlagsCanDoubleReplacing; 147 148 version(legacyVST2Chunks) 149 {} 150 else 151 { 152 flags |= effFlagsProgramChunks; 153 } 154 155 if ( client.hasGUI() ) 156 flags |= effFlagsHasEditor; 157 158 if ( client.isSynth() ) 159 flags |= effFlagsIsSynth; 160 161 _effect.flags = flags; 162 _maxParams = cast(int)(client.params().length); 163 _maxInputs = _effect.numInputs = _client.maxInputs(); 164 _maxOutputs = _effect.numOutputs = _client.maxOutputs(); 165 assert(_maxParams >= 0 && _maxInputs >= 0 && _maxOutputs >= 0); 166 _effect.numParams = cast(int)(client.params().length); 167 _effect.numPrograms = cast(int)(client.presetBank().numPresets()); 168 _effect.version_ = client.getPublicVersion().toVSTVersion(); 169 char[4] uniqueID = client.getPluginUniqueID(); 170 _effect.uniqueID = CCONST(uniqueID[0], uniqueID[1], uniqueID[2], uniqueID[3]); 171 _effect.processReplacing = &processReplacingCallback; 172 _effect.dispatcher = &dispatcherCallback; 173 _effect.setParameter = &setParameterCallback; 174 _effect.getParameter = &getParameterCallback; 175 _effect.object = cast(void*)(this); 176 177 _effect.initialDelay = _client.latencySamples(44100); // Note: we can't have a sample-rate yet 178 _effect.object = cast(void*)(this); 179 _effect.processDoubleReplacing = &processDoubleReplacingCallback; 180 181 _effect.DEPRECATED_ioRatio = 1.0; 182 _effect.DEPRECATED_process = &processCallback; 183 184 // dummmy values 185 _sampleRate = 44100.0f; 186 _maxFrames = 128; 187 188 _maxFramesInProcess = _client.maxFramesInProcess(); 189 190 _samplesAlreadyProcessed = 0; 191 192 193 // GUI thread can allocate 194 _inputScratchBuffer = mallocSlice!(Vec!float)(_maxInputs); 195 _outputScratchBuffer = mallocSlice!(Vec!float)(_maxOutputs); 196 197 for (int i = 0; i < _maxInputs; ++i) 198 _inputScratchBuffer[i] = makeVec!float(); 199 200 for (int i = 0; i < _maxOutputs; ++i) 201 _outputScratchBuffer[i] = makeVec!float(); 202 203 _zeroesBuffer = makeVec!float(); 204 205 _inputPointers = mallocSlice!(float*)(_maxInputs); 206 _outputPointers = mallocSlice!(float*)(_maxOutputs); 207 208 // because effSetSpeakerArrangement might never come, take a default 209 chooseIOArrangement(_maxInputs, _maxOutputs); 210 sendResetMessage(); 211 212 // Create host callback wrapper 213 _host = mallocNew!VSTHostFromClientPOV(hostCallback, &_effect); 214 client.setHostCommand(_host); 215 216 if ( client.receivesMIDI() ) 217 { 218 _host.wantEvents(); 219 } 220 221 _graphicsMutex = makeMutex(); 222 } 223 224 ~this() 225 { 226 _client.destroyFree(); 227 228 for (int i = 0; i < _maxInputs; ++i) 229 _inputScratchBuffer[i].destroy(); 230 231 for (int i = 0; i < _maxOutputs; ++i) 232 _outputScratchBuffer[i].destroy(); 233 234 _inputScratchBuffer.freeSlice(); 235 _outputScratchBuffer.freeSlice(); 236 237 _zeroesBuffer.destroy(); 238 239 _inputPointers.freeSlice(); 240 _outputPointers.freeSlice(); 241 242 _host.destroyFree(); 243 244 _messageQueue.destroy(); 245 246 version(legacyVST2Chunks) 247 {} 248 else 249 { 250 if (_lastStateChunk) 251 { 252 free(_lastStateChunk.ptr); 253 _lastStateChunk = null; 254 } 255 } 256 } 257 258 private: 259 260 VSTHostFromClientPOV _host; 261 Client _client; 262 263 float _sampleRate; // samplerate from opcode thread POV 264 int _maxFrames; // max frames from opcode thread POV 265 int _maxFramesInProcess; // max frames supported by the plugin, buffers will be splitted to follow this. 266 int _maxInputs; 267 int _maxOutputs; 268 int _maxParams; 269 270 // Actual channels the host will give. 271 IO _hostIOFromOpcodeThread; 272 273 // Logical number of channels the plugin will use. 274 // This might be different with hosts that call effSetSpeakerArrangement with 275 // an invalid number of channels (like Audition which uses 1-1 even if not available). 276 IO _processingIOFromOpcodeThread; 277 278 // Fills _hostIOFromOpcodeThread and _processingIOFromOpcodeThread 279 final void chooseIOArrangement(int numInputs, int numOutputs) nothrow @nogc 280 { 281 _hostIOFromOpcodeThread = IO(numInputs, numOutputs); 282 283 // Note: _hostIOFromOpcodeThread may contain invalid stuff 284 // Compute acceptable number of channels based on I/O legality. 285 286 // Find the legal I/O combination with the highest score. 287 int bestScore = -10000; 288 IO bestProcessingIO = _hostIOFromOpcodeThread; 289 bool found = false; 290 291 foreach(LegalIO io; _client.legalIOs()) 292 { 293 // The reasoning is: try to match exactly inputs and outputs. 294 // If this isn't possible, better have the largest number of channels, 295 // all other things being equal. 296 // Note: this heuristic will prefer 1-2 to 2-1 if 1-1 was asked. 297 int score = 0; 298 if (io.numInputChannels == numInputs) 299 score += 2000; 300 else 301 score += (io.numInputChannels - numInputs); 302 303 if (io.numOutputChannels == numOutputs) 304 score += 1000; 305 else 306 score += (io.numOutputChannels - numOutputs); 307 308 if (score > bestScore) 309 { 310 bestScore = score; 311 bestProcessingIO = IO(io.numInputChannels, io.numOutputChannels); 312 } 313 } 314 _processingIOFromOpcodeThread = bestProcessingIO; 315 } 316 317 // Same data, but on the audio thread point of view. 318 IO _hostIOFromAudioThread; 319 IO _processingIOFromAudioThread; 320 321 long _samplesAlreadyProcessed; // For hosts that don't provide time info, fake it by counting samples. 322 323 ERect _editRect; // structure holding the UI size 324 325 Vec!float[] _inputScratchBuffer; // input buffer, one per possible input 326 Vec!float[] _outputScratchBuffer; // input buffer, one per output 327 Vec!float _zeroesBuffer; // used for disconnected inputs 328 float*[] _inputPointers; // where processAudio will take its audio input, one per possible input 329 float*[] _outputPointers; // where processAudio will output audio, one per possible output 330 331 // stores the last asked state chunk 332 version(legacyVST2Chunks) 333 {} 334 else 335 { 336 337 ubyte[] _lastStateChunk = null; 338 } 339 340 // Inter-locked message queue from opcode thread to audio thread 341 LockedQueue!AudioThreadMessage _messageQueue; 342 343 UncheckedMutex _graphicsMutex; 344 345 final bool isValidParamIndex(int i) pure const nothrow @nogc 346 { 347 return i >= 0 && i < _maxParams; 348 } 349 350 final bool isValidInputIndex(int index) pure const nothrow @nogc 351 { 352 return index >= 0 && index < _maxInputs; 353 } 354 355 final bool isValidOutputIndex(int index) pure const nothrow @nogc 356 { 357 return index >= 0 && index < _maxOutputs; 358 } 359 360 void sendResetMessage() 361 { 362 AudioThreadMessage msg = AudioThreadMessage(AudioThreadMessage.Type.resetState, 363 _maxFrames, 364 _sampleRate, 365 _hostIOFromOpcodeThread, 366 _processingIOFromOpcodeThread); 367 _messageQueue.pushBack(msg); 368 } 369 370 /// VST opcode dispatcher 371 final VstIntPtr dispatcher(int opcode, int index, ptrdiff_t value, void *ptr, float opt) nothrow @nogc 372 { 373 // Important message from Cockos: 374 // "Assume everything can (and WILL) run at the same time as your 375 // process/processReplacing, except: 376 // - effOpen/effClose 377 // - effSetChunk -- while effGetChunk can run at the same time as audio 378 // (user saves project, or for automatic undo state tracking), effSetChunk 379 // is guaranteed to not run while audio is processing. 380 // So nearly everything else should be threadsafe." 381 382 switch(opcode) 383 { 384 case effClose: // opcode 1 385 return 0; 386 387 case effSetProgram: // opcode 2 388 { 389 int presetIndex = cast(int)value; 390 PresetBank bank = _client.presetBank(); 391 if (bank.isValidPresetIndex(presetIndex)) 392 bank.loadPresetFromHost(presetIndex); 393 return 0; 394 } 395 396 case effGetProgram: // opcode 3 397 { 398 // FUTURE: will probably need to be zero with internal preset management 399 return _client.presetBank.currentPresetIndex(); 400 } 401 402 case effSetProgramName: // opcode 4 403 { 404 char* p = cast(char*)ptr; 405 int len = cast(int)strlen(p); 406 PresetBank bank = _client.presetBank(); 407 Preset current = bank.currentPreset(); 408 if (current !is null) 409 { 410 current.setName(p[0..len]); 411 } 412 return 0; 413 } 414 415 case effGetProgramName: // opcode 5 416 { 417 char* p = cast(char*)ptr; 418 if (p !is null) 419 { 420 PresetBank bank = _client.presetBank(); 421 Preset current = bank.currentPreset(); 422 if (current !is null) 423 { 424 stringNCopy(p, 24, current.name()); 425 } 426 } 427 return 0; 428 } 429 430 case effGetParamLabel: // opcode 6 431 { 432 char* p = cast(char*)ptr; 433 if (!isValidParamIndex(index)) 434 *p = '\0'; 435 else 436 { 437 stringNCopy(p, 8, _client.param(index).label()); 438 } 439 return 0; 440 } 441 442 case effGetParamDisplay: // opcode 7 443 { 444 char* p = cast(char*)ptr; 445 if (!isValidParamIndex(index)) 446 *p = '\0'; 447 else 448 { 449 _client.param(index).toDisplayN(p, 8); 450 } 451 return 0; 452 } 453 454 case effGetParamName: // opcode 8 455 { 456 char* p = cast(char*)ptr; 457 if (!isValidParamIndex(index)) 458 *p = '\0'; 459 else 460 { 461 stringNCopy(p, 32, _client.param(index).name()); 462 } 463 return 0; 464 } 465 466 case effSetSampleRate: // opcode 10 467 { 468 _sampleRate = opt; 469 sendResetMessage(); 470 return 0; 471 } 472 473 case effSetBlockSize: // opcode 11 474 { 475 if (value < 0) 476 return 1; 477 478 _maxFrames = cast(int)value; 479 sendResetMessage(); 480 return 0; 481 } 482 483 case effMainsChanged: // opcode 12 484 { 485 if (value == 0) 486 { 487 // Audio processing was switched off. 488 // The plugin must flush its state because otherwise pending data 489 // would sound again when the effect is switched on next time. 490 // MAYDO: this sounds backwards... 491 sendResetMessage(); 492 } 493 else // "resume()" in VST parlance 494 { 495 // Audio processing was switched on. Update the latency. #154 496 int latency = _client.latencySamples(_sampleRate); 497 _effect.initialDelay = latency; 498 } 499 return 0; 500 } 501 502 case effEditGetRect: // opcode 13 503 { 504 if ( _client.hasGUI() && ptr) 505 { 506 // Cubase may call effEditOpen and effEditGetRect simultaneously 507 _graphicsMutex.lock(); 508 509 int widthLogicalPixels, heightLogicalPixels; 510 511 bool isOBS = _host.getDAW() == DAW.OBSStudio; 512 513 bool ok; 514 if (isOBS) 515 ok = _client.getDesiredGUISize(&widthLogicalPixels, &heightLogicalPixels); 516 else 517 ok = _client.getGUISize(&widthLogicalPixels, &heightLogicalPixels); 518 519 if (ok) 520 { 521 _graphicsMutex.unlock(); 522 _editRect.top = 0; 523 _editRect.left = 0; 524 _editRect.right = cast(short)(widthLogicalPixels); 525 _editRect.bottom = cast(short)(heightLogicalPixels); 526 *cast(ERect**)(ptr) = &_editRect; 527 return 1; 528 } 529 else 530 { 531 _graphicsMutex.unlock(); 532 ptr = null; 533 return 0; 534 } 535 } 536 ptr = null; 537 return 0; 538 } 539 540 case effEditOpen: // opcode 14 541 { 542 if ( _client.hasGUI() ) 543 { 544 // Cubase may call effEditOpen and effEditGetRect simultaneously 545 _graphicsMutex.lock(); 546 _client.openGUI(ptr, null, GraphicsBackend.autodetect); 547 _graphicsMutex.unlock(); 548 return 1; 549 } 550 else 551 return 0; 552 } 553 554 case effEditClose: // opcode 15 555 { 556 if ( _client.hasGUI() ) 557 { 558 _graphicsMutex.lock(); 559 _client.closeGUI(); 560 _graphicsMutex.unlock(); 561 return 1; 562 } 563 else 564 return 0; 565 } 566 567 case DEPRECATED_effIdentify: // opcode 22 568 return CCONST('N', 'v', 'E', 'f'); 569 570 case effGetChunk: // opcode 23 571 { 572 version(legacyVST2Chunks) 573 {} 574 else 575 { 576 ubyte** ppData = cast(ubyte**) ptr; 577 bool wantBank = (index == 0); 578 if (ppData) 579 { 580 // Note: we have no concern whether the host demanded a bank or preset chunk here. 581 auto presetBank = _client.presetBank(); 582 _lastStateChunk = presetBank.getStateChunkFromCurrentState(); 583 *ppData = _lastStateChunk.ptr; 584 return cast(int)_lastStateChunk.length; 585 } 586 } 587 return 0; 588 } 589 590 case effSetChunk: // opcode 24 591 { 592 version(legacyVST2Chunks) 593 { 594 return 0; 595 } 596 else 597 { 598 if (!ptr) 599 return 0; 600 601 bool isBank = (index == 0); 602 ubyte[] chunk = (cast(ubyte*)ptr)[0..value]; 603 auto presetBank = _client.presetBank(); 604 605 bool err; 606 presetBank.loadStateChunk(chunk, &err); 607 if (err) 608 return 0; // Chunk didn't parse 609 else 610 return 1; // success 611 } 612 } 613 614 case effProcessEvents: // opcode 25, "host usually call ProcessEvents just before calling ProcessReplacing" 615 VstEvents* pEvents = cast(VstEvents*) ptr; 616 if (pEvents != null) 617 { 618 VstEvent** allEvents = pEvents.events.ptr; 619 for (int i = 0; i < pEvents.numEvents; ++i) 620 { 621 VstEvent* pEvent = allEvents[i]; 622 if (pEvent) 623 { 624 if (pEvent.type == kVstMidiType) 625 { 626 VstMidiEvent* pME = cast(VstMidiEvent*) pEvent; 627 628 // Enqueue midi message to be processed by the audio thread. 629 // Note that not all information is kept, some is discarded like in IPlug. 630 MidiMessage msg = MidiMessage(pME.deltaFrames, 631 pME.midiData[0], 632 pME.midiData[1], 633 pME.midiData[2]); 634 _messageQueue.pushBack(makeMIDIMessage(msg)); 635 } 636 else 637 { 638 // FUTURE handle sysex 639 } 640 } 641 } 642 return 1; 643 } 644 return 0; 645 646 case effCanBeAutomated: // opcode 26 647 { 648 if (!isValidParamIndex(index)) 649 return 0; 650 if (_client.param(index).isAutomatable) 651 return 1; 652 return 0; 653 } 654 655 case effString2Parameter: // opcode 27 656 { 657 if (!isValidParamIndex(index)) 658 return 0; 659 660 if (ptr == null) 661 return 0; 662 663 // MAYDO: Sounds a bit insufficient? Will return 0 in case of error. 664 // also it will run into C locale problems. 665 double parsed = atof(cast(char*)ptr); 666 _client.setParameterFromHost(index, parsed); 667 return 1; 668 } 669 670 case DEPRECATED_effGetNumProgramCategories: // opcode 28 671 return 1; // no real program categories 672 673 case effGetProgramNameIndexed: // opcode 29 674 { 675 char* p = cast(char*)ptr; 676 if (p !is null) 677 { 678 PresetBank bank = _client.presetBank(); 679 if (!bank.isValidPresetIndex(index)) 680 return 0; 681 const(char)[] name = bank.preset(index).name(); 682 stringNCopy(p, 24, name); 683 return (name.length > 0) ? 1 : 0; 684 } 685 else 686 return 0; 687 } 688 689 case effGetInputProperties: // opcode 33 690 { 691 if (ptr == null) 692 return 0; 693 694 if (!isValidInputIndex(index)) 695 return 0; 696 697 VstPinProperties* pp = cast(VstPinProperties*) ptr; 698 pp.flags = kVstPinIsActive; 699 700 if ( (index % 2) == 0 && index < _maxInputs) 701 pp.flags |= kVstPinIsStereo; 702 703 sprintf(pp.label.ptr, "Input %d", index); 704 return 1; 705 } 706 707 case effGetOutputProperties: // opcode 34 708 { 709 if (ptr == null) 710 return 0; 711 712 if (!isValidOutputIndex(index)) 713 return 0; 714 715 VstPinProperties* pp = cast(VstPinProperties*) ptr; 716 pp.flags = kVstPinIsActive; 717 718 if ( (index % 2) == 0 && index < _maxOutputs) 719 pp.flags |= kVstPinIsStereo; 720 721 sprintf(pp.label.ptr, "Output %d", index); 722 return 1; 723 } 724 725 case effGetPlugCategory: // opcode 35 726 if ( _client.isSynth() ) 727 return kPlugCategSynth; 728 else 729 return kPlugCategEffect; 730 731 case effSetSpeakerArrangement: // opcode 42 732 { 733 VstSpeakerArrangement* pInputArr = cast(VstSpeakerArrangement*) value; 734 VstSpeakerArrangement* pOutputArr = cast(VstSpeakerArrangement*) ptr; 735 if (pInputArr !is null && pOutputArr !is null ) 736 { 737 int numInputs = pInputArr.numChannels; 738 int numOutputs = pOutputArr.numChannels; 739 chooseIOArrangement(numInputs, numOutputs); 740 sendResetMessage(); 741 return 0; // MAYDO: this looks very wrong 742 } 743 return 1; 744 } 745 746 case effSetBypass: // opcode 44 747 // Unfortunately, we were unable to find any VST2 hsot that would use effSetBypass 748 // So disable it since it can't be out untested. 749 return 0; 750 751 case effGetEffectName: // opcode 45 752 { 753 char* p = cast(char*)ptr; 754 if (p !is null) 755 { 756 stringNCopy(p, 32, _client.pluginName()); 757 return 1; 758 } 759 return 0; 760 } 761 762 case effGetVendorString: // opcode 47 763 { 764 char* p = cast(char*)ptr; 765 if (p !is null) 766 { 767 stringNCopy(p, 64, _client.vendorName()); 768 return 1; 769 } 770 return 0; 771 } 772 773 case effGetProductString: // opcode 48 774 { 775 char* p = cast(char*)ptr; 776 if (p !is null) 777 { 778 _client.getPluginFullName(p, 64); 779 return 1; 780 } 781 return 0; 782 } 783 784 case effCanDo: // opcode 51 785 { 786 char* str = cast(char*)ptr; 787 if (str is null) 788 return 0; 789 790 if (strcmp(str, "receiveVstTimeInfo") == 0) 791 return 1; 792 793 // Unable to find a host that will actually support it. 794 // Have to disable it to avoid being untested. 795 /* 796 if (strcmp(str, "bypass") == 0) 797 { 798 return _client.hasBypass() ? 1 : 0; 799 } 800 */ 801 802 if (_client.sendsMIDI()) 803 { 804 if (strcmp(str, "sendVstEvents") == 0) 805 return 1; 806 if (strcmp(str, "sendVstMidiEvent") == 0) 807 return 1; 808 if (strcmp(str, "sendVstMidiEvents") == 0) 809 return 1; 810 } 811 812 if (_client.receivesMIDI()) 813 { 814 if (strcmp(str, "receiveVstEvents") == 0) 815 return 1; 816 817 // Issue #198, Bitwig Studio need this 818 if (strcmp(str, "receiveVstMidiEvent") == 0) 819 return 1; 820 821 if (strcmp(str, "receiveVstMidiEvents") == 0) 822 return 1; 823 } 824 825 return 0; 826 } 827 828 case effGetVstVersion: // opcode 58 829 return 2400; // version 2.4 830 831 default: 832 return 0; // unknown opcode, should never happen 833 } 834 } 835 836 // 837 // Processing buffers and callbacks 838 // 839 840 // Resize copy buffers according to maximum block size. 841 void resizeScratchBuffers(int nFrames) nothrow @nogc 842 { 843 for (int i = 0; i < _maxInputs; ++i) 844 _inputScratchBuffer[i].resize(nFrames); 845 846 for (int i = 0; i < _maxOutputs; ++i) 847 _outputScratchBuffer[i].resize(nFrames); 848 849 _zeroesBuffer.resize(nFrames); 850 _zeroesBuffer.fill(0); 851 } 852 853 854 void processMessages() nothrow @nogc 855 { 856 // Race condition here. 857 // Being a tryPop, there is a tiny chance that we miss a message from the queue. 858 // Thankfully it isn't that bad: 859 // - we are going to read it next buffer 860 // - not clearing the state for a buffer duration does no harm 861 // - plugin is initialized first with the maximum amount of input and outputs 862 // so missing such a message isn't that bad: the audio callback will have some outputs that are untouched 863 // (a third thread might start a collect while the UI thread takes the queue lock) which is another unlikely race condition. 864 // Perhaps it's the one to favor, I don't know. 865 // TODO: Objectionable decision, for MIDI input, think about impact. 866 867 AudioThreadMessage msg = void; 868 while(_messageQueue.tryPopFront(msg)) 869 { 870 final switch(msg.type) with (AudioThreadMessage.Type) 871 { 872 case resetState: 873 resizeScratchBuffers(msg.maxFrames); 874 875 _hostIOFromAudioThread = msg.hostIO; 876 _processingIOFromAudioThread = msg.processingIO; 877 878 _client.resetFromHost(msg.samplerate, 879 msg.maxFrames, 880 _processingIOFromAudioThread.inputs, 881 _processingIOFromAudioThread.outputs); 882 break; 883 884 case midi: 885 _client.enqueueMIDIFromHost(msg.midiMessage); 886 } 887 } 888 } 889 890 void process(float **inputs, float **outputs, int sampleFrames) nothrow @nogc 891 { 892 processMessages(); 893 int hostInputs = _hostIOFromAudioThread.inputs; 894 int hostOutputs = _hostIOFromAudioThread.outputs; 895 int usedInputs = _processingIOFromAudioThread.inputs; 896 int usedOutputs = _processingIOFromAudioThread.outputs; 897 int minOutputs = (usedOutputs < hostOutputs) ? usedOutputs : hostOutputs; 898 899 // Not sure if the hosts would support an overwriting of these pointers, so copy them 900 for (int i = 0; i < usedInputs; ++i) 901 { 902 // Points to zeros if the host provides a buffer, or the host buffer otherwise. 903 // Note: all input channels point on same buffer, but it's ok since input channels are const 904 _inputPointers[i] = (i < hostInputs) ? inputs[i] : _zeroesBuffer.ptr; 905 } 906 907 for (int i = 0; i < usedOutputs; ++i) 908 { 909 _outputPointers[i] = _outputScratchBuffer[i].ptr; 910 } 911 912 clearMidiOutBuffer(); 913 _client.processAudioFromHost(_inputPointers[0..usedInputs], 914 _outputPointers[0..usedOutputs], 915 sampleFrames, 916 _host.getVSTTimeInfo(_samplesAlreadyProcessed)); 917 _samplesAlreadyProcessed += sampleFrames; 918 919 // accumulate on available host output channels 920 for (int i = 0; i < minOutputs; ++i) 921 { 922 float* source = _outputScratchBuffer[i].ptr; 923 float* dest = outputs[i]; 924 for (int f = 0; f < sampleFrames; ++f) 925 dest[f] += source[f]; 926 } 927 sendMidiEvents(); 928 } 929 930 void processReplacing(float **inputs, float **outputs, int sampleFrames) nothrow @nogc 931 { 932 processMessages(); 933 int hostInputs = _hostIOFromAudioThread.inputs; 934 int hostOutputs = _hostIOFromAudioThread.outputs; 935 int usedInputs = _processingIOFromAudioThread.inputs; 936 int usedOutputs = _processingIOFromAudioThread.outputs; 937 int minOutputs = (usedOutputs < hostOutputs) ? usedOutputs : hostOutputs; 938 939 // Some hosts (Live, Orion, and others) send identical input and output pointers. 940 // This is actually legal in VST. 941 // We copy them to a scratch buffer to keep the constness guarantee of input buffers. 942 for (int i = 0; i < usedInputs; ++i) 943 { 944 if (i < hostInputs) 945 { 946 float* source = inputs[i]; 947 float* dest = _inputScratchBuffer[i].ptr; 948 dest[0..sampleFrames] = source[0..sampleFrames]; 949 _inputPointers[i] = dest; 950 } 951 else 952 { 953 _inputPointers[i] = _zeroesBuffer.ptr; 954 } 955 } 956 957 for (int i = 0; i < usedOutputs; ++i) 958 { 959 if (i < hostOutputs) 960 _outputPointers[i] = outputs[i]; 961 else 962 _outputPointers[i] = _outputScratchBuffer[i].ptr; // dummy output 963 } 964 965 clearMidiOutBuffer(); 966 _client.processAudioFromHost(_inputPointers[0..usedInputs], 967 _outputPointers[0..usedOutputs], 968 sampleFrames, 969 _host.getVSTTimeInfo(_samplesAlreadyProcessed)); 970 _samplesAlreadyProcessed += sampleFrames; 971 972 // Fills remaining host channels (if any) with zeroes 973 for (int i = minOutputs; i < hostOutputs; ++i) 974 { 975 float* dest = outputs[i]; 976 for (int f = 0; f < sampleFrames; ++f) 977 dest[f] = 0; 978 } 979 sendMidiEvents(); 980 } 981 982 void processDoubleReplacing(double **inputs, double **outputs, int sampleFrames) nothrow @nogc 983 { 984 processMessages(); 985 int hostInputs = _hostIOFromAudioThread.inputs; 986 int hostOutputs = _hostIOFromAudioThread.outputs; 987 int usedInputs = _processingIOFromAudioThread.inputs; 988 int usedOutputs = _processingIOFromAudioThread.outputs; 989 int minOutputs = (usedOutputs < hostOutputs) ? usedOutputs : hostOutputs; 990 991 // Existing inputs gets converted to float 992 // Non-connected inputs are zeroes 993 // 994 // Note about converting double to float: 995 // on both white noise and sinusoids, a conversion from 996 // double to float yield a relative RMS difference of 997 // -152dB. It would really extraordinary if anyone can tell 998 // the difference, as -110 dB RMS already exercise the limits 999 // of audition. 1000 for (int i = 0; i < usedInputs; ++i) 1001 { 1002 if (i < hostInputs) 1003 { 1004 double* source = inputs[i]; 1005 float* dest = _inputScratchBuffer[i].ptr; 1006 for (int f = 0; f < sampleFrames; ++f) 1007 dest[f] = source[f]; 1008 _inputPointers[i] = dest; 1009 } 1010 else 1011 _inputPointers[i] = _zeroesBuffer.ptr; 1012 } 1013 1014 for (int i = 0; i < usedOutputs; ++i) 1015 { 1016 _outputPointers[i] = _outputScratchBuffer[i].ptr; 1017 } 1018 1019 clearMidiOutBuffer(); 1020 _client.processAudioFromHost(_inputPointers[0..usedInputs], 1021 _outputPointers[0..usedOutputs], 1022 sampleFrames, 1023 _host.getVSTTimeInfo(_samplesAlreadyProcessed)); 1024 _samplesAlreadyProcessed += sampleFrames; 1025 1026 // Converts back to double on available host output channels 1027 for (int i = 0; i < minOutputs; ++i) 1028 { 1029 float* source = _outputScratchBuffer[i].ptr; 1030 double* dest = outputs[i]; 1031 for (int f = 0; f < sampleFrames; ++f) 1032 dest[f] = cast(double)source[f]; 1033 } 1034 1035 // Fills remaining host channels (if any) with zeroes 1036 for (int i = minOutputs; i < hostOutputs; ++i) 1037 { 1038 double* dest = outputs[i]; 1039 for (int f = 0; f < sampleFrames; ++f) 1040 dest[f] = 0; 1041 } 1042 sendMidiEvents(); 1043 } 1044 1045 void clearMidiOutBuffer() 1046 { 1047 if (!_client.sendsMIDI()) 1048 return; 1049 _client.clearAccumulatedOutputMidiMessages(); 1050 } 1051 1052 void sendMidiEvents() 1053 { 1054 if (!_client.sendsMIDI()) 1055 return; 1056 1057 const(MidiMessage)[] messages = _client.getAccumulatedOutputMidiMessages(); 1058 foreach(MidiMessage msg; messages) 1059 { 1060 VstMidiEvent event; 1061 event.type = kVstMidiType; 1062 event.byteSize = VstMidiEvent.sizeof; 1063 event.deltaFrames = msg.offset; 1064 event.flags = 0; // not played live, doesn't need specially high-priority 1065 event.noteLength = 0; // not available 1066 event.noteOffset = 0; // not available 1067 event.midiData[0] = 0; 1068 event.midiData[1] = 0; 1069 event.midiData[2] = 0; 1070 event.midiData[3] = 0; 1071 1072 int written = msg.toBytes(cast(ubyte*)(event.midiData.ptr), 3); 1073 if (written == 0) 1074 { 1075 // nothing written, do not send this message. 1076 // which means we must support more message types. 1077 continue; 1078 } 1079 1080 event.detune = 0; 1081 event.noteOffVelocity = 0; // why it's here? 1082 event.reserved1 = 0; 1083 event.reserved2 = 0; 1084 _host.sendVstMidiEvent(cast(VstEvent*)&event); 1085 } 1086 } 1087 } 1088 1089 // This look-up table speed-up unimplemented opcodes 1090 private static immutable ubyte[64] opcodeShouldReturn0Immediately = 1091 [ 1, 0, 0, 0, 0, 0, 0, 0, // opcodes 0 to 7 1092 0, 1, 0, 0, 0, 0, 0, 0, // opcodes 8 to 15 1093 1, 1, 1, 1, 1, 1, 0, 0, // opcodes 16 to 23 1094 0, 0, 0, 0, 0, 0, 1, 1, // opcodes 24 to 31 1095 1, 0, 0, 0, 1, 1, 1, 1, // opcodes 32 to 39 1096 1, 1, 0, 1, 0, 0, 1, 0, // opcodes 40 to 47 1097 0, 1, 1, 0, 1, 1, 1, 1, // opcodes 48 to 55 1098 1, 1, 0, 1, 1, 1, 1, 1 ]; // opcodes 56 to 63 1099 1100 // 1101 // VST callbacks 1102 // 1103 extern(C) private nothrow 1104 { 1105 VstIntPtr dispatcherCallback(AEffect *effect, int opcode, int index, ptrdiff_t value, void *ptr, float opt) nothrow @nogc 1106 { 1107 VstIntPtr result = 0; 1108 1109 // Short-circuit inconsequential opcodes to gain speed 1110 if (cast(uint)opcode >= 64) 1111 return 0; 1112 if (opcodeShouldReturn0Immediately[opcode]) 1113 return 0; 1114 1115 ScopedForeignCallback!(true, true) scopedCallback; 1116 scopedCallback.enter(); 1117 1118 version(logVSTDispatcher) 1119 { 1120 char[128] buf; 1121 snprintf(buf.ptr, 128, "dispatcher effect %p opcode %d".ptr, effect, opcode); 1122 debugLog(buf.ptr); 1123 } 1124 1125 auto plugin = cast(VST2Client)(effect.object); 1126 result = plugin.dispatcher(opcode, index, value, ptr, opt); 1127 if (opcode == effClose) 1128 { 1129 destroyFree(plugin); 1130 } 1131 return result; 1132 } 1133 1134 // VST callback for DEPRECATED_process 1135 void processCallback(AEffect *effect, float **inputs, float **outputs, int sampleFrames) nothrow @nogc 1136 { 1137 FPControl fpctrl; 1138 fpctrl.initialize(); 1139 1140 auto plugin = cast(VST2Client)effect.object; 1141 plugin.process(inputs, outputs, sampleFrames); 1142 } 1143 1144 // VST callback for processReplacing 1145 void processReplacingCallback(AEffect *effect, float **inputs, float **outputs, int sampleFrames) nothrow @nogc 1146 { 1147 FPControl fpctrl; 1148 fpctrl.initialize(); 1149 1150 auto plugin = cast(VST2Client)effect.object; 1151 plugin.processReplacing(inputs, outputs, sampleFrames); 1152 } 1153 1154 // VST callback for processDoubleReplacing 1155 void processDoubleReplacingCallback(AEffect *effect, double **inputs, double **outputs, int sampleFrames) nothrow @nogc 1156 { 1157 FPControl fpctrl; 1158 fpctrl.initialize(); 1159 1160 auto plugin = cast(VST2Client)effect.object; 1161 plugin.processDoubleReplacing(inputs, outputs, sampleFrames); 1162 } 1163 1164 // VST callback for setParameter 1165 void setParameterCallback(AEffect *effect, int index, float parameter) nothrow @nogc 1166 { 1167 FPControl fpctrl; 1168 fpctrl.initialize(); 1169 1170 auto plugin = cast(VST2Client)effect.object; 1171 Client client = plugin._client; 1172 1173 if (!plugin.isValidParamIndex(index)) 1174 return; 1175 1176 client.setParameterFromHost(index, parameter); 1177 } 1178 1179 // VST callback for getParameter 1180 float getParameterCallback(AEffect *effect, int index) nothrow @nogc 1181 { 1182 FPControl fpctrl; 1183 fpctrl.initialize(); 1184 1185 auto plugin = cast(VST2Client)(effect.object); 1186 Client client = plugin._client; 1187 1188 if (!plugin.isValidParamIndex(index)) 1189 return 0.0f; 1190 1191 float value; 1192 value = client.param(index).getForHost(); 1193 return value; 1194 } 1195 } 1196 1197 /// Access to VST host from the VST client perspective. 1198 /// The IHostCommand subset is accessible from the plugin client with no knowledge of the format 1199 final class VSTHostFromClientPOV : IHostCommand 1200 { 1201 public: 1202 nothrow: 1203 @nogc: 1204 1205 this(HostCallbackFunction hostCallback, AEffect* effect) 1206 { 1207 _hostCallback = hostCallback; 1208 _effect = effect; 1209 } 1210 1211 /** 1212 * Deprecated: This call is Deprecated, but was added to support older hosts (like MaxMSP). 1213 * Plugins (VSTi2.0 thru VSTi2.3) call this to tell the host that the plugin is an instrument. 1214 */ 1215 void wantEvents() nothrow @nogc 1216 { 1217 callback(DEPRECATED_audioMasterWantMidi, 0, 1, null, 0); 1218 } 1219 1220 /// Request plugin window resize. 1221 override bool requestResize(int width, int height) nothrow @nogc 1222 { 1223 DAW daw = getDAW(); 1224 bool isAbletonLive = daw == DAW.AbletonLive; // #DAW-specific 1225 bool isOBS = daw == DAW.OBSStudio; 1226 1227 if (canDo(HostCaps.SIZE_WINDOW) || isAbletonLive || isOBS) 1228 { 1229 return (callback(audioMasterSizeWindow, width, height, null, 0.0f) != 0); 1230 } 1231 else 1232 return false; 1233 } 1234 1235 override bool notifyResized() 1236 { 1237 return false; 1238 } 1239 1240 override void beginParamEdit(int paramIndex) nothrow @nogc 1241 { 1242 callback(audioMasterBeginEdit, paramIndex, 0, null, 0.0f); 1243 } 1244 1245 override void paramAutomate(int paramIndex, float value) nothrow @nogc 1246 { 1247 callback(audioMasterAutomate, paramIndex, 0, null, value); 1248 } 1249 1250 override void endParamEdit(int paramIndex) nothrow @nogc 1251 { 1252 callback(audioMasterEndEdit, paramIndex, 0, null, 0.0f); 1253 } 1254 1255 override DAW getDAW() nothrow @nogc 1256 { 1257 DAW daw = identifyDAW(productString()); 1258 1259 // Issue #863 1260 // OBS Studio can't be arsed to identify correctly, is uses 1261 // audioMasterGetVendorString instead of audioMasterGetProductString. 1262 if (daw == DAW.Unknown) 1263 daw = identifyDAWWithVendorString(vendorString()); 1264 1265 return daw; 1266 } 1267 1268 override PluginFormat getPluginFormat() 1269 { 1270 return PluginFormat.vst2; 1271 } 1272 1273 const(char)* vendorString() nothrow @nogc 1274 { 1275 int res = cast(int)callback(audioMasterGetVendorString, 0, 0, _vendorStringBuf.ptr, 0.0f); 1276 if (res == 1) 1277 { 1278 // Force lowercase 1279 for (char* p = _vendorStringBuf.ptr; *p != '\0'; ++p) 1280 { 1281 if (*p >= 'A' && *p <= 'Z') 1282 *p += ('a' - 'A'); 1283 } 1284 return _vendorStringBuf.ptr; 1285 } 1286 else 1287 return "unknown"; 1288 } 1289 1290 const(char)* productString() nothrow @nogc 1291 { 1292 int res = cast(int)callback(audioMasterGetProductString, 0, 0, _productStringBuf.ptr, 0.0f); 1293 if (res == 1) 1294 { 1295 // Force lowercase 1296 for (char* p = _productStringBuf.ptr; *p != '\0'; ++p) 1297 { 1298 if (*p >= 'A' && *p <= 'Z') 1299 *p += ('a' - 'A'); 1300 } 1301 return _productStringBuf.ptr; 1302 } 1303 else 1304 return "unknown"; 1305 } 1306 1307 /// Gets VSTTimeInfo structure, null if not all flags are supported 1308 TimeInfo getVSTTimeInfo(long fallbackTimeInSamples) nothrow @nogc 1309 { 1310 TimeInfo info; 1311 int filters = kVstTempoValid; 1312 VstTimeInfo* ti = cast(VstTimeInfo*) callback(audioMasterGetTime, 0, filters, null, 0); 1313 if (ti && ti.sampleRate > 0) 1314 { 1315 info.timeInSamples = cast(long)(0.5f + ti.samplePos); 1316 if ((ti.flags & kVstTempoValid) && ti.tempo > 0) 1317 info.tempo = ti.tempo; 1318 info.hostIsPlaying = (ti.flags & kVstTransportPlaying) != 0; 1319 } 1320 else 1321 { 1322 // probably a very simple host, fake time 1323 info.timeInSamples = fallbackTimeInSamples; 1324 } 1325 return info; 1326 } 1327 1328 /// Capabilities 1329 1330 enum HostCaps 1331 { 1332 SEND_VST_EVENTS, // Host supports send of Vst events to plug-in. 1333 SEND_VST_MIDI_EVENTS, // Host supports send of MIDI events to plug-in. 1334 SEND_VST_TIME_INFO, // Host supports send of VstTimeInfo to plug-in. 1335 RECEIVE_VST_EVENTS, // Host can receive Vst events from plug-in. 1336 RECEIVE_VST_MIDI_EVENTS, // Host can receive MIDI events from plug-in. 1337 REPORT_CONNECTION_CHANGES, // Host will indicates the plug-in when something change in plug-in´s routing/connections with suspend()/resume()/setSpeakerArrangement(). 1338 ACCEPT_IO_CHANGES, // Host supports ioChanged(). 1339 SIZE_WINDOW, // used by VSTGUI 1340 OFFLINE, // Host supports offline feature. 1341 OPEN_FILE_SELECTOR, // Host supports function openFileSelector(). 1342 CLOSE_FILE_SELECTOR, // Host supports function closeFileSelector(). 1343 START_STOP_PROCESS, // Host supports functions startProcess() and stopProcess(). 1344 SHELL_CATEGORY, // 'shell' handling via uniqueID. If supported by the Host and the Plug-in has the category kPlugCategShell 1345 SEND_VST_MIDI_EVENT_FLAG_IS_REALTIME, // Host supports flags for VstMidiEvent. 1346 SUPPLY_IDLE // ??? 1347 } 1348 1349 bool canDo(HostCaps caps) nothrow 1350 { 1351 const(char)* capsString = hostCapsString(caps); 1352 assert(capsString !is null); 1353 1354 // note: const is casted away here 1355 return callback(audioMasterCanDo, 0, 0, cast(void*)capsString, 0.0f) == 1; 1356 } 1357 1358 bool sendVstMidiEvent(VstEvent* event) 1359 { 1360 VstEvents events; 1361 memset(&events, 0, VstEvents.sizeof); 1362 events.numEvents = 1; 1363 events.events[0] = event; // PERF: could use the VLA in VstEvents to pass more at once. 1364 return callback(audioMasterProcessEvents, 0, 0, &events, 0.0f) == 1; 1365 } 1366 1367 private: 1368 1369 AEffect* _effect; 1370 HostCallbackFunction _hostCallback; 1371 char[65] _vendorStringBuf; 1372 char[96] _productStringBuf; 1373 int _vendorVersion; 1374 1375 VstIntPtr callback(VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt) nothrow @nogc 1376 { 1377 // Saves FP state 1378 FPControl fpctrl; 1379 fpctrl.initialize(); 1380 return _hostCallback(_effect, opcode, index, value, ptr, opt); 1381 } 1382 1383 static const(char)* hostCapsString(HostCaps caps) pure nothrow 1384 { 1385 switch (caps) 1386 { 1387 case HostCaps.SEND_VST_EVENTS: return "sendVstEvents"; 1388 case HostCaps.SEND_VST_MIDI_EVENTS: return "sendVstMidiEvent"; 1389 case HostCaps.SEND_VST_TIME_INFO: return "sendVstTimeInfo"; 1390 case HostCaps.RECEIVE_VST_EVENTS: return "receiveVstEvents"; 1391 case HostCaps.RECEIVE_VST_MIDI_EVENTS: return "receiveVstMidiEvent"; 1392 case HostCaps.REPORT_CONNECTION_CHANGES: return "reportConnectionChanges"; 1393 case HostCaps.ACCEPT_IO_CHANGES: return "acceptIOChanges"; 1394 case HostCaps.SIZE_WINDOW: return "sizeWindow"; 1395 case HostCaps.OFFLINE: return "offline"; 1396 case HostCaps.OPEN_FILE_SELECTOR: return "openFileSelector"; 1397 case HostCaps.CLOSE_FILE_SELECTOR: return "closeFileSelector"; 1398 case HostCaps.START_STOP_PROCESS: return "startStopProcess"; 1399 case HostCaps.SHELL_CATEGORY: return "shellCategory"; 1400 case HostCaps.SEND_VST_MIDI_EVENT_FLAG_IS_REALTIME: return "sendVstMidiEventFlagIsRealtime"; 1401 case HostCaps.SUPPLY_IDLE: return "supplyIdle"; 1402 default: 1403 assert(false); 1404 } 1405 } 1406 } 1407 1408 1409 /** Four Character Constant (for AEffect->uniqueID) */ 1410 private int CCONST(int a, int b, int c, int d) pure nothrow @nogc 1411 { 1412 return (a << 24) | (b << 16) | (c << 8) | (d << 0); 1413 } 1414 1415 struct IO 1416 { 1417 int inputs; /// number of input channels 1418 int outputs; /// number of output channels 1419 } 1420 1421 // 1422 // Message queue 1423 // 1424 1425 private: 1426 1427 /// A message for the audio thread. 1428 /// Intended to be passed from a non critical thread to the audio thread. 1429 struct AudioThreadMessage 1430 { 1431 enum Type 1432 { 1433 resetState, // reset plugin state, set samplerate and buffer size (samplerate = fParam, buffersize in frames = iParam) 1434 midi 1435 } 1436 1437 this(Type type_, int maxFrames_, float samplerate_, IO hostIO_, IO processingIO_) pure const nothrow @nogc 1438 { 1439 type = type_; 1440 maxFrames = maxFrames_; 1441 samplerate = samplerate_; 1442 hostIO = hostIO_; 1443 processingIO = processingIO_; 1444 } 1445 1446 Type type; 1447 int maxFrames; 1448 float samplerate; 1449 IO hostIO; 1450 IO processingIO; 1451 MidiMessage midiMessage; 1452 } 1453 1454 AudioThreadMessage makeMIDIMessage(MidiMessage midiMessage) pure nothrow @nogc 1455 { 1456 AudioThreadMessage msg; 1457 msg.type = AudioThreadMessage.Type.midi; 1458 msg.midiMessage = midiMessage; 1459 return msg; 1460 }