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