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 }