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