Technical Overview

 

SDK Overview

Plugins (modules) components

Plugins are composed of several parts:

  1. Graphics, aka Graphical User Interface (GUI).
  2. Controller.
  3. Processor, aka Digital Signal Processing (DSP).
  4. Parameters ('P').

Each part is optional. Most modules you write will only need one part. For example, if you are making a new graphical control you will need to write a GUI class. If you are making an audio effect, like a Reverb, you will need to write a Processor class. The Controller class is seldom needed and is used only in special situations like hosting other plugin standards.

Parameters are for saving and recalling your plugin's settings. For example the settings of controls like sliders and buttons.

The different parts have different roles and responsibilities, and different lifetimes. For example, the Graphics code is required only when the plugins control panel is open. The processor is the part that generates or modifies audio signals, it's very important that it is never interrupted while it is working, else you will experience glitches and stuttering in your audio. For these reasons, each part is implemented as a separate class, and the Processor works independently from the other parts. i.e. it runs in a separate "real-time" thread. All parts have access to the plugin state via its Parameters. If any part changes a parameter, the other parts are notified. This is how the parts communicate with each other.

How the plugin DSP works

The most important part of the plugin is the sub_process function. It does the actual signal processing. Here is the Gain example (you can find it in the Gain folder, Gain.cpp file). The double slashes // indicates remarks.

// Process audio.
void Gain::subProcess( int bufferOffsetint sampleFrames )
{
	// assign pointers to your in/output buffers. Each buffer is an array of float samples.
	float* in1  = bufferOffset + pinInput1.getBuffer();
	float* in2  = bufferOffset + pinInput2.getBuffer();
	float* out1 = bufferOffset + pinOutput1.getBuffer();
 
	forint s = sampleFrames; s > 0; --s ) // sampleFrames: How many samples to process.
	{
		float input1 = *in1;	// get the sample 'POINTED TO' by in1.
		float input2 = *in2;
 
		// Multiplying the two input's samples together.
		float result = input1 * input2;
 
		// store the result in the output buffer.
		*out1 = result;
 
		// increment the pointers (move to next sample in buffers).
		++in1;
		++in2;
		++out1;
	}

The first thing about this code is it uses 'pointers' to access the sample buffers. In the VST system, sound data is handled in blocks of samples. i.e. a group of samples is recorded from your soundcard, passed to the VST module, then finally passed to the speakers. Audio is processed in 'chunks' this way for efficiency. Your module is passed two things:

  • a 'pointer' to the first sample it needs to deal with.
  • a count of how many samples to process (called sampleFrames)

module1

If you haven't programmed in C++ before, you may find it easier to visualize the sample data as being in an array.

float* in1  = bufferOffset + pinInput1.getBuffer();

