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 
    Gain;
interface

uses
    FP_Def, FP_PlugClass, FP_DelphiPlug, ActiveX, Forms, Editor;

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 
     // the number of parameters      NumParams = 2;
     // the parameter indexes      prmGainLeft = 0;
     prmGainRight = 1;
    // minimum and maximum values of the gain parameters
     GainMinimum = 0;
     GainMaximum = 20;
// the information structure describing this plugin to the host
var
   PlugInfo : TFruityPlugInfo = (
   SDKVersion : CurrentSDKVersion;
     LongName : 'FruityGain_D';
    ShortName : 'F.Gain D';
     Flags : FPF_Type_Effect;
     NumParams : NumParams;
     DefPoly : 0 // infinite
   );

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 
     TFruityGain = class(TDelphiFruityPlug)     public
       GainLeftInt : integer;
       GainRightInt : integer;
       GainLeft : single;
       GainRight : single;
      constructor Create(Tag: integer);
    function Dispatcher(ID,Index,Value:Integer):Integer; override;
       procedure Idle; override;
       procedure SaveRestoreState(const Stream:IStream; Save:LongBool); override;
       // names (see FPN_Param) (Name must be at least 256 chars long)

       procedure GetName(Section,Index,Value:Integer;Name:PChar); override;
       // events function

      ProcessParam(Index,Value,RECFlags:Integer):Integer; override;
      // effect processing (source & dest can be the same)

       procedure Eff_Render(SourceBuffer,DestBuffer:PWAV32FS;Length:Integer); override;
       // specific to this plugin

       procedure GainIntToSingle;        procedure ResetParams;
     end;

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 

uses
    SysUtils, Controls, Windows;

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 
  PlugHost := Host;
  Result := TFruityGain.Create(Tag);
end;

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 } 
constructor TFruityGain.Create(Tag: integer); begin inherited Create; Info := @PlugInfo; HostTag := Tag; EditorForm := TEditorForm.Create(nil); with TEditorForm(EditorForm) do begin Plugin := Self; LeftGainTrack.Min := GainMinimum; LeftGainTrack.Max := GainMaximum; RightGainTrack.Min := GainMinimum; RightGainTrack.Max := GainMaximum; end; ResetParams; end;

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.

Top

 

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.

Top

 

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; 
... // all sorts of unimportant stuff
exports CreatePlugInstance; begin end.

Top