Back to Software

VSTi Tutorial

28 Mar 2019
Progress: Complete

Preamble

This page will describe how to compile a minimal VST instrument using the VST SDK 2.4.

The current version of the VST SDK is 3.x, and Steinberg are heavily against anyone using the older SDK. There have been a number of DMCA takedown requests by Steinberg to open-source projects that include the SDK 2.4 source files. The interface for VST3 was rewritten, and the files have a .vst3 extension. VST2 uses a platform specific extension (.dll on Windows, .so on linux, etc).

Why would anyone want to develop for the older system? Well aside from the fact that the majority of virtual instruments use the older format, the reason that VST3 has the (possibly outdated) reputation for buggy implementations is that the VST3 API is a lot more complicated.

The bare minimum files actually required to compile a VST2 can still be downloaded as part of the VST3 SDK from Steinberg. They have recently relaxed the stipulation that you register and sign away your soul before you can download anything. However, you will probably also want the documentation and example projects that come with the VST2 SDK. The best advice I can give here is to acquire the files illegitimately. It is in breach of the licence to redistribute it, but quite frankly I regret not acquiring the VST2SDK ten years ago.

It is quite pleasant to prototype a synthesizer algorithm in a high-level language (dare I say, Web Audio API?) but once you've nailed the sound down, you want the low latency and plentiful polyphony only afforded by compiled code. The fastest way to shoehorn your algorithm into a VSTi is to look at the musical instrument equivalent of "Hello world": a sine-wave synthesizer.

Toolchain

When I think of a VST, I think of a Windows DLL. That is what we are interested in producing here.

I will be describing in detail how to do this using Visual Studio 2017. There are other compilers you can use, but on Windows this one does make the most sense. The "community edition" of Visual Studio is free, but you do have to sign up with a (free) Microsoft account to use it beyond the trial period.

Within Visual Studio, a "solution" (with a .sln extension) is a collection of "projects". The projects have file extensions of either .vcproj or .vcxproj depending on how old the version of Visual Studio was. If you try to open the examples that come with the VST2 SDK, Visual Studio will offer to convert them to the newer format. With only a few minor hiccups[note] I was able to compile the example projects this way. The only thing that might catch you out is if your Visual Studio installation is missing the components it needs, so you may need to run the installer again to bring those in.

If you have the sample projects working, you can export one of them as a template and use it to create a new project. However, for this tutorial we will start from scratch. Start by selecting File ⇒ New Project and selecting Empty Project.

This will create a directory in the location you've chosen (here it's C:\VSTtutorial\sinewave) filled with a solution, project file and some other gunk. Into this directory we now want to copy the necessary files from the SDK. It's up to you how you want the folder structure to look, but the most sensible thing is to have a subfolder named vstsdk2.4 that contains the same file hierarchy as the SDK. That way, if you want to share your source code, you can leave this directory empty, and all that's needed to compile the code is to paste the SDK contents in there.

The bare minimum files needed are the C++ and header files in public.sdk\source\vst2.x\*, and the "interface" header files in pluginterfaces\vst2.x\*.

Next we need to add the files into the Visual Studio project. The structure of the folder layout in the solution explorer is completely arbitrary, and bears no resemblance to the real directory structure of the files.

You can add a "filter" to group the VST files together. Within that filter add the "existing items" that are the VST SDK files. If we follow the example projects, the interface header files go into a separate filter, but it doesn't actually make any difference. You can also create two new items, I've called them sinewave.cpp and sinewave.h, these will contain all of the code we'll be writing.

The solution for our project now looks like this:

while the directory tree looks like this:

There is one more file we will need. To instruct the linker on which functions to export in the DLL, we need a module definition file. The example that comes with the VST SDK is called vstplug.def located in the public.sdk\samples\vst2.x\win folder, and it contains just three lines:

EXPORTS
	VSTPluginMain
	main=VSTPluginMain

