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