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 module dplug.client.params;
18 
19 import core.atomic;
20 import core.stdc.stdio;
21 import core.stdc.string;
22 
23 import std.math;
24 import std.algorithm.comparison;
25 import std.string;
26 import std.conv;
27 
28 import dplug.core.math;
29 import dplug.core.sync;
30 import dplug.core.nogc;
31 import dplug.core.alignedbuffer;
32 import dplug.client.client;
33 
34 
35 /// Plugin parameter.
36 /// Implement the Observer pattern for UI support.
37 /// Note: Recursive mutexes are needed here because `getNormalized()`
38 /// could need locking an already taken mutex.
39 class Parameter
40 {
41 public:
42 nothrow:
43 @nogc:
44 
45     /// Returns: Parameters name. Displayed when the plugin has no UI.
46     string name() pure const nothrow @nogc
47     {
48         return _name;
49     }
50 
51     /// Returns: Parameters unit label.
52     string label() pure const nothrow @nogc
53     {
54         return _label;
55     }
56 
57     /// Returns: Index of parameter in the parameter list.
58     int index() pure const nothrow @nogc
59     {
60         return _index;
61     }
62 
63     /// From a normalized double [0..1], set the parameter value.
64     void setFromHost(double hostValue) nothrow @nogc
65     {
66         setNormalized(hostValue);
67         notifyListeners();
68     }
69 
70     /// Returns: A normalized double [0..1], represents the parameter value.
71     double getForHost() nothrow @nogc
72     {
73         return getNormalized();
74     }
75 
76     void toDisplayN(char* buffer, size_t numBytes) nothrow @nogc
77     {
78         toStringN(buffer, numBytes);
79     }
80 
81     /// Adds a parameter listener.
82     void addListener(IParameterListener listener) nothrow @nogc
83     {
84         _listeners.pushBack(listener);
85     }
86 
87     /// Removes a parameter listener.
88     void removeListener(IParameterListener listener) nothrow @nogc
89     {
90         int index = _listeners.indexOf(listener);
91         if (index != -1)
92             _listeners.removeAndReplaceByLastElement(index);
93     }
94 
95     /// Warns the host that a parameter will be edited.
96     void beginParamEdit() nothrow @nogc
97     {
98         _client.hostCommand().beginParamEdit(_index);
99         foreach(listener; _listeners)
100             listener.onBeginParameterEdit(this);
101     }
102 
103     /// Warns the host that a parameter has finished being edited.
104     void endParamEdit() nothrow @nogc
105     {
106         _client.hostCommand().endParamEdit(_index);
107         foreach(listener; _listeners)
108             listener.onEndParameterEdit(this);
109     }
110 
111     /// Returns: A normalized double, representing the parameter value.
112     abstract double getNormalized() nothrow @nogc;
113 
114     /// Returns: A normalized double, representing the default parameter value.
115     abstract double getNormalizedDefault() nothrow @nogc;
116 
117     /// Returns: A string associated with the normalized normalized.
118     abstract void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len) nothrow @nogc;
119 
120     /// Returns: A normalized normalized associated with the string.
121     /// Can throw Exceptions.
122     abstract bool normalizedValueFromString(string valueString, out double result) nothrow @nogc;
123 
124     ~this()
125     {
126         _valueMutex.destroy();
127     }
128 
129 protected:
130 
131     this(int index, string name, string label) nothrow @nogc
132     {
133         _client = null;
134         _name = name;
135         _label = label;
136         _index = index;
137         _valueMutex = makeMutex();
138         _listeners = makeAlignedBuffer!IParameterListener();
139     }
140 
141     /// From a normalized double, set the parameter value.
142     /// No guarantee at all that getNormalized will return the same,
143     /// because this value is rounded to fit.
144     abstract void setNormalized(double hostValue) nothrow @nogc;
145 
146     /// Display parameter (without label).
147     abstract void toStringN(char* buffer, size_t numBytes) nothrow @nogc;
148 
149     void notifyListeners() nothrow @nogc
150     {
151         foreach(listener; _listeners)
152             listener.onParameterChanged(this);
153     }
154 
155 package:
156 
157     /// Parameters are owned by a client, this is used to make them refer back to it.
158     void setClientReference(Client client) nothrow @nogc
159     {
160         _client = client;
161     }
162 
163 private:
164 
165     /// weak reference to parameter holder, set after parameter creation
166     Client _client;
167 
168     int _index;
169     string _name;
170     string _label;
171     AlignedBuffer!IParameterListener _listeners;
172     UncheckedMutex _valueMutex;
173 
174 }
175 
176 /// Parameter listeners are called whenever a parameter is changed from the host POV.
177 /// Intended making GUI controls call `setDirty()` and move with automation.
178 interface IParameterListener
179 {
180 nothrow @nogc:
181 
182     /// Called when a parameter value was changed
183     /// You'll probably want to call `setDirtyWhole()` or `setDirty()` in it
184     /// to make the graphics respond to host changing a parameter.
185     void onParameterChanged(Parameter sender);
186 
187     /// Called when a parameter value start being changed due to an UI element
188     void onBeginParameterEdit(Parameter sender);
189 
190     /// Called when a parameter value stops being changed
191     void onEndParameterEdit(Parameter sender);
192 }
193 
194 
195 /// A boolean parameter
196 class BoolParameter : Parameter
197 {
198 public:
199     this(int index, string name, bool defaultValue) nothrow @nogc
200     {
201         super(index, name, "");
202         _value = defaultValue;
203         _defaultValue = defaultValue;
204     }
205 
206     override void setNormalized(double hostValue)
207     {
208         _valueMutex.lock();
209         bool newValue = (hostValue >= 0.5);
210         atomicStore(_value, newValue);
211         _valueMutex.unlock();
212     }
213 
214     override double getNormalized() 
215     {
216         return value() ? 1.0 : 0.0;
217     }
218 
219     override double getNormalizedDefault() 
220     {
221         return _defaultValue ? 1.0 : 0.0;
222     }
223 
224     override void toStringN(char* buffer, size_t numBytes)
225     {
226         bool v = value();
227 
228         if (v)
229             snprintf(buffer, numBytes, "yes");
230         else
231             snprintf(buffer, numBytes, "no");
232     }
233 
234     /// Returns: A string associated with the normalized normalized.
235     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
236     {
237         bool value = (normalizedValue >= 0.5);
238         if (value)
239             snprintf(buffer, len, "yes");
240         else
241             snprintf(buffer, len, "no");
242     }
243 
244     /// Returns: A normalized normalized associated with the string.
245     override bool normalizedValueFromString(string valueString, out double result)
246     {
247         if (valueString == "yes")
248         {
249             result = 1;
250             return true;
251         }
252         else if (valueString == "no")
253         {
254             result = 0;
255             return true;
256         }
257         else
258             return false;
259     }
260 
261     /// Toggles the parameter value from the UI thread.
262     final void toggleFromGUI() nothrow @nogc
263     {
264         setFromGUI(value());
265     }
266 
267     /// Sets the parameter value from the UI thread.
268     final void setFromGUI(bool newValue) nothrow @nogc
269     {
270         _valueMutex.lock();
271         atomicStore(_value, newValue);
272         double normalized = getNormalized();
273         _valueMutex.unlock();
274 
275         _client.hostCommand().paramAutomate(_index, normalized);
276         notifyListeners();
277     }
278 
279     /// Gets current value.
280     final bool value() nothrow @nogc
281     {
282         bool v = void;
283         _valueMutex.lock();
284         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
285         _valueMutex.unlock();
286         return v;
287     }
288 
289     /// Same as value but doesn't use locking, and doesn't use ordering.
290     /// Which make it a better fit for the audio thread.
291     final bool valueAtomic() nothrow @nogc
292     {
293         return atomicLoad!(MemoryOrder.raw)(_value);
294     }
295 
296     /// Returns: default value.
297     final bool defaultValue() pure const nothrow @nogc
298     {
299         return _defaultValue;
300     }
301 
302 private:
303     shared(bool) _value;
304     bool _defaultValue;
305 }
306 
307 /// An integer parameter
308 class IntegerParameter : Parameter
309 {
310 public:
311     this(int index, string name, string label, int min = 0, int max = 1, int defaultValue = 0) nothrow @nogc
312     {
313         super(index, name, label);
314         _name = name;
315         _value = _defaultValue = clampValue!int(defaultValue, min, max);
316         _min = min;
317         _max = max;
318     }
319 
320     override void setNormalized(double hostValue)
321     {
322         int newValue = fromNormalized(hostValue);
323         _valueMutex.lock();
324         atomicStore(_value, newValue);
325         _valueMutex.unlock();
326     }
327 
328     override double getNormalized()
329     {
330         int v = value();
331         double normalized = toNormalized(value());
332         return normalized;
333     }
334 
335     override double getNormalizedDefault()
336     {
337         double normalized = toNormalized(_defaultValue);
338         return normalized;
339     }
340 
341     override void toStringN(char* buffer, size_t numBytes)
342     {
343         int v =  value();
344         snprintf(buffer, numBytes, "%d", v);
345     }
346 
347     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
348     {
349         int denorm = fromNormalized(normalizedValue);
350         snprintf(buffer, len, "%d", denorm);
351     }
352 
353     override bool normalizedValueFromString(string valueString, out double result)
354     {
355         if (valueString.length > 63)
356             return false;
357 
358         // Because the input string is not zero-terminated
359         char[64] buf;
360         snprintf(buf.ptr, buf.length, "%.*s", valueString.length, valueString.ptr);
361 
362         int denorm;
363         if (1 == sscanf(buf.ptr, "%d", denorm))
364         {
365             result = toNormalized(denorm);
366             return true;
367         }
368         else
369             return false;
370     }
371 
372     /// Gets the current parameter value.
373     final int value() nothrow @nogc
374     {
375         int v = void;
376         _valueMutex.lock();
377         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
378         _valueMutex.unlock();
379         return v;
380     }
381 
382     /// Same as value but doesn't use locking, and doesn't use ordering.
383     /// Which make it a better fit for the audio thread.
384     final int valueAtomic() nothrow @nogc
385     {
386         return atomicLoad!(MemoryOrder.raw)(_value);
387     }
388 
389     final void setFromGUI(int value) nothrow @nogc
390     {
391         if (value < _min)
392             value = _min;
393         if (value > _max)
394             value = _max;
395 
396         _valueMutex.lock();
397         atomicStore(_value, value);
398         double normalized = getNormalized();
399         _valueMutex.unlock();
400 
401         _client.hostCommand().paramAutomate(_index, normalized);
402         notifyListeners();
403     }
404 
405     final void setFromGUINormalized(double normalizedValue) nothrow @nogc
406     {
407         assert(normalizedValue >= 0 && normalizedValue <= 1);
408         setFromGUI(fromNormalized(normalizedValue));
409     }
410 
411     /// Returns: minimum possible values.
412     final int minValue() pure const nothrow @nogc
413     {
414         return _min;
415     }
416 
417     /// Returns: maximum possible values.
418     final int maxValue() pure const nothrow @nogc
419     {
420         return _max;
421     }
422 
423     /// Returns: number of possible values.
424     final int numValues() pure const nothrow @nogc
425     {
426         return 1 + _max - _min;
427     }
428 
429     /// Returns: default value.
430     final int defaultValue() pure const nothrow @nogc
431     {
432         return _defaultValue;
433     }
434 
435 private:
436     shared(int) _value;
437     int _min;
438     int _max;
439     int _defaultValue;
440 
441     final int fromNormalized(double normalizedValue) nothrow @nogc
442     {
443         double mapped = _min + (_max - _min) * normalizedValue;
444 
445         // slightly incorrect rounding, but lround is crashing
446         int rounded = void;
447         if (mapped >= 0)
448             rounded = cast(int)(0.5f + mapped);
449         else
450             rounded = cast(int)(-0.5f + mapped);
451 
452         return clampValue!int(rounded, _min, _max);
453     }
454 
455     final double toNormalized(int value) nothrow @nogc
456     {
457         return clampValue!double( (cast(double)value - _min) / (_max - _min), 0.0, 1.0);
458     }
459 }
460 
461 class EnumParameter : IntegerParameter
462 {
463 public:
464     this(int index, string name, const(string[]) possibleValues, int defaultValue = 0) nothrow @nogc
465     {
466         super(index, name, "", 0, cast(int)(possibleValues.length) - 1, defaultValue);
467 
468         _possibleValues = possibleValues;
469     }
470 
471     override void toStringN(char* buffer, size_t numBytes)
472     {
473         int v = value();
474         int toCopy = max(0, min( cast(int)(numBytes) - 1, cast(int)(_possibleValues[v].length)));
475         memcpy(buffer, _possibleValues[v].ptr, toCopy);
476         // add terminal zero
477         if (numBytes > 0)
478             buffer[toCopy] = '\0';
479     }
480 
481     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
482     {
483         const(char[]) valueLabel = _possibleValues[ fromNormalized(normalizedValue) ];
484         snprintf(buffer, len, "%.*s", valueLabel.length, valueLabel.ptr);
485     }
486 
487     override bool normalizedValueFromString(string valueString, out double result)
488     {
489         foreach(int i; 0..cast(int)(_possibleValues.length))
490             if (_possibleValues[i] == valueString)
491             {
492                 result = toNormalized(i);
493                 return true;
494             }
495 
496         return false;
497     }
498 
499     final string getValueString(int n) nothrow @nogc
500     {
501         return _possibleValues[n];
502     }
503 
504 private:
505     const(string[]) _possibleValues;
506 }
507 
508 private
509 {
510     T clampValue(T)(T x, T min, T max) pure nothrow @nogc
511     {
512         if (x < min)
513             return min;
514         else if (x > max)
515             return max;
516         else
517             return x;
518     }
519 }
520 
521 /// A float parameter
522 /// This is an abstract class, mapping from normalized to parmeter values is left to the user.
523 class FloatParameter : Parameter
524 {
525 public:
526     this(int index, string name, string label, double min, double max, double defaultValue) nothrow @nogc
527     {
528         super(index, name, label);
529 
530         // If you fail in this assertion, this means your default value is out of range.
531         assert(defaultValue >= min && defaultValue <= max);
532 
533         _defaultValue = defaultValue;
534         _name = name;
535         _value = _defaultValue;
536         _min = min;
537         _max = max;
538     }
539 
540     /// Gets current value.
541     final double value() nothrow @nogc
542     {
543         double v = void;
544         _valueMutex.lock();
545         v = atomicLoad!(MemoryOrder.raw)(_value); // already sequenced by mutex locks
546         _valueMutex.unlock();
547         return v;
548     }
549 
550     /// Same as value but doesn't use locking, and doesn't use ordering.
551     /// Which make it a better fit for the audio thread.
552     final double valueAtomic() nothrow @nogc
553     {
554         return atomicLoad!(MemoryOrder.raw)(_value);
555     }
556 
557     final double minValue() pure const nothrow @nogc
558     {
559         return _min;
560     }
561 
562     final double maxValue() pure const nothrow @nogc
563     {
564         return _max;
565     }
566 
567     final double defaultValue() pure const nothrow @nogc
568     {
569         return _defaultValue;
570     }
571 
572     final void setFromGUINormalized(double normalizedValue) nothrow @nogc
573     {
574         assert(normalizedValue >= 0 && normalizedValue <= 1);
575         setFromGUI(fromNormalized(normalizedValue));
576     }
577 
578     /// Sets the number of decimal digits after the dot to be displayed.
579     final void setDecimalPrecision(int digits) nothrow @nogc
580     {
581         assert(digits >= 0);
582         assert(digits <= 9);
583         _formatString[3] = cast(char)('0' + digits);
584     }
585 
586     final void setFromGUI(double value) nothrow @nogc
587     {
588         if (value < _min)
589             value = _min;
590         if (value > _max)
591             value = _max;
592 
593         _valueMutex.lock();
594         atomicStore(_value, value);
595         double normalized = getNormalized();
596         _valueMutex.unlock();
597 
598         _client.hostCommand().paramAutomate(_index, normalized);
599         notifyListeners();
600     }
601 
602     override void setNormalized(double hostValue)
603     {
604         double v = fromNormalized(hostValue);
605         _valueMutex.lock();
606         atomicStore(_value, v);
607         _valueMutex.unlock();
608     }
609 
610     override double getNormalized()
611     {
612         return toNormalized(value());
613     }
614 
615     override double getNormalizedDefault() 
616     {
617         return toNormalized(_defaultValue);
618     }
619 
620     override void toStringN(char* buffer, size_t numBytes)
621     {
622         snprintf(buffer, numBytes, _formatString.ptr, value());
623     }
624 
625     override void stringFromNormalizedValue(double normalizedValue, char* buffer, size_t len)
626     {
627         double denorm = fromNormalized(normalizedValue);
628         snprintf(buffer, len, _formatString.ptr, denorm);
629     }
630 
631     override bool normalizedValueFromString(string valueString, out double result)
632     {
633         if (valueString.length > 63)
634             return false;
635 
636         // Because the input string is not zero-terminated
637         char[64] buf;
638         snprintf(buf.ptr, buf.length, "%.*s", valueString.length, valueString.ptr);
639 
640         int denorm;
641         if (1 == sscanf(buf.ptr, "%f", denorm))
642         {
643             result = toNormalized(denorm);
644             return true;
645         }
646         else
647             return false;
648     }
649 
650     /// Override it to specify mapping from parameter values to normalized [0..1]
651     abstract double toNormalized(double value) nothrow @nogc;
652 
653     /// Override it to specify mapping from normalized [0..1] to parameter value
654     abstract double fromNormalized(double value) nothrow @nogc;
655 
656 private:
657     shared(double) _value;
658     double _min;
659     double _max;
660     double _defaultValue;
661 
662     // format string for string conversion, is overwritten by `setDecimalPrecision`.
663     char[6] _formatString = "%2.2f"; 
664 }
665 
666 /// Linear-mapped float parameter (eg: dry/wet)
667 class LinearFloatParameter : FloatParameter
668 {
669     this(int index, string name, string label, float min, float max, float defaultValue) nothrow @nogc
670     {
671         super(index, name, label, min, max, defaultValue);
672     }
673 
674     override double toNormalized(double value)
675     {
676         return clampValue!double( (value - _min) / (_max - _min), 0.0, 1.0);
677     }
678 
679     override double fromNormalized(double normalizedValue)
680     {
681         return clampValue!double(_min + (_max - _min) * normalizedValue, _min, _max);
682     }
683 }
684 
685 /// Float parameter following an exponential type of mapping (eg: cutoff frequency)
686 class LogFloatParameter : FloatParameter
687 {
688     this(int index, string name, string label, double min, double max, double defaultValue) nothrow @nogc
689     {
690         assert(min > 0 && max > 0);
691         super(index, name, label, min, max, defaultValue);
692     }
693 
694     override double toNormalized(double value)
695     {
696         double result = log(value / _min) / log(_max / _min);
697         if (result < 0)
698             result = 0;
699         if (result > 1)
700             result = 1;
701         return result;
702     }
703 
704     override double fromNormalized(double normalizedValue)
705     {
706         return _min * exp(normalizedValue * log(_max / _min));
707     }
708 }
709 
710 /// A parameter with [-inf to value] dB log mapping
711 class GainParameter : FloatParameter
712 {
713     this(int index, string name, double max, double defaultValue, double shape = 2.0) nothrow @nogc
714     {
715         super(index, name, "dB", -double.infinity, max, defaultValue);
716         _shape = shape;
717         setDecimalPrecision(1);
718     }
719 
720     override double toNormalized(double value)
721     {
722         double maxAmplitude = deciBelToFloat(_max);
723         double result = ( deciBelToFloat(value) / maxAmplitude ) ^^ (1 / _shape);
724         if (result < 0)
725             result = 0;
726         if (result > 1)
727             result = 1;
728         assert(isFinite(result));
729         return result;
730     }
731 
732     override double fromNormalized(double normalizedValue)
733     {
734         return floatToDeciBel(  (normalizedValue ^^ _shape) * deciBelToFloat(_max));
735     }
736 
737 private:
738     double _shape;
739 }
740 
741 /// Float parameter following a x^N type mapping (eg: something that doesn't fit in the other categories)
742 class PowFloatParameter : FloatParameter
743 {
744     this(int index, string name, string label, double min, double max, double defaultValue, double shape) nothrow @nogc
745     {
746         super(index, name, label, min, max, defaultValue);
747         _shape = shape;
748     }
749 
750     override double toNormalized(double value)
751     {
752         double result = clampValue!double( (value - _min) / (_max - _min), 0.0, 1.0) ^^ (1 / _shape);
753         assert(result >= 0 && result <= 1);
754         return result;
755     }
756 
757     override double fromNormalized(double normalizedValue)
758     {
759         double v = _min + (normalizedValue ^^ _shape) * (_max - _min);
760         return clampValue!double(v, _min, _max);
761     }
762 
763 private:
764     double _shape;
765 }