SynthLab SDK
SynthEngine Template Code

You can find the SynthEngine template code in the SynthLab_SDK/source folder in two files, synthengine.h and synthengine.cpp. This is the minimum implementation but with a bunch of helper code already added for you that mostly deals with deocding MIDI messages, and managing the array of voice objects that do the rendering.

SynthEngine Operational Phases

The SynthEngine Operational Phases are discussed in detail in the synth book and so that theory will not be repeated here. However, we do want to to step through the operational phase methods, declared as virtual as these are the main interface functions that the SynthEngine will be calling.

Construction Phase

For the SynthEngine, there is no such thing as "standalone" operation as it is alredy in standalone mode, acting as the sole object your frameowork needs to interact with. The engine is the fountainhead of all synth shared databases, parameters, and MIDI data and most of that is created within at construction.

Constructor
The constructor's main role is in constructing the SynthVoice members, passing them the shared resources that are instantiated during construction. Your plugin framework will instantiate the object and the constructor requires a committment by passing the maximum block size during instantiation. The framework may always render blocks smaller than this size. The constructor has only one argument, which is that block size.

SynthEngine::SynthEngine(uint32_t blockSize)
{
// --- create databases
if (!wavetableDatabase)
wavetableDatabase = std::make_shared<WavetableDatabase>();
if (!sampleDatabase)
sampleDatabase = std::make_shared<PCMSampleDatabase>();
// --- create the smart pointers
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- reset is the constructor for this kind of smartpointer
//
// Pass our this pointer for the IMIDIData interface - safe
synthVoices[i].reset(new SynthVoice(midiInputData, /* shared MIDI input data */
midiOutputData, /* shared MIDI output data; not used in example synths */
parameters->voiceParameters, /* shared voice parameters */
wavetableDatabase, /* shared wavetable data */
sampleDatabase, /* shared PCM sample data */
blockSize)); /* maximum block processing size */
}
// --- voice object
voiceProcessInfo.init(0, 2, blockSize);
}
// ---

Destructor
The SynthEngine is one of a few objects with a dedicated destructor as nearly all dynamic resources are maintained with smart pointers. The destructor is used to elimiate all dynamically declared PCM sample arrays that were extracted from WAV files at load time, and then later shared via the PCM sample database.

// --- destroy me
SynthEngine::~SynthEngine()
{
if (sampleDatabase)
sampleDatabase->clearSampleSources();
}

Reset & Intialize Phase

The SynthEngine simply forwards these calls to its member voice objects and these are direct consequences of your plugin framework calling reset() and initialize() at load time.

// --- reset sub-objects
bool SynthEngine::reset(double _sampleRate)
{
// --- reset array of voices
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- smart poitner access looks normal (->)
synthVoices[i]->reset(_sampleRate); // this calls reset() on the smart-pointers underlying naked pointer
}
return true;
}
// --- Initialize sub-objects
bool SynthEngine::initialize(const char* dllPath)
{
// --- loop
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- init
synthVoices[i]->initialize(dllPath);
}
return true;
}
//

Update Phase

Your plugin framework will access the engine's parameter structure pointers and use them to alter variables based on the user's GUI control settings. After that, the framework should call the SynthEngine::setParameters() method to perform the updates. The engine template code is very light, simply calling the update() method on the voices. When you add unison mode, or any other mode of operation that affect voices in a different way, you need to add that code here. For example in unison mode, the SynthLab example synths will set the voice detuning, and oscillator starting phases during this function. We will add this code in the next section.

// --- set parameters is the update() function for the engine
void SynthEngine::setParameters(std::shared_ptr<SynthEngineParameters>& _parameters)
{
// --- store parameters
parameters = _parameters;
// --- engine mode: poly, mono or unison
parameters->voiceParameters->synthModeIndex = parameters->synthModeIndex;
// --- update the voices one by one; the voice does NOT update it sub-components here
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- needed for modules
synthVoices[i]->update();
}
}
// ---

Render Phase

The render phase is detailed in the synth book and questions about the block processing or other details are answered there so it will not be repeated here. The render phase is very simple and has three parts:

  1. process the MIDI messgages for the entire block of data, one message at a time until the queue is empty
  2. loop through the active voices, call the render() function on them, and accumulate their audio output buffers
  3. apply global volume and (optionally) global FX (called master FX in many manuals)
// --- voice render template code
bool SynthEngine::render(SynthProcessInfo& synthProcessInfo)
{
// --- may do thie before
synthProcessInfo.flushBuffers();
// --- issue MIDI events for this block
uint32_t midiEvents = synthProcessInfo.getMidiEventCount();
for (uint32_t i = 0; i < midiEvents; i++)
{
// --- get the event
midiEvent event = *synthProcessInfo.getMidiEvent(i);
// --- process it
processMIDIEvent(event);
}
// --- -12dB per active channel to avoid clipping
double gainFactor = 1.0;
// --- this is important
voiceProcessInfo.setSamplesInBlock(synthProcessInfo.getSamplesInBlock());
midiInputData->setAuxDAWDataFloat(kBPM, synthProcessInfo.BPM);
midiInputData->setAuxDAWDataFloat(kTSNumerator, synthProcessInfo.timeSigNumerator);
midiInputData->setAuxDAWDataUINT(kTSDenominator, synthProcessInfo.timeSigDenomintor);
midiInputData->setAuxDAWDataFloat(kAbsBufferTime, synthProcessInfo.absoluteBufferTime_Sec);
// --- loop through voices and render/accumulate them
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- blend active voices
if (synthVoices[i]->isVoiceActive())
{
// --- render and accumulate
synthVoices[i]->render(voiceProcessInfo);
accumulateVoice(synthProcessInfo, gainFactor);
}
}
// --- add global volume
applyGlobalVolume(synthProcessInfo);
// --- note that this is const, and therefore read-only
return true;
}
//

Render Helper Functions

The template engine object includes a couple of helper functions for accumulating the voice audio output buffers and applying global volume adjustments.

// --- acculumulate: loop over voices and accumulate their output buffers in parallel
void SynthEngine::accumulateVoice(SynthProcessInfo& synthProcessInfo, double scaling)
// --- apply global volume to the audio in the buffers; use this as a template for applying global effects
void SynthEngine::applyGlobalVolume(SynthProcessInfo& synthProcessInfo)
//

Processing MIDI

The engine processes incoming MIDI events that it finds in the MIDI event queue in the SynthProcInfo structure that the frameworks passes it during the render operation. The template version is empty, but we will add meaningful MIDI event coding in the next section.

// --- note on
bool SynthEngine::processMIDIEvent(midiEvent& event)
{
return true;
}
//

This completes the tour of the template object. In the next section, we will modify the engine template to add the new SynthVoice from the previous section.


synthlab_4.png