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 20 import dplug.client.binrange; 21 import dplug.client.client; 22 import dplug.client.params; 23 24 // The current situation is really complicated. 25 // 26 // There are 3 types of chunks: 27 // - "preset" and "bank" chunks are used by VST2. The whole preset "bank" structure exists for VST2. 28 // Changing a preset and loading another changes the bank. VST2 is the only format that could 29 // store unused presets. 30 // - AU and AAX uses "state chunks" which are storing a single preset and a preset index. 31 // On load, the bank from factory is restored but the single preset stored will be changed. 32 // However, in AU and AAX the whole concept of the preset bank is there for nothing. 33 34 /// I can see no reason why dplug shouldn't be able to maintain 35 /// backward-compatibility with older versions in the future. 36 /// However, never say never. 37 /// This number will be incremented for every backward-incompatible change. 38 enum int DPLUG_SERIALIZATION_MAJOR_VERSION = 0; 39 40 /// This number will be incremented for every backward-compatible change 41 /// that is significant enough to bump a version number 42 enum int DPLUG_SERIALIZATION_MINOR_VERSION = 0; 43 44 /// A preset is a slot in a plugin preset list 45 final class Preset 46 { 47 public: 48 49 this(string name, const(float)[] normalizedParams) nothrow @nogc 50 { 51 _name = name.mallocDup; 52 _normalizedParams = normalizedParams.mallocDup; 53 } 54 55 ~this() nothrow @nogc 56 { 57 clearName(); 58 free(_normalizedParams.ptr); 59 } 60 61 void setNormalized(int paramIndex, float value) nothrow @nogc 62 { 63 _normalizedParams[paramIndex] = value; 64 } 65 66 const(char)[] name() pure nothrow @nogc 67 { 68 return _name; 69 } 70 71 void setName(const(char)[] newName) nothrow @nogc 72 { 73 clearName(); 74 _name = newName.mallocDup; 75 } 76 77 void saveFromHost(Client client) nothrow @nogc 78 { 79 auto params = client.params(); 80 foreach(int i, param; params) 81 { 82 _normalizedParams[i] = param.getNormalized(); 83 } 84 } 85 86 void loadFromHost(Client client) nothrow @nogc 87 { 88 auto params = client.params(); 89 foreach(int i, param; params) 90 { 91 if (i < _normalizedParams.length) 92 param.setFromHost(_normalizedParams[i]); 93 else 94 { 95 // this is a new parameter that old presets don't know, set default 96 param.setFromHost(param.getNormalizedDefault()); 97 } 98 } 99 } 100 101 void serializeBinary(O)(auto ref O output) nothrow @nogc if (isOutputRange!(O, ubyte)) 102 { 103 output.writeLE!int(cast(int)_name.length); 104 105 foreach(i; 0..name.length) 106 output.writeLE!ubyte(_name[i]); 107 108 output.writeLE!int(cast(int)_normalizedParams.length); 109 110 foreach(np; _normalizedParams) 111 output.writeLE!float(np); 112 } 113 114 /// Throws: A `mallocEmplace`d `Exception` 115 void unserializeBinary(ref ubyte[] input) @nogc 116 { 117 clearName(); 118 int nameLength = input.popLE!int(); 119 _name = mallocSlice!char(nameLength); 120 foreach(i; 0..nameLength) 121 { 122 ubyte ch = input.popLE!ubyte(); 123 _name[i] = ch; 124 } 125 126 int paramCount = input.popLE!int(); 127 128 foreach(int ip; 0..paramCount) 129 { 130 float f = input.popLE!float(); 131 132 // MAYDO: best-effort recovery? 133 if (!isValidNormalizedParam(f)) 134 throw mallocNew!Exception("Couldn't unserialize preset: an invalid float parameter was parsed"); 135 136 // There may be more parameters when downgrading 137 if (ip < _normalizedParams.length) 138 _normalizedParams[ip] = f; 139 } 140 } 141 142 static bool isValidNormalizedParam(float f) nothrow @nogc 143 { 144 return (isFinite(f) && f >= 0 && f <= 1); 145 } 146 147 private: 148 char[] _name; 149 float[] _normalizedParams; 150 151 void clearName() nothrow @nogc 152 { 153 if (_name !is null) 154 { 155 free(_name.ptr); 156 _name = null; 157 } 158 } 159 } 160 161 /// A preset bank is a collection of presets 162 final class PresetBank 163 { 164 public: 165 166 // Extends an array or Preset 167 Vec!Preset presets; 168 169 // Create a preset bank 170 // Takes ownership of this slice, which must be allocated with `malloc`, 171 // containing presets allocated with `mallocEmplace`. 172 this(Client client, Preset[] presets_) nothrow @nogc 173 { 174 _client = client; 175 176 // Copy presets to own them 177 presets = makeVec!Preset(presets_.length); 178 foreach(size_t i; 0..presets_.length) 179 presets[i] = presets_[i]; 180 _current = 0; 181 } 182 183 ~this() nothrow @nogc 184 { 185 // free all presets 186 foreach(p; presets) 187 { 188 // if you hit a break-point here, maybe your 189 // presets weren't allocated with `mallocEmplace` 190 p.destroyFree(); 191 } 192 } 193 194 inout(Preset) preset(int i) inout nothrow @nogc 195 { 196 return presets[i]; 197 } 198 199 int numPresets() nothrow @nogc 200 { 201 return cast(int)presets.length; 202 } 203 204 int currentPresetIndex() nothrow @nogc 205 { 206 return _current; 207 } 208 209 Preset currentPreset() nothrow @nogc 210 { 211 int ind = currentPresetIndex(); 212 if (!isValidPresetIndex(ind)) 213 return null; 214 return presets[ind]; 215 } 216 217 bool isValidPresetIndex(int index) nothrow @nogc 218 { 219 return index >= 0 && index < numPresets(); 220 } 221 222 // Save current state to current preset. This updates the preset bank to reflect the state change. 223 // This will be unnecessary once we haver internal preset management. 224 void putCurrentStateInCurrentPreset() nothrow @nogc 225 { 226 presets[_current].saveFromHost(_client); 227 } 228 229 void loadPresetByNameFromHost(string name) nothrow @nogc 230 { 231 foreach(int index, preset; presets) 232 if (preset.name == name) 233 loadPresetFromHost(index); 234 } 235 236 void loadPresetFromHost(int index) nothrow @nogc 237 { 238 putCurrentStateInCurrentPreset(); 239 presets[index].loadFromHost(_client); 240 _current = index; 241 } 242 243 /// Enqueue a new preset and load it 244 void addNewDefaultPresetFromHost(string presetName) nothrow @nogc 245 { 246 Parameter[] params = _client.params; 247 float[] values = mallocSlice!float(params.length); 248 scope(exit) values.freeSlice(); 249 250 foreach(int i, param; _client.params) 251 values[i] = param.getNormalizedDefault(); 252 253 presets.pushBack(mallocNew!Preset(presetName, values)); 254 loadPresetFromHost(cast(int)(presets.length) - 1); 255 } 256 257 /// Allocates and fill a preset chunk 258 /// The resulting buffer should be freed with `free`. 259 ubyte[] getPresetChunk(int index) nothrow @nogc 260 { 261 auto chunk = makeVec!ubyte(); 262 writeChunkHeader(chunk); 263 presets[index].serializeBinary(chunk); 264 return chunk.releaseData(); 265 } 266 267 /// Parse a preset chunk and set parameters. 268 /// May throw an Exception. 269 void loadPresetChunk(int index, ubyte[] chunk) 270 { 271 checkChunkHeader(chunk); 272 presets[index].unserializeBinary(chunk); 273 274 // Not sure why it's there in IPlug, this whole function is probably not 275 // doing what it should 276 //putCurrentStateInCurrentPreset(); 277 } 278 279 /// Allocate and fill a bank chunk 280 /// The resulting buffer should be freed with `free`. 281 ubyte[] getBankChunk() nothrow @nogc 282 { 283 putCurrentStateInCurrentPreset(); 284 auto chunk = makeVec!ubyte(); 285 writeChunkHeader(chunk); 286 287 // write number of presets 288 chunk.writeLE!int(cast(int)(presets.length)); 289 290 foreach(int i, preset; presets) 291 preset.serializeBinary(chunk); 292 return chunk.releaseData(); 293 } 294 295 /// Parse a bank chunk and set parameters. 296 /// May throw an Exception. 297 void loadBankChunk(ubyte[] chunk) @nogc 298 { 299 checkChunkHeader(chunk); 300 301 int numPresets = chunk.popLE!int(); 302 303 // TODO: is there a way to have a dynamic number of presets in the bank? Check with VST and AU 304 numPresets = min(numPresets, presets.length); 305 foreach(preset; presets[0..numPresets]) 306 preset.unserializeBinary(chunk); 307 } 308 309 /// Gets a state chunk to save the current state. 310 /// The returned state chunk should be freed with `free`. 311 deprecated("Use getStateChunkFromCurrentState() instead") alias getStateChunk = getStateChunkFromCurrentState; 312 ubyte[] getStateChunkFromCurrentState() nothrow @nogc 313 { 314 auto chunk = makeVec!ubyte(); 315 writeChunkHeader(chunk); 316 317 auto params = _client.params(); 318 319 chunk.writeLE!int(_current); 320 321 chunk.writeLE!int(cast(int)params.length); 322 foreach(param; params) 323 chunk.writeLE!float(param.getNormalized()); 324 return chunk.releaseData; 325 } 326 327 /// Gets a state chunk that would be the current state _if_ 328 /// preset `presetIndex` was made current first. So it's not 329 /// changing the client state. 330 /// The returned state chunk should be freed with `free()`. 331 ubyte[] getStateChunkFromPreset(int presetIndex) const nothrow @nogc 332 { 333 auto chunk = makeVec!ubyte(); 334 writeChunkHeader(chunk); 335 336 auto p = preset(presetIndex); 337 chunk.writeLE!int(presetIndex); 338 339 chunk.writeLE!int(cast(int)p._normalizedParams.length); 340 foreach(param; p._normalizedParams) 341 chunk.writeLE!float(param); 342 return chunk.releaseData; 343 } 344 345 /// Loads a chunk state, update current state. 346 /// May throw an Exception. 347 void loadStateChunk(ubyte[] chunk) @nogc 348 { 349 checkChunkHeader(chunk); 350 351 // This avoid to overwrite the preset 0 while we modified preset N 352 int presetIndex = chunk.popLE!int(); 353 if (!isValidPresetIndex(presetIndex)) 354 throw mallocNew!Exception("Invalid preset index in state chunk"); 355 else 356 _current = presetIndex; 357 358 // Load parameters values 359 auto params = _client.params(); 360 int numParams = chunk.popLE!int(); 361 foreach(int i; 0..numParams) 362 { 363 float normalized = chunk.popLE!float(); 364 if (i < params.length) 365 params[i].setFromHost(normalized); 366 } 367 } 368 369 private: 370 Client _client; 371 int _current; // should this be only in VST client? 372 373 enum uint DPLUG_MAGIC = 0xB20BA92; 374 375 void writeChunkHeader(O)(auto ref O output) const @nogc if (isOutputRange!(O, ubyte)) 376 { 377 // write magic number and dplug version information (not the tag version) 378 output.writeBE!uint(DPLUG_MAGIC); 379 output.writeLE!int(DPLUG_SERIALIZATION_MAJOR_VERSION); 380 output.writeLE!int(DPLUG_SERIALIZATION_MINOR_VERSION); 381 382 // write plugin version 383 output.writeLE!int(_client.getPublicVersion().toAUVersion()); 384 } 385 386 void checkChunkHeader(ref ubyte[] input) @nogc 387 { 388 // nothing to check with minor version 389 uint magic = input.popBE!uint(); 390 if (magic != DPLUG_MAGIC) 391 throw mallocNew!Exception("Can not load, magic number didn't match"); 392 393 // nothing to check with minor version 394 int dplugMajor = input.popLE!int(); 395 if (dplugMajor > DPLUG_SERIALIZATION_MAJOR_VERSION) 396 throw mallocNew!Exception("Can not load chunk done with a newer, incompatible dplug library"); 397 398 int dplugMinor = input.popLE!int(); 399 // nothing to check with minor version 400 401 // TODO: how to handle breaking binary compatibility here? 402 int pluginVersion = input.popLE!int(); 403 } 404 }