FruityGain (Delphi)        FruityGain (C++)        Osc3 (Delphi)        Osc3 (C++)        Sine (Delphi)        Sine (C++)

Sine (C++)

TestPlug.h
Testplug.cpp

This page contains a walkthrough of the code of the Sine example. It doesn't include an explanation of the things that are the same as in the FruityGain example, so you might first want to read that.

 

TestPlug.h
This unit contains the plugin class.

We start at the top. We include the afxwin.h (Visual C++ only) and the sdk headers (fp_plugclass.h and fp_cplug.h). Then we declare the number of parameters we'll use.

#ifndef TestPlugH
#define TestPlugH
#include <afxwin.h> #include "..\fp_plugclass.h" #include "..\fp_cplug.h" const NumParamsConst = 1; // the amount of parameters const StateSizeConst = NumParamsConst * sizeof(int); // the size of all parameters together

Then we arrive at the plugin class declaration. It's derived from TCPPFruityPlug. The ParamValue array holds the current value for the parameter. VoiceList is a list meant to store our own information about the voices that are created. This list will be used in Gen_Render to render all voices one after the other.

class TTestPlug : public TCPPFruityPlug {
private:
CDialog* EditorForm;
public:
int ParamValue[NumParamsConst];
CPtrList VoiceList; TTestPlug(int Tag, TFruityPlugHost *Host); virtual void _stdcall DestroyObject(); virtual int _stdcall Dispatcher(int ID, int Index, int Value); virtual void _stdcall Idle(); virtual void _stdcall SaveRestoreState(IStream *Stream, BOOL Save); virtual void _stdcall GetName(int Section, int Index, int Value, char *Name); virtual int _stdcall ProcessParam(int Index, int Value, int RECFlags); virtual void _stdcall Gen_Render(PWAV32FS DestBuffer, int &Length); virtual TVoiceHandle _stdcall TriggerVoice(PVoiceParams voiceParams, int SetTag); virtual void _stdcall Voice_Release(TVoiceHandle Handle); virtual void _stdcall Voice_Kill(TVoiceHandle Handle); virtual int _stdcall Voice_ProcessEvent(TVoiceHandle Handle, int EventID, int EventValue, int Flags); };
//--------------------------------------------------------------------------- #endif

Furthermore we define all the regulars : DestroyObject, Dispatcher, SaveRestoreState, GetName and ProcessParam. One function you won't find here is Eff_Render, since we're not making an effect. Instead we have Gen_Render, as this is a full generator. If it were a hybrid generator, we wouldn't define Gen_Render either but go with Voice_Render.
We also define TriggerVoice, Voice_Release, Voice_Kill and Voice_ProcessEvent to handle the creation and destruction of voices.

Top

 

Testplug.cpp

In the implementation, we include the plugin and editor header files. We also declare the PlugInfo variable. Note that we're using FPF_Type_FullGen for Flags, to specify that we're creating a generator that handles rendering through Gen_Render. If we wanted FL Studio to call Voice_Render, we'd have to specify FPF_Type_HybridGen.

//---------------------------------------------------------------------------
#include <afxwin.h>
#include <objidl.h>
#include "TestPlug.h"
#include "SynthForm.h"
// the information structure describing this plugin to the host
TFruityPlugInfo PlugInfo = {
    CurrentSDKVersion,
    "Sine (VC++)",
    "Sine",
    FPF_Type_FullGen,
    NumParamsConst,
    0, // infinite polyphony
    0  // no internal controllers
};

Next up is the structure type that holds all data for a voice. The Params field holds the voice parameter structure that was passed to us by the host in TriggerVoice. HostTag is used for communication with the host.
The Gated field tells us when a voice has been released. CurrentPitch allows us to detect changes in Pitch in Gen_Render and update some variables only when necessary. Position and Speed are used to go through the sine wavetable (see Gen_Render). Finally, LastLVol and LastRVol are used for level ramping, which should avoid clicks when a voice ends abruptly.

struct TVoice
{
PVoiceParams Params;
TVoiceHandle HostTag;
bool Gated;
unsigned int Position;
int CurrentPitch;
int Speed;
float LastLVol;
float LastRVol;
};

Next comes the implementation of CreatePlugInstance, the function that will be exported by the plugin dll. We simply create a TTestPlug object, passing the creation parameters, and return it as the result of the function (note that the AFX_MANAGE_STATE line is only for Visual C++).

