SynthLab SDK
Minimal Standalone Synth

You can combine the stand-alone objects from the last sections into a single object that will render one complete note event. We'll put together a simple object that will generate a note event for the following block diagram using the objects we've already built - most of the code is already done for us. This will give you the most minimal impelementation possible and does not include global MIDI message decoding (for note and CC events) or other higher level operations. But you will be able to take this object and put it into your plugin framework, then send it MIDI events to render a note.


minSynth_1.png


You will build a container object here that will implement the five phases of synth operation and generate a C++ class you can integrate with your plugin framework today! It will also give you a lot to think about and will introduce the Voice and Engine objects in the next sections. For simplicity, let's lay out the object to mimic the wrap these 5 phases of operation and provide access to the output buffers of fresly rendered audio goodness. For this object, we will continue to keep a separate namespace and not use namespace SynthLab { } just yet.

Block Processing This object will use block processing for all modules, unlike the standalone objects where we let the LFO and EG just render single frames of data. This will also demonstrate how the modulation values are granulized to cut down on calling the expensive update( ) function.

The MinSynth Class

Here is an ultra-simple C++ object that maintains the set of synth modules we need to create a single note event. I named the five functions after the same named functions on the SynthModules, though the arguments are not exactly the same. Instead, the arguments consist of whatever we need to perform the task for that function.

  • notice the protected members are the synth modules
  • the render( ) function returns a const AudioBuffer pointer; it is the output of the DCA which is the last in the signal path of the block drawing.
class MinSynth
{
public:
// --- construct/destruct
MinSynth();
~MinSynth() {}
// --- operational phases
bool reset(double _sampleRate);
bool update();
const std::shared_ptr<SynthLab::AudioBuffer> render(uint32_t samplesToProcess = 1);
bool doNoteOn(SynthLab::MIDINoteEvent& noteEvent);
bool doNoteOff(SynthLab::MIDINoteEvent& noteEvent);
protected:
// --- synth components
std::unique_ptr<SynthLab::SynthLFO> lfo = nullptr;
std::unique_ptr<SynthLab::EnvelopeGenerator> ampEG = nullptr;
std::unique_ptr<SynthLab::WTOscillator> wtOsc = nullptr;
std::unique_ptr<SynthLab::SynthFilter> filter = nullptr;
std::unique_ptr<SynthLab::DCA> dca = nullptr;
//
};

constructor, reset() and update( )
Now look at the constructor, reset() and update( ) functions.

  • in the update( ) function, I am altering the parameters but NOT calling the update( ) function on the sub componens; this is because update( ) will be called at the top of the render cycle automatically
  • notice the update( ) function code for the filter: I am setting the value of MOD_KNOB_D to 1.0 because it is the Bipolar Modulation input intensity control (study the documentation!); if this value is 0.0, the LFO modulation will have no affecgt
  • ordinarily, the update( ) function would alter parameters on these objects as a result of GUI control changes, and you would place this code in your GUI handler for your plugin framework. Here, I am just hardcoding it for simplicity.
