1 /** 2 FL Plugin client. 3 4 Copyright: Guillaume Piolat 2023. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 */ 7 module dplug.flp.client; 8 9 import core.atomic; 10 import core.stdc.stdio: snprintf; 11 import core.stdc.string: strlen, memmove, memset; 12 13 import std.array; 14 15 import dplug.core.nogc; 16 import dplug.core.vec; 17 import dplug.core.sync; 18 import dplug.core.thread; 19 import dplug.core.runtime; 20 import dplug.core.binrange; 21 import dplug.client.client; 22 import dplug.client.params; 23 import dplug.client.graphics; 24 import dplug.client.midi; 25 import dplug.client.daw; 26 import dplug.flp.types; 27 28 import std.math: round; 29 30 31 //debug = logFLPClient; 32 33 final extern(C++) class FLPCLient : TFruityPlug 34 { 35 nothrow @nogc: 36 37 this(TFruityPlugHost pHost, TPluginTag tag, Client client, bool* err) 38 { 39 this.HostTag = tag; 40 this.Info = &_fruityPlugInfo; 41 this._host = pHost; 42 this._client = client; 43 initializeInfo(); 44 45 _hostCommand = mallocNew!FLHostCommand(pHost, tag); 46 _client.setHostCommand(_hostCommand); 47 48 // If a synth ("generator" in FL dialect), it must supports 0-2. 49 // If an effect, it must supports 2-2. 50 // Else fail instantiation. 51 52 bool compatibleIO; 53 if (_client.isSynth) 54 { 55 initializeVoices(); 56 compatibleIO = _client.isLegalIO(0, 2); 57 } 58 else 59 compatibleIO = _client.isLegalIO(2, 2); 60 61 *err = false; 62 if (!compatibleIO) 63 *err = true; 64 65 _graphicsMutex = makeMutex; 66 _midiInputMutex = makeMutex; 67 68 if (_client.receivesMIDI) 69 _hostCommand.wantsMIDIInput(); 70 71 _hostCommand.disableIdleNotifications(); 72 73 _mixingTimeInSamples = 0; 74 _hostTicksReference = 0; 75 _hostTicksChanged = false; 76 } 77 78 ~this() 79 { 80 destroyFree(_hostCommand); 81 } 82 83 // <Implements TFruityPlug> 84 85 // Important SDK note about FL plug-in threading: 86 // 87 // " (G) = called from GUI thread, 88 // (M) = called from mixer thread, 89 // (GM) = both, 90 // (S) = called from MIDI synchronization thread 91 // (M) calls are done inside the plugin lock (LockPlugin / UnlockPlugin)" 92 // 93 // Comment: LockPlugin/UnlockPlugin is implemented at the discretion of the client, contrarily 94 // to what this comment seems to imply. 95 // 96 // "TriggerVoice and Voice_ functions are also called inside the plugin lock 97 // assume that any other call is not locked! (so call LockPlugin / UnlockPlugin 98 // where necessary, but no more than that. 99 // Don't call back to the host while inside a LockPlugin / UnlockPlugin block" 100 // 101 // In this client we'll tag the overrides with UDAs @guiThread @mixerThread and midiSyncThread. 102 // In addition, variables that are @mixerThread are only ever accessed from mixer thread. 103 104 private enum guiThread = 0; 105 private enum mixerThread = 0; 106 private enum midiSyncThread = 0; 107 108 extern(System) override 109 { 110 @guiThread 111 void DestroyObject() 112 { 113 destroyFree(_client); 114 _client = null; 115 destroyFree(this); 116 } 117 118 @guiThread @mixerThread 119 intptr_t Dispatcher(intptr_t ID, intptr_t Index, intptr_t Value) 120 { 121 122 // Note: it's not really documented what the return values should be. 123 // In general it seems opcode dependent, with a value of zero maybe meaning "unhandled". 124 ScopedForeignCallback!(false, true) scopedCallback; 125 scopedCallback.enter(); 126 127 debug(logFLPClient) 128 debugLogf("Dispatcher ID = %llu index = %llu value = %llu\n", ID, Index, Value); 129 130 switch (ID) 131 { 132 //@guiThread 133 case FPD_ShowEditor: /* 0 */ 134 if (Value == 0) 135 { 136 // hide editor 137 if (_client.hasGUI) 138 { 139 _graphicsMutex.lock(); 140 _client.closeGUI(); 141 _graphicsMutex.unlock(); 142 this.EditorHandle = null; 143 } 144 } 145 else 146 { 147 if (_client.hasGUI) 148 { 149 void* parent = cast(void*) Value; 150 _graphicsMutex.lock(); 151 void* windowHandle = _client.openGUI(parent, 152 null, 153 GraphicsBackend.autodetect); 154 _graphicsMutex.unlock(); 155 this.EditorHandle = windowHandle; 156 } 157 } 158 return 0; // no error, apparently 159 160 161 // @guiThread, says the documentation 162 case FPD_ProcessMode: /* 1 */ 163 // "this ID can be ignored" 164 // Gives a quality hint. 165 166 // Tell how many internal presets there are. 167 // Quite arbitrarily, this is where we choose to change preset number. 168 // Doing this at plugin creation is ignored. 169 _hostCommand.setNumPresets( _client.presetBank.numPresets() ); 170 171 // Again, for some reason having this in the constructor doesn't work. 172 // Hack to put it in FPD_ProcessMode. 173 if (_client.sendsMIDI) 174 _hostCommand.enableMIDIOut(); 175 176 return 0; 177 178 // @guiThread @mixerThread 179 case FPD_Flush: /* 2 */ 180 // "FPD_Flush warns the plugin that the next samples do not follow immediately 181 // in time to the previous block of samples. In other words, the continuity is 182 // broken." 183 // Interesting, Dplug plugins normally handle this correctly already, since it's 184 // common while the DAW is looping. 185 return 0; 186 187 // @guiThread 188 case FPD_SetBlockSize: /* 3 */ 189 // Client maxframes will change at next buffer asynchronously. 190 // Works from any thread. 191 atomicStore(_hostMaxFrames, Value); 192 return 0; 193 194 // @guiThread 195 case FPD_SetSampleRate: /* 4 */ 196 // Client sampleRate will change at next buffer asynchronously. 197 // Works from any thread. 198 atomicStore(_hostSampleRate, Value); 199 return 0; // right return value according to TTestPlug 200 201 // @guiThread 202 case FPD_WindowMinMax: /* 5 */ 203 _graphicsMutex.lock(); 204 IGraphics graphics = _client.getGraphics(); 205 206 // Find min size, in logical pixels. 207 int minX = 1, minY = 1; 208 graphics.getNearestValidSize(&minX,& minY); 209 210 // Find max size, in logical pixels. 211 int maxX = 32768, maxY = 32768; 212 graphics.getNearestValidSize(&maxX, &maxY); 213 214 _graphicsMutex.unlock(); 215 216 // The FL SDK doesn't define the TRect and Tpoint, those are Delphi types who 217 // match the Windows RECT and POINT types. 218 TRect* outRect = cast(TRect*)Index; 219 outRect.x1 = minX; 220 outRect.y1 = minY; 221 outRect.x2 = maxX; 222 outRect.y2 = maxY; 223 TPoint* outSnap = cast(TPoint*)Value; 224 outSnap.x = 1; // quite smooth really 225 outSnap.y = 1; 226 return 0; 227 228 case FPD_KillAVoice: /* 6 */ 229 return 0; // refuse to kill a voice 230 231 case FPD_UseVoiceLevels: /* 7 */ 232 // "return 0 if the plugin doesn't support the default per-voice level Index" 233 return 0; 234 235 case FPD_SetPreset: /* 9 */ 236 { 237 int presetIndex = cast(int)Index; 238 if (!_client.presetBank.isValidPresetIndex(presetIndex)) 239 return 0; 240 241 // Load preset, doesn't change "current" preset in PresetBank, doesn't 242 // overwrite presetbank. 243 _client.presetBank.preset(presetIndex).loadFromHost(_client); 244 return 0; 245 } 246 247 case FPD_ChanSampleChanged: /* 10 */ 248 debug(logFLPClient) debugLog("Not implemented\n"); 249 break; 250 251 case FPD_SetEnabled: /* 11 */ // 252 bool bypassed = (Value == 0); 253 atomicStore(_hostBypass, bypassed); 254 break; 255 256 case FPD_SetPlaying: /* 12 */ 257 atomicStore(_hostHostPlaying, Value != 0); 258 return 0; 259 260 case FPD_SongPosChanged: /* 13 */ // 261 // song position has been relocated (loop, click in timeline...) 262 263 double ticks, samples; 264 _hostCommand.getMixingTimeInTicks(ticks, samples); 265 266 // If `samples` weren't = 0, we'd have judged FL unfairly and it can loop in 267 // increment lower than a tick. Interesting. 268 assert(samples == 0); 269 270 atomicStore(_hostTicksReference, ticks); 271 atomicStore(_hostTicksChanged, true); 272 273 return 0; 274 275 case FPD_SetTimeSig: /* 14 */ 276 return 0; // ignored 277 278 case FPD_CollectFile: /* 15 */ 279 case FPD_SetInternalParam: /* 16 */ 280 debug(logFLPClient) debugLog("Not implemented\n"); 281 break; 282 283 case FPD_SetNumSends: /* 17 */ // 284 return 0; // ignored 285 286 case FPD_LoadFile: /* 18 */ 287 debug(logFLPClient) debugLog("Not implemented\n"); 288 break; 289 290 case FPD_SetFitTime: /* 19 */ 291 // ignored 292 return 0; 293 294 case FPD_SetSamplesPerTick: /* 20 */ 295 // "FPD_SetSamplesPerTick lets you know how many samples there are in a "tick" 296 // (the basic period of time in FL Studio). This changes when the tempo, PPQ 297 // or sample rate have changed. This can be called from the mixing thread." 298 float fValue = *cast(float*)(&Value); 299 atomicStore(_hostSamplesInATick, fValue); 300 return 0; 301 302 case FPD_SetIdleTime: /* 21 */ 303 return 0; 304 305 case FPD_SetFocus: /* 22 */ 306 return 0; 307 308 case FPD_Transport: /* 23 */ 309 debug(logFLPClient) debugLog("Not implemented\n"); 310 break; 311 312 case FPD_MIDIIn: /* 24 */ 313 { 314 // Not sure when this message should come. 315 debug(logFLPClient) debugLog("FPD_MIDIIn\n"); 316 break; 317 } 318 319 case FPD_RoutingChanged: /* 25 */ 320 // ignore, seems to be for sidechain and tracks names changing. 321 break; 322 323 case FPD_GetParamInfo: /* 26 */ 324 { 325 // makes no sense to interpolate parameter values (when values are not levels) 326 enum int PI_CantInterpolate = 1; 327 328 // parameter is a normalized (0..1) single float. (Integer otherwise) 329 enum int PI_Float = 2; 330 331 // parameter appears centered in event editors 332 enum int PI_Centered = 4; 333 334 if (!_client.isValidParamIndex(cast(int)Index)) 335 return 0; 336 337 Parameter param = _client.param(cast(int)Index); 338 if (auto bp = cast(BoolParameter)param) 339 { 340 return PI_CantInterpolate; 341 } 342 else if (auto ip = cast(IntegerParameter)param) 343 { 344 return PI_CantInterpolate; 345 } 346 else if (auto fp = cast(FloatParameter)param) 347 { 348 return 0; 349 } 350 else 351 { 352 assert(false); // FUTURE whenever there are more parameter types around. 353 } 354 } 355 356 case FPD_ProjLoaded: /* 27 */ 357 // "called after a project has been loaded, to leave a chance to kill 358 // automation (that could be loaded after the plugin is created)" 359 // Well, we don't mess with user sessions around here. 360 return 0; 361 362 case FPD_WrapperLoadState: /* 28 */ 363 debug(logFLPClient) debugLog("Not implemented\n"); 364 break; 365 366 case FPD_ShowSettings: /* 29 */ 367 // When Settings window is selected or not. 368 return 0; 369 370 case FPD_SetIOLatency: /* 30 */ 371 return 0; // FL gives input/output latency here. Nice idea. 372 373 case FPD_PreferredNumIO: /* 32 */ 374 debug(logFLPClient) debugLog("Not implemented\n"); 375 break; 376 377 case FPD_GetGUIColor: /* 33 */ 378 return 0; // background color, apparently 379 380 case FPD_CloseAllWindows: /* 34 */ 381 case FPD_RenderWindowBitmap: /* 35 */ 382 case FPD_StealKBFocus: /* 36 */ 383 case FPD_GetHelpContext: /* 37 */ 384 case FPD_RegChanged: /* 38 */ 385 case FPD_ArrangeWindows: /* 39 */ 386 debug(logFLPClient) debugLog("Not implemented\n"); 387 break; 388 389 case FPD_PluginLoaded: /* 40 */ 390 // ignored 391 return 0; 392 393 case FPD_ContextInfoChanged: /* 41 */ 394 // "Index holds the type of information (see CI_ constants), call 395 // `FHD_GetContextInfo` for the new value(s)" 396 debug(logFLPClient) debugLogf("Context info %d changed\n", Index); 397 // TODO probably something to do for CI_TrackPan and CI_TrackVolume, 398 // host does give them 399 return 0; 400 401 case FPD_ProjectInfoChanged: /* 42 */ 402 case FPD_GetDemoPlugins: /* 43 */ 403 case FPD_UnLockDemoPlugins: /* 44 */ 404 case FPD_ColorWasPicked: /* 46 */ 405 debug(logFLPClient) debugLog("Not implemented\n"); 406 break; 407 408 case FPD_IsInDebugMode: /* 47 */ 409 // When testing, didn't see what it changes anyway, perhaps logging. 410 debug(logFLPClient) 411 return 1; 412 else 413 return 0; 414 415 case FPD_ColorsHaveChanged: /* 48 */ 416 // We don't really care about that. 417 return 0; 418 419 420 case FPD_GetStateSizeEstimate: /* 49 */ 421 return _client.params().length * 8; 422 423 case FPD_UseIncreasedMIDIResolution: /* 50 */ 424 // increased MIDI resolution is supported, this seems related to REC_FromMIDI 425 // having an updated range. 426 // It is also ignored by FL12 and probably earlier FL. 427 return 1; 428 429 case FPD_ConvertStringToValue: /* 51 */ 430 return 0; 431 432 433 case FPD_GetParamType: /* 52 */ 434 435 // My theory is that FL12 used that to display parameter properly in "Browse 436 // Parameters" view, but FL20 doesn't use it anymore in favor of string 437 // conversions. 438 return 0; 439 440 /* 441 Parameter p = _client.param(iparam); 442 if (p.label == "ms") 443 return PT_Ms; 444 else if (p.label == "%") 445 return PT_Percent; 446 else if (p.label == "Hz") 447 return PT_Hz; 448 else 449 return PT_Value; 450 */ 451 452 default: 453 // unknown ID 454 break; 455 456 } 457 return 0; 458 } 459 460 @guiThread 461 void Idle_Public() 462 { 463 // "This function is called continuously. It allows the plugin to perform certain tasks 464 // that are not time-critical and which do not take up a lot of time either. For 465 // example, TDelphiFruityPlug and TCPPFruityPlug implement this function to show a hint 466 // message when the mouse moves over a control in the editor." 467 // Well, thank you but not needed. 468 } 469 470 @guiThread 471 void SaveRestoreState(IStream Stream, BOOL Save) 472 { 473 // SDK documentation says it's for Parameters mostly, so indeed we need the full chunk, 474 // not just the extra binary state. 475 476 ScopedForeignCallback!(false, true) scopedCallback; 477 scopedCallback.enter(); 478 479 static immutable ubyte[8] MAGIC = ['D', 'F', 'L', '0', 0, 0, 0, 0]; 480 481 // Being @guiThread, we assume SaveRestoreState is not called twice simultaneously. 482 // Hence, _lastChunk is used for both saving and restoring. 483 484 if (Save) 485 { 486 debug(logFLPClient) debugLog("SaveRestoreState save a chunk\n"); 487 488 _lastChunk.clearContents(); 489 490 // We need additional framing, since FL provide no chunk length on read. 491 // Our chunk looks like this: 492 // ------------- 493 // 0000 "DFL0" // Version of our chunking for dplug:flp client. 494 // 0004 len // Bytes in following chunk, 32-bit uint, Little Endian. 495 // 0008 <chunk> // Chunk given by dplug:client. 496 // ------------- 497 498 for (int n = 0; n < 8; ++n) 499 _lastChunk.pushBack(MAGIC[n]); // add room for len too 500 501 size_t sizeBefore = _lastChunk.length; 502 _client.presetBank.appendStateChunkFromCurrentState(_lastChunk); 503 size_t sizeAfter = _lastChunk.length; 504 size_t len = cast(int)(sizeAfter - sizeBefore); 505 506 // If you fail here, your saved chunk exceeds 2gb, which is probably an error. 507 assert(len + 8 <= int.max); 508 509 // Update len field 510 ubyte[] lenLoc = _lastChunk[4..8]; 511 writeLE!uint(lenLoc, cast(uint)len); 512 513 ULONG written; 514 Stream.Write(_lastChunk.ptr, cast(int)_lastChunk.length, &written); 515 } 516 else 517 { 518 debug(logFLPClient) debugLog("SaveRestoreState load a chunk\n"); 519 520 ubyte[8] header; 521 ULONG read; 522 HRESULT hr = Stream.Read(header.ptr, 8, &read); 523 if (hr < 0 || read != 8) 524 return; 525 526 if (header[0..4] != MAGIC[0..4]) 527 return; // unrecognized chunks and/or version 528 529 bool err; 530 const(ubyte)[] lenLoc = header[4..8]; 531 uint len = popLE!uint(lenLoc, &err); 532 if (err) 533 return; 534 535 // plan to read as much from Stream 536 _lastChunk.resize(len); 537 hr = Stream.Read(_lastChunk.ptr, len, &read); 538 if (hr < 0 || read != len) 539 return; 540 541 // Load chunk in client 542 _client.presetBank.loadStateChunk(_lastChunk[], &err); 543 if (err) 544 return; 545 } 546 } 547 548 // names (see FPN_Param) (Name must be at least 256 chars long) 549 @guiThread 550 void GetName(int Section, int Index, int Value, char *Name) 551 { 552 ScopedForeignCallback!(false, true) scopedCallback; 553 scopedCallback.enter(); 554 555 if (Section == FPN_Param) 556 { 557 if (!_client.isValidParamIndex(Index)) 558 return; 559 string name = _client.param(Index).name; 560 snprintf(Name, 256, "%.*s", cast(int)name.length, name.ptr); 561 } 562 else if (Section == FPN_ParamValue) 563 { 564 if (!_client.isValidParamIndex(Index)) 565 return; 566 Parameter param = _client.param(Index); 567 param.toDisplayN(Name, 256); 568 size_t len = strlen(Name); 569 string unitLabel = param.label(); 570 571 // Add the unit if enough room. 572 if ((unitLabel.length > 0) && (len + unitLabel.length < 254)) 573 { 574 int labelLen = cast(int)unitLabel.length; 575 snprintf(Name + len, 256 - len, "%.*s", labelLen, unitLabel.ptr); 576 } 577 } 578 else if (Section == FPN_Preset) 579 { 580 if (!_client.presetBank.isValidPresetIndex(Index)) 581 return; 582 583 const(char)[] name = _client.presetBank.preset(Index).name; 584 snprintf(Name, 256, "%.*s", cast(int)name.length, name.ptr); 585 } 586 else 587 { 588 debug(logFLPClient) debugLogf("Unsupported name Section = %d\n", Section); 589 } 590 version(DigitalMars) 591 Name[255] = '\0'; // DigitalMars snprintf workaround 592 } 593 594 // events 595 @guiThread @mixerThread 596 int ProcessEvent(int EventID, int EventValue, int Flags) 597 { 598 switch (EventID) 599 { 600 case FPE_Tempo: 601 float tempo = *cast(float*)(&EventValue); 602 atomicStore(_hostTempo, tempo); 603 break; 604 605 case FPE_MaxPoly: 606 // ignored, we use 100 as default value instead 607 int maxPoly = EventValue; 608 break; 609 610 case FPE_MIDI_Pitch: 611 // AFAIK this is never called 612 debug(logFLPClient) debugLogf("FPE_MIDI_Pitch = %d\n", EventValue); 613 break; 614 615 616 default: 617 break; 618 } 619 620 debug(logFLPClient) debugLogf("ProcessEvent %d %d %d\n", EventID, EventValue, Flags); 621 return 0; 622 } 623 624 @guiThread @mixerThread 625 int ProcessParam(int Index, int Value, int RECFlags) 626 { 627 int origValue = Value; 628 ScopedForeignCallback!(false, true) scopedCallback; 629 scopedCallback.enter(); 630 631 if ( ! _client.isValidParamIndex(Index)) 632 { 633 return Value; // well, as gain example 634 } 635 636 // Rather protracted callback. 637 // 638 // "First, you need to check if REC_FromMIDI is included. If it is, this means that the 639 // Value parameter contains a value between 0 and <smthg>. This Value then needs to be 640 // translated to fall in the range that the plugin uses for the parameter. For this 641 // reason, TDelphiFruityPlug and TCPPFruityPlug implement the function TranslateMidi. 642 // You pass it Value and the minimum and maximum value of your parameter, and it 643 // returns the right value. 644 // REC_FromMIDI is really important and has to be supported by the plugin. It is not 645 // just used by FL Studio to provide you with a new parameter value, but also to 646 // determine the minimum and maximum values for a parameter." 647 648 Parameter param = _client.param(Index); 649 float Valuef; // use instead of Value, if the parameter is FloatParameter. 650 651 if (RECFlags & REC_FromMIDI) 652 { 653 // Example says 1073741824 as max value 654 // Doc says 65536 as max value, but it is wrong. 655 double normalizeMIDI = 1.0 / 1073741824.0; 656 657 // Before FL20, this maximum value is 65536. 658 if (_host.majorVersion() < 20) 659 normalizeMIDI = 1.0 / 65536.0; 660 661 double fNormValue = Value * normalizeMIDI; 662 663 if (auto bp = cast(BoolParameter)param) 664 { 665 Value = (fNormValue >= 0.5 ? 1 : 0); 666 } 667 else if (auto ip = cast(IntegerParameter)param) 668 { 669 Value = ip.fromNormalized(fNormValue); 670 } 671 else if (auto fp = cast(FloatParameter)param) 672 { 673 Valuef = fp.fromNormalized(fNormValue); 674 } 675 else 676 { 677 assert(false); // TODO whenever there is more parameter types around. 678 } 679 } 680 else 681 { 682 Valuef = 0.0f; // whatever, will be unused 683 if (auto fp = cast(FloatParameter)param) 684 { 685 Valuef = *cast(float*)&Value; 686 } 687 } 688 689 // At this point, both Value (or Valuef) contain a value provided by the host. 690 // In non-normalized space. 691 692 if (RECFlags & REC_UpdateValue) 693 { 694 // Choosing to ignore REC_UpdateControl here, not sure why it would be the host 695 // prerogative. Especially with the issue of double-updates when editing. 696 // 697 // Parameters setFromHost take only normalized things, so that's what we do, we 698 // get (back?) to normalized space. 699 if (auto bp = cast(BoolParameter)param) 700 { 701 bp.setFromHost(Value ? 1.0 : 0.0); 702 } 703 else if (auto ip = cast(IntegerParameter)param) 704 { 705 ip.setFromHost(ip.toNormalized(Value)); 706 } 707 else if (auto fp = cast(FloatParameter)param) 708 { 709 fp.setFromHost(fp.toNormalized(Valuef)); 710 } 711 else 712 { 713 assert(false); 714 } 715 } 716 else if (RECFlags & REC_GetValue) 717 { 718 if (auto bp = cast(BoolParameter)param) 719 { 720 Value = bp.value() ? 1 : 0; 721 } 722 else if (auto ip = cast(IntegerParameter)param) 723 { 724 Value = ip.value(); 725 } 726 else if (auto fp = cast(FloatParameter)param) 727 { 728 float v = fp.value(); 729 Value = *cast(int*)&v; 730 } 731 else 732 { 733 assert(false); 734 } 735 } 736 return Value; 737 } 738 739 // effect processing (source & dest can be the same) 740 @mixerThread 741 void Eff_Render(PWAV32FS SourceBuffer, PWAV32FS DestBuffer, int Length) 742 { 743 ScopedForeignCallback!(false, true) scopedCallback; 744 scopedCallback.enter(); 745 746 resetClientIfNeeded(2, 2, Length); 747 enqueuePendingMIDIInputMessages(); 748 749 bool bypass = atomicLoad(_hostBypass); 750 751 TimeInfo info; 752 updateTimeInfoBegin(info); 753 754 // clear MIDI out buffers 755 if (_client.sendsMIDI) 756 _client.clearAccumulatedOutputMidiMessages(); 757 758 if (bypass) 759 { 760 // Note: no delay compensation. 761 // Do nothing for MIDI messages, same as VST3. Not sure what should happen here. 762 memmove(DestBuffer, SourceBuffer, Length * float.sizeof * 2); 763 } 764 else 765 { 766 float*[2] pInputs = [ _inputBuf[0].ptr, _inputBuf[1].ptr ]; 767 float*[2] pOutputs = [ _outputBuf[0].ptr, _outputBuf[1].ptr ]; 768 769 deinterleaveBuffers(SourceBuffer, pInputs[0], pInputs[1], Length); 770 771 pOutputs[0][0..Length] = pInputs[0][0..Length]; 772 pOutputs[1][0..Length] = pInputs[1][0..Length]; 773 774 _client.processAudioFromHost(pInputs[0..2], pOutputs[0..2], Length, info); 775 776 pOutputs = [ _outputBuf[0].ptr, _outputBuf[1].ptr ]; 777 interleaveBuffers(pOutputs[0], pOutputs[1], DestBuffer, Length); 778 } 779 sendPendingMIDIOutput(); 780 updateTimeInfoEnd(Length); 781 } 782 783 // generator processing (can render less than length) 784 @mixerThread 785 void Gen_Render(PWAV32FS DestBuffer, ref int Length) 786 { 787 // Oddity about Length: 788 // "The Length parameter in Gen_Render serves a somewhat different purpose than in 789 // Eff_Render. It still specifies how many samples are in the buffers for each 790 // channel, just like in Eff_Render. But this value is a maximum in Gen_Render. 791 // The generator may choose to generate less samples than Length specifies. In this 792 // case, Length has to be set to the actual amount of samples that were generated 793 // before the function returns. For this reason, Length in Gen_Render can be altered 794 // by the function (it is a var parameter in Delphi and a reference (&) in C++)." 795 // 796 // But here we ignores that and just generates the maximum amount. 797 798 // "You can take a look at Osc3 for an example of what Gen_Render has to do." 799 // It seems FLStudio has an envelope and a knowledge of internal voices. 800 801 ScopedForeignCallback!(false, true) scopedCallback; 802 scopedCallback.enter(); 803 804 resetClientIfNeeded(0, 2, Length); 805 enqueuePendingMIDIInputMessages(); 806 807 bool bypass = atomicLoad(_hostBypass); // Note: it seem FL prefers to simply not send 808 // MIDI rather than bypassing synths. Untested. 809 810 TimeInfo info; 811 updateTimeInfoBegin(info); 812 813 // clear MIDI out buffers 814 if (_client.sendsMIDI) 815 _client.clearAccumulatedOutputMidiMessages(); 816 817 if (bypass) 818 { 819 // Do nothing for MIDI messages, same as VST3. Not sure what should happen here. 820 memset(DestBuffer, 0, Length * float.sizeof * 2); 821 } 822 else 823 { 824 float*[2] pInputs = [ _inputBuf[0].ptr, _inputBuf[1].ptr ]; 825 float*[2] pOutputs = [ _outputBuf[0].ptr, _outputBuf[1].ptr ]; 826 _client.processAudioFromHost(pInputs[0..0], pOutputs[0..2], Length, info); 827 pOutputs = [ _outputBuf[0].ptr, _outputBuf[1].ptr ]; 828 interleaveBuffers(pOutputs[0], pOutputs[1], DestBuffer, Length); 829 } 830 sendPendingMIDIOutput(); 831 updateTimeInfoEnd(Length); 832 } 833 834 // <voice handling> 835 // Some documentation says all such voice handling function are actually only @mixerThread. 836 // Contradicts what the header says: "(GM)". 837 // We'll make a trust call here and consider the function ARE in @mixerThread. 838 @guiThread @mixerThread 839 TVoiceHandle TriggerVoice(TVoiceParams* VoiceParams, intptr_t SetTag) 840 { 841 float noteInMidiScale = VoiceParams.InitLevels.Pitch / 100.0f; 842 843 // FUTURE: put the reminder in some other MIDI message 844 // but that would be per-note pitch bend... 845 846 int noteNumber = cast(int) round( noteInMidiScale ); 847 float fractionalNote = noteInMidiScale - noteNumber; 848 849 if (noteNumber < 0 || noteNumber > 127) 850 return 0; 851 852 int ivoice = allocVoice(VoiceParams, SetTag, ++_totalVoicesTriggered, noteNumber); 853 854 if (ivoice == -1) 855 return 0; // hopefully it means "no voice created" 856 857 // Since from documentation, mixer lock is taken here, we can absolutely enqueue MIDI 858 // messages from here. 859 860 float Vol = VoiceParams.InitLevels.Vol; 861 int velocity = cast(int)(128.0f * Vol); 862 if (velocity < 1) velocity = 1; 863 if (velocity > 127) velocity = 127; 864 865 int channel = 0; 866 _client.enqueueMIDIFromHost( makeMidiMessageNoteOn(0, channel, noteNumber, velocity) ); 867 868 // The handle is simply 1 + ivoice, so that we don't return zero. 869 return 1 + ivoice; 870 } 871 872 @guiThread @mixerThread 873 void Voice_Release(TVoiceHandle Handle) 874 { 875 if (Handle == 0) 876 return; 877 878 int channel = 0; 879 int midiNote = voiceInfo(Handle).midiNote; 880 int noteOffVelocity = 100; // unused, FUTURE 881 _client.enqueueMIDIFromHost( makeMidiMessageNoteOff(0, channel, midiNote) ); 882 int index = cast(int)(Handle - 1); 883 freeVoiceIndex(index); 884 } 885 886 @guiThread @mixerThread 887 void Voice_Kill(TVoiceHandle Handle) 888 { 889 if (Handle == 0) 890 return; 891 892 if (voiceInfo(Handle).state == VOICE_PLAYING) 893 { 894 // Send note off, since it went from trigger to kill without release. 895 int channel = 0; 896 int midiNote = voiceInfo(Handle).midiNote; 897 int noteOffVelocity = 100; // unused, FUTURE 898 _client.enqueueMIDIFromHost( makeMidiMessageNoteOff(0, channel, midiNote) ); 899 900 int index = cast(int)(Handle - 1); 901 freeVoiceIndex(index); 902 } 903 904 // Do nothing, we already sent a Note Off in Voice_release. 905 } 906 907 @guiThread @mixerThread 908 int Voice_ProcessEvent(TVoiceHandle Handle, 909 intptr_t EventID, 910 intptr_t EventValue, 911 intptr_t Flags) 912 { 913 if (Handle == 0) 914 return 0; 915 916 // Is this ever called? Haven't seen it. 917 debugLogf("TODO Voice_ProcessEvent %d\n", EventID); 918 919 return 0; 920 } 921 922 @guiThread @mixerThread 923 int Voice_Render(TVoiceHandle Handle, PWAV32FS DestBuffer, ref int Length) 924 { 925 // Shouldn't be called ever, as we don't support generators that renders their voices 926 // separately. 927 return 0; 928 } 929 // </voice handling> 930 931 932 // (see FPF_WantNewTick) called before a new tick is mixed (not played) 933 // internal controller plugins should call OnControllerChanged from here 934 @mixerThread 935 void NewTick() 936 { 937 } 938 939 // (see FHD_WantMIDITick) called when a tick is being played (not mixed) (not used yet) 940 @midiSyncThread 941 void MIDITick() 942 { 943 } 944 945 // MIDI input message 946 @guiThread @mixerThread 947 void MIDIIn(ref int Msg) 948 { 949 // If host calls this despite not receiving MIDI, we should evaluate our assumptions 950 // regarding FL and MIDI Input. 951 assert(_client.receivesMIDI); 952 953 // This is our own Mutex 954 ubyte status = Msg & 255; 955 ubyte data1 = (Msg >>> 8) & 255; 956 ubyte data2 = (Msg >>> 16) & 255; 957 958 // In practice, MIDIIn is called from the mixer thread, so no sync issue are seen 959 // happen with guiThread calling `MIDIIn`. But since it's still possible from 960 // documentation, let's be good citizens and use a separate buffer. 961 // Then enqueue it from the mixer thread before a buffer. 962 // Important: FLStudio pass no offset. It seems to splits buffers alongside MIDI 963 // messages, which is quite commendable. 964 int offset = 0; 965 MidiMessage msg = MidiMessage(offset, status, data1, data2); 966 967 _midiInputMutex.lock(); 968 _incomingMIDI.pushBack(msg); 969 _midiInputMutex.unlock(); 970 971 // Why would we "kill" the message? Not sure. FLStudio uses a rather clean Port + 972 // Channel way to route MIDI. 973 // So: let's not kill it. 974 bool kill = false; 975 if (kill) 976 { 977 enum int MIDIMsg_Null = 0xFFFF_FFFF; 978 Msg = MIDIMsg_Null; // kill message 979 } 980 } 981 982 // buffered messages to itself (see PlugMsg_Delayed) 983 @midiSyncThread 984 void MsgIn(intptr_t Msg) 985 { 986 // Not sure why it's there. 987 } 988 989 // voice handling 990 @guiThread @mixerThread 991 int OutputVoice_ProcessEvent(TOutVoiceHandle Handle, intptr_t EventID, intptr_t EventValue, 992 intptr_t Flags) 993 { 994 // Not implemented, as we never report Output Voices, a FL-specific feature. 995 // Not sure what the return value should be from the SDK, but probaby FLStudio won't 996 // call this. 997 return 0; 998 } 999 1000 @guiThread @mixerThread 1001 void OutputVoice_Kill(TVoiceHandle Handle) 1002 { 1003 // Not implemented, as we never report Output Voices, a FL-specific feature. 1004 } 1005 1006 // </Implements TFruityPlug> 1007 } 1008 1009 private: 1010 1011 enum double MiddleCFreq = 523.251130601197; 1012 enum double MiddleCMul = cast(float)0x10000000 * MiddleCFreq * cast(float)0x10; 1013 1014 Client _client; /// Wrapped generic client. 1015 TFruityPlugHost _host; /// A whole lot of callbacks to host. 1016 TFruityPlugInfo _fruityPlugInfo; /// Plug-in formation for the host to read. 1017 FLHostCommand _hostCommand; /// Host command object. 1018 UncheckedMutex _graphicsMutex; /// An oddity mandated by dplug:client. 1019 1020 char[128] _longNameBuf; /// Buffer for plugin long name. 1021 char[32] _shortNameBuf; /// Buffer for plugin short name. 1022 1023 shared(size_t) _hostMaxFrames = 512; /// Max frames that the host demanded. 1024 @mixerThread int _clientMaxFrames = 0; /// Max frames last used by client. 1025 shared(size_t) _hostSampleRate = 44100; /// Samplerate that the host demanded. 1026 @mixerThread int _clientSampleRate = 0; /// Samplerate last used by client. 1027 shared(float) _hostTempo = 120.0f; /// Tempo reported by host. 1028 shared(bool) _hostHostPlaying = false; /// Whether the host is playing. 1029 shared(bool) _hostBypass = false; /// Is the plugin "enabled". 1030 1031 @mixerThread float[][2] _inputBuf; /// Temp buffers to deinterleave and pass to plug-in. 1032 @mixerThread float[][2] _outputBuf; /// Plug-in outoput, deinterleaved. 1033 1034 // Time management 1035 @mixerThread long _mixingTimeInSamples; /// Only ever updated in mixer thread. Current time. 1036 shared(double) _hostTicksReference; /// Last tick reference given by host. 1037 shared(bool) _hostTicksChanged; /// Set to true if tick reference changed. If true, 1038 /// Look at `_hostTicksReference` value. 1039 shared(float) _hostSamplesInATick = 32.0f; /// Last known conversion from ticks to samples. 1040 1041 Vec!MidiMessage _incomingMIDI; /// Incoming MIDI messages for next buffer. 1042 UncheckedMutex _midiInputMutex; /// Protects access to _incomingMIDI. 1043 Vec!ubyte _lastChunk; 1044 1045 void initializeInfo() 1046 { 1047 int flags = FPF_NewVoiceParams; 1048 version(OSX) 1049 flags |= FPF_MacNeedsNSView; 1050 if (_client.isSynth) flags |= FPF_Generator; 1051 if (!_client.hasGUI) flags |= FPF_NoWindow; // SDK says it's not implemented? mm. 1052 if (_client.sendsMIDI) flags |= FPF_MIDIOut; 1053 if (_client.receivesMIDI) flags |= FPF_GetNoteInput; // Generators ignore this apparently. 1054 1055 if (_client.tailSizeInSeconds() == float.infinity) 1056 { 1057 flags |= FPF_CantSmartDisable; 1058 } 1059 1060 _client.getPluginName(_longNameBuf.ptr, 128); 1061 _client.getPluginName(_shortNameBuf.ptr, 32); // yup, same name 1062 _fruityPlugInfo.SDKVersion = 1; 1063 _fruityPlugInfo.LongName = _longNameBuf.ptr; 1064 _fruityPlugInfo.ShortName = _shortNameBuf.ptr; 1065 _fruityPlugInfo.Flags = flags; 1066 _fruityPlugInfo.NumParams = cast(int)(_client.params.length); 1067 _fruityPlugInfo.DefPoly = 0; 1068 _fruityPlugInfo.NumOutCtrls = 0; 1069 _fruityPlugInfo.NumOutVoices = 0; 1070 _fruityPlugInfo.Reserved[] = 0; 1071 } 1072 1073 @mixerThread 1074 void resetClientIfNeeded(int numInputs, int numOutputs, int framesJustGiven) 1075 { 1076 int hostMaxFrames = cast(int) atomicLoad(_hostMaxFrames); 1077 int hostSampleRate = cast(int) atomicLoad(_hostSampleRate); 1078 1079 // FLStudio would have an issue if it was the case, since we did use an atomic. 1080 assert (framesJustGiven <= hostMaxFrames); 1081 1082 bool maxFramesChanged = hostMaxFrames != _clientMaxFrames; 1083 bool sampleRateChanged = hostSampleRate != _clientSampleRate; 1084 1085 if (maxFramesChanged || sampleRateChanged) 1086 { 1087 _client.resetFromHost(hostSampleRate, hostMaxFrames, numInputs, numOutputs); 1088 _clientMaxFrames = hostMaxFrames; 1089 _clientSampleRate = hostSampleRate; 1090 1091 _inputBuf[0].reallocBuffer(hostMaxFrames); // even if unused in generator case. 1092 _inputBuf[1].reallocBuffer(hostMaxFrames); 1093 _outputBuf[0].reallocBuffer(hostMaxFrames); 1094 _outputBuf[1].reallocBuffer(hostMaxFrames); 1095 1096 // Report new latency 1097 _hostCommand.reportLatency(_client.latencySamples(_clientSampleRate)); 1098 } 1099 } 1100 1101 @mixerThread 1102 void deinterleaveBuffers(float[2]* input, float* leftOutput, float* rightOutput, int frames) 1103 { 1104 for (int n = 0; n < frames; ++n) 1105 { 1106 leftOutput[n] = input[n][0]; 1107 rightOutput[n] = input[n][1]; 1108 } 1109 } 1110 1111 @mixerThread 1112 void interleaveBuffers(float* leftInput, float* rightInput, float[2]* output, int frames) 1113 { 1114 for (int n = 0; n < frames; ++n) 1115 { 1116 output[n][0] = leftInput[n]; 1117 output[n][1] = rightInput[n]; 1118 } 1119 } 1120 1121 @mixerThread 1122 void enqueuePendingMIDIInputMessages() 1123 { 1124 if (!_client.receivesMIDI) 1125 return; 1126 1127 _midiInputMutex.lock(); 1128 foreach(msg; _incomingMIDI[]) 1129 { 1130 _client.enqueueMIDIFromHost(msg); 1131 } 1132 _incomingMIDI.clearContents(); 1133 _midiInputMutex.unlock(); 1134 } 1135 1136 @mixerThread 1137 void sendPendingMIDIOutput() 1138 { 1139 if (!_client.sendsMIDI) 1140 return; 1141 1142 const(MidiMessage)[] outMsgs = _client.getAccumulatedOutputMidiMessages(); 1143 1144 foreach(msg; outMsgs) 1145 { 1146 ubyte[4] b = [0, 0, 0, 0]; 1147 int len = msg.toBytes(b.ptr, 3); 1148 1149 if (len == 0 || len > 3) 1150 { 1151 // nothing written, or length exceeded, ignore this message 1152 continue; 1153 } 1154 1155 TMIDIOutMsg outMsg; 1156 outMsg.Status = b[0]; 1157 outMsg.Data1 = b[1]; 1158 outMsg.Data2 = b[2]; 1159 1160 // FUTURE: MIDI out for FLPlugins will need a way to change 1161 // its output port... else not really usable in FL. 1162 // Well, you can still multiplex on channels I guess. 1163 outMsg.Port = 0; 1164 1165 // Let's trust FL to not need that pointer beyond that host call. 1166 _hostCommand.sendMIDIMessage(*cast(uint*)&outMsg); 1167 } 1168 } 1169 1170 // <voice pool> 1171 1172 enum int VOICE_NOT_PLAYING = 0; 1173 enum int VOICE_PLAYING = 1; 1174 1175 void initializeVoices() 1176 { 1177 availableVoices = MAX_FL_POLYPHONY; 1178 for (int n = 0; n < MAX_FL_POLYPHONY; ++n) 1179 { 1180 availableVoiceList[n] = n; 1181 voicePool[n].state = VOICE_NOT_PLAYING; 1182 } 1183 } 1184 1185 // -1 if nothing available. 1186 int allocVoiceIndex() 1187 { 1188 if (availableVoices <= 0) 1189 return -1; 1190 1191 int index = availableVoiceList[--availableVoices]; 1192 assert(voicePool[index].state == VOICE_NOT_PLAYING); 1193 voicePool[index].state = VOICE_PLAYING; 1194 return index; 1195 } 1196 1197 void freeVoiceIndex(int voiceIndex) 1198 { 1199 // Note: we don't check that FL gives back right voice ID there. Real trust going on there. 1200 assert(voicePool[voiceIndex].state == VOICE_PLAYING); 1201 voicePool[voiceIndex].state = VOICE_NOT_PLAYING; 1202 availableVoiceList[availableVoices++] = voiceIndex; 1203 assert(availableVoices <= MAX_FL_POLYPHONY); 1204 } 1205 1206 static struct VoiceInfo 1207 { 1208 int state; 1209 TVoiceParams* params; 1210 intptr_t tag; 1211 int numTotalVoiceTriggered; 1212 int midiNote; // 0 to 127 1213 1214 bool isPlaying() 1215 { 1216 return state != VOICE_NOT_PLAYING; 1217 } 1218 } 1219 1220 enum int MAX_FL_POLYPHONY = 100; // maximum possible of voices for generators. 1221 VoiceInfo[MAX_FL_POLYPHONY] voicePool; 1222 1223 ref VoiceInfo voiceInfo(TVoiceHandle handle) 1224 { 1225 assert(handle != 0); 1226 return voicePool[handle - 1]; 1227 } 1228 1229 // stack of available voice indices. 1230 // availableVoiceList[0..availableVoices] are the available indices. 1231 int[MAX_FL_POLYPHONY] availableVoiceList; 1232 1233 // Number of available voice indices. 1234 int availableVoices; 1235 1236 // A more unique, and increasing, identifier. In the FUTURE, this may be used to kill a voice 1237 // on host request. 1238 int _totalVoicesTriggered; 1239 1240 // Allocate voice in voice pool. 1241 // Returns: -1 if not available (not supposed to ever happen). 1242 int allocVoice(TVoiceParams* VoiceParams, intptr_t SetTag, int totalVoiceCount, int midiNote) 1243 { 1244 int index = allocVoiceIndex(); 1245 1246 if (index == -1) 1247 return -1; 1248 1249 voicePool[index].state = VOICE_PLAYING; 1250 voicePool[index].params = VoiceParams; 1251 voicePool[index].tag = SetTag; 1252 voicePool[index].numTotalVoiceTriggered = totalVoiceCount; 1253 voicePool[index].midiNote = midiNote; 1254 return index; 1255 } 1256 1257 // </voice pool> 1258 1259 1260 @mixerThread 1261 void updateTimeInfoBegin(out TimeInfo info) 1262 { 1263 if (cas(&_hostTicksChanged, true, false)) 1264 { 1265 float samplesInTick = atomicLoad(_hostSamplesInATick); 1266 double hostTicks = atomicLoad(_hostTicksReference); 1267 1268 // Not sure if .t2 should be added, but well. 1269 // I haven't seen FL loop with non-zero t2. 1270 _mixingTimeInSamples = cast(long)(hostTicks * samplesInTick); 1271 } 1272 1273 info.tempo = atomicLoad(_hostTempo); 1274 info.hostIsPlaying = atomicLoad(_hostHostPlaying); 1275 info.timeInSamples = _mixingTimeInSamples; 1276 } 1277 1278 @mixerThread 1279 void updateTimeInfoEnd(int samplesElapsed) 1280 { 1281 _mixingTimeInSamples += samplesElapsed; 1282 } 1283 } 1284 1285 class FLHostCommand : IHostCommand 1286 { 1287 public: 1288 nothrow @nogc: 1289 1290 this(TFruityPlugHost pHost,TPluginTag tag) 1291 { 1292 _host = pHost; 1293 _tag = tag; 1294 } 1295 1296 ~this() 1297 { 1298 } 1299 1300 override void beginParamEdit(int paramIndex) 1301 { 1302 // not needed in FL 1303 } 1304 1305 override void paramAutomate(int paramIndex, float value) 1306 { 1307 // "In order to make your parameters recordable in FL Studio, you have to call this 1308 // function whenever a parameter is changed from within your plugin (probably because 1309 // the user turned a wheel or something). You need to pass HostTag in the Sender 1310 // parameter. To let the host know which parameter has just been changed, pass the 1311 // parameter index in Index. Finally, pass the new value (as an integer) in Value." 1312 1313 _host.OnParamChanged(_tag, paramIndex, *cast(int*)&value); 1314 } 1315 1316 override void endParamEdit(int paramIndex) 1317 { 1318 // not needed in FL 1319 } 1320 1321 override bool requestResize(int widthLogicalPixels, int heightLogicalPixels) 1322 { 1323 return false; 1324 } 1325 1326 override bool notifyResized() 1327 { 1328 _host.Dispatcher(_tag, FHD_EditorResized, 0, 0); 1329 return true; 1330 } 1331 1332 override DAW getDAW() 1333 { 1334 return DAW.FLStudio; 1335 } 1336 1337 PluginFormat getPluginFormat() 1338 { 1339 return PluginFormat.flp; 1340 } 1341 1342 void setNumPresets(int numPresets) 1343 { 1344 int res = cast(int) _host.Dispatcher(_tag, FHD_SetNumPresets, 0, numPresets); 1345 } 1346 1347 void wantsMIDIInput() 1348 { 1349 _host.Dispatcher(_tag, FHD_WantMIDIInput, 0, 1); 1350 } 1351 1352 void reportLatency(int latencySamples) 1353 { 1354 _host.Dispatcher(_tag, FHD_SetLatency, 0, latencySamples); 1355 } 1356 1357 void disableIdleNotifications() 1358 { 1359 _host.Dispatcher(_tag, FHD_WantIdle, 0, 0); 1360 } 1361 1362 void enableMIDIOut() 1363 { 1364 _host.Dispatcher(_tag, FHD_ActivateMIDI, 0, 0); 1365 } 1366 1367 void sendMIDIMessage(uint Msg) 1368 { 1369 _host.MIDIOut_Delayed(_tag, Msg); // _host.MIDIOut doesn't work! 1370 } 1371 1372 void getMixingTimeInTicks(out double ticks, out double samplesOffset) 1373 { 1374 enum int GT_Beats = 0; // beats 1375 enum int GT_AbsoluteMS = 1; // absolute milliseconds 1376 enum int GT_RunningMS = 2; // running milliseconds 1377 enum int GT_MSSinceStart = 3; // milliseconds since soundcard restart 1378 enum int GT_Ticks = 4; // ticks 1379 enum int GT_LocalTime = 1 << 31; // time relative to song start 1380 1381 enum int GT_FlagsMask = 0xFFFFFF00; 1382 enum int GT_TimeFormatMask = 0x000000FF; 1383 1384 _time.t = 0; 1385 _time.t2 = 0; 1386 intptr_t Value = cast(intptr_t) &_time; 1387 intptr_t res = _host.Dispatcher(_tag, FHD_GetMixingTime, GT_Ticks | GT_LocalTime, Value); 1388 ticks = _time.t; 1389 samplesOffset = _time.t2; 1390 } 1391 1392 private: 1393 TFruityPlugHost _host; 1394 TPluginTag _tag; 1395 TFPTime _time; 1396 }