iPlug2Docs
An alternative iPlug2 Documentation
This documentation is aimed at beginners in audio, C/C++ and iPlug2 development. It will only focus on developing VST3 plugins with the use of Visual Studio.
Some side-notes for understanding this documentation:
unconfirmed This means that I am not sure if the following is correct.
What does this do? This means that everyone is free to open a pull request to answer my question.
Table Of Contents
- iPlug2Docs
- Table Of Contents
- Getting started
- A plugins structure
- Files
- UI
- DSP
- Parameters
- Senders
- Fonts
- Debugging
- Misc
- Using IControls
- Using layers
Getting started
To get started I recommend creating a vst-dev
folder that will contain all your plugins as well as the iPlug2
SDK, which you can simply clone from GitHub:
vst-dev/ $ git clone https://github.com/iPlug2/iPlug2
Visual Studio is the industry standard for C/C++ developing and also recommended in that case. We will use it to code, compile and debug our plugins all in one software.
After cloning, go into the iPlug2
directory and pick IPlugEffect
in the Examples
folder. Double click the .sln (Visual Studio Solution) to open the corresponding Visual Studio Project.
Other resources
- iPlug2 Official API Docs - doxygen
- iPlug2 Official Wiki
- iPlug2 Forum
- iPlug2 Discord
- Oli Larkin - An Introduction to iPlug2 by The Audio Programmer
Tips and Tricks
In cases that you don't have a 100% done, handwritten, image-included documentation to hand, it can be useful to use Visual Studios tools like auto-completion, Go-To-Definition and Go-To-Declaration (Right click on variable).
A plugins structure
A plugin is divided into two parts: The UI and the digital signal processing (DSP). They both run independent from each other and communicate through so called Senders.
Files
Visual Studio shows our project files in the Solution Explorer (View -> Solution Explorer or CTRL+ALT+L). Note that sometimes the files are different than the ones shown in your normal file explorer, as VS treats files differently. In most cases everything works fine if the Solution Explorer shows them normally.
Set IPluginEffect-vst3 as a startup project in the Solution Explorer using a right click and "Set as Startup Project".
Extend the project to reveal all of the source files.
You should also set your Solution Configuration to "Debug" or "Trace" when developing and only switch it to "Release" when you actually plan to share the plugin somewhere. Make sure the Solution Platform is correct (You most likely use a an x64 device). You can find these options at the top of Visual Studio.
config.h
As the name suggests, config.h
is a configuration file containing #define
statements for constant values. The #define
keyword passes a constant to the compiler, which replaces it with the corresponding value in every file it compiles.
Let's take a look at the most important values.
Name | Note |
---|---|
PLUG_NAME | The name of your plugin |
PLUG_MFR | The manufacturer's name |
PLUG_VERSION_HEX & PLUG_VERSION_STR | The version of your plugin in both hexadecimal and string format |
PLUG_UNIQUE_ID | A unique identifier with the length of 4 characters |
PLUG_MFR_ID | A unique identifier for the manufacturer, also with the length of 4 characters |
PLUG_URL_STR | A link to your website |
PLUG_EMAIL_STR | Your E-Mail address |
PLUG_COPYRIGHT_STR | A copyright string |
PLUG_CLASS_NAME | The name of the class the host (like your DAW) should use. Don't use quotations here |
BUNDLE_NAME, BUNDLE_MFR and BUNDLE_DOMAIN | Three strings defining the bundle identifier. This is similar to the package name in app development. Often the website of the developer is used: dev.jondoe.myplugin1. It is used to uniquely identify each plugin, even if they have the same name |
PLUG_CHANNEL_IO | unconfirmed Different options for input and output combinations seperated by a space. "1-1 2-2" means that you can either have 1 input, 1 output or 2 inputs, 2 outputs |
PLUG_WIDTH & PLUG_HEIGHT | The width and height of your plugin in pixels |
VST3_SUBCATEGORY | unconfirmed Not quite sure but I think (https://steinbergmedia.github.io/vst3_doc/vstinterfaces/group__plugType.html)[those are the ones] |
ROBOTO_FN | The reference to the "Roboto Regular" font. See fonts |
IPlugEffect.h
This file is a so called header file and holds all declarations. You can declare and define an object in one line (int a = 42;
), but you can also declare it first (int a;
) and define it later on (a = 42;
). C/C++ developers like to declare their variables, enumerations and classes in headers files that end on .h
. Sometimes, when working with C++, the extensions .hpp
is used to show that C++ is used, but this is not the case here.
In the header file
IPlugEffect.cpp
The first thing we do in our actual code is include the header file using #include "IPlugEffect.h"
. This makes sure that all objects are already declared and ready to work with. We also include IPlug_include_in_plug_src.h
What does this do? as well as IControls.h
which gives us some control objects to use.
Constructor
The constructor is the starting point of every class, in our case IPlugEffect
. This is also used to initialize the plugin, any variables, the UI and more.
IPlugEffect.h
defines the IPlugEffect Class, a constructor with the same name (which is how you know that it's the constructor) and a function called ProcessBlock
that will become important later on.
We use the double colon (::) operator to overwrite the constructor of the class. So we don't have to reinvent the wheel, there is another class, simply called Plugin that acts as an API to Steinbergs VST3 SDK and that we can inherit (Basically copy everything) from:
IPlugEffect::IPlugEffect(const InstanceInfo& info) : Plugin(info, MakeConfig(kNumParams, kNumPresets)) {
// Initialize stuff in here
}
Notice that kNumParams
and kNumPresets
are defined in config.h
.
Next, parameters are defined.
After that,
See this to find out, why the next portion is wrapped inside IPLUG_EDITOR
.
The next step is to write a lambda function that creates the UI Window. We pass the plugin's width and height, the FPS Is this the MAX-FPS or the FPS the plugin should try to reach? as well as a scale factor Didn't understand that one yet.
mMakeGraphicsFunc = [&]() {
return MakeGraphics(*this, PLUG_WIDTH, PLUG_HEIGHT, PLUG_FPS, GetScaleForScreen(PLUG_WIDTH, PLUG_HEIGHT));
};
The final step is to initialize our UI with another lambda function. See UI for further details.
UI
The UI is initialized in the mLayoutFunc
function. It passes a IGraphics* object which represents our plugin's window. Here any fonts are loaded, settings defined and, most importantly, elements attached to the window. Those elements are objects of type IControl
. Drawing is often not done in this function by the way, as it only ones once when the plugin is started Does it run when the plugin window is opened again?. Instead the backend UI library IGraphics goes through each attached control and checks if it is dirty
. If it is, it redraws it using the IControl's Draw()
function and sets it back to clean
.
For further information refer to Using IControls.
If you want to add a knob for example, you can do it like this:
mLayoutFunc = [&](IGraphics* pGraphics) {
// Add a corner resizer so we can easily resize the window
pGraphics->AttachCornerResizer(EUIResizerMode::Scale, false);
// Set the background color to COLOR_GRAY (127, 127, 127)
pGraphics->AttachPanelBackground(COLOR_GRAY);
// Load font, use name "Roboto-Regular" to reference it and load it from ROBOTO_FN ("Roboto-Regular.ttf")
pGraphics->LoadFont("Roboto-Regular", ROBOTO_FN);
// Get the bounds of the window
const IRECT b = pGraphics->GetBounds();
// Create a new IVKnobControl knob and attach it to the window
// GetCentredInside(100) centers the object inside the current window with a width of 100 pixels
// GetVShifted(-100) shifts the object down up on the virtual axis (up = 0px, down = PLUG_HEIGHTpx)
pGraphics->AttachControl(new IVKnobControl(b.GetCentredInside(100).GetVShifted(-100)));
};
In the original code we also pass kGain
to the function. We do this so the plugin knows that the knob represents this parameter. kGain
is defined in IPlugEffect.h
in EParams and just translates to an integer (0) that is used as a kind of ID for the parameter. See Parameters for more.
DSP
Parameters
A parameter is a value of a certain type that can be changed by the host DAW, the plugin itself or the user tweaking knobs on the UI.
Every parameter should be defined by a unique integer > -1. We use an enumeration to do define parameters, which has the advantage of kNumParams
always being correct:
enum EParams
{
kGain = 0,
kOtherParameter,
...
kNumParams
};
After that we have to initialize the parameter in the constructor of our plugin:
GetParam(kGain)->InitDouble("Gain", 0.0, 0.0, 100.0, 0.01, "%");
Every Init function takes similar parameters:
Parameter | Type | Notes |
---|---|---|
name | const char* | |
defaultValue | Depends | The value the parameter should have when starting the plugin or reseting the value |
minVal | number | |
maxVal | number | |
step | number | The size of individual steps when turning the knob. Should be relatively small to not 'stutter' |
listItems | List of const char* | Only used in InitEnum: A list of strings to use as options |
label | const char* | A suffix for the parameter. Something like "%", "s" or "ms" or whatever |
flags | const char* | Defines additional information, such as if the parameter can be automated. See EFlags |
group | const char* | Heellpp what are groups? |
All initializer functions are:
Function | Notes |
---|---|
InitBool | |
InitEnum | Don't pass options, but only the number of options it will have using nEnums |
InitEnum | Just pass {"Option 1", "Option 2", ..} as the value for listItems |
InitInt | |
InitDouble | |
InitSeconds | |
InitMilliseconds | |
InitFrequency | |
InitPitch | |
InitGain | |
InitPercentage | |
InitAngleDegrees | |
Init | Initialized the parameter based on another parameter |
Copied straight from IPlugParameter.h
in the source:
void InitBool(const char* name, bool defaultValue, const char* label = "", int flags = 0, const char* group = "", const char* offText = "off", const char* onText = "on");
void InitEnum(const char* name, int defaultValue, int nEnums, const char* label = "", int flags = 0, const char* group = "", const char* listItems = 0, ...);
void InitEnum(const char* name, int defaultValue, const std::initializer_list<const char*>& listItems, int flags = 0, const char* group = "");
void InitInt(const char* name, int defaultValue, int minVal, int maxVal, const char* label = "", int flags = 0, const char* group = "");
void InitDouble(const char* name, double defaultVal, double minVal, double maxVal, double step, const char* label = "", int flags = 0, const char* group = "", const Shape& shape = ShapeLinear(), EParamUnit unit = kUnitCustom, DisplayFunc displayFunc = nullptr);
void InitSeconds(const char* name, double defaultVal = 1., double minVal = 0., double maxVal = 10., double step = 0.1, int flags = 0, const char* group = "");
void InitMilliseconds(const char* name, double defaultVal = 1., double minVal = 0., double maxVal = 100., int flags = 0, const char* group = "");
void InitFrequency(const char* name, double defaultVal = 1000., double minVal = 0.1, double maxVal = 10000., double step = 0.1, int flags = 0, const char* group = "");
void InitPitch(const char* name, int defaultVal = 60, int minVal = 0, int maxVal = 128, int flags = 0, const char* group = "", bool middleCisC4 = false);
void InitGain(const char* name, double defaultVal = 0., double minVal = -70., double maxVal = 24., double step = 0.5, int flags = 0, const char* group = "");
void InitPercentage(const char* name, double defaultVal = 0., double minVal = 0., double maxVal = 100., int flags = 0, const char* group = "");
void InitAngleDegrees(const char* name, double defaultVal = 0., double minVal = 0., double maxVal = 360., int flags = 0, const char* group = "");
void Init(const IParam& p, const char* searchStr = "", const char* replaceStr = "", const char* newGroup = "");
We can pass our parameters to functions like
pGraphics->AttachControl(..., kGain)
to show that the parameter is representing that particular control. What effects does this have?)
Senders
- ISender
- ISenderData
Fonts
- Recommended to load as image
Debugging
- Trace
- VST3PluginTestHost
Misc
IPattern
Todo
IColor
You can create a new color using the IColor
constructor. Notice that the colors are in format IColor(alpha, red, green, blue)
.
There are several predefined color constants, like COLOR_RED, COLOR_WHITE and so on, defined in IGraphics/IGraphicsStructs.h
.
IRECT
IRECT (Written in all caps) is a struct representing a rectangular area. Use the IRECT
constructor like this: IRECT(left, top, right, bottom)
.
You can then use functions like myRect.GetPadded(-5)
to create a new IRECT
that is 5 units smaller on each side.
iPlug2 also gives us some handy functions like myRect.GetGridCell()
that lets us split up a rect into multiple, smaller sub-rects automatically.
Drawing functions
Todo
EFlags
Todo
Using IControls
Rendering everything every frame not only is very CPU-Intense, but also just not logical when it comes to structuring your project. For this reason, iPlug2 uses so called IControls
. An IControl is a class that has functions for drawing and event handling, for running code when the user interacts with an IControl. Lets create an IControl that turns green when clicked on.
// Create a new class that inherits from an IControl
class ColorChangerControl : public IControl {
public:
bool wasClicked = false;
// Initialize the base IControl class. We will later pass an IRECT to specify the IControl's size ("bounding rect")
ColorChangerControl(const IRECT& bounds) : IControl(bounds) {
// Do something when initialized
}
// The Draw() function; Takes an IGraphics object we can use to render/draw on
void Draw(IGraphics& g) {
if (wasClicked) {
// Fill the entire IControl with a red color
// mRECT is an IRECT defining the bounding box, automatically created by IControl() when passing <bounds>
g.FillRect(COLOR_GREEN, mRECT);
} else {
g.FillRect(COLOR_RED, mRECT);
}
}
void OnMouseDown(float x, float y, const IMouseMod& mod) {
wasClicked = true;
// Set this object dirty, meaning it will be re-rendered
SetDirty(); // This is redundant AFAIK, Draw() should get called automatically
}
};
With that, you just have to create an instance of this class in the mLayoutFunc function:
// Let's keep all of this
mLayoutFunc = [&](IGraphics* pGraphics) {
pGraphics->AttachCornerResizer(EUIResizerMode::Scale, false);
pGraphics->AttachPanelBackground(COLOR_GRAY);
pGraphics->LoadFont("Roboto-Regular", ROBOTO_FN);
// Bounding box of the plugin window
const IRECT b = pGraphics->GetBounds();
// Attach a IControl to the plugin window
// Pass an IRECT defining the size of the IControl
pGraphics->AttachControl(new ColorChangerControl(IRECT(50, 50, 200, 200)));
};
IControl methods
Beside the already used Draw() and OnMouseDown() methods, there are also other functions you can use.
Event functions
Todo (See the OnMouse* functions here)
Note on OnMouseOver: You need to set
pGraphics->enableMouseOver(true)
in the mLayoutFunc first, as this is a very CPU-Intense function for iPlug2.
File dialogs
Todo
Communication between UI and DSP
See ISender
Using layers
Todo - (Don't actually know what they do yet)