Officially a module definition file should start with the LIBRARY statement but the above seems to work. I pasted vstplug.def into the top directory of the project (we will provide the path to it in the linker options in a moment).

Right click on the sinewave project (not the solution) in the solution explorer and select properties.

Under General, change Target Extension to .dll and Configuration type to Dynamic Library (.dll)

Under C/C++ ⇒ General, set Additional Include Directories to be vstsdk2.4. If you didn't follow my folder structure above, you may need to put a different path here.

Under C/C++ ⇒ Preprocessor, set the Preprocessor Definitions to the following:

WIN32;NDEBUG;_WINDOWS;_USRDLL;_CRT_SECURE_NO_DEPRECATE=1;%(PreprocessorDefinitions)

_CRT_SECURE_NO_DEPRECATE suppresses a lot of errors that the SDK code throws for using deprecated functions. I'm not entirely clear on whether the other definitions are required, I just copied the line from one of the example projects.

Under C/C++ ⇒ Language, set Conformance mode to No. This suppresses more errors that the SDK would throw otherwise.

Finally, under Linker ⇒ Input, set Module Definition File to vstplug.def (or, if you pasted it outside of the root project dir, give the relative path to it).

With everything else left to the defaults, that is all of the configuration needed to build a DLL that can be loaded by a VST host. If you paste in the contents of sinewave.h and sinewave.cpp from lower down this page, it should compile correctly, and the DLL should appear in the active build directory of the project.

However, it's also worth configuring the debugger settings. SaviHost is a standalone VST host that we can use to launch our VST. Normally you rename the exe file to be the same as the dll you want to launch, put them in the same folder and then run the exe to use it. If all you want is to be able to press Ctrl+F5 and see your VST running, it should be enough to drop the name of that exe file in as the Command under Debugging. But with a tiny bit more effort we can get the debugger to work properly including breakpoints and exception handling.

Download savihost.exe and put it in a known location — you don't need to rename it. Under Debugging, set the Command to be the path to savihost.exe, and under Command Arguments enter $(TargetPath) /noload /nosave /noexc /noft

The first argument is of course the path to our DLL. The extra arguments are documented as part of VSTHost, an earlier project by the same author as SaviHost. Noload and nosave just skip the loading of the previous setup in order to save time (it will still save your MIDI and ASIO config). Noexc is an instruction to run in "less secure mode" and noft (no fault tolerance) instructs "even less secure mode" essentially letting all errors fall through to the debugger, instead of trying to catch them. These options were specifically added for developers like us.

There are dozens of other options in the project properties that differ between the sample projects and the project we've just created, but I have not looked into what any of them do. The above seems to be the minimum required. Now let's start programming!

Let's make a sine wave

AudioEffect is the original VST interface. AudioEffectX is the extented VST interface, which is what the VST 2 SDK uses, and it inherits and extends the API.

The AudioEffectX class contains a large number of methods which we can optionally override with our own code. There's only a small number of them which are actually required though. processReplacing() is probably the most important, as it's the part where audio buffers come in and go out. The "replacing" refers to the fact that we're generating the processed audio into a new buffer. The older process() method is (I think) deprecated. There is also an optional additional processDoubleReplacing() which works with 64 bit floats, but if you choose to implement this you still need to implement the 32 bit version as well.

My sinewave.h header file looks like this:

#include "public.sdk/source/vst2.x/audioeffectx.h"
#include <math.h>
#define PI 3.1415926535897

class SineWave : public AudioEffectX
{
public:
  SineWave(audioMasterCallback audioMaster);
  ~SineWave() {};
  
  virtual void processReplacing(float** inputs, float** outputs, VstInt32 sampleFrames);
  virtual VstInt32 processEvents(VstEvents* events);
  
  virtual VstInt32 canDo(char* text);
  virtual VstInt32 getNumMidiInputChannels();
  virtual VstInt32 getNumMidiOutputChannels();
  virtual void setSampleRate(float sampleRate);
  
protected:
  float fs;
  float freq;
  float vol;
  float phase;
};

