Technical Overview

 

 overview2

SynthEdit divides modules into two sections:

1 - Audio Processing (DSP) Digital Signal Processing.

2 - Graphics (GUI) Display and user-interaction.

Audio code needs to run at a high priority and low latency without interruption for screen redrawing. Graphics code need to run at a lower priority and have access to the Window's user interface (screen, keyboard, mouse etc). SynthEdit achieves these goals by dividing each module into two classes. Each runs in it's own thread. Two separate, loosely coupled tasks. The SDK provides a mechanism for communicating between the two threads.

The GUI class is polled every 50ms ( 20Hz ). The exact rate varies between VST Hosts. This is the maximum rate at which animations and VU Meters can update.

Some modules may require only Audio Processing code (e.g. Gain module), some will require Graphics routines only (Bitmap display module), some will require both (e.g. Waveshaper).

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 se_gain folder, Module.cpp file). The double slashes // indicates remarks.

void Module::sub_process(long buffer_offset, long sampleFrames )
{
// assign some pointers to your in/output buffers. usually blocks (array) of 96 samples
float *in1 = buffer_offset + input1;
float *in2 = buffer_offset + input2;
float *out1 = buffer_offset + output1;

while(--sampleFrames >= 0) // sampleFrames = samples to process (can vary). repeat (loop) that many times
{
float input1 = *in1; // get the sample 'POINTED TO' by in1
float input2 = *in2;

// do the actual processing (multiplying the two input samples together)
float result = input1 * input2;

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

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

The first confusing thing about this code is that I've used '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 visualise the sample data as being in an array (well, it is).

One extra complication. SynthEdit delivers various events to your module. An event may occur part-way though a buffer. In this case the module processes the buffer in sections. Each buffer contains arround 96 samples. So your module may get asked: "Process the first 50 samples of the block", then later: "Process the remaining 46 samples"
So our example code starts...

float *in1 = buffer_offset + input1;

This code calculates where in the block of samples to start processing. "input1" points to the buffer start, "buffer_offset" is what sample index to start at. The star symbol, *, means 'pointer to". So the variable "in1" 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 initialised in the same manner.

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

while(--sampleFrames >= 0)
{
}

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 backwards, 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...

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

*out1 = result;

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

in1++;

++ (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 module

The gain module has 3 plugs. You specify them in function getPinProperties()...

bool Module::getPinProperties (long index, SEPinProperties* properties)
{
switch( index )
{
// typical input plug (inputs are listed first)
case 0:
properties->name = "Input";
properties->variable_address = &input1;
properties->direction = DR_IN;
properties->datatype = DT_FSAMPLE;
properties->default_value = "0";
break;
case 1:
properties->name = "Input";
properties->variable_address = &input2;
properties->direction = DR_IN;
properties->datatype = DT_FSAMPLE;
properties->default_value = "5";
break;
// typical output plug
case 2:
properties->name = "Output";
properties->variable_address = &output1;
properties->direction = DR_OUT;
properties->datatype = DT_FSAMPLE;
break;
default:
return false; // host will ask for plugs 0,1,2,3 etc. return false to signal when done
};

return true;
}

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

properties->datatype_extra = "5,0,5,0";

Other types of plugs

Text and Filename plugs

Here's how to configure a text plug.

properties->name = "Text Value";
properties->variable_address = &text_input;
properties->direction = DR_IN;
properties->datatype = DT_TEXT;

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

properties->flags = IO_FILENAME;
properties->datatype_extra ="wav"; // file extension

in your header file, module.h, declare the variable as char *...

char *text_input;

SynthEdit will manage allocation and removal of the text string's memory. 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).

properties->name = "Transpose";
properties->variable_address = &transpose_ammount;
properties->datatype_extra = "Semitone=1,Octave=12";
properties->default_value = "1";
properties->direction = DR_IN;
properties->datatype = DT_ENUM;

To specify a range of values instead...

properties->datatype_extra = "range -12,12";

in your header file, module.h, declare the variable as short (16 bit integer)...

short transpose_ammount;

To access the value in your code, use it as a standard variable (no pointers)....

x = transpose_ammount;

Boolean (True/False) Parameters

properties->name = "Phatness On/Off";
properties->variable_address = &bool_input;
properties->direction = DR_PARAMETER;
properties->datatype = DT_BOOL;
break;

In header...

bool bool_input;

Other supported datatypes are float (DT_FLOAT) and int (DT_INT)

 MIDI plugs

case 0:
properties->name = "MIDI In";
properties->direction = DR_IN;
properties->datatype = DT_MIDI2;
break;
case 1:
properties->name = "MIDI Out";
properties->direction = DR_OUT;
properties->datatype = DT_MIDI2;
break;

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

void Module::OnMidiData( unsigned long p_clock, unsigned long midi_msg, short pin_id)
{
// system messages are for timing info etc
// they don't have a channel, in this example just ignore them
bool is_system_msg = ( midi_msg & SYSTEM_MSG ) == SYSTEM_MSG;

if( !is_system_msg )
{
// The first byte of a midi message contains the status (note on/off etc) and the channel
int chan = midi_msg & 0x0f;
int stat = midi_msg & 0xf0;

// the next two bytes depend on the type of message
int b2 = ( midi_msg / 0x100 ) & 0xff;
int b3 = ( midi_msg / 0x10000 ) & 0xff;

switch( stat )
{
case NOTE_ON: // byte 2 is the note number. byte 3 is velocity
// do something
break;

case NOTE_OFF:
// do something
break;
}
}

Send MIDI out like so...

// assemble the MIDI message bytes into an 32 bit integer
midi_msg = stat + chan + (b2 << 8) + (b3 << 16);

// send the MIDI
getPin(PN_MIDI_OUT)->TransmitMIDI( p_clock, midi_msg );

The MIDI transposer example (se_transposer) is a full MIDI module example.

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 BlockSize() 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 )