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 }