This code calculates where in the block of samples to start processing. 'in1' points to the buffer start. The star symbol, *, means 'pointer to'. So the variable 'input1' is a pointer to the first sample. As we process the block, this pointer will move forward one sample at a time. (it's a fancy array index if you like).

The 2nd input and the output pointer are initialized in the same manner.

Next, we set up a loop to process the requested number of samples...

forint s = sampleFrames; s > 0; --s ) // sampleFrames = how many samples to process (can vary). repeat (loop) that many times
{

This code counts out the required number of samples. The double minus sign means 'decrement' (subtract 1). So this is a simple backward counting loop. There is no special reason it counts backward, it just less typing this way.

Most plugins will have the above code, what comes next is the actual signal processing that makes this module different...

// Multiplying the two input's samples together.
float result = input1 * input2;

Pretty simple stuff, the two input signals are multiplied together. Audio signals are represented by floating point numbers, typically between plus/minus 1.0
In SynthEdit, audio signals are represented as voltages (plus/minus 10.0 Volts). So you have to mentally convert between the two scales(10 Volts = 1.0, 5 Volts = 0.5 etc).
If this module's input1 was set at 0.5 for example, and an audio signal was fed into input2, the signal would emerge at half it's original level. This is how SynthEdit's "Level Adjust" module works.

Now we copy the result of our calculation to the output buffer...

// store the result in the output buffer.
*out1 = result;

That's it. One sample done, move on to the next one...

// increment the pointers (move to next sample in buffers).
++in1;
++in2;
++out1;

++ (double plus sign) means 'increment', or "move on to the next sample". This is the same as..

in1 = in1 + 1;

Setting up your Module's plugs

gain

The gain module has 3 plugs. You specify them in your modules XML...

  <Plugin id="SynthEdit Gain example V3" name="Gain3" category="SDK Examples" helpUrl="gain.htm">
    <Audio>
      <Pin id="0" name="Input1" direction="in"  datatype="float" rate="audio" default="0.8" linearInput="true" />
      <Pin id="1" name="Input2" direction="in"  datatype="float" rate="audio" default="0.8" />
      <Pin id="2" name="Output" direction="out" datatype="float" rate="audio" />
    </Audio>
  </Plugin>

When you connect a slider to a plug, the slider's range is set 0 to 10. You can specify a different range, for example 0 to 5...

      <Pin name="Bandwidth Oct" datatype="float" metadata="0,5" />

Other types of plugs

Text and Filename plugs

Here's how to configure a text plug.

      <Pin name="Text Pin" datatype="string" default="Hello!" />

if you also want a 'browse' button for files...

      <Pin name="Filename" datatype="string" default="joystick_knob" isFilename="true" metadata="bmp" />

in your header file, module.h, declare the variable as a 'StringInPin' ...

StringInPin pinText;

It's currently not possible to specify multiple file extensions. However, the end user can choose other file types by choosing: File type = "All Files (*.*)" in the file dialog.

List and Range plugs

Here's how to configure a List plug (enumerated type). These are available only on the Processor.

<Pin name="Mode" datatype="enum" isMinimised="true" metadata="Log,Linear" />

To specify a range of values instead...

 metadata="range -12,12"

Boolean (True/False) Parameters

      <Pin name="Mouse Down" datatype="bool" />

Other supported datatypes include float, int, MIDI and BLOBs.

 MIDI plugs

<Pin id="0" name="MIDI In" direction="in" datatype="midi"/>

To receive MIDI data include the function OnMidiData(..) like so...

void SampleExclusiveFilter::onMidiMessage( int pinunsigned charmidiMessageint size )
{
	int stat,b1,b2,b3,chan;
 
	stat = midiMessage[0] & 0xf0;
	bool is_system_msg = ( stat & SYSTEM_MSG ) == SYSTEM_MSG;
 
	if( is_system_msg )
	{
		goto pass_filter;
	}
 
	chan = midiMessage[0] & 0x0f;
 
	b1 = midiMessage[0];
	b2 = midiMessage[1];
	b3 = midiMessage[2];
 
	if( stat == NOTE_ON && b3 != 0 )  // b3 != 0 tests for note-offs (note on vel = 0)
	{
            // do something...
	}
 
pass_filter:
 
}

Send MIDI out like so...

unsigned char midiMessage[3] = {0,0,0};
unsigned char chan = 0;
unsigned char velocity = 100;
midiMessage[0] = NOTE_OFF | chan; midiMessage[1] = note;
midiMessage[2] = velocity; pinMIDIOut.send( midiMessage, sizeof(midiMessage) );

Audio Buffer Size

The exact size of SynthEdit's audio buffers varies. Typically it's 96 samples. Soundcards usually have a larger buffer size, perhaps 512 or 1024 samples. SynthEdit sub-divides the larger buffer into smaller pieces before sending it to your module.
Sometimes the soundcard buffer size does not divide evenly by 96. In such cases SynthEdit chooses a different size, a size that divides nicely into the soundcard buffer size, as close as possible to 96.
You can query the exact size with the getBlockSize() function. The block size never changes during processing.

Audio buffers are aligned on a 16-byte boundary. This simplifies SSE programming a little. (SSE instructions process 4 samples at a time, each sample being a 4-byte floating point number )