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 }