1 /** 2 * Definitions of presets and preset banks. 3 * 4 * Copyright: Copyright Auburn Sounds 2015 and later. 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.client.preset; 9 10 import core.stdc.stdlib: free; 11 12 import std.range.primitives; 13 import std.math: isFinite; 14 15 import dplug.core.vec; 16 import dplug.core.nogc; 17 import dplug.core.binrange; 18 19 import dplug.client.client; 20 import dplug.client.params; 21 22 // The current situation is quite complicated. 23 // 24 // See https://github.com/AuburnSounds/Dplug/wiki/Roles-of-the-PresetBank 25 // for an explanation of the bizarre "PresetBank". 26 27 /// Dplug chunks start with this. 28 enum uint DPLUG_MAGIC = 0xB20BA92; 29 30 /// I can see no reason why Dplug shouldn't be able to maintain 31 /// state chunks backward-compatibility with older versions in the future. 32 /// However, never say never. 33 /// This number will be incremented for every backward-incompatible change. 34 version(futureBinState) enum int DPLUG_SERIALIZATION_MAJOR_VERSION = 1; 35 else enum int DPLUG_SERIALIZATION_MAJOR_VERSION = 0; 36 37 /// This number will be incremented for every backward-compatible change 38 /// that is significant enough to bump a version number 39 enum int DPLUG_SERIALIZATION_MINOR_VERSION = 0; 40 41 // Compilation failures for futureBinState in combinations that don't work (yet!) 42 version(futureBinState) 43 { 44 version(VST2) 45 { 46 version(legacyVST2Chunks) static assert(0, "futureBinState requires futureVST2Chunks for VST2!"); 47 } 48 } 49 50 /// A preset is a slot in a plugin preset list 51 final class Preset 52 { 53 public: 54 55 this(string name, const(float)[] normalizedParams, const(ubyte)[] stateData = null) nothrow @nogc 56 { 57 _name = name.mallocDup; 58 _normalizedParams = normalizedParams.mallocDup; 59 if (stateData) 60 _stateData = stateData.mallocDup; 61 } 62 63 ~this() nothrow @nogc 64 { 65 clearName(); 66 free(_normalizedParams.ptr); 67 clearData(); 68 } 69 70 void setNormalized(int paramIndex, float value) nothrow @nogc 71 { 72 _normalizedParams[paramIndex] = value; 73 } 74 75 const(char)[] name() pure nothrow @nogc 76 { 77 return _name; 78 } 79 80 void setName(const(char)[] newName) nothrow @nogc 81 { 82 clearName(); 83 _name = newName.mallocDup; 84 } 85 86 version(futureBinState) void setStateData(ubyte[] data) nothrow @nogc 87 { 88 clearData(); 89 _stateData = data.mallocDup; 90 } 91 92 void saveFromHost(Client client) nothrow @nogc 93 { 94 auto params = client.params(); 95 foreach(size_t i, param; params) 96 { 97 _normalizedParams[i] = param.getNormalized(); 98 } 99 100 version(futureBinState) 101 {{ 102 clearData(); // Forget possible existing _stateData 103 Vec!ubyte chunk; 104 client.saveState(chunk); 105 _stateData = chunk.releaseData; 106 }} 107 } 108 109 // TODO: should this return an error if any encountered? 110 void loadFromHost(Client client) nothrow @nogc 111 { 112 auto params = client.params(); 113 foreach(size_t i, param; params) 114 { 115 if (i < _normalizedParams.length) 116 param.setFromHost(_normalizedParams[i]); 117 else 118 { 119 // this is a new parameter that old presets don't know, set default 120 param.setFromHost(param.getNormalizedDefault()); 121 } 122 } 123 124 version(futureBinState) 125 { 126 // loadFromHost seems to best effort, no error reported on parse failure 127 client.loadState(_stateData); 128 } 129 } 130 131 static bool isValidNormalizedParam(float f) nothrow @nogc 132 { 133 return (isFinite(f) && f >= 0 && f <= 1); 134 } 135 136 inout(float)[] getNormalizedParamValues() inout nothrow @nogc 137 { 138 return _normalizedParams; 139 } 140 141 version(futureBinState) ubyte[] getStateData() nothrow @nogc 142 { 143 return _stateData; 144 } 145 146 private: 147 char[] _name; 148 float[] _normalizedParams; 149 ubyte[] _stateData; 150 151 void clearName() nothrow @nogc 152 { 153 if (_name !is null) 154 { 155 free(_name.ptr); 156 _name = null; 157 } 158 } 159 160 void clearData() nothrow @nogc 161 { 162 if (_stateData !is null) 163 { 164 free(_stateData.ptr); 165 _stateData = null; 166 } 167 } 168 } 169 170 /// A preset bank is a collection of presets 171 final class PresetBank 172 { 173 public: 174 nothrow: 175 @nogc: 176 177 // Extends an array or Preset 178 Vec!Preset presets; 179 180 // Create a preset bank 181 // Takes ownership of this slice, which must be allocated with `malloc`, 182 // containing presets allocated with `mallocEmplace`. 183 this(Client client, Preset[] presets_) 184 { 185 _client = client; 186 187 // Copy presets to own them 188 presets = makeVec!Preset(presets_.length); 189 foreach(size_t i; 0..presets_.length) 190 presets[i] = presets_[i]; 191 192 // free input slice with `free` 193 free(presets_.ptr); 194 195 _current = 0; 196 } 197 198 ~this() 199 { 200 // free all presets 201 foreach(p; presets) 202 { 203 // if you hit a break-point here, maybe your 204 // presets weren't allocated with `mallocEmplace` 205 p.destroyFree(); 206 } 207 } 208 209 inout(Preset) preset(int i) inout 210 { 211 return presets[i]; 212 } 213 214 int numPresets() 215 { 216 return cast(int)presets.length; 217 } 218 219 int currentPresetIndex() 220 { 221 return _current; 222 } 223 224 Preset currentPreset() 225 { 226 int ind = currentPresetIndex(); 227 if (!isValidPresetIndex(ind)) 228 return null; 229 return presets[ind]; 230 } 231 232 bool isValidPresetIndex(int index) 233 { 234 return index >= 0 && index < numPresets(); 235 } 236 237 // Save current state to current preset. This updates the preset bank to reflect the state change. 238 // This will be unnecessary once we haver internal preset management. 239 void putCurrentStateInCurrentPreset() 240 { 241 presets[_current].saveFromHost(_client); 242 } 243 244 void loadPresetByNameFromHost(string name) 245 { 246 foreach(size_t index, preset; presets) 247 if (preset.name == name) 248 loadPresetFromHost(cast(int)index); 249 } 250 251 void loadPresetFromHost(int index) 252 { 253 putCurrentStateInCurrentPreset(); 254 presets[index].loadFromHost(_client); 255 _current = index; 256 } 257 258 /// Enqueue a new preset and load it 259 void addNewDefaultPresetFromHost(string presetName) 260 { 261 Parameter[] params = _client.params; 262 float[] values = mallocSlice!float(params.length); 263 scope(exit) values.freeSlice(); 264 265 foreach(size_t i, param; _client.params) 266 values[i] = param.getNormalizedDefault(); 267 268 presets.pushBack(mallocNew!Preset(presetName, values)); 269 loadPresetFromHost(cast(int)(presets.length) - 1); 270 } 271 272 /// Gets a state chunk to save the current state. 273 /// The returned state chunk should be freed with `free`. 274 ubyte[] getStateChunkFromCurrentState() 275 { 276 auto chunk = makeVec!ubyte(); 277 this.writeStateChunk(chunk); 278 return chunk.releaseData; 279 } 280 281 /// Gets a state chunk to save the current state, but provide a `Vec` to append to. 282 /// 283 /// Existing `chunk` content is preserved and appended to. 284 /// 285 /// This is faster than allocating a new state chunk everytime. 286 void appendStateChunkFromCurrentState(ref Vec!ubyte chunk) 287 { 288 this.writeStateChunk(chunk); 289 } 290 291 /// Gets a state chunk that would be the current state _if_ 292 /// preset `presetIndex` was made current first. So it's not 293 /// changing the client state. 294 /// The returned state chunk should be freed with `free()`. 295 ubyte[] getStateChunkFromPreset(int presetIndex) 296 { 297 auto chunk = makeVec!ubyte(); 298 this.writePresetChunkData(chunk, presetIndex); 299 return chunk.releaseData; 300 } 301 302 /// Loads a chunk state, update current state. 303 /// Return: *err set to true in case of error. 304 void loadStateChunk(const(ubyte)[] chunk, bool* err) 305 { 306 int mVersion = checkChunkHeader(chunk, err); 307 if (*err) 308 return; 309 310 // This avoid to overwrite the preset 0 while we modified preset N 311 int presetIndex = chunk.popLE!int(err); 312 if (*err) 313 return; 314 315 if (!isValidPresetIndex(presetIndex)) 316 { 317 *err = true; // Invalid preset index in state chunk 318 return; 319 } 320 else 321 _current = presetIndex; 322 323 version(futureBinState) 324 { 325 // Binary state future tag. 326 if (mVersion == 0) 327 { 328 loadChunkV1(chunk, err); 329 if (*err) 330 return; 331 } 332 else 333 { 334 loadChunkV2(chunk, err); 335 if (*err) 336 return; 337 } 338 } 339 else 340 { 341 342 loadChunkV1(chunk, err); 343 if (*err) 344 return; 345 } 346 347 *err = false; 348 } 349 350 private: 351 Client _client; 352 int _current; // should this be only in VST client? 353 354 // Loads chunk without binary save state 355 void loadChunkV1(ref const(ubyte)[] chunk, bool* err) nothrow @nogc 356 { 357 // Load parameters values 358 auto params = _client.params(); 359 int numParams = chunk.popLE!int(err); if (*err) return; 360 foreach(int i; 0..numParams) 361 { 362 float normalized = chunk.popLE!float(err); if (*err) return; 363 if (i < params.length) 364 params[i].setFromHost(normalized); 365 } 366 *err = false; 367 } 368 369 version(futureBinState) 370 { 371 // Loads chunk with binary save state 372 void loadChunkV2(ref const(ubyte)[] chunk, bool* err) nothrow @nogc 373 { 374 // Load parameters values 375 auto params = _client.params(); 376 int numParams = chunk.popLE!int(err); if (*err) return; 377 foreach(int i; 0..numParams) 378 { 379 float normalized = chunk.popLE!float(err); if (*err) return; 380 if (i < params.length) 381 params[i].setFromHost(normalized); 382 } 383 384 int dataLength = chunk.popLE!int(err); if (*err) return; 385 bool parseSuccess = _client.loadState(chunk[0..dataLength]); 386 if (!parseSuccess) 387 { 388 *err = true; // Invalid user-defined state chunk 389 return; 390 } 391 392 *err = false; 393 } 394 } 395 396 version(futureBinState) 397 { 398 // Writes preset chunk with V2 method 399 void writeStateChunk(ref Vec!ubyte chunk) nothrow @nogc 400 { 401 writeChunkHeader(chunk, 1); 402 403 auto params = _client.params(); 404 405 chunk.writeLE!int(_current); 406 407 chunk.writeLE!int(cast(int)params.length); 408 foreach(param; params) 409 chunk.writeLE!float(param.getNormalized()); 410 411 size_t chunkLenIndex = chunk.length; 412 chunk.writeLE!int(0); // temporary chunk length value 413 414 size_t before = chunk.length; 415 _client.saveState(chunk); // append state to chunk 416 size_t after = chunk.length; 417 418 // Write actual value for chunk length. 419 ubyte[] indexPos = chunk.ptr[chunkLenIndex.. chunkLenIndex+4]; 420 indexPos.writeLE!int(cast(int)(after - before)); 421 } 422 } else { 423 424 // Writes chunk with V1 method 425 void writeStateChunk(ref Vec!ubyte chunk) nothrow @nogc 426 { 427 writeChunkHeader(chunk, 0); 428 429 auto params = _client.params(); 430 431 chunk.writeLE!int(_current); 432 433 chunk.writeLE!int(cast(int)params.length); 434 foreach(param; params) 435 chunk.writeLE!float(param.getNormalized()); 436 } 437 } 438 439 version(futureBinState) 440 { 441 // Writes preset chunk with V2 method 442 void writePresetChunkData(ref Vec!ubyte chunk, int presetIndex) nothrow @nogc 443 { 444 writeChunkHeader(chunk, 1); 445 446 auto p = preset(presetIndex); 447 chunk.writeLE!int(presetIndex); 448 449 chunk.writeLE!int(cast(int)p._normalizedParams.length); 450 foreach(param; p._normalizedParams) 451 chunk.writeLE!float(param); 452 453 chunk.writeLE!int(cast(int)p._stateData.length); 454 chunk.pushBack(p._stateData[0..$]); 455 } 456 } 457 else 458 { 459 // Writes chunk with V1 method 460 void writePresetChunkData(ref Vec!ubyte chunk, int presetIndex) nothrow @nogc 461 { 462 writeChunkHeader(chunk, 0); 463 464 auto p = preset(presetIndex); 465 chunk.writeLE!int(presetIndex); 466 467 chunk.writeLE!int(cast(int)p._normalizedParams.length); 468 foreach(param; p._normalizedParams) 469 chunk.writeLE!float(param); 470 } 471 } 472 473 void writeChunkHeader(O)(auto ref O output, int version_=DPLUG_SERIALIZATION_MAJOR_VERSION) const @nogc if (isOutputRange!(O, ubyte)) 474 { 475 // write magic number and dplug version information (not the tag version) 476 output.writeBE!uint(DPLUG_MAGIC); 477 output.writeLE!int(DPLUG_SERIALIZATION_MAJOR_VERSION); 478 output.writeLE!int(DPLUG_SERIALIZATION_MINOR_VERSION); 479 480 // write plugin version 481 output.writeLE!int(_client.getPublicVersion().toAUVersion()); 482 } 483 484 // Return Dplug chunk major version, or -1 in case of error. 485 // *err set accordingly. 486 int checkChunkHeader(ref const(ubyte)[] input, bool* err) nothrow @nogc 487 { 488 // nothing to check with minor version 489 uint magic = input.popBE!uint(err); if (*err) return -1; 490 if (magic != DPLUG_MAGIC) 491 { 492 *err = true; // Can not load, magic number didn't match 493 return -1; 494 } 495 496 // nothing to check with minor version 497 int dplugMajor = input.popLE!int(err); if (*err) return -1; 498 if (dplugMajor > DPLUG_SERIALIZATION_MAJOR_VERSION) 499 { 500 *err = true; // Can not load chunk done with a newer, incompatible dplug library 501 return -1; 502 } 503 504 int dplugMinor = input.popLE!int(err); if (*err) return -1; 505 // nothing to check with minor version 506 507 // TODO: how to handle breaking binary compatibility here? 508 int pluginVersion = input.popLE!int(err); 509 if (*err) 510 return -1; 511 512 *err = false; 513 return dplugMajor; 514 } 515 } 516 517 /// Loads an array of `Preset` from a FXB file content. 518 /// Gives ownership of the result, in a way that can be returned by `buildPresets`. 519 /// IMPORTANT: if you store your presets in FXB form, the following limitations 520 /// * One _add_ new parameters to the plug-in, no reorder or deletion 521 /// * Don't remap the parameter (in a way that changes its normalized value) 522 /// They are the same limitations that exist in Dplug in minor plugin version. 523 /// 524 /// Params: 525 /// maxCount Maximum number of presets to take, -1 for all of them 526 /// 527 /// Example: 528 /// override Preset[] buildPresets() 529 /// { 530 /// return loadPresetsFromFXB(this, import("factory-presets.fxb")); 531 /// } 532 /// 533 /// Note: It is **heavily recommended** to create the FXB chunk with the Dplug `presetbank` tool. 534 /// Else it is unknown if it will work. 535 Preset[] loadPresetsFromFXB(Client client, string inputFBXData, int maxCount = -1) nothrow @nogc 536 { 537 ubyte[] fbxCopy = cast(ubyte[]) mallocDup(inputFBXData); 538 const(ubyte)[] inputFXB = fbxCopy; 539 scope(exit) free(fbxCopy.ptr); 540 541 Vec!Preset result = makeVec!Preset(); 542 543 static int CCONST(int a, int b, int c, int d) pure nothrow @nogc 544 { 545 return (a << 24) | (b << 16) | (c << 8) | (d << 0); 546 } 547 548 void parse(bool* err) 549 { 550 uint bankChunkID; 551 uint bankChunkLen; 552 inputFXB.readRIFFChunkHeader(bankChunkID, bankChunkLen, err); if (*err) return; 553 554 if (bankChunkID != CCONST('C', 'c', 'n', 'K')) 555 { 556 *err = true; 557 return; 558 } 559 560 inputFXB.skipBytes(bankChunkLen, err); if (*err) return; 561 uint fbxChunkID = inputFXB.popBE!uint(err); if (*err) return; 562 563 if (fbxChunkID != CCONST('F', 'x', 'B', 'k')) 564 { 565 *err = true; 566 return; 567 } 568 inputFXB.skipBytes(4, err); if (*err) return; // parse fxVersion 569 570 // if uniqueID has changed, then the bank is not compatible and should error 571 char[4] uid = client.getPluginUniqueID(); 572 uint uidChunk = inputFXB.popBE!uint(err); if (*err) return; 573 if (uidChunk != CCONST(uid[0], uid[1], uid[2], uid[3])) 574 { 575 *err = true; // chunk is not for this plugin 576 return; 577 } 578 579 // fxVersion. We ignore it, since compat is supposed 580 // to be encoded in the unique ID already 581 inputFXB.popBE!uint(err); if (*err) return; 582 583 int numPresets = inputFXB.popBE!int(err); if (*err) return; 584 if ((maxCount != -1) && (numPresets > maxCount)) 585 numPresets = maxCount; 586 if (numPresets < 1) 587 { 588 *err = true; // no preset in bank, probably not what you want 589 return; 590 } 591 592 inputFXB.skipBytes(128, err); if (*err) return; 593 594 // Create presets 595 for(int presetIndex = 0; presetIndex < numPresets; ++presetIndex) 596 { 597 Preset p = client.makeDefaultPreset(); 598 uint presetChunkID; 599 uint presetChunkLen; 600 inputFXB.readRIFFChunkHeader(presetChunkID, presetChunkLen, err); if (*err) return; 601 if (presetChunkID != CCONST('C', 'c', 'n', 'K')) 602 { 603 *err = true; 604 return; 605 } 606 inputFXB.skipBytes(presetChunkLen, err); if (*err) return; 607 608 presetChunkID = inputFXB.popBE!uint(err); if (*err) return; 609 if (presetChunkID != CCONST('F', 'x', 'C', 'k')) 610 { 611 *err = true; 612 return; 613 } 614 int presetVersion = inputFXB.popBE!uint(err); if (*err) return; 615 if (presetVersion != 1) 616 { 617 *err = true; 618 return; 619 } 620 621 uint uidChunk2 = inputFXB.popBE!uint(err); if (*err) return; 622 if (uidChunk2 != CCONST(uid[0], uid[1], uid[2], uid[3])) 623 { 624 *err = true; 625 return; 626 } 627 628 // fxVersion. We ignore it, since compat is supposed 629 // to be encoded in the unique ID already 630 inputFXB.skipBytes(4, err); if (*err) return; 631 632 int numParams = inputFXB.popBE!int(err); if (*err) return; 633 if (numParams < 0) 634 { 635 *err = true; 636 return; 637 } 638 639 // parse name 640 char[28] nameBuf; 641 int nameLen = 28; 642 foreach(nch; 0..28) 643 { 644 char c = cast(char) inputFXB.popBE!ubyte(err); if (*err) return; 645 nameBuf[nch] = c; 646 if (c == '\0' && nameLen == 28) 647 nameLen = nch; 648 } 649 p.setName(nameBuf[0..nameLen]); 650 651 // parse parameter normalized values 652 int paramRead = numParams; 653 if (paramRead > cast(int)(client.params.length)) 654 paramRead = cast(int)(client.params.length); 655 for (int param = 0; param < paramRead; ++param) 656 { 657 float pval = inputFXB.popBE!float(err); if (*err) return; 658 p.setNormalized(param, pval); 659 } 660 661 // skip excess parameters (this case should never happen so not sure if it's to be handled) 662 for (int param = paramRead; param < numParams; ++param) 663 { 664 inputFXB.skipBytes(4, err); if (*err) return; 665 } 666 667 result.pushBack(p); 668 } 669 } 670 671 bool err; 672 parse(&err); 673 674 if (err) 675 { 676 // Your preset file for the plugin is not meant to be invalid, so this is a bug. 677 // If you fail here, parsing has created an `error()` call. 678 assert(false); 679 } 680 681 return result.releaseData(); 682 }