We inherit from AudioEffectX and then give the prototypes to override the methods we care about. The protected variables are things I've added to keep track of our sine wave. The VstInt32 stuff is a typedef int in aeffect.h – an annoyingly old-fashioned approach to cross-platform support but I don't care enough to do anything about it.

Now I'll walk us through the C++ file. To complete the example, just paste all of the following code snippets together.

We start with including the header file and implementing createEffectInstance. This is basically boilerplate code to invoke our AudioEffectX-derived constructor. Our DLL's exported function VSTPluginMain is just a wrapper around createEffectInstance.

#include "sinewave.h"

AudioEffect* createEffectInstance(audioMasterCallback audioMaster)
{
  return new SineWave(audioMaster);
}

Next we implement that constructor. We leave the destructor blank for now.

SineWave::SineWave(audioMasterCallback audioMaster)
  : AudioEffectX(audioMaster, 0, 0)
{
  setNumInputs(0);
  setNumOutputs(2);
  setUniqueID('asdf');
  canProcessReplacing();
  isSynth();
  
  fs = 44100; // Guess
}

There are a few subtleties here. First of all "inputs" and "outputs" refer to audio buffers, not MIDI data. Since we're a VST instrument, we have zero audio inputs. The Unique ID is something assigned to us by Steinberg, here I've just entered asdf. Notice that it's in single quotes. The unique ID is a 32 bit number, not a string. A four-character single-quoted string is shorthand for this (a double quoted string would be a char array, and have a null-terminating byte appended). Hosts make use of the Unique ID to identify one VST from another.

I'm initializing the fs variable here but it should get set to the correct value when setSampleRate() is called later. The last two arguments to AudioEffectX are the number of programs, and the number of parameters. I will go into detail about these in the next section, but we leave them zero for now.

Remember, the functions that we'll be overriding are all called by the host. So canDo() is called by the host when it wants to know what our capabilities are.

VstInt32 SineWave::canDo(char* text)
{
  if (!strcmp(text, "receiveVstEvents"))
    return 1;
  if (!strcmp(text, "receiveVstMidiEvent"))
    return 1;
  return -1;  // explicitly can't do; 0 => don't know
}

VstInt32 SineWave::getNumMidiInputChannels()
{
  return 1; // or up to 16 if you prefer
}

VstInt32 SineWave::getNumMidiOutputChannels()
{
  return 0; // no MIDI output back to host
}

That should be fairly self-explanatory. There are more things that can be reported by canDo, look at the plugCanDo definitions at the start of audioeffectx.cpp. Like much of the VST API, the lines above are technically optional, but things are more likely to work if we have them.

