FruityGain (Delphi) FruityGain (C++) Osc3 (Delphi) Osc3 (C++) Sine (Delphi) Sine (C++)
Sine (Delphi) |
TestPlug.pas
SynthForm.pas
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.pas
This unit contains the plugin class.
We start at the top. We include the sdk units (FP_DelphiPlug, FP_PlugClass and FP_Def) and ActiveX for IStream (in SaveRestoreState).
unit TestPlug; interface uses Windows, Classes, ActiveX, FP_PlugClass, FP_DelphiPlug, FP_Def, FP_Extra; const NumParamsConst = 1; var |
We also define 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.
Next up is the record that holds all data for a voice. The Params field holds
the voice parameter record 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.
type |
Then we arrive at the plugin class declaration. It's derived from TDelphiFruityPlug. 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.
type TTestPlug = class(TDelphiFruityPlug) public ParamValue : array[0..NumParamsConst-1] of integer; VoiceList : TList; procedure DestroyObject; override; function Dispatcher(ID, Index, Value: integer): integer; override; procedure SaveRestoreState(const Stream: IStream; Save: LongBool); override; procedure GetName(Section, Index, Value: integer; Name: pchar); override; function ProcessParam(ThisIndex, ThisValue, RECFlags: integer): integer; override; procedure Gen_Render(DestBuffer: PWAV32FS; var Length: integer); override; function TriggerVoice(VoiceParams: PVoiceParams; SetTag: integer): TVoiceHandle; override; procedure Voice_Release(Handle: TVoiceHandle); override; procedure Voice_Kill(Handle: TVoiceHandle); override; function Voice_ProcessEvent(Handle: TVoiceHandle; EventID, EventValue, Flags: integer): integer; override; // internal procedure constructor Create(SetTag: integer); end; |
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.
function CreatePlugInstance(Host: TFruityPlugHost; Tag: integer): TFruityPlug; stdcall; |
And of course we can't go on without having declared CreatePlugInstance, the function that's exported by the plugin dll so FL can load it.
implementation uses |
The implementation part starts with the uses clause for the editor form's unit, SynthForm and some other units required by this plugin.
// create an initialized plugin & return a pointer to the struct function CreatePlugInstance(Host: TFruityPlugHost; Tag: integer): TFruityPlug; begin Result := TTestPlug.Create(Tag, Host); end; |
This is 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.
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.
// create the object constructor TTestPlug.Create; var Info := @PlugInfo; VoiceList := TList.Create; EditorForm := TSynthEditorForm.Create(nil); with TSynthEditorForm(EditorForm) do begin FruityPlug := Self; for i := 0 to NumParamsConst-1 do ParamValue[i] := TTrackBar(ParamCtrl[i]).Position; end; end; |
When the editor form is created, we tell it who we are, by assigning the Self pointer to its FruityPlug field. We then initialize our ParamValue array with the current values of the controls (set in the property editor). When the editor form was created, it initialized its ParamCtrl array with the parameter controls. Finally, we call ProcessAllParams to make all the necessary changes.
// destroy the object procedure TTestPlug.DestroyObject; begin VoiceList.Free; inherited; end; |
DestroyObject gets called by the host to destroy the plugin object. The important thing here is that we free the VoiceList object. Since FL normally deletes all voices before calling DestroyObject, we assume the list is empty.
function TTestPlug.Dispatcher(ID, Index, Value: integer): integer; begin Result := 0; case ID of FPD_ShowEditor: ... // this is discussed already for FruityGain FPD_SetSampleRate: begin SmpRate := Value; PitchMul := MiddleCMul/SmpRate; end; end; end; |
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.
// create a new voice function TTestPlug.TriggerVoice(VoiceParams: PVoiceParams; SetTag: integer): TVoiceHandle; var // add to the list VoiceList.Add(Voice); Result := TVoiceHandle(Voice); end; |
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 record 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 record, after casting it to a TVoiceHandle. This way it's
both uniqe and very easy (and quick) to know the voice later on.
procedure TTestPlug.Voice_Release(Handle: TVoiceHandle); begin PVoice(Handle)^.Gated := TRUE; // releasing end; |
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 PVoice to get at the state.
Since we returned the pointer to the record as the result of TriggerVoice, FL
passes the same value to this function so it's safe to do this.
// free a voice procedure TTestPlug.Voice_Kill(Handle: TVoiceHandle); begin VoiceList.Remove(pointer(Handle)); Dispose(pointer(Handle)); end; |
This is where we actually free the memory associated with the voice. First we remove the record from the voice list (by casting the handle to a pointer). Then we free the memory.
function TTestPlug.Voice_ProcessEvent(Handle: TVoiceHandle; EventID, EventValue, Flags: integer): integer; begin Result := 0; end; |
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 also calculate a level from this plugin's only parameter.
procedure TTestPlug.Gen_Render(DestBuffer: PWAV32FS; var Length: integer); var |
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.
for i := 0 to VoiceList.Count-1 do begin voice := VoiceList[i]; // ramp to zero if the voice was released if voice^.Gated then begin LVol := 0; RVol := 0; end // 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 then begin voice^.CurrentPitch := voice^.Params^.FinalLevels.Pitch; voice^.Speed := GetStep_Cents(voice^.CurrentPitch); end; |
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 j := 0 to Length-1 do begin Buffer^[j] := PlugHost.Wavetables[0]^[voice^.Position shr WaveT_Shift] * level; // !!! make sure to disable the overflow thingy in Delphi's compiler, as this is based on an overflow trick {$Q-} // shutting off overflow checking inc(voice^.Position, voice^.Speed); end; // add the temporary buffer to the output buffer with ramping PlugHost.AddWave_32FM_32FS_Ramp(Buffer, DestBuffer, Length, LVol, RVol, voice^.LastLVol, voice^.LastRVol); end; |
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 := VoiceList.Count-1 downto 0 do begin voice := VoiceList[i]; if voice^.Gated then PlugHost.Voice_Kill(voice^.HostTag, TRUE); end; end; |
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.
SynthForm.pas
There are no big differences between the editor form of this plugin and that
of FruityGain.