1 /*
2 Cockos WDL License
3 
4 Copyright (C) 2005 - 2015 Cockos Incorporated
5 Copyright (C) 2015 and later Auburn Sounds
6 
7 Portions copyright other contributors, see each source file for more
8 information.
9 This software is provided 'as-is', without any express or implied warranty.
10 In no event will the authors be held liable for any damages arising from the
11 use of this software.
12 
13 Permission is granted to anyone to use this software for any purpose, including
14 commercial applications, and to alter it and redistribute it freely, subject to
15 the following restrictions:
16 
17 1. The origin of this software must not be misrepresented; you must not claim 
18    that you wrote the original software. If you use this software in a product, 
19    an acknowledgment in the product documentation would be appreciated but is 
20    not required.
21 1. Altered source versions must be plainly marked as such, and must not be 
22    misrepresented as being the original software.
23 1. This notice may not be removed or altered from any source distribution.
24 */
25 /**
26     Definitions of plug-in `Parameter`, and its many variants.
27 */
28 module dplug.client.params;
29 
30 import core.atomic;
31 import core.stdc.stdio;
32 import core.stdc.string;
33 
34 import std.math: isNaN, log, exp, isFinite;
35 
36 import dplug.core.math;
37 import dplug.core.sync;
38 import dplug.core.nogc;
39 import dplug.core.vec;
40 import dplug.core.string;
41 import dplug.client.client;
42 
43 /// Parameter listeners are called whenever:
44 /// - a parameter is changed, 
45 /// - a parameter starts/stops being edited,
46 /// - a parameter starts/stops being hovered by mouse
47 ///
48 /// Most common use case is for UI controls repaints (the `setDirtyXXX` calls)
49 interface IParameterListener
50 {
51 nothrow:
52 @nogc:
53 
54     /// A parameter value was changed, from the UI or the host (automation).
55     ///
56     /// Suggestion: Maybe call `setDirtyWhole()`/`setDirty()` in it to request
57     ///             a graphic repaint.
58     ///
59     /// WARNING: this WILL be called from any thread, including the audio 
60     ///          thread.
61     void onParameterChanged(Parameter sender);
62 
63     /// A parameter value _starts_ being changed due to an UI element.
64     void onBeginParameterEdit(Parameter sender);
65 
66     /// Called when a parameter value _stops_ being changed.
67     void onEndParameterEdit(Parameter sender);
68 
69     /// Called when a widget that can changes this parameter is mouseover, and wants to signal
70     /// that to the listeners.
71     ///
72     /// `onBeginParameterHover`/`onEndParameterHover` is called by widgets from an UI thread 
73     /// (typically on `onMouseEnter`/`onMouseLeave`) when the mouse has entered a widget that 
74     /// could change its parameter.
75     ///
76     /// It is useful be display the parameter value or related tooltips elsewhere in the UI,
77     /// in another widget.
78     ///
79     /// To dispatch such messages to listeners, see `Parameter.
80     ///
81     /// Not all widgets will want to signal this though, for example a widget that handles plenty
82     /// of Parameters will not want to signal them all the time.
83     ///
84     /// If `onBeginParameterHover` was ever called, then the same widget should also call 
85     /// `onEndParameterHover` when sensible.
86     void onBeginParameterHover(Parameter sender);
87     ///ditto
88     void onEndParameterHover(Parameter sender);
89 }
90 
91 
92 
93 /// Plugin parameter.
94 /// Implement the Observer pattern for UI support.
95 /// Note: Recursive mutexes are needed here because `getNormalized()`
96 /// could need locking an already taken mutex.
97 ///
98 /// Listener patter:
99 ///     Every `Parameter` maintain a list of `IParameterListener`.
100 ///     This is typically used by UI elements to update with parameters 
101 ///     changes from all 
102 ///     kinds of sources (UI itself, host automation...).
103 ///     But they are not necessarily UI elements.
104 ///
105 /// FUTURE: easier to make widget if every `Parameter` has a 
106 ///         `setFromGUINormalized` call.
107 class Parameter
108 {
109 public:
110 nothrow:
111 @nogc:
112 
113     /// Adds a parameter listener.
114     void addListener(IParameterListener listener)
115     {
116         _listeners.pushBack(listener);
117     }
118 
119     /// Removes a parameter listener.
120     void removeListener(IParameterListener listener)
121     {
122         int index = _listeners.indexOf(listener);
123         if (index != -1)
124             _listeners.removeAndReplaceByLastElement(index);
125     }
126 
127     /// Returns: Parameters name. Displayed when the plug-in has no UI.
128     string name() pure const
129     {
130         return _name;
131     }
132 
133     /// Returns: Parameters unit label.
134     string label() pure const
135     {
136         return _label;
137     }
138 
139     /// Output name as a zero-terminated C string, truncate if needed.
140     final void toNameN(char* p, int bufLength) const nothrow @nogc
141     {
142         snprintf(p, bufLength, "%.*s", cast(int)(_name.length), _name.ptr);
143     }
144 
145     /// Output label as a zero-terminated C string, truncate if needed.
146     final void toLabelN(char* p, int bufLength) const nothrow @nogc
147     {
148         snprintf(p, bufLength, "%.*s", cast(int)(_label.length), _label.ptr);
149     }
150 
151     /// Returns: Index of parameter in the parameter list.
152     int index() pure const
153     {
154         return _index;
155     }
156 
157     /// Returns: Whether parameter is automatable.
158     bool isAutomatable() pure const
159     {
160         return _isAutomatable;
161     }
162 
163     /// Makes parameter non-automatable.
164     Parameter nonAutomatable()
165     {
166         _isAutomatable = false;
167         return this;
168     }
169 
170     /// From a normalized double [0..1], set the parameter value.
171     /// This is a Dplug internal call, not for plug-in code.
172     void setFromHost(double hostValue)
173     {
174         // If edited by a widget, REFUSE host changes, since they could be
175         // "in the past" and we know we want newer values anyway.
176         if (isEdited())
177             return; 
178 
179         setNormalized(hostValue);
180         notifyListeners();
181     }
182 
183     /// Returns: A normalized double [0..1], represents parameter value.
184     /// This is a Dplug internal call, not for plug-in code.
185     double getForHost()
186     {
187         return getNormalized();
188     }
189 
190     /// Output a string representation of a `Parameter`.
191     void toDisplayN(char* buffer, size_t numBytes)
192     {
193         toStringN(buffer, numBytes);
194     }
195 
196     /// Warns the host that a parameter will be edited.
197     /// Should only ever be called from the UI thread.
198     void beginParamEdit()
199     {
200         atomicOp!"+="(_editCount, 1);
201         _client.hostCommand().beginParamEdit(_index);
202         foreach(listener; _listeners)
203             listener.onBeginParameterEdit(this);
204     }
205 
206     /// Warns the host that a parameter has finished being edited.
207     /// Should only ever be called from the UI thread.
208     void endParamEdit()
209     {
210         _client.hostCommand().endParamEdit(_index);
211         foreach(listener; _listeners)
212             listener.onEndParameterEdit(this);
213         atomicOp!"-="(_editCount, 1);
214     }
215 
216     /// Warns the listeners that a parameter is being hovered in the UI.
217     /// Should only ever be called from the UI thread.
218     ///
219     /// This doesn't communicate anything to the host.
220     ///
221     /// Note: Widgets are not forced to signal this if they do not want other
222     /// widgets to display a parameter value.
223     void beginParamHover()
224     {
225         debug _hoverCount += 1;
226         foreach(listener; _listeners)
227             listener.onBeginParameterHover(this);
228     }
229 
230     /// Warns the listeners that a parameter has finished being hovered in the UI.
231     /// Should only ever be called from the UI thread.
232     /// This doesn't communicate anything to the host.
233     ///
234     /// Note: Widgets are not forced to signal this if they do not want other
235     /// widgets to display a parameter value. 
236     /// 
237     /// Warning: calls to `beginParamHover`/`endParamHover` must be balanced.
238     void endParamHover()
239     {
240         foreach(listener; _listeners)
241             listener.onEndParameterHover(this);
242         debug _hoverCount -= 1;
243     }
244 
245     /// Returns: A normalized double, representing the parameter value.
246     abstract double getNormalized();
247 
248     /// Returns: A normalized double, representing the default parameter value.
249     abstract double getNormalizedDefault();
250 
251     /// Returns: A string associated with the normalized value.
252     abstract void stringFromNormalizedValue(double normalizedValue, 
253                                             char* buffer, 
254                                             size_t len);
255 
256     /// Returns: A normalized value associated with the string.
257     abstract bool normalizedValueFromString(const(char)[] valueString, out double result);
258 
259     /// Returns: `true` if the parameters has only discrete values, `false` if continuous.
260     abstract bool isDiscrete();
261 
262     ~this()
263     {
264         _valueMutex.destroy();
265 
266         // If you fail here, it means your calls to beginParamEdit and endParamEdit 
267         // were not balanced correctly.
268         debug assert(atomicLoad(_editCount) == 0);
269 
270         // If you fail here, it means your calls to beginParamHover and endParamHover
271         // were not balanced correctly.
272         debug assert(_hoverCount == 0);
273     }
274 
275 protected:
276 
277     this(int index, string name, string label)
278     {
279         _client = null;
280         _name = name;
281         _label = label;
282         _index = index;
283         _valueMutex = makeMutex();
284         _listeners = makeVec!IParameterListener();
285     }
286 
287     /// From a normalized double, set the parameter value.
288     /// No guarantee at all that getNormalized will return the same,
289     /// because this value is rounded to fit.
290     abstract void setNormalized(double hostValue);
291 
292     /// Display parameter (without label). This always adds a terminal zero within `numBytes`.
293     abstract void toStringN(char* buffer, size_t numBytes);
294 
295     final void notifyListeners()
296     {
297         foreach(listener; _listeners)
298             listener.onParameterChanged(this);
299     }
300 
301     final void checkBeingEdited()
302     {
303         // If you fail here, you have changed the value of a Parameter from the UI
304         // without enclosing within a pair of `beginParamEdit()`/`endParamEdit()`.
305         // This will cause some hosts like Apple Logic not to record automation.
306         //
307         // When setting a Parameter from an UI widget, it's important to call `beginParamEdit()`
308         // and `endParamEdit()` too.
309         debug assert(isEdited());
310     }
311 
312     final bool isEdited()
313     {
314         return atomicLoad(_editCount) > 0;
315     }
316 
317 package:
318 
319     /// Parameters are owned by a client, this is used to make them refer back to it.
320     void setClientReference(Client client)
321     {
322         _client = client;
323     }
324 
325 private:
326 
327     /// weak reference to parameter holder, set after parameter creation
328     Client _client;
329 
330     int _index;
331     string _name;
332     string _label;
333     /// Parameter is automatable by default.
334     bool _isAutomatable = true;
335 
336     /// The list of active `IParameterListener` to receive parameter changes.
337     Vec!IParameterListener _listeners;
338 
339     // Current number of calls into `beginParamEdit()`/`endParamEdit()` pair.
340     // Only checked in debug mode.
341     shared(int) _editCount = 0; // if > 0, the UI is editing this parameter
342 
343     // Current number of calls into `beginParamHover()`/`endParamHover()` pair.
344     // Only checked in debug mode.
345     debug int _hoverCount = 0;
346 
347     UncheckedMutex _valueMutex;
348 }
349 
350 
351 /// A boolean parameter
352 class BoolParameter : Parameter
353 {
354 public:
355     this(int index, string name, bool defaultValue) nothrow @nogc
356     {
357         super(index, name, "");
358         _value = defaultValue;
359         _defaultValue = defaultValue;
360     }
361 
362     override void setNormalized(double hostValue)
363     {
364         _valueMutex.lock();
365         bool newValue = (hostValue >= 0.5);
366         atomicStore(_value, newValue);
367         _valueMutex.unlock();
368     }
369 
370     override double getNormalized() 
371     {
372         return value() ? 1.0 : 0.0;
373     }
374 
375     override double getNormalizedDefault() 
376     {
377         return _defaultValue ? 1.0 : 0.0;
378     }
379 
380     override void toStringN(char* buffer, size_t numBytes)
381     {
382         bool v = value();
383 
384         if (v)
385             snprintf(buffer, numBytes, "yes");
386         else
387             snprintf(buffer, numBytes, "no");
388     }
389 
390     /// Returns: A string associated with the normalized normalized.
391     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
392     {
393         bool value = (normalizedValue >= 0.5);
394         if (value)
395             snprintf(buffer, len, "yes");
396         else
397             snprintf(buffer, len, "no");
398     }
399 
400     /// Returns: A normalized normalized associated with the string.
401     override bool normalizedValueFromString(const(char)[] valueString, out double result)
402     {
403         if (valueString == "yes")
404         {
405             result = 1;
406             return true;
407         }
408         else if (valueString == "no")
409         {
410             result = 0;
411             return true;
412         }
413         else
414             return false;
415     }
416 
417     override bool isDiscrete() 
418     {
419         return true;
420     }
421     
422     /// Toggles the parameter value from the UI thread.
423     final void toggleFromGUI() nothrow @nogc
424     {
425         setFromGUI(!value());
426     }
427 
428     /// Sets the parameter value from the UI thread.
429     final void setFromGUI(bool newValue) nothrow @nogc
430     {
431         checkBeingEdited();
432         _valueMutex.lock();
433         atomicStore(_value, newValue);
434         double normalized = getNormalized();
435         _valueMutex.unlock();
436 
437         _client.hostCommand().paramAutomate(_index, normalized);
438         notifyListeners();
439     }
440 
441     /// Sets the value of the parameter from UI, using a normalized value.
442     /// Note: If `normValue` is < 0.5, this is set to false.
443     ///       If `normValue` is >= 0.5, this is set to true.
444     final void setFromGUINormalized(double normValue) nothrow @nogc
445     {
446         assert(!isNaN(normValue));
447         bool val = (normValue >= 0.5);
448         setFromGUI(val);
449     }
450 
451     /// Get current value.
452     final bool value() nothrow @nogc
453     {
454         bool v = void;
455         _valueMutex.lock();
456         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
457         _valueMutex.unlock();
458         return v;
459     }
460 
461     /// Get current value but doesn't use locking, using the `raw` memory order.
462     /// Which might make it a better fit for the audio thread.
463     /// The various `readParam!T` functions use that.²
464     final bool valueAtomic() nothrow @nogc
465     {
466         return atomicLoad!(MemoryOrder.raw)(_value);
467     }
468 
469     /// Returns: default value.
470     final bool defaultValue() pure const nothrow @nogc
471     {
472         return _defaultValue;
473     }
474 
475 private:
476     shared(bool) _value;
477     bool _defaultValue;
478 }
479 
480 /// An integer parameter
481 class IntegerParameter : Parameter
482 {
483 public:
484     this(int index, string name, string label, int min = 0, int max = 1, int defaultValue = 0) nothrow @nogc
485     {
486         super(index, name, label);
487         _name = name;
488         int clamped = defaultValue;
489         if (clamped < min) 
490             clamped = min;
491         if (clamped > max) 
492             clamped = max;
493         _value = clamped;
494         _defaultValue = clamped;
495         _min = min;
496         _max = max;
497     }
498 
499     override void setNormalized(double hostValue)
500     {
501         int newValue = fromNormalized(hostValue);
502         _valueMutex.lock();
503         atomicStore(_value, newValue);
504         _valueMutex.unlock();
505     }
506 
507     override double getNormalized()
508     {
509         double normalized = toNormalized(value());
510         return normalized;
511     }
512 
513     override double getNormalizedDefault()
514     {
515         double normalized = toNormalized(_defaultValue);
516         return normalized;
517     }
518 
519     override void toStringN(char* buffer, size_t numBytes)
520     {
521         int v =  value();
522         snprintf(buffer, numBytes, "%d", v);
523     }
524 
525     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
526     {
527         int denorm = fromNormalized(normalizedValue);
528         snprintf(buffer, len, "%d", denorm);
529     }
530 
531     override bool normalizedValueFromString(const(char)[] valueString, out double result)
532     {
533         if (valueString.length > 127)
534             return false;
535 
536         // Because the input string is not zero-terminated
537         char[128] buf;
538         snprintf(buf.ptr, buf.length, "%.*s", cast(int)(valueString.length), valueString.ptr);
539 
540         bool err = false;
541         int denorm = convertStringToInteger(buf.ptr, false, &err);
542         if (err)
543             return false;
544 
545         result = toNormalized(denorm);
546         return true;
547     }
548 
549     override bool isDiscrete() 
550     {
551         return true;
552     }
553 
554     /// Gets the current parameter value.
555     final int value() nothrow @nogc
556     {
557         int v = void;
558         _valueMutex.lock();
559         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
560         _valueMutex.unlock();
561         return v;
562     }
563 
564     /// Same as value but doesn't use locking, and doesn't use ordering.
565     /// Which make it a better fit for the audio thread.
566     final int valueAtomic() nothrow @nogc
567     {
568         return atomicLoad!(MemoryOrder.raw)(_value);
569     }
570 
571     /// Sets the parameter value from the UI thread.
572     /// If the parameter is outside [min .. max] inclusive, then it is
573     /// clamped. This is not an error to do so.
574     final void setFromGUI(int value) nothrow @nogc
575     {
576         checkBeingEdited();
577 
578         if (value < _min)
579             value = _min;
580         if (value > _max)
581             value = _max;
582 
583         _valueMutex.lock();
584         atomicStore(_value, value);
585         double normalized = getNormalized();
586         _valueMutex.unlock();
587 
588         _client.hostCommand().paramAutomate(_index, normalized);
589         notifyListeners();
590     }
591 
592     /// Sets the value of the parameter from UI, using a normalized value.
593     /// Note: If `normValue` is not inside [0.0 .. 1.0], then it is clamped.
594     ///       This is not an error.
595     final void setFromGUINormalized(double normValue) nothrow @nogc
596     {
597         assert(!isNaN(normValue));
598         if (normValue < 0.0) normValue = 0.0;
599         if (normValue > 1.0) normValue = 1.0;
600         setFromGUI(fromNormalized(normValue));
601     }
602 
603     /// Returns: minimum possible values.
604     final int minValue() pure const nothrow @nogc
605     {
606         return _min;
607     }
608 
609     /// Returns: maximum possible values.
610     final int maxValue() pure const nothrow @nogc
611     {
612         return _max;
613     }
614 
615     /// Returns: number of possible values.
616     final int numValues() pure const nothrow @nogc
617     {
618         return 1 + _max - _min;
619     }
620 
621     /// Returns: default value.
622     final int defaultValue() pure const nothrow @nogc
623     {
624         return _defaultValue;
625     }
626 
627     final int fromNormalized(double normalizedValue) nothrow @nogc
628     {
629         double mapped = _min + (_max - _min) * normalizedValue;
630 
631         // BUG slightly incorrect rounding, but lround is crashing
632         int rounded;
633         if (mapped >= 0)
634             rounded = cast(int)(0.5f + mapped);
635         else
636             rounded = cast(int)(-0.5f + mapped);
637 
638         if (rounded < _min) 
639             rounded = _min;
640         if (rounded > _max)
641             rounded = _max;
642         return rounded;
643     }
644 
645     final double toNormalized(int value) nothrow @nogc
646     {
647         double v = (cast(double)value - _min) / (_max - _min);
648         if (v < 0.0)
649             v = 0.0;
650         if (v > 1.0)
651             v = 1.0;
652         return v;
653     }
654 
655 private:
656     shared(int) _value;
657     int _min;
658     int _max;
659     int _defaultValue;
660 }
661 
662 class EnumParameter : IntegerParameter
663 {
664 public:
665     this(int index, string name, const(string[]) possibleValues, int defaultValue = 0) nothrow @nogc
666     {
667         super(index, name, "", 0, cast(int)(possibleValues.length) - 1, defaultValue);
668 
669         // Duplicate all strings internally to avoid disappearing strings.
670         _possibleValues.resize(possibleValues.length);
671         foreach(size_t n, string label; possibleValues)
672         {
673             _possibleValues[n] = mallocDupZ(label);
674         }
675     }
676 
677     ~this()
678     {
679         foreach(size_t n, const(char)[] label; _possibleValues[])
680         {
681             freeSlice(cast(char[]) label); // const_cast
682         }
683     }
684 
685     override void toStringN(char* buffer, size_t numBytes)
686     {
687         int v = value();
688         int toCopy = cast(int)(_possibleValues[v].length);
689         int avail = cast(int)(numBytes) - 1;
690         if (toCopy > avail)
691             toCopy = avail;
692         if (toCopy < 0)
693             toCopy = 0;
694         memcpy(buffer, _possibleValues[v].ptr, toCopy); // memcpy OK
695         // add terminal zero
696         if (numBytes > 0)
697             buffer[toCopy] = '\0';
698     }
699 
700     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
701     {
702         const(char[]) valueLabel = _possibleValues[ fromNormalized(normalizedValue) ];
703         snprintf(buffer, len, "%.*s", cast(int)(valueLabel.length), valueLabel.ptr);
704     }
705 
706     override bool normalizedValueFromString(const(char)[] valueString, out double result)
707     {
708         foreach(int i; 0..cast(int)(_possibleValues.length))
709             if (_possibleValues[i] == valueString)
710             {
711                 result = toNormalized(i);
712                 return true;
713             }
714 
715         return false;
716     }
717 
718     final const(char)[] getValueString(int n) nothrow @nogc
719     {
720         return _possibleValues[n];
721     }
722 
723 private:
724     Vec!(char[]) _possibleValues;
725 }
726 
727 /// A float parameter
728 /// This is an abstract class, mapping from normalized to parmeter values is left to the user.
729 class FloatParameter : Parameter
730 {
731 public:
732     this(int index, string name, string label, double min, double max, double defaultValue) nothrow @nogc
733     {
734         super(index, name, label);
735 
736         // If you fail in this assertion, this means your default value is out of range.
737         // What to do: get back to your `buildParameters` function and give a default 
738         // in range of [min..max].
739         assert(defaultValue >= min && defaultValue <= max);
740 
741         _defaultValue = defaultValue;
742         _name = name;
743         _value = _defaultValue;
744         _min = min;
745         _max = max;
746     }
747 
748     /// Gets current value.
749     final double value() nothrow @nogc
750     {
751         double v = void;
752         _valueMutex.lock();
753         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
754         _valueMutex.unlock();
755         return v;
756     }
757 
758     /// Same as value but doesn't use locking, and doesn't use ordering.
759     /// Which make it a better fit for the audio thread.
760     final double valueAtomic() nothrow @nogc
761     {
762         return atomicLoad!(MemoryOrder.raw)(_value);
763     }
764 
765     final double minValue() pure const nothrow @nogc
766     {
767         return _min;
768     }
769 
770     final double maxValue() pure const nothrow @nogc
771     {
772         return _max;
773     }
774 
775     final double defaultValue() pure const nothrow @nogc
776     {
777         return _defaultValue;
778     }
779 
780     /// Sets the value of the parameter from UI, using a normalized value.
781     /// Note: If `normValue` is not inside [0.0 .. 1.0], then it is clamped.
782     ///       This is not an error.
783     final void setFromGUINormalized(double normValue) nothrow @nogc
784     {
785         assert(!isNaN(normValue));
786         if (normValue < 0.0) normValue = 0.0;
787         if (normValue > 1.0) normValue = 1.0;
788         setFromGUI(fromNormalized(normValue));
789     }
790 
791     /// Sets the number of decimal digits after the dot to be displayed.
792     final void setDecimalPrecision(int digits) nothrow @nogc
793     {
794         assert(digits >= 0);
795         assert(digits <= 9);
796         _formatString[3] = cast(char)('0' + digits);
797     }
798 
799     /// Helper for `setDecimalPrecision` that returns this, help when in parameter creation.
800     final FloatParameter withDecimalPrecision(int digits) nothrow @nogc
801     {
802         setDecimalPrecision(digits);
803         return this;
804     }
805 
806     /// Sets the value of the parameter from UI, using a normalized value.
807     /// Note: If `value` is not inside [min .. max], then it is clamped.
808     ///       This is not an error.
809     /// See_also: `setFromGUINormalized`
810     final void setFromGUI(double value) nothrow @nogc
811     {
812         assert(!isNaN(value));
813 
814         checkBeingEdited();
815         if (value < _min)
816             value = _min;
817         if (value > _max)
818             value = _max;
819 
820         _valueMutex.lock();
821         atomicStore(_value, value);
822         double normalized = getNormalized();
823         _valueMutex.unlock();
824 
825         _client.hostCommand().paramAutomate(_index, normalized);
826         notifyListeners();
827     }
828 
829     override void setNormalized(double hostValue)
830     {
831         double v = fromNormalized(hostValue);
832         _valueMutex.lock();
833         atomicStore(_value, v);
834         _valueMutex.unlock();
835     }
836 
837     override double getNormalized()
838     {
839         return toNormalized(value());
840     }
841 
842     override double getNormalizedDefault() 
843     {
844         return toNormalized(_defaultValue);
845     }
846 
847     override void toStringN(char* buffer, size_t numBytes)
848     {
849         snprintf(buffer, numBytes, _formatString.ptr, value());
850 
851         // DigitalMars's snprintf doesn't always add a terminal zero
852         version(DigitalMars)
853             if (numBytes > 0)
854             {
855                 buffer[numBytes-1] = '\0';
856             }
857     }
858 
859     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
860     {
861         double denorm = fromNormalized(normalizedValue);
862         snprintf(buffer, len, _formatString.ptr, denorm);
863     }
864 
865     override bool normalizedValueFromString(const(char)[] valueString, out double result)
866     {
867         if (valueString.length > 127) // ??? TODO doesn't bode well with VST3 constraints
868             return false;
869 
870         // Because the input string is not necessarily zero-terminated
871         char[128] buf;
872         snprintf(buf.ptr, buf.length, "%.*s", cast(int)(valueString.length), valueString.ptr);
873 
874         bool err = false;
875         double denorm = convertStringToDouble(buf.ptr, false, &err);
876         if (err)
877             return false; // didn't parse a double
878         result = toNormalized(denorm);
879         return true;
880     }
881 
882     override bool isDiscrete() 
883     {
884         return false; // continous
885     }
886 
887     /// Override it to specify mapping from parameter values to normalized [0..1]
888     abstract double toNormalized(double value) nothrow @nogc;
889 
890     /// Override it to specify mapping from normalized [0..1] to parameter value
891     abstract double fromNormalized(double value) nothrow @nogc;
892 
893 private:
894     shared(double) _value;
895     double _min;
896     double _max;
897     double _defaultValue;
898 
899     // format string for string conversion, is overwritten by `setDecimalPrecision`.
900     char[6] _formatString = "%2.2f"; 
901 }
902 
903 /// Linear-mapped float parameter (eg: dry/wet)
904 class LinearFloatParameter : FloatParameter
905 {
906     this(int index, string name, string label, float min, float max, float defaultValue) nothrow @nogc
907     {
908         super(index, name, label, min, max, defaultValue);
909     }
910 
911     override double toNormalized(double value)
912     {
913         double v = (value - _min) / (_max - _min);
914         if (v < 0.0)
915             v = 0.0;
916         if (v > 1.0)
917             v = 1.0;
918         return v;
919     }
920 
921     override double fromNormalized(double normalizedValue)
922     {
923         double v = _min + (_max - _min) * normalizedValue;
924         if (v < _min)
925             v = _min;
926         if (v > _max)
927             v = _max;
928         return v;
929     }
930 }
931 
932 /// Float parameter following an exponential type of mapping (eg: cutoff frequency)
933 class LogFloatParameter : FloatParameter
934 {
935     this(int index, string name, string label, double min, double max, double defaultValue) nothrow @nogc
936     {
937         assert(min > 0 && max > 0);
938         super(index, name, label, min, max, defaultValue);
939     }
940 
941     override double toNormalized(double value)
942     {
943         double result = log(value / _min) / log(_max / _min);
944         if (result < 0)
945             result = 0;
946         if (result > 1)
947             result = 1;
948         return result;
949     }
950 
951     override double fromNormalized(double normalizedValue)
952     {
953         return _min * exp(normalizedValue * log(_max / _min));
954     }
955 }
956 
957 /// A parameter with [-inf to value] dB log mapping
958 class GainParameter : FloatParameter
959 {
960     this(int index, string name, double max, double defaultValue, double shape = 2.0) nothrow @nogc
961     {
962         super(index, name, "dB", -double.infinity, max, defaultValue);
963         _shape = shape;
964         setDecimalPrecision(1);
965     }
966 
967     override double toNormalized(double value)
968     {
969         double maxAmplitude = convertDecibelToLinearGain(_max);
970         double result = ( convertDecibelToLinearGain(value) / maxAmplitude ) ^^ (1 / _shape);
971         if (result < 0)
972             result = 0;
973         if (result > 1)
974             result = 1;
975         assert(isFinite(result));
976         return result;
977     }
978 
979     override double fromNormalized(double normalizedValue)
980     {
981         return convertLinearGainToDecibel(  (normalizedValue ^^ _shape) * convertDecibelToLinearGain(_max));
982     }
983 
984 private:
985     double _shape;
986 }
987 
988 /// Float parameter following a x^N type mapping (eg: something that doesn't fit in the other categories)
989 class PowFloatParameter : FloatParameter
990 {
991     this(int index, string name, string label, double min, double max, double defaultValue, double shape) nothrow @nogc
992     {
993         super(index, name, label, min, max, defaultValue);
994         _shape = shape;
995     }
996 
997     override double toNormalized(double value)
998     {
999         double v = (value - _min) / (_max - _min);
1000         if (v < 0.0)
1001             v = 0.0;
1002         if (v > 1.0)
1003             v = 1.0;
1004         v = v ^^ (1 / _shape);
1005 
1006         // Note: It's not entirely impossible to imagine a particular way that 1 would be exceeded, since pow
1007         // is implemented with an exp approximation and a log approximation.
1008         // TODO: produce ill case in isolation to see
1009         assert(v >= 0 && v <= 1); // will still assert on NaN
1010         return v;
1011     }
1012 
1013     override double fromNormalized(double normalizedValue)
1014     {
1015         double v = _min + (normalizedValue ^^ _shape) * (_max - _min);
1016          if (v < _min)
1017             v = _min;
1018         if (v > _max)
1019             v = _max;
1020         return v;
1021     }
1022 
1023 private:
1024     double _shape;
1025 }