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 }