1 /* 2 Cockos WDL License 3 4 Copyright (C) 2005 - 2015 Cockos Incorporated 5 Copyright (C) 2015 - 2017 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 /// Base client implementation. Every plugin format implementation hold a `Client` member. 19 module dplug.client.client; 20 21 import core.atomic; 22 import core.stdc..string; 23 import core.stdc.stdio; 24 25 import std.container; 26 27 import dplug.core.nogc; 28 import dplug.core.math; 29 import dplug.core.vec; 30 31 import dplug.client.params; 32 import dplug.client.preset; 33 import dplug.client.midi; 34 import dplug.client.graphics; 35 import dplug.client.daw; 36 37 38 version = lazyGraphicsCreation; 39 40 /// A plugin client can send commands to the host. 41 /// This interface is injected after the client creation though. 42 interface IHostCommand 43 { 44 nothrow @nogc: 45 void beginParamEdit(int paramIndex); 46 void paramAutomate(int paramIndex, float value); 47 void endParamEdit(int paramIndex); 48 bool requestResize(int width, int height); 49 DAW getDAW(); 50 } 51 52 // Plugin version in major.minor.patch form. 53 struct PluginVersion 54 { 55 int major; 56 int minor; 57 int patch; 58 59 int toVSTVersion() pure const nothrow @nogc 60 { 61 assert(major < 10 && minor < 10 && patch < 10); 62 return major * 1000 + minor * 100 + patch*10; 63 } 64 65 int toAUVersion() pure const nothrow @nogc 66 { 67 assert(major < 256 && minor < 256 && patch < 256); 68 return (major << 16) | (minor << 8) | patch; 69 } 70 71 int toAAXPackageVersion() pure const nothrow @nogc 72 { 73 // For AAX, considered binary-compatible unless major version change 74 return major; 75 } 76 } 77 78 79 // Statically known features of the plugin. 80 // There is some default for explanation purpose, but you really ought to override them all. 81 // Most of it is redundant with plugin.json, in the future the JSON will be parsed instead. 82 struct PluginInfo 83 { 84 string vendorName = "Witty Audio"; 85 86 /// Used in AU only. 87 char[4] vendorUniqueID = "Wity"; 88 89 string pluginName = "Destructatorizer"; 90 91 /// Used for both VST and AU. 92 /// In AU it is namespaced by the manufacturer. In VST it 93 /// should be unique. While it seems no VST host use this 94 /// ID as a unique way to identify a plugin, common wisdom 95 /// is to try to get a sufficiently random one. 96 char[4] pluginUniqueID = "WiDi"; 97 98 // Plugin version information. 99 // It's important that the version you fill at runtime is identical to the 100 // one in `plugin.json` else you won't pass AU validation. 101 // 102 // Note: For AU, 0.x.y is supposed to mean "do not cache", however it is 103 // unknown what it actually changes. AU caching hasn't caused any problem 104 // and can probably be ignored. 105 deprecated("Use publicVersion instead") alias pluginVersion = publicVersion; 106 PluginVersion publicVersion = PluginVersion(0, 0, 0); 107 108 /// True if the plugin has a graphical UI. Easy way to disable it. 109 bool hasGUI = false; 110 111 /// True if the plugin "is a synth". This has only a semantic effect. 112 bool isSynth = false; 113 114 /// True if the plugin should receive MIDI events. 115 /// Warning: receiving MIDI forces you to call `getNextMidiMessages` 116 /// with the right number of `frames`, every buffer. 117 bool receivesMIDI = false; 118 119 /// Used for being at the right place in list of plug-ins. 120 PluginCategory category; 121 122 /// Used as name of the bundle in VST. 123 string VSTBundleIdentifier; 124 125 /// Used as name of the bundle in AU. 126 string AUBundleIdentifier; 127 128 /// Used as name of the bundle in AAX. 129 string AAXBundleIdentifier; 130 } 131 132 /// This allows to write things life tempo-synced LFO. 133 struct TimeInfo 134 { 135 /// BPM 136 double tempo = 120; 137 138 /// Current time from the beginning of the song in samples. 139 long timeInSamples = 0; 140 141 /// Whether the host sequencer is currently playing 142 bool hostIsPlaying; 143 } 144 145 /// Describe a combination of input channels count and output channels count 146 struct LegalIO 147 { 148 int numInputChannels; 149 int numOutputChannels; 150 } 151 152 /// Plugin interface, from the client point of view. 153 /// This client has no knowledge of thread-safety, it must be handled externally. 154 /// User plugins derivate from this class. 155 /// Plugin formats wrappers owns one dplug.plugin.Client as a member. 156 class Client 157 { 158 public: 159 nothrow: 160 @nogc: 161 162 this() 163 { 164 _info = buildPluginInfo(); 165 166 // Create legal I/O combinations 167 _legalIOs = buildLegalIO(); 168 169 // Create parameters. 170 _params = buildParameters(); 171 172 // Check parameter consistency 173 // This avoid mistake when adding/reordering parameters in a plugin. 174 foreach(int i, Parameter param; _params) 175 { 176 // If you fail here, this means your buildParameter() override is incorrect. 177 // Check the values of the index you're giving. 178 // They should be 0, 1, 2, ..., N-1 179 // Maybe you have duplicated a line or misordered them. 180 assert(param.index() == i); 181 182 // Sets owner reference. 183 param.setClientReference(this); 184 } 185 186 // Create presets 187 _presetBank = mallocNew!PresetBank(this, buildPresets()); 188 189 190 _maxFramesInProcess = maxFramesInProcess(); 191 192 _maxInputs = 0; 193 _maxOutputs = 0; 194 foreach(legalIO; _legalIOs) 195 { 196 if (_maxInputs < legalIO.numInputChannels) 197 _maxInputs = legalIO.numInputChannels; 198 if (_maxOutputs < legalIO.numOutputChannels) 199 _maxOutputs = legalIO.numOutputChannels; 200 } 201 202 version (lazyGraphicsCreation) {} 203 else 204 { 205 createGraphicsLazily(); 206 } 207 208 _midiQueue = makeMidiQueue(); 209 } 210 211 ~this() 212 { 213 // Destroy graphics 214 if (_graphics !is null) 215 { 216 // Acquire _graphicsIsAvailable forever 217 // so that it's the last time the audio uses it, 218 // and we can wait for its exit in _graphics destructor 219 while(!cas(&_graphicsIsAvailable, true, false)) 220 { 221 // MAYDO: relax CPU 222 } 223 _graphics.destroyFree(); 224 } 225 226 // Destroy presets 227 _presetBank.destroyFree(); 228 229 // Destroy parameters 230 foreach(p; _params) 231 p.destroyFree(); 232 _params.freeSlice(); 233 _legalIOs.freeSlice(); 234 } 235 236 final int maxInputs() pure const nothrow @nogc 237 { 238 return _maxInputs; 239 } 240 241 final int maxOutputs() pure const nothrow @nogc 242 { 243 return _maxOutputs; 244 } 245 246 /// Returns: Array of parameters. 247 final inout(Parameter[]) params() inout nothrow @nogc 248 { 249 return _params; 250 } 251 252 /// Returns: Array of legal I/O combinations. 253 final LegalIO[] legalIOs() nothrow @nogc 254 { 255 return _legalIOs; 256 } 257 258 /// Returns: true if the following I/O combination is a legal one. 259 /// < 0 means "do not check" 260 final bool isLegalIO(int numInputChannels, int numOutputChannels) pure const nothrow @nogc 261 { 262 foreach(io; _legalIOs) 263 if ( ( (numInputChannels < 0) 264 || 265 (io.numInputChannels == numInputChannels) ) 266 && 267 ( (numOutputChannels < 0) 268 || 269 (io.numOutputChannels == numOutputChannels) ) 270 ) 271 return true; 272 273 return false; 274 } 275 276 /// Returns: Array of presets. 277 final PresetBank presetBank() nothrow @nogc 278 { 279 return _presetBank; 280 } 281 282 /// Returns: The parameter indexed by index. 283 final inout(Parameter) param(int index) inout nothrow @nogc 284 { 285 return _params.ptr[index]; 286 } 287 288 /// Returns: true if index is a valid parameter index. 289 final bool isValidParamIndex(int index) const nothrow @nogc 290 { 291 return index >= 0 && index < _params.length; 292 } 293 294 /// Returns: true if index is a valid input index. 295 final bool isValidInputIndex(int index) nothrow @nogc 296 { 297 return index >= 0 && index < maxInputs(); 298 } 299 300 /// Returns: true if index is a valid output index. 301 final bool isValidOutputIndex(int index) nothrow @nogc 302 { 303 return index >= 0 && index < maxOutputs(); 304 } 305 306 // Note: openGUI, getGUISize and closeGUI are guaranteed 307 // synchronized by the client implementation 308 final void* openGUI(void* parentInfo, void* controlInfo, GraphicsBackend backend) nothrow @nogc 309 { 310 createGraphicsLazily(); 311 return (cast(IGraphics)_graphics).openUI(parentInfo, controlInfo, _hostCommand.getDAW(), backend); 312 } 313 314 final bool getGUISize(int* width, int* height) nothrow @nogc 315 { 316 createGraphicsLazily(); 317 auto graphics = (cast(IGraphics)_graphics); 318 if (graphics) 319 { 320 graphics.getGUISize(width, height); 321 return true; 322 } 323 else 324 return false; 325 } 326 327 /// ditto 328 final void closeGUI() nothrow @nogc 329 { 330 (cast(IGraphics)_graphics).closeUI(); 331 } 332 333 // This should be called only by a client implementation. 334 void setParameterFromHost(int index, float value) nothrow @nogc 335 { 336 param(index).setFromHost(value); 337 } 338 339 /// Override if you create a plugin with UI. 340 /// The returned IGraphics must be allocated with `mallocEmplace`. 341 IGraphics createGraphics() nothrow @nogc 342 { 343 return null; 344 } 345 346 /// Getter for the IGraphics interface 347 /// This is intended for the audio thread and has acquire semantics. 348 /// Not reentrant! You can't call this twice without a graphicsRelease first. 349 /// Returns: null if feedback from audio thread is not welcome. 350 final IGraphics graphicsAcquire() nothrow @nogc 351 { 352 if (cas(&_graphicsIsAvailable, true, false)) 353 return _graphics; 354 else 355 return null; 356 } 357 358 /// Mirror function to release the IGraphics from the audio-thread. 359 /// Do not call if graphicsAcquire() returned `null`. 360 final void graphicsRelease() nothrow @nogc 361 { 362 // graphicsAcquire should have been called before 363 // MAYDO: which memory order here? Don't looks like we need a barrier. 364 atomicStore(_graphicsIsAvailable, true); 365 } 366 367 // Getter for the IHostCommand interface 368 final IHostCommand hostCommand() nothrow @nogc 369 { 370 return _hostCommand; 371 } 372 373 /// Override to clear state (eg: resize and clear delay lines) and allocate buffers. 374 /// Important: This will be called by the audio thread. 375 /// So you should not use the GC in this callback. 376 abstract void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc; 377 378 /// Override to set the plugin latency in samples. 379 /// Plugin latency can depend on `sampleRate` but no other value. 380 /// If you want your latency to depend on a `Parameter` your only choice is to 381 /// pessimize the needed latency and compensate in the process callback. 382 /// Returns: Plugin latency in samples. 383 /// Note: this can absolutely be called before `reset` was called, be prepared. 384 int latencySamples(double sampleRate) pure const nothrow @nogc 385 { 386 return 0; // By default, no latency introduced by plugin 387 } 388 389 /// Override to set the plugin tail length in seconds. 390 /// This is the amount of time before silence is reached with a silent input. 391 /// Returns: Plugin tail size in seconds. 392 float tailSizeInSeconds() pure const nothrow @nogc 393 { 394 return 0.100f; // default: 100ms 395 } 396 397 /// Override to declare the maximum number of samples to accept 398 /// If greater, the audio buffers will be splitted up. 399 /// This splitting have several benefits: 400 /// - help allocating temporary audio buffers on the stack 401 /// - keeps memory usage low and reuse it 402 /// - allow faster-than-buffer-size parameter changes 403 /// Returns: Maximum number of samples 404 int maxFramesInProcess() pure const nothrow @nogc 405 { 406 return 0; // default returns 0 which means "do not split" 407 } 408 409 /// Process some audio. 410 /// Override to make some noise. 411 /// In processAudio you are always guaranteed to get valid pointers 412 /// to all the channels the plugin requested. 413 /// Unconnected input pins are zeroed. 414 /// This callback is the only place you may call `getNextMidiMessages()` (it is 415 /// even required for plugins receiving MIDI). 416 /// 417 /// Number of frames are guaranteed to be less or equal to what the last reset() call said. 418 /// Number of inputs and outputs are guaranteed to be exactly what the last reset() call said. 419 /// Warning: Do not modify the pointers! 420 abstract void processAudio(const(float*)[] inputs, // array of input channels 421 float*[] outputs, // array of output channels 422 int frames, // number of sample in each input & output channel 423 TimeInfo timeInfo // time information associated with this signal frame 424 ) nothrow @nogc; 425 426 /// Should only be called in `processAudio`. 427 /// This return a slice of MIDI messages corresponding to the next `frames` samples. 428 /// Useful if you don't want to process messages every samples, or every split buffer. 429 final const(MidiMessage)[] getNextMidiMessages(int frames) nothrow @nogc 430 { 431 return _midiQueue.getNextMidiMessages(frames); 432 } 433 434 /// Returns a new default preset. 435 final Preset makeDefaultPreset() nothrow @nogc 436 { 437 // MAYDO: use mallocSlice for perf 438 auto values = makeVec!float(); 439 foreach(param; _params) 440 values.pushBack(param.getNormalizedDefault()); 441 return mallocNew!Preset("Default", values.releaseData); 442 } 443 444 // Getters for fields in _info 445 446 final bool hasGUI() pure const nothrow @nogc 447 { 448 return _info.hasGUI; 449 } 450 451 final bool isSynth() pure const nothrow @nogc 452 { 453 return _info.isSynth; 454 } 455 456 final bool receivesMIDI() pure const nothrow @nogc 457 { 458 return _info.receivesMIDI; 459 } 460 461 final string vendorName() pure const nothrow @nogc 462 { 463 return _info.vendorName; 464 } 465 466 final char[4] getVendorUniqueID() pure const nothrow @nogc 467 { 468 return _info.vendorUniqueID; 469 } 470 471 final string pluginName() pure const nothrow @nogc 472 { 473 return _info.pluginName; 474 } 475 476 final PluginCategory pluginCategory() pure const nothrow @nogc 477 { 478 return _info.category; 479 } 480 481 final string VSTBundleIdentifier() pure const nothrow @nogc 482 { 483 return _info.VSTBundleIdentifier; 484 } 485 486 final string AUBundleIdentifier() pure const nothrow @nogc 487 { 488 return _info.AUBundleIdentifier; 489 } 490 491 final string AAXBundleIdentifier() pure const nothrow @nogc 492 { 493 return _info.AAXBundleIdentifier; 494 } 495 496 /// Returns: Plugin "unique" ID. 497 final char[4] getPluginUniqueID() pure const nothrow @nogc 498 { 499 return _info.pluginUniqueID; 500 } 501 502 /// Returns: Plugin full name "$VENDOR $PRODUCT" 503 final void getPluginFullName(char* p, int bufLength) const nothrow @nogc 504 { 505 snprintf(p, bufLength, "%.*s %.*s", 506 _info.vendorName.length, _info.vendorName.ptr, 507 _info.pluginName.length, _info.pluginName.ptr); 508 } 509 510 /// Returns: Plugin version in x.x.x.x decimal form. 511 deprecated("Use getPublicVersion instead") alias getPluginVersion = getPublicVersion; 512 final PluginVersion getPublicVersion() pure const nothrow @nogc 513 { 514 return _info.publicVersion; 515 } 516 517 /// Boilerplate function to get the value of a `FloatParameter`, for use in `processAudio`. 518 final float readFloatParamValue(int paramIndex) nothrow @nogc 519 { 520 auto p = param(paramIndex); 521 assert(cast(FloatParameter)p !is null); // check it's a FloatParameter 522 return unsafeObjectCast!FloatParameter(p).valueAtomic(); 523 } 524 525 /// Boilerplate function to get the value of an `IntParameter`, for use in `processAudio`. 526 final int readIntegerParamValue(int paramIndex) nothrow @nogc 527 { 528 auto p = param(paramIndex); 529 assert(cast(IntegerParameter)p !is null); // check it's an IntParameter 530 return unsafeObjectCast!IntegerParameter(p).valueAtomic(); 531 } 532 533 final int readEnumParamValue(int paramIndex) nothrow @nogc 534 { 535 auto p = param(paramIndex); 536 assert(cast(EnumParameter)p !is null); // check it's an EnumParameter 537 return unsafeObjectCast!EnumParameter(p).valueAtomic(); 538 } 539 540 /// Boilerplate function to get the value of a `BoolParameter`,for use in `processAudio`. 541 final bool readBoolParamValue(int paramIndex) nothrow @nogc 542 { 543 auto p = param(paramIndex); 544 assert(cast(BoolParameter)p !is null); // check it's a BoolParameter 545 return unsafeObjectCast!BoolParameter(p).valueAtomic(); 546 } 547 548 /// For plugin format clients only. 549 final void setHostCommand(IHostCommand hostCommand) nothrow @nogc 550 { 551 _hostCommand = hostCommand; 552 } 553 554 /// For plugin format clients only. 555 /// Enqueues an incoming MIDI message. 556 void enqueueMIDIFromHost(MidiMessage message) 557 { 558 _midiQueue.enqueue(message); 559 } 560 561 /// For plugin format clients only. 562 /// Calls processAudio repeatedly, splitting the buffers. 563 /// Splitting allow to decouple memory requirements from the actual host buffer size. 564 /// There is few performance penalty above 512 samples. 565 void processAudioFromHost(float*[] inputs, 566 float*[] outputs, 567 int frames, 568 TimeInfo timeInfo 569 ) nothrow @nogc 570 { 571 572 if (_maxFramesInProcess == 0) 573 { 574 processAudio(inputs, outputs, frames, timeInfo); 575 } 576 else 577 { 578 // Slice audio in smaller parts 579 while (frames > 0) 580 { 581 // Note: the last slice will be smaller than the others 582 int sliceLength = frames; 583 if (sliceLength > _maxFramesInProcess) 584 sliceLength = _maxFramesInProcess; 585 586 processAudio(inputs, outputs, sliceLength, timeInfo); 587 588 // offset all input buffer pointers 589 for (int i = 0; i < cast(int)inputs.length; ++i) 590 inputs[i] = inputs[i] + sliceLength; 591 592 // offset all output buffer pointers 593 for (int i = 0; i < cast(int)outputs.length; ++i) 594 outputs[i] = outputs[i] + sliceLength; 595 596 frames -= sliceLength; 597 598 // timeInfo must be updated 599 timeInfo.timeInSamples += sliceLength; 600 } 601 assert(frames == 0); 602 } 603 } 604 605 /// For plugin format clients only. 606 /// Calls `reset()`. 607 /// Must be called by the audio thread. 608 void resetFromHost(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc 609 { 610 // Clear outstanding MIDI messages (now invalid) 611 _midiQueue.initialize(); 612 613 // We potentially give to the client implementation a lower value 614 // for the maximum number of frames 615 if (_maxFramesInProcess != 0 && _maxFramesInProcess < maxFrames) 616 maxFrames = _maxFramesInProcess; 617 618 // Calls the reset virtual call 619 reset(sampleRate, maxFrames, numInputs, numOutputs); 620 } 621 622 protected: 623 624 /// Override this method to implement parameter creation. 625 /// This is an optional overload, default implementation declare no parameters. 626 /// The returned slice must be allocated with `malloc`/`mallocSlice` and contains 627 /// `Parameter` objects created with `mallocEmplace`. 628 Parameter[] buildParameters() 629 { 630 return []; 631 } 632 633 /// Override this methods to load/fill presets. 634 /// This function must return a slice allocated with `malloc`, 635 /// that contains presets crteated with `mallocEmplace`. 636 Preset[] buildPresets() nothrow @nogc 637 { 638 auto presets = makeVec!Preset(); 639 presets.pushBack( makeDefaultPreset() ); 640 return presets.releaseData(); 641 } 642 643 /// Override this method to tell what plugin you are. 644 /// Mandatory override, fill the fields with care. 645 abstract PluginInfo buildPluginInfo(); 646 647 /// Override this method to tell which I/O are legal. 648 /// The returned slice must be allocated with `malloc`/`mallocSlice`. 649 abstract LegalIO[] buildLegalIO(); 650 651 IGraphics _graphics; 652 653 // Used as a flag that _graphics can be used (by audio thread or for destruction) 654 shared(bool) _graphicsIsAvailable = false; 655 656 IHostCommand _hostCommand; 657 658 PluginInfo _info; 659 660 private: 661 Parameter[] _params; 662 663 PresetBank _presetBank; 664 665 LegalIO[] _legalIOs; 666 667 int _maxInputs, _maxOutputs; // maximum number of input/outputs 668 669 // Cache result of maxFramesInProcess(), maximum frame length 670 int _maxFramesInProcess; 671 672 // Container for awaiting MIDI messages. 673 MidiQueue _midiQueue; 674 675 final void createGraphicsLazily() nothrow @nogc 676 { 677 // First GUI opening create the graphics object 678 // no need to protect _graphics here since the audio thread 679 // does not write to it 680 if ( (_graphics is null) && hasGUI()) 681 { 682 // Why is the IGraphics created lazily? This allows to load a plugin very quickly, 683 // without opening its logical UI 684 IGraphics graphics = createGraphics(); 685 686 // Don't forget to override the createGraphics method! 687 assert(graphics !is null); 688 689 _graphics = graphics; 690 691 // Now that the UI is fully created, we enable the audio thread to use it 692 atomicStore(_graphicsIsAvailable, true); 693 } 694 } 695 } 696 697 /// Should be called in Client class during compile time 698 /// to parse a `PluginInfo` from a supplied json file. 699 PluginInfo parsePluginInfo(string json) 700 { 701 import std.json; 702 import std..string; 703 import std.conv; 704 705 JSONValue j = parseJSON(json); 706 707 static bool toBoolean(JSONValue value) 708 { 709 if (value.type == JSON_TYPE.TRUE) 710 return true; 711 if (value.type == JSON_TYPE.FALSE) 712 return false; 713 throw new Exception(format("Expected a boolean, got %s instead", value)); 714 } 715 716 // Check that a string is "x.y.z" 717 // FUTURE: support larger integers than 0 to 9 in the string 718 static PluginVersion parsePluginVersion(string value) 719 { 720 bool isDigit(char ch) 721 { 722 return ch >= '0' && ch <= '9'; 723 } 724 725 if ( value.length != 5 || 726 !isDigit(value[0]) || 727 value[1] != '.' || 728 !isDigit(value[2]) || 729 value[3] != '.' || 730 !isDigit(value[4])) 731 { 732 throw new Exception("\"publicVersion\" should follow the form x.y.z (eg: \"1.0.0\")"); 733 } 734 735 PluginVersion ver; 736 ver.major = value[0] - '0'; 737 ver.minor = value[2] - '0'; 738 ver.patch = value[4] - '0'; 739 return ver; 740 } 741 742 PluginInfo info; 743 info.vendorName = j["vendorName"].str; 744 info.vendorUniqueID = j["vendorUniqueID"].str; 745 info.pluginName = j["pluginName"].str; 746 info.pluginUniqueID = j["pluginUniqueID"].str; 747 info.isSynth = toBoolean(j["isSynth"]); 748 info.hasGUI = toBoolean(j["hasGUI"]); 749 info.receivesMIDI = toBoolean(j["receivesMIDI"]); 750 info.publicVersion = parsePluginVersion(j["publicVersion"].str); 751 752 string CFBundleIdentifierPrefix = j["CFBundleIdentifierPrefix"].str; 753 754 string sanitizedName = sanitizeBundleString(info.pluginName); 755 info.VSTBundleIdentifier = CFBundleIdentifierPrefix ~ ".vst." ~ sanitizedName; 756 info.AUBundleIdentifier = CFBundleIdentifierPrefix ~ ".audiounit." ~ sanitizedName; 757 info.AAXBundleIdentifier = CFBundleIdentifierPrefix ~ ".aax." ~ sanitizedName; 758 759 PluginCategory category = parsePluginCategory(j["category"].str); 760 if (category == PluginCategory.invalid) 761 throw new Exception("Invalid \"category\" in plugin.json. Check out dplug.client.daw for valid values (eg: \"effectDynamics\")."); 762 info.category = category; 763 return info; 764 } 765 766 private string sanitizeBundleString(string s) pure 767 { 768 string r = ""; 769 foreach(dchar ch; s) 770 { 771 if (ch >= 'A' && ch <= 'Z') 772 r ~= ch; 773 else if (ch >= 'a' && ch <= 'z') 774 r ~= ch; 775 else if (ch == '.') 776 r ~= ch; 777 else 778 r ~= "-"; 779 } 780 return r; 781 }