FruityGain (Delphi) FruityGain (C++) Osc3 (Delphi) Osc3 (C++) Sine (Delphi) Sine (C++)
FruityGain (C++) |
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.
Gain.cpp
So we reach the implementation.
... // some compiler-specific includes #include |
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.