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