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