SynthLab SDK
SynthEngine Client-Side Code

Programming the SynthEngine from the client-side (from your plugin framework) was designed to be as simple as possible, requiring only a handful of function calls. The most important code that you will need to write involves setting up the SynthBlockProc structure that contains your audio buffers and MIDI messages for a single block process render cycle. Note that this code is going to be extremely dependent on your plugin framework and you will need to know how to access your client's audio and MIDI data buffers.

SynthProcessInfo Structure

The SynthProcessInfo structure is covered in Block Audio Processing and this is key to the engine's operation as it is the argument that is passed during the render() call to service MIDI events and to fill buffers with synthesized audio. In my implementations, the framework's processing object creates a static instance of the structure, initializes it, and then loads it with MIDI events prior to calling the render function.

Setting up and initializing your SynthProcessInfo struct is simple:

  1. declare/instantiate the structure
  2. call the init() function to allocate the audio input and output buffers; typically this is done one and only one time, but if you want to change the input/output channel count or the block size before the SynthEngine is created, just call the init() function again with the new parameters. NOTE: once the SynthEngine has been constructed, you can not change the buffer sizes!
// --- setup proc structure
#include "synthbase.h"
// --- (1) static instance
SynthLab::SynthProcessInfo synthBlockProcInfo;
// --- (2) one time initiailization
synthBlockProcInfo.init(SynthLab::NO_CHANNELS, /* number of input audio channels; use for sidechain inputs; not used in SynthLab examples */
SynthLab::STEREO_CHANNELS, /* number of output audio channels; all SynthLab synths feature a stereo output */
64); /* block size in frames: 64 = 64 sets of stereo frames (2 samples/channel) */
//

MIDI Events
You will need to push the MIDI events that occurred during the time in the audio block into the structure prior to calling the render() method on the synth. There are two functions to help; one which clears out the last set of MIDI events and another to push a new event on the FIFO stack (which is really just a std::vector). See the MIDI Note Events section for information on MIDI event structures and Enqueueing MIDI Events section for more information about adding MIDI events to the SynthProcessInfo's queue. You code will look like this:

// --- always clear out the events from the last audio block process
synthBlockProcInfo.clearMidiEvents();
// --- parse MIDI events that occur during each audio block and create the event structure
SynthLab::midiEvent synthEvent(NOTE_ON, /* NOTE_ON is declared in synthconstants.h */
0, /* Channel 0 (MIDI Channel 1) */
60, /* midi note number, Middle C */
127, /* velocity */
0); /* buffer time, not used */
// --- and push structure into SynthProcessInfo, which adds them to its vector
synthBlockProcInfo.pushMidiEvent(synthEvent);
//

SynthEngine Operational Phases

The rest of the SynthEngine operation is simple, as long as you have the SynthProcessInfo structure initialized and ready to be loaded with MIDI events. There are four operational phases plus creation:

  1. reset with sample rate (must be done any time the sample rate changes)
  2. initialize with path information (path information is needed for PCM samples, and optional for everything else)
  3. do GUI control updates
  1. render audio into output buffers

Creation
The SynthEngine needs to know the audio block processing size from the very beginning of its existence. Once set, this size should not change. You have two options for declaring the SynthEngine:

  1. static: if you declare a static instance of the engine, the block size is set to 64 automatically, and can not be changed
  2. dynamic: you provide the block size in the constructor of the dynamically allocated object
// --- static;
SynthLab::SynthEngine theStaticEngine; //< -- block size is 64 by default
// --- dynamic with smart pointer
synthEngine.reset(new SynthLab::SynthEngine(64)); //< -- adjust block size here
//

Reset and Initialize
The reset and intialize phases are simple requiring one function all each and described in SynthEngine Template Code; note that these should normally be called in succession, startint with the reset() function. One of the reasons for the ordering here involves the location of the path variable for the initialize() function, which may not be known at construction time on the framework processing object. Since it is likely you will instantiate the engine as part of construction, the initialize() function must not be called until you have the path to send it. See SynthEngine Template Code for more information on this path.

