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 }