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