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 import core.stdc.stdlib: free;
25 
26 import dplug.core.nogc;
27 import dplug.core.math;
28 import dplug.core.vec;
29 import dplug.core.sync;
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 enum PluginFormat
39 {
40     vst2, // Virtual Studio Technology v2
41     vst3, // Virtual Studio Technology v3
42     aax,  // Avid Audio eXtension
43     auv2, // Audio Unit v2
44     lv2,  // LADSPA Version 2 
45     clap, // CLever Audio Plug-in
46     flp,  // Fruity Loops Plug-in, aka FP, aka FL
47 }
48 
49 
50 /// A plugin client can send commands to the host.
51 /// This interface is injected after the client creation though.
52 interface IHostCommand
53 {
54 nothrow @nogc:
55 
56     /// Notifies the host that editing of a parameter has begun from UI side.
57     void beginParamEdit(int paramIndex);
58 
59     /// Notifies the host that a parameter was edited from the UI side.
60     /// This enables the host to record automation.
61     /// It is illegal to call `paramAutomate` outside of a `beginParamEdit`/`endParamEdit` pair.
62     ///
63     /// Params:
64     ///    value Normalized parameter value.
65     void paramAutomate(int paramIndex, float value);
66 
67     /// Notifies the host that editing of a parameter has finished from UI side.
68     void endParamEdit(int paramIndex);
69 
70     /// Requests to the host a resize of the plugin window's PARENT window, given logical pixels of plugin window.
71     ///
72     /// Note: UI widgets and plugin format clients have different coordinate systems.
73     ///
74     /// Params:
75     ///     width New width of the plugin, in logical pixels.
76     ///     height New height of the plugin, in logical pixels.
77     /// Returns: `true` if the host parent window has been resized.
78     bool requestResize(int widthLogicalPixels, int heightLogicalPixels);
79 
80     /// Tells the host that the plugin window HAS resized already, and the parent need to update. 
81     /// Only useful for FL Studio own plugin format right now.
82     /// To be useful, the `requestResize` must return false for this format, so that manual resize is performed.
83     /// Returns: `true` is the host will act on it, `false` if not supported in this format.
84     bool notifyResized();
85 
86     /// Report the identied host name (DAW).
87     /// MAYDO: not available for LV2.
88     DAW getDAW();
89 
90     /// Gets the plugin format used at runtime. Version identifier may not be enough in the future, in case of 
91     /// unity builds.
92     PluginFormat getPluginFormat();
93 }
94 
95 // Plugin version in major.minor.patch form.
96 struct PluginVersion
97 {
98 nothrow:
99 @nogc:
100     int major;
101     int minor;
102     int patch;
103 
104     int toVSTVersion() pure const
105     {
106         assert(major < 10 && minor < 10 && patch < 10);
107         return major * 1000 + minor * 100 + patch*10;
108     }
109 
110     int toAUVersion() pure const
111     {
112         assert(major < 256 && minor < 256 && patch < 256);
113         return (major << 16) | (minor << 8) | patch;
114     }
115 
116     int toAAXPackageVersion() pure const
117     {
118         // For AAX, considered binary-compatible unless major version change
119         return major;
120     }
121 
122     void toVST3VersionString(char* outBuffer, int bufLength) const
123     {
124         snprintf(outBuffer, bufLength, "%d.%d.%d", major, minor, patch);
125 
126         // DigitalMars's snprintf doesn't always add a terminal zero
127         if (bufLength > 0)
128             outBuffer[bufLength-1] = '\0';
129     }
130 
131     alias toCLAPVersionString = toVST3VersionString;
132 }
133 
134 
135 // Statically known features of the plugin.
136 // There is some default for explanation purpose, but you really ought to override them all.
137 // Most of it is redundant with plugin.json, in the future the JSON will be parsed instead.
138 struct PluginInfo
139 {
140     string vendorName = "Witty Audio";
141 
142     /// A four char vendor "unique" ID
143     char[4] vendorUniqueID = "Wity";
144 
145     /// The vendor email adress for support. Can be null.
146     string vendorSupportEmail = null;
147 
148     /// Plugin name.
149     string pluginName = "Destructatorizer";
150 
151     /// Plugin web page. Can be null.
152     string pluginHomepage = null;
153 
154     /// Used for both VST and AU.
155     /// In AU it is namespaced by the manufacturer. In VST it
156     /// should be unique. While it seems no VST host use this
157     /// ID as a unique way to identify a plugin, common wisdom
158     /// is to try to get a sufficiently random one.
159     char[4] pluginUniqueID = "WiDi";
160 
161     // Plugin version information.
162     // It's important that the version you fill at runtime is identical to the
163     // one in `plugin.json` else you won't pass AU validation.
164     //
165     // Note: For AU, 0.x.y is supposed to mean "do not cache", however it is
166     //       unknown what it actually changes. AU caching hasn't caused any problem
167     //       and can probably be ignored.
168     PluginVersion publicVersion = PluginVersion(0, 0, 0);
169 
170     /// True if the plugin has a graphical UI. Easy way to disable it.
171     bool hasGUI = false;
172 
173     /// True if the plugin "is a synth". This has only a semantic effect.
174     bool isSynth = false;
175 
176     /// True if the plugin should receive MIDI events.
177     /// Warning: receiving MIDI forces you to call `getNextMidiMessages`
178     /// with the right number of `frames`, every buffer.
179     bool receivesMIDI = false;
180 
181     /// True if the plugin sends MIDI events.
182     bool sendsMIDI = false;
183 
184     /// Used for being at the right place in list of plug-ins.
185     PluginCategory category;
186 
187     /// Used as name of the bundle in VST.
188     string VSTBundleIdentifier;
189 
190     /// Used as name of the bundle in AU.
191     string AUBundleIdentifier;
192 
193     /// Used as name of the bundle in AAX.
194     string AAXBundleIdentifier;
195 
196     /// Used as CLAP identifier ("com.wittyaudio.Destructatorizer")
197     string CLAPIdentifier;
198 
199     /// Used as CLAP identifier for factory preset provider ("com.wittyaudio.Destructatorizer.factory")
200     string CLAPIdentifierFactory;
201 }
202 
203 /// This allows to write things life tempo-synced LFO.
204 struct TimeInfo
205 {
206     /// BPM
207     double tempo = 120;
208 
209     /// Current time from the beginning of the song in samples.
210     /// This time can be negative, which is normal at beginning of a song.
211     long timeInSamples = 0;
212 
213     /// Whether the host sequencer is currently playing
214     bool hostIsPlaying;
215 }
216 
217 /// Describe a combination of input channels count and output channels count
218 /// FUTURE: this should look more like clap_audio_ports_config_t
219 struct LegalIO
220 {
221     int numInputChannels;
222     int numOutputChannels;
223 }
224 
225 /// This is the interface used by the GUI, to reduce coupling and avoid exposing the whole of `Client` to it.
226 /// It should eventually allows to supersede/hide IHostCommand.
227 interface IClient
228 {
229 nothrow:
230 @nogc:
231     /// Requests a resize of the plugin window, notifying the host.
232     /// Returns: `true` if succeeded.
233     ///
234     /// Params:
235     ///     width New width of the plugin, in logical pixels.
236     ///     height New height of the plugin, in logical pixels.
237     bool requestResize(int widthLogicalPixels, int heightLogicalPixels);
238 
239     /// Notify AFTER a manual resize of the plugin, so that the host updates its window.
240     /// Returns `true` if succeeded. Not needed if `requestResize` returned true.
241     bool notifyResized();
242 
243     /// Report the identied host name (DAW).
244     DAW getDAW();
245 
246     /// Gets the plugin format used at runtime. Version identifier may not be enough in the future, in case of 
247     /// unity builds.
248     PluginFormat getPluginFormat();
249 }
250 
251 /// Plugin interface, from the client point of view.
252 /// This client has no knowledge of thread-safety, it must be handled externally.
253 /// User plugins derivate from this class.
254 /// Plugin formats wrappers owns one dplug.plugin.Client as a member.
255 ///
256 /// Note: this is an architecture failure since there are 3 users of that interface:
257 ///   1. the plugin "client" implementation (= product), 
258 ///   2. the format client
259 ///   3. the UI, directly
260 ///  Those should be splitted cleanly.
261 class Client : IClient
262 {
263 public:
264 nothrow:
265 @nogc:
266 
267     this()
268     {
269         _info = buildPluginInfo();
270 
271         // Create legal I/O combinations
272         _legalIOs = buildLegalIO();
273 
274         // Create parameters.
275         _params = buildParameters();
276 
277         // Check parameter consistency
278         // This avoid mistake when adding/reordering parameters in a plugin.
279         foreach(size_t i, Parameter param; _params)
280         {
281             // If you fail here, this means your buildParameter() override is incorrect.
282             // Check the values of the index you're giving.
283             // They should be 0, 1, 2, ..., N-1
284             // Maybe you have duplicated a line or misordered them.
285             assert(param.index() == i);
286 
287             // Sets owner reference.
288             param.setClientReference(this);
289         }
290 
291         _maxFramesInProcess = maxFramesInProcess();
292 
293         _maxInputs = 0;
294         _maxOutputs = 0;
295         foreach(legalIO; _legalIOs)
296         {
297             if (_maxInputs < legalIO.numInputChannels)
298                 _maxInputs = legalIO.numInputChannels;
299             if (_maxOutputs < legalIO.numOutputChannels)
300                 _maxOutputs = legalIO.numOutputChannels;
301         }
302 
303         _inputMidiQueue = makeMidiQueue(); // PERF: only init those for plugins that need it?
304         _outputMidiQueue = makeMidiQueue();
305 
306         if (sendsMIDI)
307         {
308             _midiOutFromUIMutex = makeMutex();
309         }
310 
311         version(legacyBinState)
312         {}
313         else
314         {
315             // Snapshot default extra state here, before any preset is created, so that `makeDefaultPreset`
316             // can work even if called multiple times.
317             saveState(_defaultStateData);
318         }
319 
320         // Create presets last, so that we enjoy the presence of built Parameters,
321         // and default I/O configuration.
322         _presetBank = mallocNew!PresetBank(this, buildPresets());
323     }
324 
325     ~this()
326     {
327         // Destroy graphics
328         if (_graphics !is null)
329         {
330             // Acquire _graphicsIsAvailable forever
331             // so that it's the last time the audio uses it,
332             // and we can wait for its exit in _graphics destructor
333             while(!cas(&_graphicsIsAvailable, true, false))
334             {
335                 // MAYDO: relax CPU
336             }
337             _graphics.destroyFree();
338         }
339 
340         // Destroy presets
341         _presetBank.destroyFree();
342 
343         // Destroy parameters
344         foreach(p; _params)
345             p.destroyFree();
346         _params.freeSlice();
347         _legalIOs.freeSlice();
348     }
349 
350     final int maxInputs() pure const nothrow @nogc
351     {
352         return _maxInputs;
353     }
354 
355     final int maxOutputs() pure const nothrow @nogc
356     {
357         return _maxOutputs;
358     }
359 
360     /// Returns: Array of parameters.
361     final inout(Parameter[]) params() inout nothrow @nogc
362     {
363         return _params;
364     }
365 
366     /// Returns: Array of legal I/O combinations.
367     final LegalIO[] legalIOs() nothrow @nogc
368     {
369         return _legalIOs;
370     }
371 
372     /// Returns: true if the following I/O combination is a legal one.
373     ///          < 0 means "do not check"
374     final bool isLegalIO(int numInputChannels, int numOutputChannels) pure const nothrow @nogc
375     {
376         foreach(io; _legalIOs)
377             if  ( ( (numInputChannels < 0)
378                     ||
379                     (io.numInputChannels == numInputChannels) )
380                   &&
381                   ( (numOutputChannels < 0)
382                     ||
383                     (io.numOutputChannels == numOutputChannels) )
384                 )
385                 return true;
386 
387         return false;
388     }
389 
390     /// Returns: Array of presets.
391     final PresetBank presetBank() nothrow @nogc
392     {
393         return _presetBank;
394     }
395 
396     /// Returns: The parameter indexed by index.
397     final inout(Parameter) param(int index) inout nothrow @nogc
398     {
399         return _params.ptr[index];
400     }
401 
402     /// Returns: true if index is a valid parameter index.
403     final bool isValidParamIndex(int index) const nothrow @nogc
404     {
405         return index >= 0 && index < _params.length;
406     }
407 
408     /// Returns: true if index is a valid input index.
409     final bool isValidInputIndex(int index) nothrow @nogc
410     {
411         return index >= 0 && index < maxInputs();
412     }
413 
414     /// Returns: true if index is a valid output index.
415     final bool isValidOutputIndex(int index) nothrow @nogc
416     {
417         return index >= 0 && index < maxOutputs();
418     }
419 
420     /// Note: openGUI, getGUISize, getGraphics and closeGUI are guaranteed
421     /// synchronized by the client implementation
422     /// Only allowed for client implementation.
423     final void* openGUI(void* parentInfo, void* controlInfo, GraphicsBackend backend) nothrow @nogc
424     {
425         createGraphicsLazily();
426         assert(_hostCommand !is null);
427         return (cast(IGraphics)_graphics).openUI(parentInfo, controlInfo, this, backend);
428     }
429 
430     /// Only allowed for client implementation.
431     final bool getGUISize(int* widthLogicalPixels, int* heightLogicalPixels) nothrow @nogc
432     {
433         createGraphicsLazily();
434         auto graphics = (cast(IGraphics)_graphics);
435         if (graphics)
436         {
437             graphics.getGUISize(widthLogicalPixels, heightLogicalPixels);
438             return true;
439         }
440         else
441             return false;
442     }
443 
444     /// Only allowed for client implementation.
445     /// This is helpful for some hosts, like OBS and Cubase, but not others like FL Studio.
446     final bool getDesiredGUISize(int* widthLogicalPixels, int* heightLogicalPixels) nothrow @nogc
447     {
448         createGraphicsLazily();
449         auto graphics = (cast(IGraphics)_graphics);
450         if (graphics)
451         {
452             graphics.getDesiredGUISize(widthLogicalPixels, heightLogicalPixels);
453             return true;
454         }
455         else
456             return false;
457     }
458 
459     /// Close the plugin UI if one was opened.
460     /// Note: OBS Studio will happily call effEditClose without having called effEditOpen.
461     /// Only allowed for client implementation.
462     final void closeGUI() nothrow @nogc
463     {
464         auto graphics = (cast(IGraphics)_graphics);
465         if (graphics)
466         {
467             graphics.closeUI();
468         }
469     }
470 
471     /// This creates the GUIGraphics object lazily, and return it without synchronization.
472     /// Only allowed for client implementation.
473     final IGraphics getGraphics()
474     {
475         createGraphicsLazily();
476         return (cast(IGraphics)_graphics);
477     }
478 
479     // This should be called only by a client implementation.
480     void setParameterFromHost(int index, float value) nothrow @nogc
481     {
482         param(index).setFromHost(value);
483     }
484 
485     /// Override if you create a plugin with UI.
486     /// The returned IGraphics must be allocated with `mallocNew`.
487     /// `plugin.json` needs to have a "hasGUI" key equal to true, else this callback is never called.
488     IGraphics createGraphics() nothrow @nogc
489     {
490         return null;
491     }
492 
493     /// Intended from inside the audio thread, in `process`.
494     /// Enqueue one MIDI message on the output MIDI priority queue, so that it is
495     /// eventually sent.
496     /// Its offset is relative to the current buffer, and you can send messages arbitrarily 
497     /// in the future too.
498     void sendMIDIMessage(MidiMessage message) nothrow @nogc
499     {
500         _outputMidiQueue.enqueue(message);
501     }
502 
503     /// Send MIDI from inside the UI.
504     /// Intended to be called from inside an UI event callback.
505     ///
506     /// Enqueue several MIDI messages in a synchronized manner, so that they are sent all at once,
507     /// as early as possible as "live" MIDI messages.
508     /// No guarantee of any timing for these messages, for example this can be in response to 
509     /// a key press on a virtual keyboard.
510     /// The messages don't have to be ordered if they are spaced, but have to be if they 
511     /// have the same `offset`. 
512     ///
513     /// Note: It is guaranteed that all messages passed this way will keep their offset 
514     ///       relationship in MIDI output. (Typically such a messages would all have a zero
515     ///       timestamp).
516     ///       Though they are sent as soon as possible in a best effort manner, their relative 
517     ///       offset is preserved.
518     ///       Its offset is relative to the current buffer, and you can send messages arbitrarily 
519     ///       in the future too.
520     void sendMIDIMessagesFromUI(const(MidiMessage)[] messages) nothrow @nogc
521     {
522         _midiOutFromUIMutex.lock();
523         
524         foreach(msg; messages)
525             _outputMidiFromUI.pushBack(msg);
526         
527         _midiOutFromUIMutex.unlock();
528     }
529 
530     /// Getter for the IGraphics interface
531     /// This is intended ONLY for the audio thread inside processing and has acquire semantics.
532     /// Not reentrant! You can't call this twice without a graphicsRelease first.
533     /// THIS CAN RETURN NULL EVEN AFTER HAVING RETURNED NON-NULL AT ONE POINT.
534     /// Returns: null if feedback from audio thread is not welcome.
535     final IGraphics graphicsAcquire() nothrow @nogc
536     {
537         if (cas(&_graphicsIsAvailable, true, false)) // exclusive, since there is only one audio thread normally
538             return _graphics;
539         else
540             return null;
541     }
542 
543     /// Mirror function to release the IGraphics from the audio-thread.
544     /// Do not call if graphicsAcquire() returned `null`.
545     final void graphicsRelease() nothrow @nogc
546     {
547         // graphicsAcquire should have been called before
548         // MAYDO: which memory order here? Don't looks like we need a barrier.
549         atomicStore(_graphicsIsAvailable, true);
550     }
551 
552     // Getter for the IHostCommand interface
553     final IHostCommand hostCommand() nothrow @nogc
554     {
555         return _hostCommand;
556     }
557 
558     /// Override to clear state (eg: resize and clear delay lines) and allocate buffers.
559     /// Note: `reset` should not be called directly by plug-in format implementations. Use 
560     /// `resetFromHost` if you write a new plug-in format client.
561     abstract void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc;
562 
563     /// Override to set the plugin latency in samples.
564     /// Plugin latency can depend on `sampleRate` but no other value.
565     /// If you want your latency to depend on a `Parameter` your only choice is to
566     /// pessimize the needed latency and compensate in the process callback.
567     ///
568     /// Dynamic latency changes are not possible in Dplug yet.
569     /// See Issue #442 => https://github.com/AuburnSounds/Dplug/issues/442
570     ///
571     /// Returns: Plugin latency in samples.
572     /// Note: this will absolutely be called before `reset` is called, so be prepared.
573     int latencySamples(double sampleRate) nothrow @nogc
574     {
575         return 0; // By default, no latency introduced by plugin
576     }
577 
578     /// Override to set the plugin tail length in seconds.
579     ///
580     /// This is the amount of time before silence is reached with a silent input, on the worst
581     /// possible settings.
582     ///
583     /// Returns: Plugin tail size in seconds.
584     ///     - Returning 0.0f means that as soon as your input is silent, the output will be silent. 
585     ///       It isn't a special value.
586     ///     - Returning `float.infinity` means that the host should not optimize calls to `processAudio`.
587     ///       If your plugin is a synth, or an effect generating sound, you MUST return `float.infinity`.
588     ///     - Otherwise, returning a particular tail size is the regular meaning.
589     ///
590     float tailSizeInSeconds() nothrow @nogc
591     {
592         // Default: always call `processAudio`. This is safest.
593         //
594         // It is recommended to setup this override at one point in developemnt, especially for an effect plugin.
595         // This allows VST3 and AU hosts to optimize things.
596         //
597         // For an effect 2 secs is a good starting point. Which should be safe for most effects plugins except delay or reverb
598         // Warning: plugins have often MUCH more tail size than expected!
599         // Don't reduce to a shorter time unless you know for sure what you are doing.
600         // Synths, and effects generating audio from MIDI should return `float.infinity`.
601 
602         return float.infinity;
603     }
604 
605     /// Override to declare the maximum number of samples to accept
606     /// If greater, the audio buffers will be splitted up.
607     /// This splitting have several benefits:
608     /// - help allocating temporary audio buffers on the stack
609     /// - keeps memory usage low and reuse it
610     /// - allow faster-than-buffer-size parameter changes (VST3)
611     /// Returns: Maximum number of samples
612     /// Warning: Some buffersize-related bugs might be hidden by having sub-buffers.
613     ///          If you are looking for a buffersize bug, maybe try to disable sub-buffers
614     ///          by returning the default 0.
615     int maxFramesInProcess() nothrow @nogc
616     {
617         return 0; // default returns 0 which means "do not split buffers"
618     }
619 
620     /// Process `frames` audio frames.
621     ///
622     /// Read audio input from `inputs` channels, and write it to `outputs` channels.
623     ///
624     /// Params:
625     ///
626     ///     inputs   Input channels pointers, each pointing to `frames` audio samples. 
627     ///              Number of pointer is equal to `numInputs` given in last `reset()` callback.
628     ///              Unconnected input pins (if any) point to zeroes.
629     ///
630     ///     outputs  Output channels pointers, each pointing to `frames` audio samples. 
631     ///              Number of pointer is equal to `numOutputs` given in last `reset()` callback.
632     ///              The output space can be used as temporary storage.
633     ///              (do not modify these non-const pointers).
634     ///
635     ///     frames   Number of audio samples for this callback. 
636     ///              This number is always <= `maxFrames`, given in last `reset()` callback.
637     ///              To force this number to arbitrarily, you may override `maxFramesInProcess`, 
638     ///              which will split host buffers in smaller chunks.
639     ///
640     ///     timeInfo Timing information for the first sample (0th) of this buffer.
641     ///
642     /// Warning: Using MIDI input? 
643     ///          If your plug-in has "receivesMidi" set to `true` in its `plugin.json`, this 
644     ///          callback is the one place where you MUST call `getNextMidiMessages()`.
645     ///          See poly-alias or simple-mono-synth examples for how.
646     ///
647     abstract void processAudio(const(float*)[] inputs,    // array of input channels
648                                float*[] outputs,           // array of output channels
649                                int frames,                // number of sample in each input & output channel
650                                TimeInfo timeInfo          // time information associated with this signal frame
651                                ) nothrow @nogc;
652 
653     /// Should only be called in `processAudio`.
654     /// This return a slice of MIDI messages corresponding to the next `frames` samples.
655     /// Useful if you don't want to process messages every samples, or every split buffer.
656     final const(MidiMessage)[] getNextMidiMessages(int frames) nothrow @nogc
657     {
658         return _inputMidiQueue.getNextMidiMessages(frames);
659     }
660 
661     /// Return default state data, to be used in constructing a programmatic preset.
662     /// Note: It is recommended to use .fbx instead of constructing presets with code.
663     /// This is intended to be used in `buildPresets` callback.
664     final const(ubyte)[] defaultStateData() nothrow @nogc
665     {
666         // Should this preset have extra state data?
667         const(ubyte)[] stateData = null;
668         version(legacyBinState)
669         {}
670         else
671         {
672             // Note: the default state data is called early, so if you plan to call 
673             // `defaultStateData` outside of buildPresets, it will have odd restrictions.
674             stateData = _defaultStateData[];
675         }
676         return stateData;
677     }
678 
679     /// Returns a new default preset.
680     /// This is intended to be used in `buildPresets` callback.
681     final Preset makeDefaultPreset() nothrow @nogc
682     {
683         // MAYDO: use mallocSlice for perf
684         auto values = makeVec!float();
685         foreach(param; _params)
686             values.pushBack(param.getNormalizedDefault());
687 
688         // Perf: one could avoid malloc to copy those arrays again there
689         float[] valuesSlice = values.releaseData;
690         Preset result = mallocNew!Preset("Default", valuesSlice, defaultStateData());
691         free(valuesSlice.ptr); // PERF: could disown this instead of copy, with another Preset constructor
692         return result;
693     }
694 
695     // Getters for fields in _info
696 
697     final bool hasGUI() pure const nothrow @nogc
698     {
699         return _info.hasGUI;
700     }
701 
702     final bool isSynth() pure const nothrow @nogc
703     {
704         return _info.isSynth;
705     }
706 
707     final bool receivesMIDI() pure const nothrow @nogc
708     {
709         return _info.receivesMIDI;
710     }
711 
712     final bool sendsMIDI() pure const nothrow @nogc
713     {
714         return _info.sendsMIDI;
715     }
716 
717     final string vendorName() pure const nothrow @nogc
718     {
719         return _info.vendorName;
720     }
721 
722     final char[4] getVendorUniqueID() pure const nothrow @nogc
723     {
724         return _info.vendorUniqueID;
725     }
726 
727     final string getVendorSupportEmail() pure const nothrow @nogc
728     {
729         return _info.vendorSupportEmail;
730     }
731 
732     final string pluginName() pure const nothrow @nogc
733     {
734         return _info.pluginName;
735     }
736 
737     final string pluginHomepage() pure const nothrow @nogc
738     {
739         return _info.pluginHomepage;
740     }
741 
742     final PluginCategory pluginCategory() pure const nothrow @nogc
743     {
744         return _info.category;
745     }
746 
747     final string VSTBundleIdentifier() pure const nothrow @nogc
748     {
749         return _info.VSTBundleIdentifier;
750     }
751 
752     final string AUBundleIdentifier() pure const nothrow @nogc
753     {
754         return _info.AUBundleIdentifier;
755     }
756 
757     final string AAXBundleIdentifier() pure const nothrow @nogc
758     {
759         return _info.AAXBundleIdentifier;
760     }
761 
762     final string CLAPIdentifier() pure const nothrow @nogc
763     {
764         return _info.CLAPIdentifier;
765     }
766 
767     final string CLAPIdentifierFactory() pure const nothrow @nogc
768     {
769         return _info.CLAPIdentifierFactory;
770     }
771 
772     /// Returns: Plugin "unique" ID.
773     final char[4] getPluginUniqueID() pure const nothrow @nogc
774     {
775         return _info.pluginUniqueID;
776     }
777 
778     /// Returns: Plugin full name "$VENDOR $PRODUCT"
779     final void getPluginFullName(char* p, int bufLength) const nothrow @nogc
780     {
781         snprintf(p, bufLength, "%.*s %.*s",
782                  cast(int)(_info.vendorName.length), _info.vendorName.ptr,
783                  cast(int)(_info.pluginName.length), _info.pluginName.ptr);
784 
785         // DigitalMars's snprintf doesn't always add a terminal zero
786         if (bufLength > 0)
787         {
788             p[bufLength-1] = '\0';
789         }
790     }
791 
792     /// Returns: Plugin name "$PRODUCT"
793     final void getPluginName(char* p, int bufLength) const nothrow @nogc
794     {
795         snprintf(p, bufLength, "%.*s",
796                  cast(int)(_info.pluginName.length), _info.pluginName.ptr);
797 
798         // DigitalMars's snprintf doesn't always add a terminal zero
799         if (bufLength > 0)
800         {
801             p[bufLength-1] = '\0';
802         }
803     }
804 
805     /// Returns: Plugin version in x.x.x.x decimal form.
806     final PluginVersion getPublicVersion() pure const nothrow @nogc
807     {
808         return _info.publicVersion;
809     }
810 
811     /// Boilerplate function to get the value of a `FloatParameter`, for use in `processAudio`.
812     final T readParam(T)(int paramIndex) nothrow @nogc
813         if (is(T == float) || is(T == double))
814     {
815         auto p = param(paramIndex);
816         assert(cast(FloatParameter)p !is null); // check it's a FloatParameter
817         return unsafeObjectCast!FloatParameter(p).valueAtomic();
818     }
819 
820     /// Boilerplate function to get the value of an `IntParameter`, for use in `processAudio`.
821     final T readParam(T)(int paramIndex) nothrow @nogc
822         if (is(T == int) && !is(T == enum))
823     {
824         auto p = param(paramIndex);
825         assert(cast(IntegerParameter)p !is null); // check it's an IntParameter
826         return unsafeObjectCast!IntegerParameter(p).valueAtomic();
827     }
828 
829     /// Boilerplate function to get the value of an `EnumParameter`, for use in `processAudio`.
830     final T readParam(T)(int paramIndex) nothrow @nogc
831         if (is(T == enum))
832     {
833         auto p = param(paramIndex);
834         assert(cast(EnumParameter)p !is null); // check it's an EnumParameter
835         return cast(T)(unsafeObjectCast!EnumParameter(p).valueAtomic());
836     }
837 
838     /// Boilerplate function to get the value of a `BoolParameter`,for use in `processAudio`.
839     final T readParam(T)(int paramIndex) nothrow @nogc
840         if (is(T == bool))
841     {
842         auto p = param(paramIndex);
843         assert(cast(BoolParameter)p !is null); // check it's a BoolParameter
844         return unsafeObjectCast!BoolParameter(p).valueAtomic();
845     }
846 
847     /// For plugin format clients only.
848     final void setHostCommand(IHostCommand hostCommand) nothrow @nogc
849     {
850         _hostCommand = hostCommand;
851 
852         // In VST3, for accuracy of parameter automation we choose to split buffers in chunks of maximum 512.
853         // This avoids painful situations where parameters could higher precision.
854         // See: https://github.com/AuburnSounds/Dplug/issues/368
855         if (hostCommand.getPluginFormat() == PluginFormat.vst3)
856         {
857             if (_maxFramesInProcess == 0 || _maxFramesInProcess > 512)
858                 _maxFramesInProcess = 512;
859         }
860     }
861 
862     /// For plugin format clients only.
863     /// Enqueues an incoming MIDI message.
864     void enqueueMIDIFromHost(MidiMessage message)
865     {
866         _inputMidiQueue.enqueue(message);
867     }
868 
869     /// For plugin format clients only.
870     /// This return a slice of MIDI messages to be sent for this (whole unsplit) buffer.
871     /// Internally, you need to either use split-buffering from this file, or if the format does
872     /// its own buffer split it needs to call `accumulateOutputMIDI` itself.
873     final const(MidiMessage)[] getAccumulatedOutputMidiMessages() nothrow @nogc
874     {
875         return _outputMidiMessages[];
876     }
877     /// For plugin format clients only.
878     /// Clear MIDI output buffer. Call it before `processAudioFromHost` or `accumulateOutputMIDI`.
879     /// What it also does it get all MIDI message from the UI, and add them to the priority queue, 
880     /// so that they may be accumulated like normal MIDI sent from the process callback.
881     final void clearAccumulatedOutputMidiMessages() nothrow @nogc
882     {
883         assert(sendsMIDI());
884 
885         _outputMidiMessages.clearContents();
886 
887         // Enqueue all messages from UI in the priority queue.
888         _midiOutFromUIMutex.lock();
889         foreach(msg; _outputMidiFromUI[])
890             _outputMidiQueue.enqueue(msg);
891         _outputMidiFromUI.clearContents();
892         _midiOutFromUIMutex.unlock();
893     }
894 
895     /// For plugin format clients only.
896     /// Calls processAudio repeatedly, splitting the buffers.
897     /// Splitting allow to decouple memory requirements from the actual host buffer size.
898     /// There is few performance penalty above 512 samples.
899     /// TODO: unclear when using this if inputs.ptr can/should be null in case of zero channels...
900     ///
901     /// CAUTION: channel such as inputs[0]/outputs[0] are MODIFIED by this function.
902     void processAudioFromHost(float*[] inputs,
903                               float*[] outputs,
904                               int frames,
905                               TimeInfo timeInfo,
906                               bool doNotSplit = false, // flag that exist in case the plugin client want to split itself
907                               ) nothrow @nogc
908     {
909         // In debug mode, fill all output audio buffers with `float.nan`.
910         // This avoids a plug-in forgetting to fill output buffers, which can happen if you
911         // implement silence detection badly.
912         // CAUTION: this assumes inputs and outputs buffers do not point into the same memory areas
913         debug
914         {
915             for (int k = 0; k < outputs.length; ++k)
916             {
917                 float* pOut = outputs[k];
918                 pOut[0..frames] = float.nan;
919             }
920         }
921 
922         if (_maxFramesInProcess == 0 || doNotSplit)
923         {
924             processAudio(inputs, outputs, frames, timeInfo);
925             if (sendsMIDI) accumulateOutputMIDI(frames);
926         }
927         else
928         {
929             // Slice audio in smaller parts
930             while (frames > 0)
931             {
932                 // Note: the last slice will be smaller than the others
933                 int sliceLength = frames;
934                 if (sliceLength > _maxFramesInProcess)
935                     sliceLength = _maxFramesInProcess;
936 
937                 processAudio(inputs, outputs, sliceLength, timeInfo);
938                 if (sendsMIDI) accumulateOutputMIDI(sliceLength);
939 
940                 // offset all input buffer pointers
941                 for (int i = 0; i < cast(int)inputs.length; ++i)
942                     inputs[i] = inputs[i] + sliceLength;
943 
944                 // offset all output buffer pointers
945                 for (int i = 0; i < cast(int)outputs.length; ++i)
946                     outputs[i] = outputs[i] + sliceLength;
947 
948                 frames -= sliceLength;
949 
950                 // timeInfo must be updated
951                 timeInfo.timeInSamples += sliceLength;
952             }
953             assert(frames == 0);
954         }
955     }
956 
957     /// For VST3 client only. Format clients that split the buffers themselves (for automation precision)
958     /// Need as well to accumulate MIDI output themselves.
959     /// See_also: `getAccumulatedOutputMidiMessages` for how to get those accumulated messages for the whole buffer.
960     final void accumulateOutputMIDI(int frames)
961     {
962         _outputMidiQueue.accumNextMidiMessages(_outputMidiMessages, frames);
963     }
964 
965     /// For plugin format clients only.
966     /// Calls `reset()`.
967     /// Must be called by the audio thread.
968     void resetFromHost(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc
969     {
970         // Clear outstanding MIDI messages (now invalid)
971         _inputMidiQueue.initialize(); // MAYDO: should it push a MIDI message to mute all voices?
972 
973         _outputMidiQueue.initialize(); // TODO: that sounds fishy, what if we have send a note on but not the note off?
974 
975         // We potentially give to the client implementation a lower value
976         // for the maximum number of frames
977         if (_maxFramesInProcess != 0 && _maxFramesInProcess < maxFrames)
978             maxFrames = _maxFramesInProcess;
979 
980         // Calls the reset virtual call
981         reset(sampleRate, maxFrames, numInputs, numOutputs);
982     }
983 
984     /// For use by plugin format clients. This gives the buffer split size to use.
985     /// (0 == no split).
986     /// This is useful in the cast the format client wants to split buffers by itself.
987     ///
988     /// Note: in VST3, no split or a split larger than 512 samples, is replaced by a split by 512.
989     final int getBufferSplitMaxFrames()
990     {
991         return _maxFramesInProcess;
992     }
993 
994     // <IClient>
995     override bool requestResize(int widthLogicalPixels, int heightLogicalPixels)
996     {
997         if (_hostCommand is null) 
998             return false;
999 
1000         return _hostCommand.requestResize(widthLogicalPixels, heightLogicalPixels);
1001     }
1002 
1003     bool notifyResized()
1004     {
1005         if (_hostCommand is null) 
1006             return false;
1007 
1008         return _hostCommand.notifyResized();
1009     }
1010 
1011     override DAW getDAW()
1012     {
1013         assert(_hostCommand !is null);
1014         return _hostCommand.getDAW();
1015     }
1016 
1017     override PluginFormat getPluginFormat()
1018     {
1019         assert(_hostCommand !is null);
1020         return _hostCommand.getPluginFormat();
1021     }
1022 
1023     version(legacyBinState)
1024     {}
1025     else
1026     {
1027         /**
1028             Write the extra state of plugin in a chunk, so that the host can restore that later.
1029             You would typically serialize arbitrary stuff with `dplug.core.binrange`.
1030             This is called quite frequently.
1031 
1032             What should go here:
1033                 * your own chunk format with hopefully your plugin major version.
1034                 * user-defined structures, like opened .wav, strings, wavetables, file paths...
1035                   You can finally make plugins with arbitrary data in presets!
1036                 * Typically stuff used to render sound identically.
1037                 * Do not put host-exposed plug-in Parameters, they are saved by other means.
1038                 * Do not put stuff that depends on I/O settings, such as:
1039                    - current sample rate
1040                    - current I/O count and layout
1041                    - maxFrames and buffering
1042                   What you put in an extra state chunk must be parameter-like,
1043                   just not those a DAW allows.
1044 
1045             Contrarily, this is a disappointing solution for:
1046                 * Storing UI size, dark mode, and all kind of editor preferences.
1047                   Indeed, when `loadState` is called, the UI might not exist at all.
1048 
1049             Note: Using state chunks comes with a BIG challenge of making your own synchronization 
1050                   with the UI. You can expect any thread to call `saveState` and `loadState`. 
1051                   A proper design would probably have you represent state in the editor and the 
1052                   audio client separately, with a clean interchange.
1053 
1054             Important: This is called at the instantiating of a plug-in to get the "default state",
1055                        so that `makeDefaultPreset()` can work. At this point, the preset bank isn't 
1056                        yet constructed, so you cannot rely on it.
1057 
1058             Warning: Just append new content to the `Vec!ubyte`, do not modify its existing content
1059                      if any exist.
1060 
1061             See_also: `loadState`.
1062         */
1063         void saveState(ref Vec!ubyte chunk) nothrow @nogc
1064         {
1065         }
1066 
1067         /**
1068             Read the extra state of your plugin from a chunk, to restore a former save.
1069             You would typically deserialize arbitrary stuff with `dplug.core.binrange`.
1070 
1071             This is called on session load or on preset load (IF the preset had a state chunk),
1072             but this isn't called on plugin instantiation.
1073 
1074             Note: Using state chunks comes with a BIG challenge of making your own synchronization 
1075                   with the UI. You can expect any thread to call `saveState` and `loadState`. 
1076                   A proper design would probably have you represent state in the editor and the 
1077                   audio client separately, with a clean interchange.
1078 
1079             Important: This should successfully parse whatever the "default state" is
1080                        so that `makeDefaultPreset()` can work.
1081 
1082             Returns: `true` on successful parse, return false to indicate a parsing error.
1083 
1084             See_also: `loadState`.
1085         */
1086         bool loadState(const(ubyte)[] chunk) nothrow @nogc
1087         {
1088             return true;
1089         }
1090     }
1091 
1092     // </IClient>
1093 
1094 protected:
1095 
1096     /// Override this method to implement parameter creation.
1097     /// This is an optional overload, default implementation declare no parameters.
1098     /// The returned slice must be allocated with `malloc`/`mallocSlice` and contains
1099     /// `Parameter` objects created with `mallocEmplace`.
1100     Parameter[] buildParameters()
1101     {
1102         return [];
1103     }
1104 
1105     /// Override this methods to load/fill presets.
1106     /// This function must return a slice allocated with `malloc`,
1107     /// that contains presets crteated with `mallocEmplace`.
1108     Preset[] buildPresets() nothrow @nogc
1109     {
1110         auto presets = makeVec!Preset();
1111         presets.pushBack( makeDefaultPreset() );
1112         return presets.releaseData();
1113     }
1114 
1115     /// Override this method to tell what plugin you are.
1116     /// Mandatory override, fill the fields with care.
1117     /// Note: this should not be called by a plugin client implementation directly.
1118     ///       Access the content of PluginInfo through the various accessors.
1119     abstract PluginInfo buildPluginInfo();
1120 
1121     /// Override this method to tell which I/O are legal.
1122     /// The returned slice must be allocated with `malloc`/`mallocSlice`.
1123     abstract LegalIO[] buildLegalIO();
1124 
1125     IGraphics _graphics;
1126 
1127     // Used as a flag that _graphics can be used (by audio thread or for destruction)
1128     shared(bool) _graphicsIsAvailable = false;
1129 
1130     // Note: when implementing a new plug-in format, the format wrapper has to call
1131     // `setHostCommand` and implement `IHostCommand`.
1132     IHostCommand _hostCommand = null;
1133 
1134     PluginInfo _info;
1135 
1136 private:
1137     Parameter[] _params;
1138 
1139     PresetBank _presetBank;
1140 
1141     LegalIO[] _legalIOs;
1142 
1143     int _maxInputs, _maxOutputs; // maximum number of input/outputs
1144 
1145     // Cache result of maxFramesInProcess(), maximum frame length
1146     int _maxFramesInProcess;
1147 
1148     // Container for awaiting MIDI messages.
1149     MidiQueue _inputMidiQueue;
1150 
1151     // Priority queue for sending MIDI messages.
1152     MidiQueue _outputMidiQueue;
1153 
1154     // Protects MIDI out from UI.
1155     UncheckedMutex _midiOutFromUIMutex;
1156 
1157     // Additional, unsorted messages to be sent, courtesy of the UI.
1158     Vec!MidiMessage _outputMidiFromUI;
1159 
1160     // Accumulated output MIDI messages, for one unsplit buffer.
1161     // Output MIDI messages, if any, are accumulated there.
1162     Vec!MidiMessage _outputMidiMessages;
1163 
1164     version(legacyBinState)
1165     {}
1166     else
1167     {
1168         /// Stores the extra state data (from a `saveState` call) from when the plugin was newly
1169         /// instantiated. This is helpful, in order to synthesize presets, and also because some 
1170         /// hosts don't restore default state when instantiating.
1171         Vec!ubyte _defaultStateData;
1172     }
1173 
1174     final void createGraphicsLazily()
1175     {
1176         // First GUI opening create the graphics object
1177         // no need to protect _graphics here since the audio thread
1178         // does not write to it.
1179         if ( (_graphics is null) && hasGUI())
1180         {
1181             // Why is the IGraphics created lazily? This allows to load a plugin very quickly,
1182             // without opening its logical UI
1183             IGraphics graphics = createGraphics();
1184 
1185             // Don't forget to override the createGraphics method!
1186             assert(graphics !is null);
1187 
1188             _graphics = graphics;
1189 
1190             // Now that the UI is fully created, we enable the audio thread to use it
1191             atomicStore(_graphicsIsAvailable, true);
1192         }
1193     }    
1194 }
1195 
1196 /// Should be called in Client class during compile time
1197 /// to parse a `PluginInfo` from a supplied json file.
1198 PluginInfo parsePluginInfo(string json)
1199 {
1200     import std.json;
1201     import std.string;
1202     import std.conv;
1203 
1204     JSONValue j = parseJSON(json);
1205 
1206     static bool toBoolean(JSONValue value)
1207     {
1208         static if (__VERSION__ >= 2087)
1209         {
1210             if (value.type == JSONType.true_)
1211                 return true;
1212             if (value.type == JSONType.false_)
1213                 return false;
1214         }
1215         else
1216         {
1217             if (value.type == JSON_TYPE.TRUE)
1218                 return true;
1219             if (value.type == JSON_TYPE.FALSE)
1220                 return false;
1221         }
1222         throw new Exception(format("Expected a boolean, got %s instead", value));
1223     }
1224 
1225     // Check that a string is "x.y.z"
1226     // FUTURE: support larger integers than 0 to 9 in the string
1227     static PluginVersion parsePluginVersion(string value)
1228     {
1229         bool isDigit(char ch)
1230         {
1231             return ch >= '0' && ch <= '9';
1232         }
1233 
1234         if ( value.length != 5  ||
1235              !isDigit(value[0]) ||
1236              value[1] != '.'    ||
1237              !isDigit(value[2]) ||
1238              value[3] != '.'    ||
1239              !isDigit(value[4]))
1240         {
1241             throw new Exception("\"publicVersion\" should follow the form x.y.z (eg: \"1.0.0\")");
1242         }
1243 
1244         PluginVersion ver;
1245         ver.major = value[0] - '0';
1246         ver.minor = value[2] - '0';
1247         ver.patch = value[4] - '0';
1248         return ver;
1249     }
1250 
1251     PluginInfo info;
1252     info.vendorName = j["vendorName"].str;
1253     info.vendorUniqueID = j["vendorUniqueID"].str;
1254     info.pluginName = j["pluginName"].str;
1255     info.pluginUniqueID = j["pluginUniqueID"].str;
1256 
1257     if ("vendorSupportEmail" in j)
1258         info.vendorSupportEmail= j["vendorSupportEmail"].str;
1259 
1260     if ("pluginHomepage" in j)
1261         info.pluginHomepage = j["pluginHomepage"].str;
1262 
1263     if ("isSynth" in j)
1264         info.isSynth = toBoolean(j["isSynth"]);
1265     info.hasGUI = toBoolean(j["hasGUI"]);
1266     if ("receivesMIDI" in j)
1267         info.receivesMIDI = toBoolean(j["receivesMIDI"]);
1268     if ("sendsMIDI" in j)
1269         info.sendsMIDI = toBoolean(j["sendsMIDI"]);
1270 
1271     // Plugins that sends MIDI must also receives MIDI.
1272     if (info.sendsMIDI && !info.receivesMIDI)
1273         throw new Exception("A plugin that sends MIDI must also receives MIDI. Caution: a plugin that receives MIDI must call getNextMidiMessages() in the audio callback");
1274 
1275     info.publicVersion = parsePluginVersion(j["publicVersion"].str);
1276 
1277     string CFBundleIdentifierPrefix = j["CFBundleIdentifierPrefix"].str;
1278 
1279     string sanitizedName = sanitizeBundleString(info.pluginName);
1280     info.VSTBundleIdentifier = CFBundleIdentifierPrefix ~ ".vst." ~ sanitizedName;
1281     info.AUBundleIdentifier = CFBundleIdentifierPrefix ~ ".audiounit." ~ sanitizedName;
1282     info.AAXBundleIdentifier = CFBundleIdentifierPrefix ~ ".aax." ~ sanitizedName;
1283     info.CLAPIdentifier = CFBundleIdentifierPrefix ~ "." ~ sanitizedName;
1284     info.CLAPIdentifierFactory = CFBundleIdentifierPrefix ~ "." ~ sanitizedName ~ ".factory";
1285 
1286     PluginCategory category = parsePluginCategory(j["category"].str);
1287     if (category == PluginCategory.invalid)
1288         throw new Exception("Invalid \"category\" in plugin.json. Check out dplug.client.daw for valid values (eg: \"effectDynamics\").");
1289     info.category = category;
1290 
1291     // See Issue #581.
1292     // Check that we aren't leaking secrets in this build, through `import("plugin.json")`.
1293     void checkNotLeakingPassword(string key)
1294     {
1295         if (key in j)
1296         {
1297             string pwd = j[key].str;
1298             if (pwd == "!PROMPT")
1299                 return;
1300 
1301             if (pwd.length > 0 && pwd[0] == '$')
1302                 return; // using an envvar
1303 
1304             throw new Exception(
1305                         "\n*************************** WARNING ***************************\n\n"
1306                         ~ "  This build is using a plain text password in plugin.json\n"
1307                         ~ "  This will leak through `import(\"plugin.json\")`\n\n"
1308                         ~ "  Solutions:\n"
1309                         ~ "    1. Use environment variables, such as:\n"
1310                         ~ "           \"iLokPassword\": \"$ILOK_PASSWORD\"\n"
1311                         ~ "    2. Use the special value \"!PROMPT\", such as:\n"
1312                         ~ "           \"keyPassword-windows\": \"!PROMPT\"\n\n"
1313                         ~ "***************************************************************\n");
1314         }
1315     }
1316     checkNotLeakingPassword("keyPassword-windows");
1317     checkNotLeakingPassword("iLokPassword");
1318 
1319     void checkNotLeakingNoPrompt(string key)
1320     {
1321         if (key in j)
1322         {
1323             string pwd = j[key].str;
1324             if (pwd.length > 0 && pwd[0] == '$')
1325                 return; // using an envvar
1326 
1327             throw new Exception(
1328                         "\n*************************** WARNING ***************************\n\n"
1329                         ~ "  This build is using a plain text password in plugin.json\n"
1330                         ~ "  This will leak through `import(\"plugin.json\")`\n\n"
1331                         ~ "  Solution:\n"
1332                         ~ "       Use environment variables, such as:\n"
1333                         ~ "           \"appSpecificPassword-altool\": \"$APP_SPECIFIC_PASSWORD\"\n\n"
1334                         ~ "***************************************************************\n");
1335         }
1336     }
1337     checkNotLeakingNoPrompt("appSpecificPassword-altool");
1338     checkNotLeakingNoPrompt("appSpecificPassword-stapler");
1339 
1340     return info;
1341 }
1342 
1343 private string sanitizeBundleString(string s) pure
1344 {
1345     string r = "";
1346     foreach(dchar ch; s)
1347     {
1348         if (ch >= 'A' && ch <= 'Z')
1349             r ~= ch;
1350         else if (ch >= 'a' && ch <= 'z')
1351             r ~= ch;
1352         else if (ch == '.')
1353             r ~= ch;
1354         else
1355             r ~= "-";
1356     }
1357     return r;
1358 }