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 |
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 { |
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
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 "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 }; |
struct TVoice |
extern "C" __declspec(dllexport) TFruityPlug * _stdcall CreatePlugInstance(TFruityPlugHost *Host, int Tag) 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) // 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() 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) 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.
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) |
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) 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) |
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) 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(); // 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) |
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++) |
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 |
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.