extern "C" __declspec(dllexport) TFruityPlug * _stdcall CreatePlugInstance(TFruityPlugHost *Host, int Tag)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    TTestPlug* plug = new TTestPlug(Tag, Host);
    return (TFruityPlug *)plug;
};
       

The constructor doesn't do anything out of the ordinary. First we call the inherited constructor and pass the necessary parameters. We also assign the address of the PlugInfo record to the Info field, so the host can access the plugin information. Then we create the voice list and the editor form.

// TTestPlug
TTestPlug::TTestPlug(int Tag, TFruityPlugHost *Host)
: TCPPFruityPlug(Tag, Host)
{
Info = &PlugInfo;
    // create the editor form
    EditorForm = new SynthEditorForm;
    EditorForm->Create(IDD_SYNTHEDITORFORM_DIALOG);
    SynthEditorForm *Form = ((SynthEditorForm *)EditorForm);
    Form->FruityPlug = this;
    // initialize the controls on the editor form
    Form->m_ControlSlider.SetRange(0, 127);
    Form->m_ControlSlider.SetPos(64);
    // get all the parameter controls into the ParamCtrl array
    Form->ParamCtrl[0] = &(Form->m_ControlSlider);
    // set the parameters to their default values
    // (read from the initial values of the controls)
    for (int i = 0; i < NumParamsConst; i++)
        ParamValue[i] = Form->ParamCtrl[i]->GetPos();

    ProcessAllParams();
}

When the editor form is created, we tell it who we are, by assigning the this pointer to its FruityPlug member. We then initialize our ParamValue array with the current values of the controls (set in the property editor), after setting the ParamCtrl array. Finally, we call ProcessAllParams to make all the necessary changes.

void _stdcall TTestPlug::DestroyObject()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    delete EditorForm;
    TCPPFruityPlug::DestroyObject(); // don't forget this
}

DestroyObject gets called by the host to destroy the plugin object. The important thing here is that we free the VoiceList object and the editor window. Since FL normally deletes all voices before calling DestroyObject, we assume the list is empty.

int _stdcall TTestPlug::Dispatcher(int ID, int Index, int Value)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    switch (ID) {
        case FPD_ShowEditor :
            ...   // showing and hiding the editor has been explained for FruityGain already
            break;
        case FPD_SetSampleRate :
            SmpRate = Value;
            PitchMul = MiddleCMul/SmpRate;
            break;
    } 
    return 0;
}

There's one difference in Dispatcher, compared to FruityGain. It also handles the FPD_SetSampleRate message, which gets sent whenever the sample rate changes. We simply set SmpRate and calculate a new pitch multiplier.

We'll skip SaveRestoreState(), GetName() and ProcessParam() here, since there are no real differences compared to FruityGain.

So we arrive at some interesting stuff: handling the creation and killing of voices.


TVoiceHandle _stdcall TTestPlug::TriggerVoice(PVoiceParams voiceParams, int SetTag)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    TVoice *voice;
    // create & init
    voice = new TVoice;
    voice->HostTag = SetTag;
    voice->Params = voiceParams;
    voice->Gated = false;
    voice->Position = 0;
    voice->CurrentPitch = voiceParams->FinalLevels.Pitch;
    voice->Speed = GetStep_Cents(voice->CurrentPitch);
    voice->LastLVol = 0;
    voice->LastRVol = 0;
    // add to the list
    VoiceList.AddTail(voice);
    return (TVoiceHandle)voice;
}

TriggerVoice gets called by FL when a voice has been created. FL passes the voice parameters and an identifying tag to the function.
We create a new pointer to a TVoice structure to store our own voice settings, and initialize it. Finally we add it to the voice list, so we have access to it at all time.
At the very end, we need to return a voice handle. This is some value we decide upon ourselves, but it needs to be unique. It will be passed to other functions later to let us know what voice the host is talking about. So we return the pointer to our TVoice structure, after casting it to a TVoiceHandle. This way it's both uniqe and very easy (and quick) to know the voice later on.

void _stdcall TTestPlug::Voice_Release(TVoiceHandle Handle)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
((TVoice *)Handle)->Gated = true;
}

Voice_Release isn't meant to kill the voice (Voice_Kill is meant for that). Instead, it gives us a warning that the voice will need to be killed. This gives us the chance to fade it out, for example. We simply respond by signalling the voice as gated, which will be checked for later in Gen_Render.
Note that we're casting the Handle parameter to a pointer to TVoice to get at the voice. Since we returned the pointer to the structure as the result of TriggerVoice, FL passes the same value to this function so it's safe to do this.

