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; 14 import std.array; 15 import std.algorithm.comparison; 16 17 import dplug.core.vec; 18 import dplug.core.nogc; 19 import dplug.core.binrange; 20 21 import dplug.client.client; 22 import dplug.client.params; 23 24 // The current situation is quite complicated. 25 // 26 // See https://github.com/AuburnSounds/Dplug/wiki/Roles-of-the-PresetBank 27 // for an explanation of the bizarre "PresetBank". 28 29 /// Dplug chunks start with this. 30 enum uint DPLUG_MAGIC = 0xB20BA92; 31 32 /// I can see no reason why Dplug shouldn't be able to maintain 33 /// state chunks backward-compatibility with older versions in the future. 34 /// However, never say never. 35 /// This number will be incremented for every backward-incompatible change. 36 enum int DPLUG_SERIALIZATION_MAJOR_VERSION = 0; 37 38 /// This number will be incremented for every backward-compatible change 39 /// that is significant enough to bump a version number 40 enum int DPLUG_SERIALIZATION_MINOR_VERSION = 0; 41 42 /// A preset is a slot in a plugin preset list 43 final class Preset 44 { 45 public: 46 47 this(string name, const(float)[] normalizedParams) nothrow @nogc 48 { 49 _name = name.mallocDup; 50 _normalizedParams = normalizedParams.mallocDup; 51 } 52 53 ~this() nothrow @nogc 54 { 55 clearName(); 56 free(_normalizedParams.ptr); 57 } 58 59 void setNormalized(int paramIndex, float value) nothrow @nogc 60 { 61 _normalizedParams[paramIndex] = value; 62 } 63 64 const(char)[] name() pure nothrow @nogc 65 { 66 return _name; 67 } 68 69 void setName(const(char)[] newName) nothrow @nogc 70 { 71 clearName(); 72 _name = newName.mallocDup; 73 } 74 75 void saveFromHost(Client client) nothrow @nogc 76 { 77 auto params = client.params(); 78 foreach(size_t i, param; params) 79 { 80 _normalizedParams[i] = param.getNormalized(); 81 } 82 } 83 84 void loadFromHost(Client client) nothrow @nogc 85 { 86 auto params = client.params(); 87 foreach(size_t i, param; params) 88 { 89 if (i < _normalizedParams.length) 90 param.setFromHost(_normalizedParams[i]); 91 else 92 { 93 // this is a new parameter that old presets don't know, set default 94 param.setFromHost(param.getNormalizedDefault()); 95 } 96 } 97 } 98 99 void serializeBinary(O)(auto ref O output) nothrow @nogc if (isOutputRange!(O, ubyte)) 100 { 101 output.writeLE!int(cast(int)_name.length); 102 103 foreach(i; 0..name.length) 104 output.writeLE!ubyte(_name[i]); 105 106 output.writeLE!int(cast(int)_normalizedParams.length); 107 108 foreach(np; _normalizedParams) 109 output.writeLE!float(np); 110 } 111 112 /// Throws: A `mallocEmplace`d `Exception` 113 void unserializeBinary(ref ubyte[] input) @nogc 114 { 115 clearName(); 116 int nameLength = input.popLE!int(); 117 _name = mallocSlice!char(nameLength); 118 foreach(i; 0..nameLength) 119 { 120 ubyte ch = input.popLE!ubyte(); 121 _name[i] = ch; 122 } 123 124 int paramCount = input.popLE!int(); 125 126 foreach(int ip; 0..paramCount) 127 { 128 float f = input.popLE!float(); 129 130 // MAYDO: best-effort recovery? 131 if (!isValidNormalizedParam(f)) 132 throw mallocNew!Exception("Couldn't unserialize preset: an invalid float parameter was parsed"); 133 134 // There may be more parameters when downgrading 135 if (ip < _normalizedParams.length) 136 _normalizedParams[ip] = f; 137 } 138 } 139 140 static bool isValidNormalizedParam(float f) nothrow @nogc 141 { 142 return (isFinite(f) && f >= 0 && f <= 1); 143 } 144 145 inout(float)[] getNormalizedParamValues() inout nothrow @nogc 146 { 147 return _normalizedParams; 148 } 149 150 private: 151 char[] _name; 152 float[] _normalizedParams; 153 154 void clearName() nothrow @nogc 155 { 156 if (_name !is null) 157 { 158 free(_name.ptr); 159 _name = null; 160 } 161 } 162 } 163 164 /// A preset bank is a collection of presets 165 final class PresetBank 166 { 167 public: 168 169 // Extends an array or Preset 170 Vec!Preset presets; 171 172 // Create a preset bank 173 // Takes ownership of this slice, which must be allocated with `malloc`, 174 // containing presets allocated with `mallocEmplace`. 175 this(Client client, Preset[] presets_) nothrow @nogc 176 { 177 _client = client; 178 179 // Copy presets to own them 180 presets = makeVec!Preset(presets_.length); 181 foreach(size_t i; 0..presets_.length) 182 presets[i] = presets_[i]; 183 184 // free input slice with `free` 185 free(presets_.ptr); 186 187 _current = 0; 188 } 189 190 ~this() nothrow @nogc 191 { 192 // free all presets 193 foreach(p; presets) 194 { 195 // if you hit a break-point here, maybe your 196 // presets weren't allocated with `mallocEmplace` 197 p.destroyFree(); 198 } 199 } 200 201 inout(Preset) preset(int i) inout nothrow @nogc 202 { 203 return presets[i]; 204 } 205 206 int numPresets() nothrow @nogc 207 { 208 return cast(int)presets.length; 209 } 210 211 int currentPresetIndex() nothrow @nogc 212 { 213 return _current; 214 } 215 216 Preset currentPreset() nothrow @nogc 217 { 218 int ind = currentPresetIndex(); 219 if (!isValidPresetIndex(ind)) 220 return null; 221 return presets[ind]; 222 } 223 224 bool isValidPresetIndex(int index) nothrow @nogc 225 { 226 return index >= 0 && index < numPresets(); 227 } 228 229 // Save current state to current preset. This updates the preset bank to reflect the state change. 230 // This will be unnecessary once we haver internal preset management. 231 void putCurrentStateInCurrentPreset() nothrow @nogc 232 { 233 presets[_current].saveFromHost(_client); 234 } 235 236 void loadPresetByNameFromHost(string name) nothrow @nogc 237 { 238 foreach(size_t index, preset; presets) 239 if (preset.name == name) 240 loadPresetFromHost(cast(int)index); 241 } 242 243 void loadPresetFromHost(int index) nothrow @nogc 244 { 245 putCurrentStateInCurrentPreset(); 246 presets[index].loadFromHost(_client); 247 _current = index; 248 } 249 250 /// Enqueue a new preset and load it 251 void addNewDefaultPresetFromHost(string presetName) nothrow @nogc 252 { 253 Parameter[] params = _client.params; 254 float[] values = mallocSlice!float(params.length); 255 scope(exit) values.freeSlice(); 256 257 foreach(size_t i, param; _client.params) 258 values[i] = param.getNormalizedDefault(); 259 260 presets.pushBack(mallocNew!Preset(presetName, values)); 261 loadPresetFromHost(cast(int)(presets.length) - 1); 262 } 263 264 /// Gets a state chunk to save the current state. 265 /// The returned state chunk should be freed with `free`. 266 ubyte[] getStateChunkFromCurrentState() nothrow @nogc 267 { 268 auto chunk = makeVec!ubyte(); 269 writeChunkHeader(chunk); 270 271 auto params = _client.params(); 272 273 chunk.writeLE!int(_current); 274 275 chunk.writeLE!int(cast(int)params.length); 276 foreach(param; params) 277 chunk.writeLE!float(param.getNormalized()); 278 return chunk.releaseData; 279 } 280 281 /// Gets a state chunk that would be the current state _if_ 282 /// preset `presetIndex` was made current first. So it's not 283 /// changing the client state. 284 /// The returned state chunk should be freed with `free()`. 285 ubyte[] getStateChunkFromPreset(int presetIndex) const nothrow @nogc 286 { 287 auto chunk = makeVec!ubyte(); 288 writeChunkHeader(chunk); 289 290 auto p = preset(presetIndex); 291 chunk.writeLE!int(presetIndex); 292 293 chunk.writeLE!int(cast(int)p._normalizedParams.length); 294 foreach(param; p._normalizedParams) 295 chunk.writeLE!float(param); 296 return chunk.releaseData; 297 } 298 299 /// Loads a chunk state, update current state. 300 /// May throw an Exception. 301 void loadStateChunk(ubyte[] chunk) @nogc 302 { 303 checkChunkHeader(chunk); 304 305 // This avoid to overwrite the preset 0 while we modified preset N 306 int presetIndex = chunk.popLE!int(); 307 if (!isValidPresetIndex(presetIndex)) 308 throw mallocNew!Exception("Invalid preset index in state chunk"); 309 else 310 _current = presetIndex; 311 312 // Load parameters values 313 auto params = _client.params(); 314 int numParams = chunk.popLE!int(); 315 foreach(int i; 0..numParams) 316 { 317 float normalized = chunk.popLE!float(); 318 if (i < params.length) 319 params[i].setFromHost(normalized); 320 } 321 } 322 323 private: 324 Client _client; 325 int _current; // should this be only in VST client? 326 327 void writeChunkHeader(O)(auto ref O output) const @nogc if (isOutputRange!(O, ubyte)) 328 { 329 // write magic number and dplug version information (not the tag version) 330 output.writeBE!uint(DPLUG_MAGIC); 331 output.writeLE!int(DPLUG_SERIALIZATION_MAJOR_VERSION); 332 output.writeLE!int(DPLUG_SERIALIZATION_MINOR_VERSION); 333 334 // write plugin version 335 output.writeLE!int(_client.getPublicVersion().toAUVersion()); 336 } 337 338 void checkChunkHeader(ref ubyte[] input) @nogc 339 { 340 // nothing to check with minor version 341 uint magic = input.popBE!uint(); 342 if (magic != DPLUG_MAGIC) 343 throw mallocNew!Exception("Can not load, magic number didn't match"); 344 345 // nothing to check with minor version 346 int dplugMajor = input.popLE!int(); 347 if (dplugMajor > DPLUG_SERIALIZATION_MAJOR_VERSION) 348 throw mallocNew!Exception("Can not load chunk done with a newer, incompatible dplug library"); 349 350 int dplugMinor = input.popLE!int(); 351 // nothing to check with minor version 352 353 // TODO: how to handle breaking binary compatibility here? 354 int pluginVersion = input.popLE!int(); 355 } 356 } 357 358 /// Loads an array of `Preset` from a FBX file content. 359 /// Gives ownership of the result, in a way that can be returned by `buildPresets`. 360 /// IMPORTANT: if you store your presets in FBX form, the following limitations 361 /// * One _add_ new parameters to the plug-in, no reorder or deletion 362 /// * Don't remap the parameter (in a way that changes its normalized value) 363 /// They are the same limitations that exist in Dplug in minor plugin version. 364 /// 365 /// Params: 366 /// maxCount Maximum number of presets to take, -1 for all of them 367 /// 368 /// Example: 369 /// override Preset[] buildPresets() 370 /// { 371 /// return loadPresetsFromFXB(this, import("factory-presets.fxb")); 372 /// } 373 /// 374 Preset[] loadPresetsFromFXB(Client client, string inputFBXData, int maxCount = -1) nothrow @nogc 375 { 376 ubyte[] fbxCopy = cast(ubyte[]) mallocDup(inputFBXData); 377 ubyte[] inputFXB = fbxCopy; 378 scope(exit) free(fbxCopy.ptr); 379 380 Vec!Preset result = makeVec!Preset(); 381 382 static int CCONST(int a, int b, int c, int d) pure nothrow @nogc 383 { 384 return (a << 24) | (b << 16) | (c << 8) | (d << 0); 385 } 386 387 try 388 { 389 uint bankChunkID; 390 uint bankChunkLen; 391 inputFXB.readRIFFChunkHeader(bankChunkID, bankChunkLen); 392 393 void error() @nogc 394 { 395 throw mallocNew!Exception("Error in parsing FXB"); 396 } 397 398 if (bankChunkID != CCONST('C', 'c', 'n', 'K')) error; 399 inputFXB.skipBytes(bankChunkLen); 400 uint fbxChunkID = inputFXB.popBE!uint(); 401 if (fbxChunkID != CCONST('F', 'x', 'B', 'k')) error; 402 inputFXB.skipBytes(4); // fxVersion 403 404 // if uniqueID has changed, then the bank is not compatible and should error 405 char[4] uid = client.getPluginUniqueID(); 406 if (inputFXB.popBE!uint() != CCONST(uid[0], uid[1], uid[2], uid[3])) error; 407 408 // fxVersion. We ignore it, since compat is supposed 409 // to be encoded in the unique ID already 410 inputFXB.popBE!uint(); 411 412 int numPresets = inputFXB.popBE!int(); 413 if ((maxCount != -1) && (numPresets > maxCount)) 414 numPresets = maxCount; 415 if (numPresets < 1) error; // no preset in band, probably not what you want 416 417 inputFXB.skipBytes(128); 418 419 // Create presets 420 for(int presetIndex = 0; presetIndex < numPresets; ++presetIndex) 421 { 422 Preset p = client.makeDefaultPreset(); 423 uint presetChunkID; 424 uint presetChunkLen; 425 inputFXB.readRIFFChunkHeader(presetChunkID, presetChunkLen); 426 if (presetChunkID != CCONST('C', 'c', 'n', 'K')) error; 427 inputFXB.skipBytes(presetChunkLen); 428 429 presetChunkID = inputFXB.popBE!uint(); 430 if (presetChunkID != CCONST('F', 'x', 'C', 'k')) error; 431 int presetVersion = inputFXB.popBE!uint(); 432 if (presetVersion != 1) error; 433 if (inputFXB.popBE!uint() != CCONST(uid[0], uid[1], uid[2], uid[3])) error; 434 435 // fxVersion. We ignore it, since compat is supposed 436 // to be encoded in the unique ID already 437 inputFXB.skipBytes(4); 438 439 int numParams = inputFXB.popBE!int(); 440 if (numParams < 0) error; 441 442 // parse name 443 char[28] nameBuf; 444 int nameLen = 28; 445 foreach(nch; 0..28) 446 { 447 char c = inputFXB.front; 448 nameBuf[nch] = c; 449 inputFXB.popFront(); 450 if (c == '\0' && nameLen == 28) 451 nameLen = nch; 452 } 453 p.setName(nameBuf[0..nameLen]); 454 455 // parse parameter normalized values 456 int paramRead = numParams; 457 if (paramRead > cast(int)(client.params.length)) 458 paramRead = cast(int)(client.params.length); 459 for (int param = 0; param < paramRead; ++param) 460 { 461 p.setNormalized(param, inputFXB.popBE!float()); 462 } 463 464 // skip excess parameters (this case should never happen so not sure if it's to be handled) 465 for (int param = paramRead; param < numParams; ++param) 466 inputFXB.skipBytes(4); 467 468 result.pushBack(p); 469 } 470 } 471 catch(Exception e) 472 { 473 destroyFree(e); 474 475 // Your preset file for the plugin is not meant to be invalid, so this is a bug. 476 // If you fail here, parsing has created an `error()` call. 477 assert(false); 478 } 479 480 return result.releaseData(); 481 }