MinSynth::MinSynth()
{
// --- create the new modules here, all in standalone mode
//
// --- LFO
lfo.reset(new SynthLab::SynthLFO(
nullptr, /* MIDI input data */
nullptr, /* LFO parameters */
64)); /* process individual samples (block size = 1)*/
// --- EG
nullptr, /* MIDI input data */
nullptr, /* EG parameters */
64)); /* process individual samples (block size = 1)*/
// --- WTO
wtOsc.reset(new SynthLab::WTOscillator(
nullptr, /* MIDI input data */
nullptr, /* WT parameters */
nullptr, /* wavetable database */
64)); /* audio block size (one sample per channel)*/
// --- FILTER
filter.reset(new SynthLab::SynthFilter(
nullptr, /* MIDI input data */
nullptr, /* filter parameters */
64)); /* block size */
// --- DCA
dca.reset(new SynthLab::DCA(
nullptr, /* MIDI input data */
nullptr, /* dca parameters */
64)); /* block size */
}
// --- forward calls to internal modules
bool MinSynth::reset(double _sampleRate)
{
// --- reset components
lfo->reset(_sampleRate);
ampEG->reset(_sampleRate);
wtOsc->reset(_sampleRate);
filter->reset(_sampleRate);
dca->reset(_sampleRate);
// --- update initialize
update();
return true; // done
}
// --- set values from GUI controls, or programmatically from other objects
// do not user for modulation, use the modulation inputs
bool MinSynth::update()
{
// --- reset components
// In each case:
// 1. get parameter structure pointer
// 2. alter values
// 3. the call to update( ) is OPTIONAL and will be done anyway during render
std::shared_ptr<SynthLab::LFOParameters> lfoParameters = lfo->getParameters();
if (lfoParameters) // should never fail
{
// --- NORMALLY these values will come from a GUI control
lfoParameters->waveformIndex = 8; //<- GUI control variable, hardcoded here
lfoParameters->modKnobValue[SynthLab::MOD_KNOB_A] = 0.25;
// --- call the update function (OPTIONAL, will be done anyway during render)
// lfo->update();
}
// --- EG
std::shared_ptr<SynthLab::EGParameters> egParameters = ampEG->getParameters();
if (egParameters) // should never fail
{
// --- NORMALLY these values will come from a GUI control
egParameters->attackTime_mSec = 50.0;//<- GUI control variable, hardcoded here
egParameters->decayTime_mSec = 100.0;
egParameters->sustainLevel = 0.707;
egParameters->releaseTime_mSec = 1000.0;
// --- call the update function (OPTIONAL, will be done anyway during render)
// ampEG->update();
}
// --- OSC
std::shared_ptr<SynthLab::WTOscParameters> wtoParameters = wtOsc->getParameters();
if (wtoParameters) // should never fail
{
// --- NORMALLY these values will come from a GUI control
wtoParameters->waveIndex = 1; // FourierWTCore parabola
wtoParameters->outputAmplitude_dB = -3.0;
// --- call the update function (OPTIONAL, will be done anyway during render)
// wtOsc->update();
}
// --- filter
std::shared_ptr<SynthLab::FilterParameters> filterParameters = filter->getParameters();
if (filterParameters) // should never fail
{
// --- NORMALLY these values will come from a GUI control
filterParameters->filterIndex = 4; // SVF LP
filterParameters->fc = 880.0;
filterParameters->Q = 5;
filterParameters->filterOutputGain_dB = -2.0;
filterParameters->modKnobValue[SynthLab::MOD_KNOB_D] = 1.0; // max intensity
// --- call the update function (OPTIONAL, will be done anyway during render)
// filter->update();
}
std::shared_ptr<SynthLab::DCAParameters> dcaParameters = dca->getParameters();
if (dcaParameters) // should never fail
{
// --- NORMALLY these values will come from a GUI control
dcaParameters->gainValue_dB = +3.0;
dcaParameters->panValue = 0.25; // 1/4 to the right, 3/4 to the left
// --- call the update function (OPTIONAL, will be done anyway during render)
// dca->update();
}
return true; // done
}
//

render( )
In this most important function, we need to render the modulation values from the LFO and EG first, then apply them to the filter and oscillator's modulation inputs. You will need to study the documentation and/or the synth book to understand better how this works, but all of the modulation array slot constants can be found in synthconstants.h and you can use the sample synth code for more references.

Notice how I render and apply the modulation values in sequence.

