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 }