The next function is only implemented because we need to know the sample rate if we want our notes to be in tune. (An audio-only plugin, such as a distortion or reverb effect, probably wouldn't care what the sample rate is.)

void SineWave::setSampleRate(float sampleRate)
{
  AudioEffectX::setSampleRate(sampleRate);
  fs = sampleRate;
}

The function is called whenever the sample rate changes, so if there are constants derived from it you can recalculate them here. It should also hopefully be called before processReplacing() is first called.

void SineWave::processReplacing(float** inputs, float** outputs, VstInt32 sampleFrames)
{
  float* out1 = outputs[0];
  float* out2 = outputs[1];
  
  while (--sampleFrames >= 0) {
    (*out1++) = (*out2++) = vol * sin(phase += freq);
  }
  while (phase > PI) phase -= 2 * PI;
}

This is the heart of our "synthesizer" – just a single sine wave at a given volume and frequency.

And of course the last and most important function we need is processEvents, that'll accept MIDI data and decide what to do with it.

VstInt32 SineWave::processEvents(VstEvents* ev)
{
  for (VstInt32 i = 0; i < ev->numEvents; i++) {
    if ((ev->events[i])->type != kVstMidiType)
      continue;
    
    VstMidiEvent* event = (VstMidiEvent*)ev->events[i];
    char* midiData = event->midiData;
    VstInt32 status = midiData[0] & 0xf0; // ignoring channel

    switch (status) {
    case 0x90: // Note on
    case 0x80: // Note off
    {
      VstInt32 note = midiData[1] & 0x7F;
      VstInt32 velocity = midiData[2] & 0x7F;
      if (status == 0x80 || velocity == 0) {
        // Note Off
        vol = 0;
      } else {
        // Note On
        // Note 69 is A (440Hz). 12 notes per octave.
        // Multiply by 2pi/fs to get frequency in units of radians per sample
        freq = (440 * 2 * PI / fs) * pow(2.0, (note - 69) / 12.0);
        vol = 0.5;
      }
    } break;
    
    case 0xE0: // Pitch bend
      break;
    case 0xB0: // Controller
      break;
      // etc

    }
  }
  return 1;
}

The Note On / Note Off are very much just there as an example. It is enough prove the VSTi works. For an actual monosynth you would need to store the list of notes held down and jump the frequency again when notes are released, only zeroing the volume when all of them have been released. You probably also want an envelope to stop the clicking. Again, all that stuff is fun and not what this tutorial is about.

The switch statement I've put into processEvents should look familiar if you have worked with MIDI before. Zero velocity triggers a note-off due to Running Status. The pitch bend and controller cases are there for you to have fun filling in. (But before you go crazy on Controller events, be aware that VSTs have a separate parameter system that we'll look at in a moment.)

Something I've ignored above is the concept of Delta Frames. When we play with a virtual instrument in real time, we want every event to be processed immediately to reduce latency, but for static playback or offline processing the host may send us MIDI events that are in the future. Each event has a property event->deltaFrames that specifies how many samples ahead this event should take place. Be aware that the deltaFrames value could be larger than the buffer size being handled by processReplacing(). A simple way to deal with this, if you are giving the notes an envelope, is to count down the note's delta frames before the envelope attack starts.

Congratulations, we have successfully said hello to the world. I'll now cover the extra bits you'll almost certainly want to add.

More methods

First let's report some important data back to the host. For each of these you'll also want to add the prototypes into the header file.

VstInt32 SineWave::getVendorVersion()
{
  return 1000;
}

The VST docs say that the version of the plugin "should be upported [sic]". I think they want us to support it.

The next ones control how our plugin is described and labelled in the host program. It's best to try changing these and seeing how the plugin appears in your DAW.

bool SineWave::getEffectName(char* name)
{
  vst_strncpy(name, "SineWave", kVstMaxEffectNameLen);
  return true;
}
  
bool SineWave::getProductString(char* text)
{
  vst_strncpy(text, "Sine Wave", kVstMaxProductStrLen);
  return true;
}
  
bool SineWave::getVendorString(char* text)
{
  vst_strncpy(text, "Sine Wave", kVstMaxVendorStrLen);
  return true;
}

Next let's talk about parameters and programs. I mentioned that CC values are not used like you might expect, because VST plugins generally use a more rigid set of state variables called parameters. When you save or load the state of the plugin, these are the numbers that get stored.

The parameters are 32-bit floating point values in the range 0 to 1. The range is not enforced, but if you go outside of it you may crash the host.

A program is a set of parameters. In some DAWs this is referred to as a preset. To be honest I don't really care about programs here, but if we wanted to add them the related methods would be getProgram, setProgram, getProgramName and setProgramName. There are also separate methods getChunk and setChunk to store and restore all of the plugin's state.

But parameters are definitely worth implementing. If you want to change settings on your synth, most of them include a GUI interface (called an Editor), and while that's a very nice thing to have, it is an awful lot of work to implement. There are many, many frameworks available to assist in making VST GUIs, including the vstgui framework that comes with the SDK. However, if all you need are a few knobs, you can specify parameters without an Editor and the VST Host will generate controls for you.

