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 }