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 }