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