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 }