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