const std::shared_ptr<SynthLab::AudioBuffer> MinSynth::render(uint32_t samplesToProcess)
{
// --- render LFO output, get modulation output
lfo->render(samplesToProcess);
double lfoNormOut = lfo->getModulationOutput()->getModValue(SynthLab::kLFONormalOutput);
// --- MODULATE: apply lfo output to the modulation inputs of the oscillator and filter
wtOsc->getModulationInput()->setModValue(SynthLab::kBipolarMod, lfoNormOut);
filter->getModulationInput()->setModValue(SynthLab::kBipolarMod, lfoNormOut);
// --- render EG output, get modulation output
ampEG->render(samplesToProcess);
double egNormOut = ampEG->getModulationOutput()->getModValue(SynthLab::kEGNormalOutput);
// --- MODULATE: apply EG output to the kEGMod modulation input of the dca
dca->getModulationInput()->setModValue(SynthLab::kEGMod, egNormOut);
// --- render oscillator
wtOsc->render(samplesToProcess);
// --- transfer information from OSC output to filter input
SynthLab::copyOutputToInput(wtOsc->getAudioBuffers(),
filter->getAudioBuffers(),
SynthLab::STEREO_TO_STEREO, 64);
// --- render filter
filter->render(samplesToProcess);
// --- transfer information from fikter output to DCA input
SynthLab::copyOutputToInput(filter->getAudioBuffers(),
dca->getAudioBuffers(),
SynthLab::STEREO_TO_STEREO, 64);
// --- render DCA
dca->render(samplesToProcess);
// --- AT THIS POINT, the rendered synth audio is sitting in the DCA's AudioBuffer output array
return dca->getAudioBuffers();
//
}

noteOn( ) and noteOff( )
These two functions are trivial as they simply forward the MIDI event data to the underlying components.

// --- just send the note event to the synth modules
bool MinSynth::doNoteOn(SynthLab::MIDINoteEvent& noteEvent)
{
lfo->doNoteOn(noteEvent);
ampEG->doNoteOn(noteEvent);
wtOsc->doNoteOn(noteEvent);
filter->doNoteOn(noteEvent);
dca->doNoteOn(noteEvent);
return true; // done
}
// --- just send the note event to the synth modules
bool MinSynth::doNoteOff(SynthLab::MIDINoteEvent& noteEvent)
{
lfo->doNoteOff(noteEvent);
ampEG->doNoteOff(noteEvent);
wtOsc->doNoteOff(noteEvent);
filter->doNoteOff(noteEvent);
dca->doNoteOff(noteEvent);
return true; // done
}

Using MinSynth
Using the object makes your plugin framework code very compact. All you need to do is follow the same steps as before, but now with a single object rather than the collection.

// --- static instance, named miniSynth
MinSynth miniSynth;
// --- initialize
miniSynth.reset(44100.0); //<- get fs from your plugin framework
// --- call the update function once to inialize
miniSynth.update();

Use the note-on and note-off handlers as normal, from your plugin framework MIDI handler function.

midiEvent.midiNoteNumber = 60; // <-- get from your MIDI handler
midiEvent.midiNoteVelocity = 127;// <-- get from your MIDI handler
// --- do the note on event
miniSynth.doNoteOn(midiEvent);
// ... or at note off time:
miniSynth.doNoteOff(midiEvent);
//

Next, update the components each time your plugin GUI handler is called. Transfer your GUI variables into the objects, but do NOT call update( ) as it will be called automatically, just prior to the render( ) function call. Finally, call the render function. These operations are shown together here. The object returns a shared pointer to the audio output data so send it to your plugin framework output before ending the function.

// --- do fresh GUI update for this block of data
update( );
// --- render a block of 64 samples
const std::shared_ptr<SynthLab::AudioBuffer> synthBuffer = miniSynth.render(64);
// --- iterate through the array, or memcpy( ) it to your plugin framework output
float* leftOutBuffer = synthBuffer->getOutputBuffer(SynthLab::LEFT_CHANNEL);
float* rightOutBuffer = synthBuffer->getOutputBuffer(SynthLab::RIGHT_CHANNEL);
for (uint32_t i = 0; i < 64; i++)
{
float leftSample = leftOutBuffer[i];
float rightSample = rightOutBuffer[i];
// --- send the two outputs to the real-world here...
}
//

Congrats! You now have a functing synth object that can render a note from a MIDI event. In the next section, we will replace the modulation code with the Modulation Matrix instead!


synthlab_4.png