// --- reset called anytime sample rate changes
// NOTE: SynthLab is designed for 44.1kHz and 48kHz with regards to the wavetables and PCM samples!
synthEngine->reset(44100.0); //< get fs from your framework
// --- once the fully qualified path is known:
synthEngine->initialize(path.c_str()); //< -- get path to DLL folder from framework or hardcode
//< hardcoding is not a good solution but OK for testing algorithms
//

GUI Parameter Updates
Details about the GUI update cycle and example code are avaialble in Updating GUI Parameters. You get the parameter structure pointers, alter variables with them, then call the update function.

// --- for GUI parameter updating
std::shared_ptr<SynthLab::SynthEngineParameters> engineParameters = nullptr;
std::shared_ptr<SynthLab::SynthVoiceParameters> voiceParameters = nullptr;
// --- during initialization/instantiation of framework's processing object
//
// --- get the engine parameters
synthEngine->getParameters(engineParameters);
// --- get the voice parameters from the engine parameters
voiceParameters = engineParameters->voiceParameters;
// --- set the various parameters for the engine-level and voice-level components
//
// --- engine
engineParameters->globalPitchBendSensCoarse = 12; // <-- get this value from your GUI
engineParameters->globalTuningCoarse = -8.750; // <-- get this value from your GUI
engineParameters->globalUnisonDetune_Cents = 10.0; // <-- get this value from your GUI
engineParameters->globalVolume_dB = -6.00; // <-- get this value from your GUI
// --- voice updates
voiceParameters->glideTime_mSec = 1000.0; // <-- get this value from your GUI
voiceParameters->lfo1Parameters->frequency_Hz = 0.25; // <-- get this value from your GUI
voiceParameters->lfo1Parameters->outputAmplitude = 0.707; // <-- get this value from your GUI
// filter EG
voiceParameters->filterEGParameters->attackTime_mSec = 20.0; // <-- get this value from your GUI
voiceParameters->filterEGParameters->decayTime_mSec = 250.0; // <-- get this value from your GUI
// etc...
// --- oscillator output amplitudes for four member oscillators
voiceParameters->osc1Parameters->outputAmplitude_dB = 0.0; // <-- get this value from your GUI
voiceParameters->osc2Parameters->outputAmplitude_dB = +3.5; // <-- get this value from your GUI
// etc...
// --- then call the setParameter() function for updates
// NOTE: the argument here is usually the same as the parameter structure you accessed earlier
// however, it is possible to get the engine a separate parameter structure; this could
// be used for advanced GUIs with multiple variation panels; this paradigm is not used in SynthLab
// examples, which always use the original parameter struct pointer
synthEngine->setParameters(engineParameters);
//

Render Audio and Access Buffers
With the SynthProcessInfo structure (named synthBlockProcInfo here) prepared and loaded with one block's worth of MIDI events, you then call the render() method, passing in this single structure as the argument. Once render() returns, you can access the audio data in the buffers and send them to your framework's output buffers.

  • note that you do not need to clear out the audio buffers once done; each call to render() will wipe the buffers clean for the next render cycle
  • SynthLab examples are all stereo synths but you may also create mono versions; in that case only the channel = 0 array will be valid
// --- render it
synthEngine->render(synthBlockProcInfo);
// --- get output buffer and write to framework
float** synthOutputs = synthBlockProcInfo.getOutputBuffers();
// --- block processing -- write to outputs
for (uint32_t sample = processBlockInfo.blockStartIndex, i = 0;
sample < processBlockInfo.blockStartIndex + processBlockInfo.blockSize;
sample++, i++)
{
// --- copy to outputs
for (uint32_t channel = 0; channel < processBlockInfo.numAudioOutChannels; channel++)
{
<your framework output buffer>[channel][sample] = synthOutputs[channel][i];
}
}
//

Engine Destruction
In the example here using smart pointers, there is nothing to do but let the smart pointer delete itself. If you declare the engine statically, it will be destroyed in a likewise manner in your processing object's destructor. If you use old fashioned allocation and pointers, you need to call the delete operator manually. There are numerous data arrays to clear, and for the SynthLab-PCM synth, around 1.4GB of PCM samples to delete, so the destruction phase is quite important!


synthlab_4.png