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