FruityGain (Delphi) FruityGain (C++) Osc3 (Delphi) Osc3 (C++) Sine (Delphi) Sine (C++)
FruityGain (Delphi) |
Gain.pas
Editor.pas
FruityGain_D.pas
Gain.pas
This is the unit that contains the plugin class definition and implementation.
First we have the uses section.
unit |
Here we include the sdk units (FP_Def, FP_PlugClass and FP_DelphiPlug). We
also include the ActiveX unit, as it defines the IStream interface which is
used in SaveRestoreState. The last unit (Editor) contains the editor form (more
about that later).
Next we define some useful constants. This is not necessary, but it can make
life a lot easier when you start changing these values. FruityGain has a seperate
gain value for the left and right channels, so we have two parameters. There
are also a minimum and maximum value defined.
const |
Then we also declare a new variable of type TFruityPlugInfo. This record will
be read by FL Studio later on to find out what kind of plugin this is. We
set SDKVersion to the constant value CurrentSDKVersion, so FL Studio
knows what we expect from it.
We also set LongName to the name of the dll, without the .dll extension.
The name of the dll should be as unique as possible. So simply 'Gain' would
have been a bad name, whereas 'XFIGOEJSL' might be a good unique name, but probably
not very nice to the user. You might use something like 'Ralfs Gain' for example
as the name of your plugin, which would make it reasonably unique.
ShortName can be anything you want, but it should also be as unique as
possible. It also shouldn't be very long (as the name suggests).
Flags actually defines what kind of plugin we're creating. We're making
an effect plugin, so we use FPF_Type_Effect. There are some other flags you
can use here too (see Plugin Flag Constants).
We tell FL Studio how many parameters we have in NumParams. We also
set DefPoly to 0, as we're creating an effect plugin.
We're now ready to declare the plugin class. Note that we're deriving from
TDelphiFruityPlug. This class implements some useful functions so it's not a
bad idea to derive from this instead of TFruityPlug. For example, TDelphiFruityPlug
implements DestroyObject in order to call Free, so you don't have to do this
anymore.
Here we first have some variables in which we store the parameter values. We
have both integer and floating point variables for this. The integer variables
are for communication with the editor controls (which use integers) and the
floating point ones are for calculations in Eff_Render (so we don't have to
calculate them every time Eff_Render is called).
type |
Then we define a constructor. This has one parameter, Tag, that is passed on
from the host. We'll get back to this when we discuss the implementation of
the constructor. We could also have overridden DestroyObject if we wanted to
do something special when that is called by FL Studio. But since we derive
from TDelphiFruityPlug, DestroyObject is already implemented to free the object,
so we don't need to do that anymore.
The rest of the functions defined will be discussed when we reach their implementation.
Just a little note here that GainIntToSingle and ResetParams are helper functions
for this specific plugin, so they probably won't be present in your own plugins.
The last declaration in the interface section is CreatePlugInstance. This is
a very important function. It is exported from the dll and called by FL Studio
to create an instance of the class. Note that it's declared stdcall.
function CreatePlugInstance(Host: TFruityPlugHost; Tag: Integer): TFruityPlug; stdcall; |
So we reach the implementation, with some more used units.
implementation |
We begin by implementing CreatePlugInstance. First we assign Host to the global variable PlugHost. This is necessary, as some functions in TDelphiFruityPlug expect it to be there. Then we create our plugin object and pass it back to FL Studio as the result of the function.
function CreatePlugInstance(Host: TFruityPlugHost; Tag: Integer): TFruityPlug; begin |
Now we reach the implementation of the constructor. The first thing we do of
course is call the inherited constructor.
We also assign the address of the PlugInfo variable we declared earlier to the
Info member of TFruityPlug. This way, FL Studio knows where to find it.
Then we assign the Tag parameter (passed by CreatePlugInstance) to the HostTag
member, so we can use it later on.
{ TFruityGain } |
We also create our editor form in the constructor. After we've done this, we
set some of its variables (discussed later when we reach Editor.pas).
Finally, we reset the parameter values. ResetParams is used for this, as resetting
the parameters is done from several places.
Next is Dispatcher. This function passes messages from FL Studio to the plugin. These are identified by the ID parameter (for a list of all values, see Dispatcher ID's). The Index and Value parameters have different meanings, depending on the value of ID.
function TFruityGain.Dispatcher(ID, Index, Value: Integer): Integer; begin Result := 0; case ID of // show the editor FPD_ShowEditor: begin if Value = 0 then begin EditorForm.Hide; EditorForm.ParentWindow := 0; end else begin EditorForm.ParentWindow := Value; EditorForm.Show; end; EditorHandle := EditorForm.Handle; end; end; end; |
Here we implement a reaction to the FPD_ShowEditor message. This tells us that
FL Studio wants the editor form to be either hidden or shown, depending on
the value of Value. Value holds the parent window we have to use to show our
editor form.
So if Value is zero, we hide the window, and then we set the parent window to
zero.
If Value is not zero, we assign it to ParentWindow for our editor, and then
we show the editor.
Note that the order of these things is important. If ParentWindow is zero while
the editor form is already or still shown, the user will see it floating on
the screen, which doesn't make a good impression.
Finally we set TFruityPlug's EditorHandle member to the handle of our editor
form.
Eff_Render is the core of an effect plugin. It gets called continuously to
process data while the plugin is active.
Data is to be read from SourceBuffer, processed, and written to DestBuffer.
These two parameters point to a buffer of interlaced samples. Interlaced means
that the samples for the two channels (left and right) are mixed together. So
first you get the first left sample, then the first right sample, then the second
left sample and so on.
Note that SourceBuffer and DestBuffer could point to the same buffer, so it
might be worth checking for this to save some time.
The PWAV32FS type makes it easy to work with these buffers, as shown in below.
procedure TFruityGain.Eff_Render(SourceBuffer, DestBuffer: PWAV32FS; Length: Integer); var left, right : single; i : integer; begin left := GainLeft; right := GainRight; for i := 0 to Length-1 do begin DestBuffer^[i, 0] := SourceBuffer^[i, 0] * left; DestBuffer^[i, 1] := SourceBuffer^[i, 1] * right; end; end; |
We first assign GainLeft and GainRight to some local variables, in case they
change while we're processing.
Then we run through the buffers, each time multiplying the channel's gain with
the current sample. As you can see, Length specifies the number of samples in
each channel, not the total amount of samples in the buffer.
GainIntToSingle is a function specific to this plugin. It translates the integer parameter values (from the editor controls) to floating point values. What's important here is that we lock and unlock the mixing threads, to make sure that this doesn't happen from two threads at the same time, or that other functions that depend on these values are called from a different thread. It's probably a bit overkill in this example, but it shows how to do it.
procedure TFruityGain.GainIntToSingle; begin // for safety when we update the actual value, we lock the mixing thread Lock; try GainLeft := (GainLeftInt / 4) + 1; GainRight := (GainRightInt / 4) + 1; finally Unlock; // and unlock it again when we're through (very important !) end; end; |
Next is the implementation of GetName. This procedure is called by FL Studio when it wants a string representation of some value. What it wants translated is specified by the Section parameter (see GetName constants). The meanings of Index and Value depend on the value of Section. Name points to a buffer that you have to put the translated string into. The memory for Name is assigned by FL Studio, so you don't need to worry about this. There's room for at least 256 characters in Name.
procedure TFruityGain.GetName(Section, Index, Value: Integer; Name: PChar); var tempsingle : single; begin if Section = FPN_Param then begin case Index of prmGainLeft : StrPCopy(Name, 'Left Gain'); prmGainRight : StrPCopy(Name, 'Right Gain'); end; end else if Section = FPN_ParamValue then begin tempsingle := (Value / 4) + 1; StrPCopy(Name, Format('%.2fx', [tempsingle])); end; end; |
We check for two Section values here : FPN_Param (a parameter name is required) and FPN_ParamValue (a parameter value is to be translated). In both cases, Index tells us what parameter FL Studio is talking about. In the case of FPN_ParamValue, Value holds the value to be translated. Don't use your own internal values for this, since it's not always the current value that has to be translated.
Next is the Idle procedure. You can use this to perform some action when the user isn't doing anything. In this example, we do something very nice which you might want to implement in your own plugins as well. We check whether the mouse is over a control (on our editor form of course) and tell FL Studio to show a hint message.
procedure TFruityGain.Idle; var Control : TControl; P : TPoint; begin if (GetCaptureControl <> nil) then Exit; GetCursorPos(P); Control := FindDragTarget(P, true); if (Control <> nil) then begin if AppHint <> Control.Hint then begin AppHint := Control.Hint; PlugHost.OnHint(HostTag, PChar(AppHint)); end; end; end; |
It's important to know here that we assigned values to the Hint properties of all controls on the editor form. We use the global AppHint variable to check if the hint is already being shown or not. If not, we call OnHint on the host object (saved in CreatePlugInstance) to let FL Studio show a hint in its own way.
Now we come to the ProcessParam function. This is a bit complicated, so I'll split it up into several parts.
function TFruityGain.ProcessParam(Index, Value, RECFlags: Integer): Integer; begin if Index < NumParams then with TEditorForm(EditorForm) do begin if RECFlags and REC_FromMIDI <> 0 then Value := TranslateMIDI(Value, GainMinimum, GainMaximum); |
First we check whether Index actually identifies a valid parameter.
Then we start checking the flags present in RECFlags. The order in which we
check these is important. We start by checking for REC_FromMIDI. This means
that Value holds a value between 0 and 65536, that needs to be translated to
the range for our parameter. How this happens depends on the minimum and maximum
values of the individual parameters. Since our parameters have the same minimum
and maximum, we don't need to check Index. TDelphiFruityPlug provides the function
TranslateMIDI, so you don't need to implement this yourself.
Next we check whether we need to change the value of a parameter to a new value
or return a parameter value to FL Studio.
if RECFlags and REC_UpdateValue <> 0 then begin case Index of prmGainLeft : GainLeftInt := Value; prmGainRight : GainRightInt := Value; end; GainIntToSingle; end |
If RECFlags includes REC_UpdateValue, we need to change a parameter's value.
So we check Index to see which parameter we have to change and set the value.
In this example, GainIntToSingle is called to also set the floating point values.
Note that we're not changing the controls on the editor form yet. That's for
a bit later.
else if RECFlags and REC_GetValue <> 0 then case Index of prmGainLeft : Value := GainLeftInt; prmGainRight : Value := GainRightInt; end; |
If REC_GetValue is in RECFlags, FL Studio expects us to return the value of a parameter as the result of the function. This flag cannot be present when REC_UpdateValue is present, so we use an if .. then .. else construct. We look at Index to know which parameter FL Studio is talking about and change Value to the integer value of the parameter.
if RECFlags and REC_UpdateControl <> 0 then ParamsToControls; end; Result := Value; end; |
The final check we make is for REC_UpdateControl. When this is not present,
don't update the editor form's controls ! We call ParamsToControls, which is
a function of our TEditorForm class (see below when we discuss Editor.pas).
This updates all parameters, but since we only have two, this isn't too bad.
If you have a lot of controls to update, it would be better to just update the
parameter identified by Index.
At the end of the function, we assign Value to Result. It's safe to always do
this, regardless of whether or not REC_GetValue is present in RECFlags.
Time to move on to the next function.
procedure TFruityGain.ResetParams; begin // start with a gain of 1.5 of both channels GainLeftInt := 2; GainRightInt := 2; GainIntToSingle; // translate the int gain to floating point value TEditorForm(EditorForm).ParamsToControls; end; |
ResetParams is an internal function to reset the parameters to their default values. Here, we also call GainIntToSingle to set the floating point variables and ParamsToControls to update the editor's controls.
Then comes SaveRestoreState. This function allows the plugin to write its state
to and read it from a stream, which could be a file, or memory, or whatever.
The Save parameter tells us whether FL Studio expects the plugin to save or
restore.
The Stream parameter is of type IStream. You can use this just like any object.
There are several functions available in IStream, but the most important are
Write and Read. One thing to watch out for, is that you have to read the exact
same amount of data from the stream as was written to this. For simple plugins,
you can do this simply by reading and writing the same data. For more complex
plugins, it might be necessary to first write the amount of data you're going
to write, or some sort of version number, so that you never make a mistake.
procedure TFruityGain.SaveRestoreState(const Stream: IStream; Save: LongBool); var templong : longint; written : longint; read : longint; begin if Save then begin templong := GainLeftInt; Stream.Write(@templong, sizeof(longint), @written); templong := GainRightInt; Stream.Write(@templong, sizeof(longint), @written); end else begin Stream.Read(@templong, sizeof(longint), @read); GainLeftInt := templong; Stream.Read(@templong, sizeof(longint), @read); GainRightInt := templong; GainIntToSingle; ProcessAllParams; end; end; end. |
First we check Save, so we know what to do. IStream.Write and IStream.Read work similarly. The first parameter is a pointer to the data to write away. Here, we use a temporary variable (templong) and pass its address using the @ operator. The second parameter is the size of the data to be written, which we find by using SizeOf. The last parameter is a pointer to a longint. In the case of Write, this will hold the number of bytes which were actually written to the stream. In the case of Read, this will hold the number of bytes actually read from the stream. After reading, we also update the parameters and controls by calling GainIntToSingle and ProcessAllParams.
And so we reach the end of the Gain.pas unit.
Editor.pas
This unit contains our editor form.
Make sure you set the BorderStyle property of your editor form to bsNone, as
the editor form will be placed inside another window.
There are two controls : LeftGainTrack and RightGainTrack, both trackbars, which
represent the parameters. There's also a popupmenu.
unit Editor; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls, FP_PlugClass, Menus; type TEditorForm = class(TForm) ... // managed by Delphi public Plugin: TFruityPlug; procedure ControlsToParams(left: boolean); procedure ParamsToControls; end; |
We add a variable called Plugin to the form class, to be able to communicate with the plugin about parameter changes. The we define two functions. ControlsToParams gets called to set a parameter after a control has changed. ParamsToControls sets the control values after the parameters have changed.
The first function to discuss is ControlsToParams.
implementation uses Gain; {$R *.DFM} { TEditorForm } procedure TEditorForm.ControlsToParams(left: boolean); begin with TFruityGain(Plugin) do begin if left then begin GainLeftInt := LeftGainTrack.Position; PlugHost.OnParamChanged(HostTag, prmGainLeft, GainLeftInt); end else begin GainRightInt := RightGainTrack.Position; PlugHost.OnParamChanged(HostTag, prmGainRight, GainRightInt); end; GainIntToSingle; end; end; |
We set the plugin's parameters, depending on which changed, to the new value. Then we call PlugHost.OnParamChanged, to let FL Studio know there's been a change. Finally, we call GainIntToSingle on the plugin to also set the floating point parameter values.
ParamsToControls gets called by the plugin when it's parameter values have changed and the controls have to be updated. This allows the plugin to know nothing of what kind of controls are used to represent the parameters.
procedure TEditorForm.ParamsToControls; begin with TFruityGain(Plugin) do begin LeftGainTrack.Position := GainLeftInt; RightGainTrack.Position := GainRightInt; end; end; |
GainTrackChange is the event handler for LeftGainTrack.OnChange and RightGainTrack.OnChange.
procedure TEditorForm.GainTrackChange(Sender: TObject); var C: TControl; s: string; begin C := TControl(Sender); ControlsToParams(C.Tag = 0); with TFruityGain(Plugin) do begin if C.Tag = 0 then s := Format('%.2fx', [GainLeft]) else s := Format('%.2fx', [GainRight]); ShowHintMsg(PChar(s)); end; end; |
We do two things here. First, we call ControlsToParams to change the plugin
parameter variables. C.Tag is 0 for LeftGainTrack and 1 for RightGainTrack.
The second thing we do is let FL Studio show a hint. Depending on the parameter
index (left gain = 0, right gain = 1), we format a string with the right parameter
value. Then we call ShowHintMsg, a method defined in TDelphiFruityPlug to tell
FL Studio what to do.
Next, PopupClick. This is the event handler for most items of the Popup popupmenu (all except the Reset item and the breaks). Take a look at this popupmenu in Delphi. The items (except Reset and the breaks) all have special values for Tag. These correspond to the Parameter Popup Menu Indexes. InPopupClick, we tell FL Studio to perform a certain action (show the event editor, for example).
procedure TEditorForm.PopupClick(Sender: TObject); var Comp : TComponent; Menu : TMenuItem; begin Menu := TMenuItem(Sender); Comp := TPopupMenu(Menu.Parent.Owner).PopupComponent; PlugHost.Dispatcher(Plugin.HostTag, FHD_ParamMenu, Comp.Tag, Menu.Tag); end; |
We retrieve the component that the popupmenu was show over from the PopupComponent property. Then we call the Dispatcher on PlugHost with the dispatcher flag (FHD_ParamMenu), the parameter indes (Comp.Tag) and the menu item index (Menu.Tag).
mnuResetClick is the event handler for the Reset item of the Popup popupmenu. We simply call ResetParams on the plugin object.
procedure TEditorForm.mnuResetClick(Sender: TObject); begin TFruityGain(Plugin).ResetParams; end; end. |
FruityGain_D.dpr
This is the project file. It's important to us here for one thing : exporting
CreatePlugInstance. Don't forget to do this, as otherwise your plugin won't
be loaded by FL Studio.
library FruityGain_D; |