FruityGain (Delphi)        FruityGain (C++)        Osc3 (Delphi)        Osc3 (C++)        Sine (Delphi)        Sine (C++) 

FruityGain (C++)

Gain.h
Gain.cpp

This page contains a run-through of the FruityGain header and source files. Because this is meant to be a tutorial for all C++ compilers (most notably C++Builder and Visual C++), all compiler-specific lines have comments stating what is going on instead of source code. There's no explanation yet for the implementation of the editor window.

Gain.h
This is the header file that contains the plugin class definition.

First we have the include section.

#ifndef GAIN_H
#define GAIN_H

#include "fp_plugclass.h"
#include "fp_cplug.h"

Here we include some sdk headers (fp_plugclass.h and fp_cplug.h).

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.

// the number of parameters
const NumParams = 2;

// the parameter indexes
const prmGainLeft  = 0;
const prmGainRight = 1;

// minimum and maximum values of the gain parameters
const GainMinimum = 0;
const GainMaximum = 20;

We're now ready to declare the plugin class. Note that we're deriving from TCPPFruityPlug. This class implements some useful functions so it's not a bad idea to derive from this instead of TFruityPlug. For example, TCPPFruityPlug implements DestroyObject in order to delete the object, so you don't have to do this anymore.

Here we first have some variables in which we store the parameter values (GainLeftInt, GainRightInt, GainLeft, GainRight). 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).

class TFruityGain : public TCPPFruityPlug {
public:
    int GainLeftInt;
    int GainRightInt;
    float GainLeft;
    float GainRight;

    TFruityGain(int Tag);

    virtual int _stdcall Dispatcher(int ID, int Index, int Value);
    virtual void _stdcall Idle();
    virtual void _stdcall SaveRestoreState(IStream *Stream, BOOL Save);

    // names (see FPN_Param) (Name must be at least 256 chars long)
    virtual void _stdcall GetName(int Section, int Index, int Value, char *Name);

    // events
    virtual int _stdcall ProcessParam(int Index, int Value, int RECFlags);

    // effect processing (source & dest can be the same)
    virtual void _stdcall Eff_Render(PWAV32FS SourceBuffer, PWAV32FS DestBuffer, int Length);

    // specific to this plugin
    void GainIntToSingle();
    void ResetParams();
};

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.

Top

 

Gain.cpp
So we reach the implementation.

... // some compiler-specific includes

#include <windows.h>
#include <stdio.h>
#include "Gain.h"


// the information structure describing this plugin to the host
TFruityPlugInfo PlugInfo = {
    CurrentSDKVersion,
    "FruityGain_CB",
    "F.Gain CB",
    FPF_Type_Effect,
    NumParams,
    0  // infinite
};

We 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 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.

extern "C" __declspec(dllexport) TFruityPlug * _stdcall CreatePlugInstance(TFruityPlugHost *Host, int Tag)
{
    PlugHost = Host;
    TFruityGain *gain = new TFruityGain(Tag);
    return (TFruityPlug *)gain;
};

// note : in Visual C++ the declaration for this function is different 
// and we use a .def file to export the function
//
//   extern "C" TFruityPlug * _stdcall CreatePlugInstance(TFruityPlugHost *Host, int Tag)

Note that we're exporting the function from the dll (using _declspec(dllexport)) and it's declared _stdcall. Also it's declared using extern "C", which prevents name mangling.

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

TFruityGain::TFruityGain(int Tag)
    : TCPPFruityPlug()
{
    Info = &PlugInfo;
    HostTag = Tag;

    ...  // here we create the editor window, which is compiler-specific

    ResetParams();
}

We also create our editor form in the constructor.
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.

int _stdcall TFruityGain::Dispatcher(int ID, int Index, int Value)
{
    switch (ID) {
        // show the editor
        case FPD_ShowEditor :
            if (Value == 0)
            {
                ...   // hide the editor window
                ...   // set the editor window's parent handle to zero
            }
            else
            {
                ...   // set the editor window's parent handle to (HWND)Value
                ...   // show the editor window
            }
            ...   // set EditorHandle to the handle of the editor window
            break;
    }

    return 0;
}

Here we implement a reaction to the FPD_ShowEditor message. This tells us that FL Studio wants the editor window 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 window.
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 window 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 window.

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.

void _stdcall TFruityGain::Eff_Render(PWAV32FS SourceBuffer, PWAV32FS DestBuffer, int Length)
{
   float left, right;

   left = GainLeft;
   right = GainRight;

   for (int i = 0; i < Length; i++)
   {
        (*DestBuffer)[i][0] = (*SourceBuffer)[i][0] * left;
        (*DestBuffer)[i][1] = (*SourceBuffer)[i][1] * right;
   }
}

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.

void TFruityGain::GainIntToSingle()
{
    // for safety when we update the actual value, we lock the mixing thread
    Lock();
    GainLeft = (GainLeftInt / 4.) + 1;
    GainRight = (GainRightInt / 4.) + 1;
    Unlock();  // and unlock it again when we're through (very important !)
}

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.

void _stdcall TFruityGain::GetName(int Section, int Index, int Value, char *Name)
{
    if (Section == FPN_Param)
    {
        switch (Index) {
            case prmGainLeft  :  strcpy(Name, "Left Gain");  break;
            case prmGainRight :  strcpy(Name, "Right Gain");  break;
        }
    }
    else if (Section == FPN_ParamValue)
    {
        float tempfloat = (Value / 4.) + 1;
        sprintf(Name, "%.2fx", tempfloat);
    }
}

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 the C++Builder 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. (Not in the Visual C++ examples, sorry)

void _stdcall TFruityGain::Idle()
{
}

Now we come to the ProcessParam function. This is a bit complicated, so I'll split it up into several parts.

int _stdcall TFruityGain::ProcessParam(int Index, int Value, int RECFlags)
{
    if (Index < NumParams)
    {
        if (RECFlags & REC_FromMIDI != 0)
            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. TCPPFruityPlug 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 & REC_UpdateValue != 0)
        {
            switch(Index) {
                case prmGainLeft  :  GainLeftInt = Value;  break;
                case prmGainRight :  GainRightInt = Value;  break;
            }
            GainIntToSingle();
        }

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 window yet. That's for a bit later.

        else if (RECFlags & REC_GetValue != 0)
        {
            switch (Index) {
                case prmGainLeft  :  Value = GainLeftInt;  break;
                case prmGainRight :  Value = GainRightInt;  break;
            }
        }

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 & REC_UpdateControl != 0)
            ...   // we update the control on the editor window here
    }

    return Value;
}

The final check we make is for REC_UpdateControl. When this is not present, don't update the editor window's controls ! If it is, update the control for this parameter.

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.

void TFruityGain::ResetParams()
{
    // start with a gain of 1.5 of both channels
    GainLeftInt = 2;
    GainRightInt = 2;
    GainIntToSingle();  // translate the int gain to floating point value

    ...   // update the editor window's controls here
}

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 update the editor window's controls afterwards.

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.

void _stdcall TFruityGain::SaveRestoreState(IStream *Stream, BOOL Save)
{
    unsigned long written, read;

    if (Save)
    {
        Stream->Write(&GainLeftInt, sizeof(long), &written);
        Stream->Write(&GainRightInt, sizeof(long), &written);
    }
    else
    {
        Stream->Read(&GainLeftInt, sizeof(long), &read);
        Stream->Read(&GainRightInt, sizeof(long), &read);

        GainIntToSingle();
        ProcessAllParams();
    }
}

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 pass the address of the integer parameter variables 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 long. 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.

Top