FruityGain (Delphi) FruityGain (C++) Osc3 (Delphi) Osc3 (C++) Sine (Delphi) Sine (C++)
Osc3 (Delphi) |
TestPlug.pas
SynthForm.pas
SynthRes.pas
This page contains a walkthrough of the code of the Osc3 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). Then we have some constants that describe the parameters. There are three oscillators in this generator plugin, so we have parameters for all of them. Oscillator 1 doesn't have a level parameter.
unit TestPlug; interface uses Windows, Forms, SysUtils, Classes, Controls, FP_DelphiPlug, ActiveX, FP_PlugClass, FP_Def, ComCtrls; const // params NumParamsConst = 3*4-1; pOsc1Level = -1; pOsc1Shape = 0; pOsc1Coarse = 1; pOsc1Fine = 2; pOsc2Level = 3; pOsc3Level = 7; StateSize = NumParamsConst * 4; // params + switches nOsc = 3; var PlugInfo: TFruityPlugInfo = ( SDKVersion : CurrentSDKVersion; LongName : 'osc3_d'; ShortName : 'FOsc3_d'; Flags : FPF_Type_FullGen; NumParams : NumParamsConst; DefPoly : 0 // infinite ); |
Then we also define the PlugInfo variable. It's mostly like the one in FruityGain. 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.
Now we define a type for the parameters of an oscillator. There's a waveform table (ShapeP), a pitch and a level.
type TOsc = record ShapeP : PWaveT; Pitch : integer; Level : single; end; |
So we arrive at the plugin class declaration. It's derived from TDelphiFruityPlug. We have two arrays. ParamValue holds the values for all 11 parameters (osc1 shape through osc3 fine). Osc holds the TOsc records for all three oscillators.
VoiceList is a list meant to store our own information about the voices that are created. We'll get back to this later when we discuss the TriggerVoice and Voice_Kill functions.
type TTestPlug = class(TDelphiFruityPlug) public ParamValue : array[0..NumParamsConst-1] of integer; Osc : array[0..nOsc-1] of TOsc; 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; // voice procedures 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 KillAllVoices; constructor Create(SetTag: integer); function Voice_Render_Internal(Handle: TVoiceHandle; DestBuffer: PWave_T; var Length: integer): 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.
Note the Voice_Render_Internal function at the end. This does what Voice_Render
would do in a hybrid generator. I'll get back to this later.
function CreatePlugInstance(Host: TFruityPlugHost; Tag: integer): TFruityPlug; stdcall; |
And of course we can't go on without having declared CreatePlugInstance.
implementation uses SynthForm; const nMaxGrains = 24; GrainLength = 512; // samples per grain |
The implementation part starts with the uses clause for the editor form's unit, SynthForm. There are also some more constants.
type // voice TPlugVoice = record HostTag : integer; Params : PVoiceParams; Pos : array[0..nOsc-1] of longword; State : integer; end; PPlugVoice = ^TPlugVoice; var SineWaveP : PWaveT; |
Now we define a record type to hold some data about the voices we create. Pointers to these records will be stored in VoiceList. HostTag will hold an identifier to talk to the host about this voice, provided by the host itself. We also store the pointer to the voice parameters that the host gives us, the position in each of the oscillators and a state field. This last one can be 0 or 1, 1 meaning we need to kill the voice. We'll come back to this.
// create an initialized plugin & return a pointer to the struct function CreatePlugInstance(Host: TFruityPlugHost; Tag: integer): TFruityPlug; begin PlugHost := Host; Result := TTestPlug.Create(Tag); end; |
This is the implementation of CreatePlugInstance, the function that will be exported by the plugin dll. As with FruityGain, we first assign the Host object to the global variable PlugHost. We then return a newly created plugin object as the result of the function.
The constructor doesn't do anything out of the ordinary. First we call the inherited constructor and initialize the HostTag field. We also assign the address of the PlugInfo record to the Info field, so FL Studio can access the plugin information. Then we create the voice list and the editor form.
// create the object constructor TTestPlug.Create; var n : integer; begin inherited Create; HostTag := SetTag; Info := @PlugInfo; // init SineWaveP := PlugHost.WaveTables[0]; VoiceList := TList.Create; EditorForm := TSynthEditorForm.Create(nil); with TSynthEditorForm(EditorForm) do begin FruityPlug := Self; for n := 0 to NumParamsConst-1 do if ParamCtrl[n] is TTrackBar then ParamValue[n] := TTrackBar(ParamCtrl[n]).Position; ProcessAllParams; 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 KillAllVoices; VoiceList.Free; inherited; end; |
DestroyObject gets called by FL Studio to destroy the plugin object. The important thing here is that we free the VoiceList object. All voices that were in it have been killed by calling KillAllVoices.
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.
// save/restore the state to/from a stream procedure TTestPlug.SaveRestoreState; begin if Save then Stream.Write(@ParamValue, NumParamsConst * 4, nil) else begin Stream.Read(@ParamValue, NumParamsConst * 4, nil); ProcessAllParams; end; end; |
No real differences in SaveRestoreState either, except that we save and load the entire ParamValue array at once. After reading it back, we also call ProcessParams to make all necesary changes.
// params function TTestPlug.ProcessParam(ThisIndex, ThisValue, RECFlags: integer): integer; begin ... // no important differences compared with FruityGain end; |
There are no big differences in ProcessParam either. There are some more parameters than in FruityGain. Also, we check RECFlags for REC_ShowHint. You don't have to process this flag. If you do, you have to show some kind of hint, or let FL Studio show a hint. Here it's done through TDelphiFruityPlug's ShowHintMsg_Percent and ShowHintMsg_Pitch.
procedure TTestPlug.GetName(Section, Index, Value: integer; Name: pchar); begin case Section of FPN_Param : StrPCopy(Name, GetLongHint(TSynthEditorForm(EditorForm).ParamCtrl[Index].Hint)); end; end; |
No differences in GetName either.
Now we return to some interesting stuff: handling the creation and killing of voices.
// create a new voice function TTestPlug.TriggerVoice(VoiceParams: PVoiceParams; SetTag: integer): TVoiceHandle; var Voice : PPlugVoice; n : integer; begin // create & init New(Voice); with Voice^ do begin HostTag := SetTag; for n := 0 to nOsc-1 do Pos[n] := 0; Params := VoiceParams; State := 1; end; // add to the list VoiceList.Add(Voice); Result := TVoiceHandle(Voice); end; |
TriggerVoice gets called by FL Studio when a voice has been created. FL Studio
passes the voice parameters and an identifying tag to the function.
We create a new pointer to a TPlugVoice record to store our own voice settings,
and initialize it with the parameters of the function. 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 TPlugVoice record. This way it's both uniqe and very easy to
know the voice later on.
procedure TTestPlug.Voice_Release(Handle: TVoiceHandle); begin PPlugVoice(Handle)^.State := -1; // 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 setting the
state of the voice to -1, which will be checked for later in Gen_Render.
Note that we're casting the Handle parameter to PPlugVoice to get at the state.
Since we returned the pointer to the record as the result of TriggerVoice, FL Studio
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 Studio does call it in a later version. We just return zero.
procedure TTestPlug.KillAllVoices; begin while VoiceList.Count > 0 do PlugHost.Voice_Kill(PPlugVoice(VoiceList.Items[0])^.HostTag); end; |
This internal function (won't be called by FL Studio), runs through the list of voices and asks the host to kill each one. It's necessary to call the host, since FL Studio created the voice and also has some memory associated with it. FL Studio will then call Voice_Kill in the plugin and that's where we release our own memory.
Now we arrive at some other internal functions. AddOsc and PutOsc create the actual wave data from an oscillator's waveform, depending on the speed and starting position in the waveform. They have mostly the same implementation, except that PutOsc replaces the existing data in the buffer, whereas AddOsc adds the new wave data to the existing data in the buffer.
// add an osc function AddOsc(SourceBuffer, DestBuffer: PWaveT; Length, Pos: longword; Speed: integer; Level: single): longword; var n : integer; i : single; begin // get input i := SourceBuffer^[Pos shr WaveT_Shift] * Level * Envelope; // store output DestBuffer^[n] := DestBuffer^[n] + i; {$Q-} // shutting of overflow checking inc(Pos, Speed); end; Result := Pos; end; |
Then we go on, getting the source data, multiplying it with the current level. The result of this is put in the destination buffer. As I'll explain when we get to Gen_Render, we're using a temporary mono buffer here, which will be converted to stereo and added to the actual buffer we got from FL Studio after all voices have been processed.
The PutOsc function is exactly the same as AddOsc, with one difference. We just assign i to the destination buffer, without looking at what's in there first.
// add an osc (replace) function PutOsc(SourceBuffer, DestBuffer: PWaveT; Length, Pos: longword; Speed: integer; Level: single): longword; begin ... // the same as AddOsc, except for the following lines // store output DestBuffer^[n] := i; ... end; |
CopyMonoToStereo does two things. It applies the pan to the buffer and it copies
a mono buffer to stereo. It simply copies all the samples in the mono buffer
to each channel of the stereo buffer after multiplying the left or right channel
pan value.
The Pan parameter is between -64 (full left) and +64 (full right). This makes
it easy to calculate, as you can see in the code.
procedure CopyMonoToStereo(SourceBuffer: PWaveT; DestBuffer: PWAV32FS; Length: integer; Pan: integer); var i : integer; LeftPan : single; RightPan : single; begin // calculate the pan LeftPan := 1; RightPan := 1; if (Pan < 0) then RightPan := (Pan+64) / 64 else RightPan := (64-Pan) / 64; for i := 0 to Length-1 do begin DestBuffer^[i, 0] := DestBuffer^[i, 0] + SourceBuffer^[i] * LeftPan; DestBuffer^[i, 1] := DestBuffer^[i ,1] + SourceBuffer^[i] * RightPan; end; end; |
ApplyEnvelope is called in Gen_Render when a voice has been rendered, before it is added to the destination buffer. When a voice is created in TriggerVoice, we set its State field to 1. ApplyEnvelope looks at the State field to find out whether to attack (state = 1), release (state = -1) or sustain (state = 0) the voice.
procedure ApplyEnvelope(Buffer: PWaveT; Length: integer; var State: integer); var i : integer; envelope : single; begin if State = 0 then // nothing to do if we have to sustain Exit; for i := 0 to Length-1 do begin if State = 1 then // apply an attacking envelope envelope := i / Length else if State = -1 then envelope := (Length-i) / Length; Buffer^[i] := Buffer^[i] * envelope; end; if State = 1 then State := 0 // proceed to sustain else if State = -1 then State := -2; // next pass we kill the voice end; |
At the end of ApplyEnvelope, we set State to zero if we were attacking (to go to sustain state), and to -2 if we were releasing (to let Gen_Render kill the voice).
We're starting to get to the end of the unit. Just two functions left, but they're about the most important ones : rendering time !
Voice_Render_Internal is the function that actually creates audio data for a given voice (using either AddOsc or PutOsc). It makes sure whether the level for this voice is zero (if T32Bit(Level).I = 0).
function TTestPlug.Voice_Render_Internal(Handle: TVoiceHandle; DestBuffer: PWave_T; var Length: integer): integer; var o, p : integer; Speed : integer; Replace : boolean; begin with PPlugVoice(Handle)^ do begin // compute osc speed & add them p := Params^.FinalLevels.Pitch; Replace := TRUE; for o := 0 to nOsc-1 do with Osc[o] do begin Speed := GetStep_Cents(p + Pitch); if T32Bit(Level).I = 0 then inc(Pos[o], Speed*Length) else if Replace then begin Pos[o] := PutOsc(ShapeP, DestBuffer, Length, Pos[o], Speed, State, Level * Param^.FinalLevels.Vol); Replace := FALSE; end else Pos[o] := AddOsc(ShapeP, DestBuffer, Length, Pos[o], Speed, State, Level * Param^.FinalLevels.Vol); end; end; Result := FVR_Ok; end; |
This function could be used for a hybrid generator as well. Just declare Voice_Render and implement it almost exactly like this, and you have a working hybrid generator. Of course PutOsc and AddOsc would need to be redefined to accept a stereo destination buffer, but for the rest it would be the same. Also, you wouldn't multiply the oscillator level with Param^.FinalLevels.Vol. That's something we need to do because we're a full generator.
The final function is this unit is Gen_Render. For a full generator, this will get called to render all currently active voices. So what we do is run through the voice list and for each voice we check whether it's scheduled to be killed (State = -2, this is set in ApplyEnvelope). If so, we call upon the host to do this. Else we call Voice_Render_Internal to render the voice.
procedure TTestPlug.Gen_Render(DestBuffer: PWAV32FS; var Length: integer); var n : integer; temp : PWaveT; voice : PPlugVoice; begin with VoiceList do begin if Count = 0 then Length := 0 // nothing to render, so tell it else begin GetMem(temp, Length shl 2); for n := Count-1 downto 0 do begin voice := PPlugVoice(List^[n]); if voice^.State = -2 then PlugHost.Voice_Kill(HostTag) // let the host kill the voice else begin Voice_Render_Internal(integer(voice), temp, Length); // render it ApplyEnvelope(temp, Length, voice^.State); CopyMonoToStereo(temp, DestBuffer, Length, voice^.Params^.FinalLevels.Pan); end; end; FreeMem(temp); end; end; end; end. |
Notice the check if there are any voices. If there aren't, we set Length to zero, indicating to FL Studio that we haven't rendered anything. We could also render for example half of the length, in which case we would set Length to half its initial value.
We also create a temporary mono buffer (outside of the loop) in which the voices are all rendered. Finally, we call ApplyEnvelope (which does exactly what it says) and CopyMonoToStereo to copy the temp buffer into the destination buffer provided by FL Studio. If we didn't do all this, we would block all other channels.
SynthForm.pas
There are no big differences between the editor form of this plugin and that
of FruityGain. There are some less important differences, but you can probably
spot those in the source code.
SynthRes.pas
This resource holds the popup menu for the editor form. This is implemented
as in the FruityGain example (except that there it's part of the editor form).