1 /**
2 * LV2 Client implementation
3 *
4 * Copyright: Ethan Reker 2018.
5 *            Guillaume Piolat 2019.
6 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 /*
9  * DISTRHO Plugin Framework (DPF)
10  * Copyright (C) 2012-2018 Filipe Coelho <falktx@falktx.com>
11  *
12  * Permission to use, copy, modify, and/or distribute this software for any purpose with
13  * or without fee is hereby granted, provided that the above copyright notice and this
14  * permission notice appear in all copies.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
17  * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
18  * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
19  * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
20  * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  */
23 
24 module dplug.lv2.lv2client;
25 
26 version(LV2):
27 
28 import std.string,
29        std.algorithm.comparison;
30 
31 import core.stdc.stdlib,
32        core.stdc.string,
33        core.stdc.stdio,
34        core.stdc.math,
35        core.stdc.stdint;
36 
37 import dplug.core.vec,
38        dplug.core.nogc,
39        dplug.core.math,
40        dplug.core.lockedqueue,
41        dplug.core.runtime,
42        dplug.core.fpcontrol,
43        dplug.core.thread,
44        dplug.core.sync;
45 
46 import dplug.client.client,
47        dplug.client.daw,
48        dplug.client.preset,
49        dplug.client.graphics,
50        dplug.client.midi,
51        dplug.client.params;
52 
53 import dplug.lv2.lv2,
54        dplug.lv2.midi,
55        dplug.lv2.ui,
56        dplug.lv2.options,
57        dplug.lv2.urid,
58        dplug.lv2.atom;
59 
60 //debug = debugLV2Client;
61 
62 nothrow:
63 @nogc:
64 
65 class LV2Client : IHostCommand
66 {
67 nothrow:
68 @nogc:
69 
70     Client _client;
71 
72     this(Client client, int legalIOIndex)
73     {
74         _client = client;
75 
76         // Implement IHostCommand itself
77         _client.setHostCommand(this);
78         _graphicsMutex = makeMutex();
79         _legalIOIndex = legalIOIndex;
80         _latencyOutput = null;
81         _eventsInput = null;
82         _eventsOutput = null;
83         _latencySamples = 0;
84         version(futureBinState)
85             initializeStateChunkTypeURI();
86     }
87 
88     ~this()
89     {
90         _client.destroyFree();
91 
92         _paramsPointers.reallocBuffer(0);
93         _paramsLastValue.reallocBuffer(0);
94 
95         _inputPointersProvided.freeSlice();
96         _outputPointersProvided.freeSlice();
97         _inputPointersProcessing.freeSlice();
98         _outputPointersProcessing.freeSlice();
99 
100         // destroy scratch buffers
101         for (int i = 0; i < _numInputs; ++i)
102             _inputScratchBuffer[i].destroy();
103         for (int i = 0; i < _numOutputs; ++i)
104             _outputScratchBuffer[i].destroy();
105         _inputScratchBuffer.freeSlice();
106         _outputScratchBuffer.freeSlice();
107     }
108 
109     void instantiate(const LV2_Descriptor* descriptor, double rate, const char* bundle_path, const(LV2_Feature*)* features)
110     {
111         LV2_Options_Option* options = null;
112         LV2_URID_Map* uridMap = null;
113 
114         assert(features !is null); // by-spec, always point to at least one item
115 
116         for(int i = 0; features[i] != null; ++i)
117         {
118             debug(debugLV2Client) debugLogf("  * host supports feature: %s\n", features[i].URI);
119 
120             if (strcmp(features[i].URI, "http://lv2plug.in/ns/ext/options#options") == 0)
121                 options = cast(LV2_Options_Option*)features[i].data;
122             else if (strcmp(features[i].URI, "http://lv2plug.in/ns/ext/urid#map") == 0)
123                 uridMap = cast(LV2_URID_Map*)features[i].data;
124         }
125 
126         // Some default value to initialize with in case we don't find an option with the max buffer size
127         _maxBufferSize = 512;
128 
129         // Retrieve max buffer size from options
130         if (options && uridMap)
131         {
132             for (int i = 0; options[i].key != 0; ++i)
133             {
134                 if (options[i].key == uridMap.map(uridMap.handle, LV2_BUF_SIZE__maxBlockLength))
135                     _maxBufferSize = *cast(const(int)*)options[i].value;
136             }
137         }
138 
139         _callResetOnNextRun = true;
140 
141         const(char)* stateChunkURIZ = "unused".ptr;
142         version(futureBinState)
143         {
144             stateChunkURIZ = stateChunkTypeURI.ptr;
145         }
146         _mappedURIs.initialize(uridMap, stateChunkURIZ);
147 
148         LegalIO selectedIO = _client.legalIOs()[_legalIOIndex];
149 
150         _numInputs = selectedIO.numInputChannels;
151         _numOutputs = selectedIO.numOutputChannels;
152         _sampleRate = cast(float)rate;
153 
154         _numParams = cast(int)(_client.params.length);
155 
156         _paramsPointers.reallocBuffer(_numParams);
157         _paramsPointers[] = null;
158 
159         _paramsLastValue.reallocBuffer(_numParams);
160         for (int i = 0; i < _numParams; ++i)
161             _paramsLastValue[i] = _client.param(i).getNormalized();
162 
163 
164         _inputPointersProcessing  = mallocSlice!(float*)(_numInputs);
165         _outputPointersProcessing = mallocSlice!(float*)(_numOutputs);
166         _inputPointersProvided    = mallocSlice!(float*)(_numInputs);
167         _outputPointersProvided   = mallocSlice!(float*)(_numOutputs);
168 
169         _inputScratchBuffer = mallocSlice!(Vec!float)(_numInputs);
170         _outputScratchBuffer = mallocSlice!(Vec!float)(_numOutputs);
171         for (int i = 0; i < _numInputs; ++i)
172             _inputScratchBuffer[i] = makeVec!float();
173         for (int i = 0; i < _numOutputs; ++i)
174             _outputScratchBuffer[i] = makeVec!float();
175         
176         resizeScratchBuffers(_maxBufferSize);
177 
178         _write_function = null;
179 
180         _currentTimeInfo = TimeInfo.init;
181         _currentTimeInfo.hostIsPlaying = true;
182 
183         if (_numOutputs > 0)
184             _latencySamples = _client.latencySamples(rate);
185     }
186 
187     void connect_port(uint32_t port, void* data)
188     {
189         // Parameters by index:
190         // - first goes all parameters by index
191         // - then audio inputs
192         // - then audio outputs
193         // - (optional) then a latency port if there is one audio output
194         // - then an events port for timings and MIDI input
195         // - (optional) then an output event port for MIDI output
196 
197 
198         int numParams = cast(int)(_client.params.length);
199         if(port < numParams)
200         {
201             _paramsPointers[port] = cast(float*)data;
202             return;
203         }
204         port -= numParams;
205         if(port < _numInputs)
206         {
207             _inputPointersProvided[port] = cast(float*)data;
208             return;
209         }
210         port -= _numInputs;
211         if(port < _numOutputs)
212         {
213             _outputPointersProvided[port] = cast(float*)data;
214             return;
215         }
216         port -= _numOutputs;
217         if (_numOutputs > 0)
218         {
219             if (port == 0)
220             {
221                 _latencyOutput = cast(float*)data;
222                 return;
223             }
224             --port;
225         }
226         if(port == 0)
227         {
228             _eventsInput = cast(LV2_Atom_Sequence*)data;
229             return;
230         }
231         --port;
232         if(port == 0)
233         {
234             _eventsOutput = cast(LV2_Atom_Sequence*)data;
235             return;
236         }
237         assert(false, "Error unknown port index");
238     }
239 
240     void activate()
241     {
242         _callResetOnNextRun = true;
243     }
244 
245     void resizeScratchBuffers(int numSamples)
246     {
247         for (int i = 0; i < _numInputs; ++i)
248         {
249             _inputScratchBuffer[i].resize(numSamples);
250             _inputScratchBuffer[i].fill(0);
251         }
252         for (int i = 0; i < _numOutputs; ++i)
253         {
254             _outputScratchBuffer[i].resize(numSamples);
255         }
256     }
257 
258     void run(uint32_t n_samples)
259     {
260         if (_maxBufferSize < n_samples)
261         {
262             _callResetOnNextRun = true;
263             _maxBufferSize = n_samples; // in case the max buffer value wasn't found within options
264             resizeScratchBuffers(_maxBufferSize);
265         }
266 
267         if(_callResetOnNextRun)
268         {
269             _callResetOnNextRun = false;
270             _client.resetFromHost(_sampleRate, _maxBufferSize, _numInputs, _numOutputs);
271         }
272 
273         if (_eventsInput !is null)
274         {
275             for(LV2_Atom_Event* event = lv2_atom_sequence_begin(&_eventsInput.body_);
276                 !lv2_atom_sequence_is_end(&_eventsInput.body_, _eventsInput.atom.size, event);
277                 event = lv2_atom_sequence_next(event))
278             {
279                 if (event is null)
280                     break;
281 
282                 if (_client.receivesMIDI() && event.body_.type == _mappedURIs.midiEvent)
283                 {
284                     // Get offset of MIDI message in that buffer
285                     int offset = cast(int)(event.time.frames);
286                     if (offset < 0)
287                         offset = 0;
288                     int bytes = event.body_.size;
289                     ubyte* data = cast(ubyte*)(event + 1);
290 
291                     if (bytes >= 1 && bytes <= 3) // else doesn't fit in a Dplug MidiMessage
292                     {
293                         ubyte byte0 = data[0];
294                         ubyte byte1 = (bytes >= 2) ? data[1] : 0;
295                         ubyte byte2 = (bytes >= 3) ? data[2] : 0;
296                         MidiMessage message = MidiMessage(offset, byte0, byte1, byte2);
297                         _client.enqueueMIDIFromHost(message);
298                     }
299                 }
300                 else if (event.body_.type == _mappedURIs.atomBlank || event.body_.type == _mappedURIs.atomObject)
301                 {
302 
303                     const (LV2_Atom_Object*) obj = cast(LV2_Atom_Object*)&event.body_;
304 
305                     if (obj.body_.otype == _mappedURIs.timePosition)
306                     {
307                         LV2_Atom* beatsPerMinute = null;
308                         LV2_Atom* frame = null;
309                         LV2_Atom* speed = null;
310 
311                         lv2AtomObjectExtractTimeInfo(obj,
312                                            _mappedURIs.timeBPM, &beatsPerMinute,
313                                            _mappedURIs.timeFrame, &frame,
314                                            _mappedURIs.timeSpeed, &speed);
315 
316                         if (beatsPerMinute != null)
317                         {
318                             if (beatsPerMinute.type == _mappedURIs.atomDouble)
319                                 _currentTimeInfo.tempo = (cast(LV2_Atom_Double*)beatsPerMinute).body_;
320                             else if (beatsPerMinute.type == _mappedURIs.atomFloat)
321                                 _currentTimeInfo.tempo = (cast(LV2_Atom_Float*)beatsPerMinute).body_;
322                             else if (beatsPerMinute.type == _mappedURIs.atomInt)
323                                 _currentTimeInfo.tempo = (cast(LV2_Atom_Int*)beatsPerMinute).body_;
324                             else if (beatsPerMinute.type == _mappedURIs.atomLong)
325                                 _currentTimeInfo.tempo = (cast(LV2_Atom_Long*)beatsPerMinute).body_;
326                         }
327                         if (frame != null)
328                         {
329                             if (frame.type == _mappedURIs.atomDouble)
330                                 _currentTimeInfo.timeInSamples = cast(long)(cast(LV2_Atom_Double*)frame).body_;
331                             else if (frame.type == _mappedURIs.atomFloat)
332                                 _currentTimeInfo.timeInSamples = cast(long)(cast(LV2_Atom_Float*)frame).body_;
333                             else if (frame.type == _mappedURIs.atomInt)
334                                 _currentTimeInfo.timeInSamples = (cast(LV2_Atom_Int*)frame).body_;
335                             else if (frame.type == _mappedURIs.atomLong)
336                                 _currentTimeInfo.timeInSamples = (cast(LV2_Atom_Long*)frame).body_;
337                         }
338                         if (speed != null)
339                         {
340                             if (speed.type == _mappedURIs.atomDouble)
341                                 _currentTimeInfo.hostIsPlaying = (cast(LV2_Atom_Double*)speed).body_ > 0.0f;
342                             else if (speed.type == _mappedURIs.atomFloat)
343                                 _currentTimeInfo.hostIsPlaying = (cast(LV2_Atom_Float*)speed).body_ > 0.0f;
344                             else if (speed.type == _mappedURIs.atomInt)
345                                 _currentTimeInfo.hostIsPlaying = (cast(LV2_Atom_Int*)speed).body_ > 0.0f;
346                             else if (speed.type == _mappedURIs.atomLong)
347                                 _currentTimeInfo.hostIsPlaying = (cast(LV2_Atom_Long*)speed).body_ > 0.0f;
348                         }
349                     }
350                 }
351             }
352         }
353 
354         // Update changed parameters
355         {
356             for (int i = 0; i < _numParams; ++i)
357             {
358                 if (_paramsPointers[i])
359                 {
360                     float currentValue = *_paramsPointers[i];
361 
362                     // Force normalization in case host sends invalid parameter values
363                     if (currentValue < 0) currentValue = 0;
364                     if (currentValue > 1) currentValue = 1;
365 
366                     if (currentValue != _paramsLastValue[i])
367                     {
368                         _paramsLastValue[i] = currentValue;
369                         _client.setParameterFromHost(i, currentValue);
370                     }
371                 }
372             }
373         }
374 
375         // Fill input and output pointers for this block, based on what we have received
376         for(int input = 0; input < _numInputs; ++input)
377         {
378             // Copy each available input to a scrach buffer, because some hosts (Mixbus/Ardour)
379             // give identical pointers for input and output buffers.
380             if (_inputPointersProvided[input])
381             {
382                 const(float)* source = _inputPointersProvided[input];
383                 float* dest = _inputScratchBuffer[input].ptr;
384                 dest[0..n_samples] = source[0..n_samples];
385             }
386             _inputPointersProcessing[input] = _inputScratchBuffer[input].ptr;
387         }
388         for(int output = 0; output < _numOutputs; ++output)
389         {
390             _outputPointersProcessing[output] = _outputPointersProvided[output] ? _outputPointersProvided[output] : _outputScratchBuffer[output].ptr;
391         }
392 
393         if (_client.sendsMIDI)
394             _client.clearAccumulatedOutputMidiMessages();
395 
396         // Process audio
397         _client.processAudioFromHost(_inputPointersProcessing, _outputPointersProcessing, n_samples, _currentTimeInfo);
398 
399         _currentTimeInfo.timeInSamples += n_samples;
400 
401         if (_client.sendsMIDI() && _eventsOutput !is null)
402         {
403             uint capacity = _eventsOutput.atom.size;
404 
405             _eventsOutput.atom.size = LV2_Atom_Sequence_Body.sizeof;
406             _eventsOutput.atom.type = _mappedURIs.atomSequence;
407             _eventsOutput.body_.unit = 0;
408             _eventsOutput.body_.pad = 0;
409 
410             const(MidiMessage)[] outMsgs = _client.getAccumulatedOutputMidiMessages();
411             if (outMsgs.length > 0)
412             {
413                 const(ubyte)* midiEventData;
414                 uint totalOffset = 0;
415 
416                 foreach(MidiMessage msg; outMsgs)
417                 {
418                     assert(msg.offset >= 0 && msg.offset <= n_samples);
419 
420                     int midiMsgSize = msg.lengthInBytes();
421 
422                     // Unknown length, drop message.
423                     if (midiMsgSize == -1)
424                         continue;
425 
426                     if ( LV2_Atom_Event.sizeof + midiMsgSize > capacity - totalOffset)
427                         break; // Note: some MIDI messages will get dropped in that case
428 
429                     LV2_Atom_Event* event = cast(LV2_Atom_Event*)(cast(char*)LV2_ATOM_CONTENTS!LV2_Atom_Sequence(&_eventsOutput.atom) + totalOffset);
430                     event.time.frames = msg.offset;
431                     event.body_.type = _mappedURIs.midiEvent;
432                     event.body_.size = midiMsgSize;
433                     int written = msg.toBytes( cast(ubyte*) LV2_ATOM_BODY(&event.body_), midiMsgSize /* enough room */);
434                     assert(written == midiMsgSize);
435                     uint size = lv2_atom_pad_size(cast(uint)(LV2_Atom_Event.sizeof) + midiMsgSize);
436                     totalOffset += size;
437                     _eventsOutput.atom.size += size;
438                 }
439             }
440         }
441 
442         // Report latency to host, expressed in frames
443         if (_latencyOutput)
444             *_latencyOutput = _latencySamples;
445     }
446 
447     void instantiateUI(const LV2UI_Descriptor* descriptor,
448                        const char*                     plugin_uri,
449                        const char*                     bundle_path,
450                        LV2UI_Write_Function            write_function,
451                        LV2UI_Controller                controller,
452                        LV2UI_Widget*                   widget,
453                        const (LV2_Feature*)*       features)
454     {
455         debug(debugLV2Client) debugLog(">instantiateUI");
456 
457         int transientWinId;
458         void* parentId = null;
459         LV2_Options_Option* options = null;
460         _uiResize = null;
461         LV2_URID_Map* uridMap = null;
462 
463         // Useful to record automation
464         _write_function = write_function;
465         _controller = controller;
466         _uiTouch = null;
467 
468         if (features !is null)
469         {
470             for (int i=0; features[i] != null; ++i)
471             {
472                 debug(debugLV2Client) debugLogf("  * host UI supports feature: %s\n", features[i].URI);
473                 if (strcmp(features[i].URI, LV2_UI__parent) == 0)
474                     parentId = cast(void*)features[i].data;
475                 else if (strcmp(features[i].URI, LV2_UI__resize) == 0)
476                     _uiResize = cast(LV2UI_Resize*)features[i].data;
477                 else if (strcmp(features[i].URI, LV2_OPTIONS__options) == 0)
478                     options = cast(LV2_Options_Option*)features[i].data;
479                 else if (strcmp(features[i].URI, LV2_URID__map) == 0)
480                     uridMap = cast(LV2_URID_Map*)features[i].data;
481                 else if (strcmp(features[i].URI, LV2_UI__touch) == 0)
482                     _uiTouch = cast(LV2UI_Touch*)features[i].data;
483             }
484         }
485 
486         // Not transmitted yet
487         /*
488         if (options && uridMap)
489         {
490             for (int i = 0; options[i].key != 0; ++i)
491             {
492                 if (options[i].key == uridTransientWinId)
493                 {
494                     transientWin = cast(void*)(options[i].value);    // sound like it lacks a dereferencing
495                 }
496             }
497         }*/
498 
499         if (widget != null)
500         {
501             _graphicsMutex.lock();
502             void* pluginWindow = cast(LV2UI_Widget)_client.openGUI(parentId, null, GraphicsBackend.autodetect);
503             _graphicsMutex.unlock();
504 
505             int widthLogicalPixels, heightLogicalPixels;
506             if (_client.getGUISize(&widthLogicalPixels, &heightLogicalPixels))
507             {
508                 _graphicsMutex.lock();
509                 _uiResize.ui_resize(_uiResize.handle, widthLogicalPixels, heightLogicalPixels);
510                 _graphicsMutex.unlock();
511             }
512 
513             *widget = cast(LV2UI_Widget)pluginWindow;
514         }
515         debug(debugLV2Client) debugLog("<instantiateUI");
516     }
517 
518     void portEventUI(uint32_t     port_index,
519                      uint32_t     buffer_size,
520                      uint32_t     format,
521                      const void*  buffer)
522     {
523         // Nothing to do since parameter changes already dirty the UI?
524     }
525 
526     void cleanupUI()
527     {
528         debug(debugLV2Client) debugLog(">cleanupUI");
529         _graphicsMutex.lock();
530         assert(_client.hasGUI());
531         _client.closeGUI();
532         _graphicsMutex.unlock();
533         debug(debugLV2Client) debugLog("<cleanupUI");
534     }
535 
536     override void beginParamEdit(int paramIndex)
537     {
538         // Note: this is untested, since it appears DISTRHO doesn't really ask 
539         // for this interface, and Carla doesn't provide one.
540         if ( (_uiTouch !is null) && (_uiTouch.touch !is null) )
541             _uiTouch.touch(_uiTouch.handle, paramIndex, true);
542     }
543 
544     override void paramAutomate(int paramIndex, float value)
545     {
546         // write back automation to host
547         assert(_write_function !is null);
548         _write_function(_controller, paramIndex, float.sizeof, 0, &value);
549     }
550 
551     override void endParamEdit(int paramIndex)
552     {
553         // Note: this is untested, since it appears DISTRHO doesn't really ask 
554         // for this interface, and Carla doesn't provide one.
555         if ( (_uiTouch !is null) && (_uiTouch.touch !is null) )
556             _uiTouch.touch(_uiTouch.handle, paramIndex, false);
557     }
558 
559     override bool requestResize(int width, int height)
560     {
561         int result = _uiResize.ui_resize(_uiResize.handle, width, height);
562         return result == 0;        
563     }
564 
565     override bool notifyResized()
566     {
567         return false;
568     }
569 
570     // Not properly implemented yet. LV2 should have an extension to get DAW information.
571     override DAW getDAW()
572     {
573         return DAW.Unknown;
574     }
575 
576     override PluginFormat getPluginFormat()
577     {
578         return PluginFormat.lv2;
579     }
580 
581     /// Get the URI used for state chunk type.
582     /// The slice has a terminal zero afterwards.
583     /// eg: "https://www.wittyaudio.com/Destructatorizer57694469#stateBinary"
584     version(futureBinState)
585     {
586         const(char)[] getStateBinaryURI()
587         {
588             return stateChunkTypeURI[0..strlen(stateChunkTypeURI.ptr)];
589         }
590 
591         LV2_URID getStateBinaryURID()
592         {
593             return _mappedURIs.stateBinary;
594         }
595 
596         LV2_URID getAtomStringURID()
597         {
598             return _mappedURIs.atomString;
599         }
600 
601         const(ubyte)[] getBase64EncodedStateZ()
602         {
603             // Fetch latest state.
604             _lastStateBinary.clearContents();
605             _client.saveState(_lastStateBinary);
606 
607             // PERF: compare to last seen state, skip base64 if unchanged.
608 
609             // Encode to base64
610             _lastStateBinaryBase64.clearContents();
611             encodeBase64(_lastStateBinary[], _lastStateBinaryBase64);
612             _lastStateBinaryBase64.pushBack(0); // Add a terminal zero, since LV2 wants a zero-terminated Atom String.
613 
614             return _lastStateBinaryBase64[];
615         }
616 
617         // Base64-decode the input extra state chunk, and gives it to plug-in client.
618         // Empty chunk is considered a success.
619         // Return: true on success.
620         bool restoreStateBinaryBase64(const(ubyte)[] base64StateBinary)
621         {
622             debug(debugLV2Client) debugLogf(">restoreStateBinaryBase64\n");
623 
624             if (base64StateBinary.length == 0)
625             {
626                 // Note: zero-length state binary not passed to loadState. Considered a success.
627                 return true;
628             }
629 
630             debug(debugLV2Client) debugLogf("  * len = %llu\n", cast(int)base64StateBinary.length);
631 
632             _lastDecodedStateBinary.clearContents;
633             bool err;
634             decodeBase64(base64StateBinary, _lastDecodedStateBinary, '+', '/', &err);
635             if (err)
636             {
637                 debug(debugLV2Client) debugLogf("chunk didn't decode\n");
638                 return false; // wrong base64 data
639             }
640 
641             debug(debugLV2Client) debugLogf("decoded\n");
642 
643             bool parsed = _client.loadState(_lastDecodedStateBinary[]);
644 
645             debug(debugLV2Client) debugLogf("  * parsed = %d\n", cast(int)parsed);
646             return parsed;
647         }
648     }
649 
650 private:
651 
652     uint _numInputs;
653     uint _numOutputs;
654 
655     // the maximum buffer size we've found, from either the options or reality of incoming buffers
656     int _maxBufferSize;
657 
658     // whether the plugin should call resetFromHost at next `run()`
659     bool _callResetOnNextRun;
660 
661     int _numParams;
662     float*[] _paramsPointers;
663     float[] _paramsLastValue;
664 
665     // Scratch input buffers in case the host doesn't provide ones.
666     Vec!float[] _inputScratchBuffer;
667 
668     // Scratch output buffers in case the host doesn't provide ones.
669     Vec!float[] _outputScratchBuffer;
670 
671     // Input pointers that were provided by the host, `null` if not provided.
672     float*[] _inputPointersProvided;
673 
674     // Output input used by processing, recomputed at each run().
675     float*[] _inputPointersProcessing;
676 
677     // Output pointers that were provided by the host, `null` if not provided.
678     float*[] _outputPointersProvided;
679 
680     // Output pointers used by processing, recomputed at each run().
681     float*[] _outputPointersProcessing;
682 
683     // Output pointer for latency reporting, `null` if not provided.
684     float* _latencyOutput;
685 
686     LV2_Atom_Sequence* _eventsInput; // may be null
687     LV2_Atom_Sequence* _eventsOutput;
688 
689     float _sampleRate;
690 
691     // The latency value expressed in frames
692     int _latencySamples;
693 
694     UncheckedMutex _graphicsMutex;
695 
696     // Set at instantiation
697     MappedURIs _mappedURIs;
698 
699     int _legalIOIndex;
700 
701     LV2UI_Write_Function _write_function;
702     LV2UI_Controller _controller;
703     LV2UI_Touch* _uiTouch;
704     LV2UI_Resize* _uiResize;
705 
706     // Current time info, eventually extrapolated when data is missing.
707     TimeInfo _currentTimeInfo;
708 
709     version(futureBinState)
710     {
711         Vec!ubyte _lastStateBinary;
712         Vec!ubyte _lastStateBinaryBase64;
713         Vec!ubyte _lastDecodedStateBinary;
714 
715         // A zero-terminated buffer holding the full URI to vendor:stateChunk.
716         char[256] stateChunkTypeURI;
717 
718         void initializeStateChunkTypeURI()
719         {
720             char[4] pluginID = _client.getPluginUniqueID();
721             CString pluginHomepageZ = CString(_client.pluginHomepage());
722             snprintf(stateChunkTypeURI.ptr, 256, "%s%2X%2X%2X%2X#%s", 
723                      pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3],
724                      "stateBinary".ptr);
725             stateChunkTypeURI[$-1] = '\0';
726         }
727     }
728 
729 }
730 
731 struct MappedURIs
732 {
733 nothrow:
734 @nogc:
735 
736     LV2_URID atomDouble;
737     LV2_URID atomFloat;
738     LV2_URID atomInt;
739     LV2_URID atomLong;
740     LV2_URID atomBlank;
741     LV2_URID atomObject;
742     LV2_URID atomSequence;
743     LV2_URID atomString;
744     LV2_URID midiEvent;
745     LV2_URID timePosition;
746     LV2_URID timeFrame;
747     LV2_URID timeBPM;
748     LV2_URID timeSpeed;
749 
750     version(futureBinState)
751         LV2_URID stateBinary;
752 
753     void initialize(LV2_URID_Map* uridMap, const(char)* stateBinaryURIZ)
754     {
755         atomDouble   = uridMap.map(uridMap.handle, LV2_ATOM__Double);
756         atomFloat    = uridMap.map(uridMap.handle, LV2_ATOM__Float);
757         atomInt      = uridMap.map(uridMap.handle, LV2_ATOM__Int);
758         atomLong     = uridMap.map(uridMap.handle, LV2_ATOM__Long);
759         atomBlank    = uridMap.map(uridMap.handle, LV2_ATOM__Blank);
760         atomObject   = uridMap.map(uridMap.handle, LV2_ATOM__Object);
761         atomSequence = uridMap.map(uridMap.handle, LV2_ATOM__Sequence);
762         atomString   = uridMap.map(uridMap.handle, LV2_ATOM__String);
763         midiEvent    = uridMap.map(uridMap.handle, LV2_MIDI__MidiEvent);
764         timePosition = uridMap.map(uridMap.handle, LV2_TIME__Position);
765         timeFrame    = uridMap.map(uridMap.handle, LV2_TIME__frame);
766         timeBPM      = uridMap.map(uridMap.handle, LV2_TIME__beatsPerMinute);
767         timeSpeed    = uridMap.map(uridMap.handle, LV2_TIME__speed);
768         version(futureBinState)
769             stateBinary  = uridMap.map(uridMap.handle, stateBinaryURIZ);
770     }
771 }
772 
773 
774 int lv2AtomObjectExtractTimeInfo(const (LV2_Atom_Object*) object,
775                                  LV2_URID tempoURID, LV2_Atom** tempoAtom,
776                                  LV2_URID frameURID, LV2_Atom** frameAtom,
777                                  LV2_URID speedURID, LV2_Atom** speedAtom)
778 {
779     int n_queries = 3;
780     int matches = 0;
781     // #define LV2_ATOM_OBJECT_FOREACH(obj, iter)
782     for (LV2_Atom_Property_Body* prop = lv2_atom_object_begin(&object.body_);
783         !lv2_atom_object_is_end(&object.body_, object.atom.size, prop);
784         prop = lv2_atom_object_next(prop))
785     {
786         for (int i = 0; i < n_queries; ++i) {
787             // uint32_t         qkey = va_arg!(uint32_t)(args);
788             // LV2_Atom** qval = va_arg!(LV2_Atom**)(args);
789             // if (qkey == prop.key && !*qval) {
790             //     *qval = &prop.value;
791             //     if (++matches == n_queries) {
792             //         return matches;
793             //     }
794             //     break;
795             // }
796             if(tempoURID == prop.key && tempoAtom) {
797                 *tempoAtom = &prop.value;
798                 ++matches;
799             }
800             else if(frameURID == prop.key && frameAtom) {
801                 *frameAtom = &prop.value;
802                 ++matches;
803             }
804             else if(speedURID == prop.key && speedAtom) {
805                 *speedAtom = &prop.value;
806                 ++matches;
807             }
808         }
809     }
810     return matches;
811 }