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