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 }