Parameters are indexed, so getParameter and setParameter are called with an index as their argument. In these functions you can have a switch statement handling each parameter individually, and if you want the range of your variables to be different, this is where you can translate or scale the values. You also need to implement getParameterLabel, getParameterDisplay and getParameterName. Name is a string describing the parameter (note that the max length is quite short, only 8 characters). Label is the unit to be printed after the parameter, such as dB or ms. Display is a string representation of the parameter, which can be different again: in the case of decibels, there is a provided function dB2string which will not only provide a meaningful number, but state -∞ when the parameter is zero.

I added a few parameters and implemented the routines along the lines of the following snippet. Remember that the PARAMETER_COUNT also needs to be specified in the call to the AudioEffectX constructor.

/* sinewave.h */
enum {
  kVolume,
  kAttack,
  kRelease,
  
  PARAMETER_COUNT
};
const char* paramNames[PARAMETER_COUNT] = {
  "Gain",
  "Attack",
  "Release"
};
const char* paramLabels[PARAMETER_COUNT] = {
  "dB",
  "ms",
  "ms"
};
  
/* sinewave.cpp */
void SineWave::setParameter(VstInt32 index, float value)
{
  switch (index) {
    case kVolume: fGain = value; break;
    case kAttack: fAttack = 1/value; break;
    case kRelease: fRelease = 1/value; break;
  }
}
  
float SineWave::getParameter(VstInt32 index, float value)
{
  switch (index) {
    case kVolume: return fGain;
    case kAttack: return 1/fAttack;
    case kRelease: return 1/fRelease;
  }
}
  
void SineWave::getParameterName(VstInt32 index, char* label)
{
  vst_strncpy(label, paramNames[index], kVstMaxParamStrLen);
}
  
void SineWave::getParameterLabel(VstInt32 index, char* label)
{
  vst_strncpy(label, paramLabels[index], kVstMaxParamStrLen);
}
  
void SineWave::getParameterDisplay(VstInt32 index, char* text)
{
  switch (index) {
    case kVolume: dB2string(fGain, text, kVstMaxParamStrLen); break;
    case kAttack: sprintf(text, "%.1f", 1000/fAttack); break;
    case kRelease: sprintf(text, "%.1f", 1000/fRelease); break;
  }
}

You cannot set what type of control a parameter is. If you want, for instance, an on/off toggle, you'll just have to deal with it being a continuous slider or knob, whatever the host has decided on. About the only customization you can do is a logical grouping of the controls. This is done via a .vstxml file. For the format of this file (unsurprisingly, it's XML) take a look at some of the examples with the SDK. You can either leave the file in the same directory as the DLL, or bake it into the DLL by specifying it as a resource in a .rc file.

That sums up this tutorial, I hope this has helped some people. Getting started is always the hardest part. Happy synthesizing!

Footnote (hiccups)

Almost all of the problems compiling the examples were fixable within a few minutes of googling. But one particularly stubborn example was "drawtest" which uses vstgui, libpng and zlib. Note: you don't have to compile all of the projects in the solution. If you right click on one project and select "Set as startup project" then it will turn bold, and F5 will only apply to that project.

To compile drawtest, you need to download libpng and zlib, rename them to be libpng and zlib (so without the version numbers) and put them in the same folder as vstgui.sf. pnglibconf.h will be missing, copy the example file libpng/scripts/pnglibconf.h.prebuilt into its place. The file list in the solution explorer for these libraries will be out of date, you will need to delete the non-existant files. Finally, on line 6965 of vstgui.cpp you need to replace png_set_gray_1_2_4_to_8 with png_set_expand_gray_1_2_4_to_8. That deprecated function was removed in 2010, but don't act surprised – the VST 2 SDK hasn't been updated since 2006.