void _stdcall TTestPlug::Voice_Kill(TVoiceHandle Handle)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    POSITION pos = VoiceList.Find((void *)Handle);
    VoiceList.RemoveAt(pos);
    delete (TVoice *)Handle;
}

This is where we actually free the memory associated with the voice. First we remove the structure from the voice list (by casting the handle to a pointer). Then we free the memory.

int _stdcall TTestPlug::Voice_ProcessEvent(TVoiceHandle Handle, int EventID, int EventValue, int Flags)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return 0;
}

Voice_ProcessEvent won't be called yet. Nonetheless we need to implement it, so we don't get an abstract function call when FL does call it in a later version. We just return zero.

The final function in this unit is Gen_Render. This is a long function so we'll split it up.

First, we get a temporary buffer from the host object. It provides two stereo buffers, but we only need one mono buffer. So we use only the first half of the first buffer. We copy the sine wavetable provided by tge host to a local variable for easier reference. We also calculate a level from the plugin's only parameter.

void _stdcall TTestPlug::Gen_Render(PWAV32FS DestBuffer, int &Length)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
    TVoice *voice;
    float LVol, RVol;
    float level;
    int i;
    PWAV32FM Buffer = (PWAV32FM)PlugHost->TempBuffers[0];  // get our temporary buffer from the host
    PWaveT SineTable = PlugHost->WaveTables[0];
    level = ParamValue[0] * 0.01 * 0.5;                    // divide by 100 (0..1.27) and only take half of that (too loud otherwise)

After that, we're ready to render every voice. We have a for loop to accomplish this.

We check if the voice has been gated. If it is, we set the target volumes (LVol and RVol for left and right channels, respectively) to zero so the voice will quickly fade out. If it hasn't been gated, we ask the host to calculate the left and right volumes from the Pan and Vol fields of the voice parameters that were passed to TriggerVoice. These values can change before Gen_Render is called, so we have to calculate it every time.

    int count = VoiceList.GetCount();
for (i = 0; i < count; i++)
{
POSITION pos = VoiceList.FindIndex(i);
voice = (TVoice *)(VoiceList.GetAt(pos));
        // ramp to zero if the voice was released
        if (voice->Gated)
        {
            LVol = 0;
            RVol = 0;
        }
        // let the host compute volumes taking into account the per-voice pan and volume
        else
            PlugHost->ComputeLRVol(LVol, RVol, voice->Params->FinalLevels.Pan, voice->Params->FinalLevels.Vol);

Next up is the part of the code that handles slides. When a note in the pianoroll is set to slide, it just means that FL will change the pitch of the voice before each call to Gen_Render. So all we do here is check if the pitch has changed (compared to CurrentPitch) and if it has, we calculate a new Speed value.

        // change the pitch if necessary (slides) 
if (voice->Params->FinalLevels.Pitch != voice->CurrentPitch)
{
voice->CurrentPitch = voice->Params->FinalLevels.Pitch;
voice->Speed = GetStep_Cents(voice->CurrentPitch);
}

Once we've done all this, we can finally start copying samples from the sine wavetable provided by the host (PlugHost->Wavetables[0]) to our temporary buffer.

        // copy samples from the sine wavetable to the destination buffer
        for (int j = 0; j < Length; j++)
{
int pos = voice->Position >> WaveT_Shift;
(*Buffer)[j] = (*SineTable)[pos] * level;
// !!! make sure to disable any overflow checking in the compiler, as this is based on an overflow trick
voice->Position += voice->Speed;
}
// add the temporary buffer to the output buffer with ramping PlugHost->AddWave_32FM_32FS_Ramp(Buffer, DestBuffer, Length, LVol, RVol, voice->LastLVol, voice->LastRVol); }

After we've filled the temporary buffer for this voice, we add it to the destination buffer by calling PlugHost->AddWave_32FM_32FS_Ramp. This adds a mono buffer to a stereo buffer with level ramping, which will avoid clicks in the sound.

    // kill voices that were released
for (i = count-1; i >= 0; i--)
{
POSITION pos = VoiceList.FindIndex(i);
voice = (TVoice *)VoiceList.GetAt(pos);
if (voice->Gated)
PlugHost->Voice_Kill(voice->HostTag, TRUE);
}
}

Once outside the voice loop, we check which voices were gated. We ask the host to kill those. The second parameter of Voice_Kill tells the host to call our own Voice_Kill method so we can get rid of the voice record as well.

Top