SynthLab SDK
SynthVoice Example

In the Minimal Standalone Synth section, you saw how to create the MinSynth C++ object that owns and maintains a small set of SynthModule objects, that it arranges and manages to render audio into output buffers. Please review that section prior to going through this exercise, where I convert the MinSynth object into a SynthVoice object and leverage off of the built-in code. Here is the class definition for the MinSynth object and the block diagram of the voice (patch) it implements.


minSynth_1.png


// --- Minimal Synth C++ object
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;
std::unique_ptr<SynthLab::ModMatrix> modMatrix = nullptr;
};
// ---

We are going to use the same set of SynthModules in the newly revised SynthVoice version with the added benefit of the template code and parameter sharing features.

Add the Voice's SynthModule Members

First, modify the synthvoice.h template file by adding the SynthModule object declarations as protected members. I am using the std::shared_ptr to manage my dynamically allocated objects but you may certainly use other methods.

// --- module files
#include "lfo.h"
#include "wtoscillator.h"
#include "synthfilter.h"
#include "oscillator.h"
#include "filtermodule.h"
#include "dca.h"
#include "modmatrix.h"
// --- now in the protected section of the template code:
protected:
// --- standalone operation only
std::shared_ptr<SynthVoiceParameters> parameters = nullptr;
// --- MinSynth members
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;
std::unique_ptr<SynthLab::ModMatrix> modMatrix = nullptr;
// --- local storage
double sampleRate = 0.0;
uint32_t blockSize = 64;
// --- interface pointer
std::shared_ptr<MidiInputData> midiInputData = nullptr;
// --- REST OF DECLARATION IS IDENTICAL TO TEMPLATE
// ...

Next, add shared parameter strucures to the SynthVoiceParameters struct in the same file. Add one new shared pointer for each object's parameters.

// --- modify the parameters
struct SynthVoiceParameters
{
SynthVoiceParameters() {}
// --- shared parameter structs for each module
//
// --- oscillator
std::shared_ptr<WTOscParameters> wtOscParameters = std::make_shared<WTOscParameters>();
// --- LFOs
std::shared_ptr<LFOParameters> lfoParameters = std::make_shared<LFOParameters>();
// --- EGs
std::shared_ptr<EGParameters> ampEGParameters = std::make_shared<EGParameters>();
// --- filters
std::shared_ptr<FilterParameters> filterParameters = std::make_shared<FilterParameters>();
// --- DCA
std::shared_ptr<DCAParameters> dcaParameters = std::make_shared<DCAParameters>();
// --- ModMatrix
std::shared_ptr<ModMatrixParameters> modMatrixParameters = std::make_shared<ModMatrixParameters>();
// --- REST OF STRUCT IS SAME AS TEMPLATE --//
// --- synth mode; engine has same variable
uint32_t synthModeIndex = enumToInt(SynthMode::kMono);
// --- synth mode; engine has same variable
uint32_t filterModeIndex = enumToInt(FilterMode::kSeries);
// --- portamento (glide)
bool enablePortamento = false;
// --- glide time
double glideTime_mSec = 0.0;
// --- legato mode
bool legatoMode = false;
// --- freerun
bool freeRunOscMode = false;
// --- unison Detune - each voice will be detuned differently
double unisonDetuneCents = 0.0;
double unisonStartPhase = 0.0;
double unisonPan = 0.0;
}
//

Construct the SynthModule Members

In the SynthVoice constructor, you create the SynthModules. Unlike the standalone versions, these will reecive non-null shared pointers to shared resources. Some of these resources came from the engine, and arrived into the voice object's constructor. The shared parameter structures are part of the voice's SynthVoiceParameter structure. So you can see how the shared resources are divided between the engine (MIDI input, wavetable, and PCM sample data) and the voice (all module parameter structures). Here is the first part of the constructor where the modules are instantiated with smart pointers.

// --- voice constructor
SynthVoice::SynthVoice(std::shared_ptr<MidiInputData> _midiInputData,
std::shared_ptr<MidiOutputData> _midiOutputData,
std::shared_ptr<SynthVoiceParameters> _parameters,
std::shared_ptr<WavetableDatabase> _wavetableDatabase,
std::shared_ptr<PCMSampleDatabase> _sampleDatabase,
uint32_t _blockSize)
: midiInputData(_midiInputData) //<- set our midi dat interface value
, midiOutputData(_midiOutputData)
, parameters(_parameters) //<- set our parameters
, wavetableDatabase(_wavetableDatabase)
, sampleDatabase(_sampleDatabase)
, blockSize(_blockSize)
{
// for standalone
if (!midiInputData)
midiInputData.reset(new (MidiInputData));
if (!midiOutputData)
midiOutputData.reset(new (MidiOutputData));
// --- this happens in stand-alone mode; does not happen otherwise;
// the first initialized SynthLab component creates its own parameters
if (!parameters)
parameters = std::make_shared<SynthVoiceParameters>();
// --- NOTE: in standalone mode, the modules will create the wavetable and PCM databases
// locally, so they do not need to be checked here.
// --- LFOs
lfo.reset(new SynthLFO(midiInputData, parameters->lfoParameters, blockSize));
// --- EGs
ampEG.reset(new EnvelopeGenerator(midiInputData, parameters->ampEGParameters, blockSize));
// --- wt oscillator
wtOsc.reset(new WTOscillator(midiInputData, parameters->wtOscParameters, wavetableDatabase, blockSize));
// --- filters
filter.reset(new SynthFilter(midiInputData, parameters->filterParameters, blockSize));
// --- DCA
dca.reset(new DCA(midiInputData, parameters->dcaParameters, blockSize));
// --- mod matrix
modMatrix.reset(new ModMatrix(parameters->modMatrixParameters));
// --- THIS IS IN THE TEMPLATE CODE...
// --- create our audio buffers
mixBuffers.reset(new SynthProcessInfo(NO_CHANNELS, STEREO_CHANNELS, blockSize));
//

Now that the objects have been created with shared resources, we can program the modulation matrix. This code is lifted directly from the MinSynth section in Minimal Standalone Synth.

// --- mod matrix can be reconfigured on the fly
//
// --- clear out
modMatrix->clearModMatrixArrays();
// --- setup possible sources and destinations; can also be done on the fly
//
// --- add the sources
modMatrix->addModSource(SynthLab::kSourceLFO1_Norm, lfo->getModulationOutput()->getModArrayPtr(SynthLab::kLFONormalOutput));
modMatrix->addModSource(SynthLab::kSourceAmpEG_Norm, ampEG->getModulationOutput()->getModArrayPtr(SynthLab::kEGNormalOutput));
// --- add the destinations
modMatrix->addModDestination(SynthLab::kDestOsc1_fo, wtOsc->getModulationInput()->getModArrayPtr(SynthLab::kBipolarMod));
modMatrix->addModDestination(SynthLab::kDestFilter1_fc_Bipolar, filter->getModulationInput()->getModArrayPtr(SynthLab::kBipolarMod));
modMatrix->addModDestination(SynthLab::kDestDCA_EGMod, dca->getModulationInput()->getModArrayPtr(SynthLab::kEGMod));
// --- hardwire the routings for now; the default hardwired intenstity is 1.0
modMatrix->getParameters()->setMM_HardwiredRouting(SynthLab::kSourceLFO1_Norm, SynthLab::kDestOsc1_fo);
modMatrix->getParameters()->setMM_HardwiredRouting(SynthLab::kSourceLFO1_Norm, SynthLab::kDestFilter1_fc_Bipolar);
modMatrix->getParameters()->setMM_HardwiredRouting(SynthLab::kSourceAmpEG_Norm, SynthLab::kDestDCA_EGMod);
//

Descend the Operational Phase Functions

Next, move through the operational phase functions, adding code as needed. Many of these will simply call the same named function on one or all of the member modules.

reset() and initialize()

  • the reset function forwards the reset() calls to the modules and sets some member variables
  • the initialization function only needs to be called on oscillators in SynthLab; that is done here even though the SynthLab wavetable oscillators do not need the path; yours might need this for parsing wavetable data files or other initialization chores
// --- reset modules
bool SynthVoice::reset(double _sampleRate)
{
sampleRate = _sampleRate;
currentMIDINote = -1;
// --- reset modules
lfo->reset(_sampleRate);
ampEG->reset(_sampleRate);
wtOsc->reset(_sampleRate);
filter->reset(_sampleRate);
dca->reset(_sampleRate);
return true;
}
// --- intialize oscillator with path
bool SynthVoice::initialize(const char* dllPath)
{
// --- initialize all sub components that need the DLL path
wtOsc->initialize(dllPath);
return true;
}
//

update()
The voice only needs to set the unison mode detuning and phase start variables on the oscillators. This is done for the sole wavetable oscillator only.

// --- updater
bool SynthVoice::update()
{
// --- do updates to sub-components
// NOTE: this is NOT for GUI control updates for normal synth operation
//
// ---- sets unison mode detuning and phase from GUI controls (optional)
wtOsc->setUnisonMode(parameters->unisonDetuneCents, parameters->unisonStartPhase);
return true;
}
//

render()
The render() function follows the same logic and code as the MinSynth object. The only difference here is the bit of code at the end, that checks to see if the amp EG has expired, so that the voice state variable may be set.

// --- render the modules
bool SynthVoice::render(SynthProcessInfo& synthProcessInfo)
{
uint32_t samplesToProcess = synthProcessInfo.getSamplesInBlock();
// --- clear for accumulation
mixBuffers->flushBuffers();
// --- render LFO outputt
lfo->render(samplesToProcess);
// --- render EG output
ampEG->render(samplesToProcess);
// --- do the modulation routings
modMatrix->runModMatrix();
// --- render oscillator
wtOsc->render(samplesToProcess);
// --- transfer information from OSC output to filter input
copyOutputToInput(wtOsc->getAudioBuffers(), /* from */
filter->getAudioBuffers(), /* to */
SynthLab::STEREO_TO_STEREO, /* stereo */
blockSize); /* block length */
// --- render filter
filter->render(samplesToProcess);
// --- transfer information from fikter output to DCA input
copyOutputToInput(filter->getAudioBuffers(), /* from */
dca->getAudioBuffers(), /* to */
SynthLab::STEREO_TO_STEREO, /* stereo */
blockSize); /* block length */
// --- render DCA
dca->render(samplesToProcess);
// --- to mains
copyOutputToOutput(dca->getAudioBuffers(), /* from */
synthProcessInfo, /* to */
STEREO_TO_STEREO, /* stereo */
samplesToProcess); /* block length */
// --- check for note off condition
if (voiceIsActive)
{
// --- has ampEG expired yet?
if (ampEG->getState() == enumToInt(EGState::kOff))
{
voiceIsActive = false;
}
}
// done
return true;
}
//

doNoteOn()
The note-on handler mainly passes the message down to the modules. But it is also where you will start the glide modulator if neeeded.Each SynthModule includes a GlideModulator object, documented in the synth book, to perform portamento.

// --- note on handler
bool SynthVoice::doNoteOn(midiEvent& event)
{
// --- calculate MIDI -> pitch value
double midiPitch = midiNoteNumberToOscFrequency(event.midiData1);
int32_t lastMIDINote = currentMIDINote;
currentMIDINote = (int32_t)event.midiData1;
MIDINoteEvent noteEvent(midiPitch, event.midiData1, event.midiData2);
// --- TEMPLATE CODE END
// --- needed forLFO modes
lfo->doNoteOn(noteEvent);
// --- EG
ampEG->doNoteOn(noteEvent);
// --- create glide info structure out of notes and times
GlideInfo glideInfo(lastMIDINote, currentMIDINote, parameters->glideTime_mSec, sampleRate);
// --- set glide mod
wtOsc->startGlideModulation(glideInfo);
wtOsc->doNoteOn(noteEvent);
// --- filter needs note on for key track
filter->doNoteOn(noteEvent);
// --- DCA
dca->doNoteOn(noteEvent);
// --- TEMPLATE CODE CONTINUED
// --- set the flag
voiceIsActive = true; // we are ON
voiceNoteState = voiceState::kNoteOnState;
// --- this saves the midi note number and velocity so that we can identify our own note
voiceMIDIEvent = event;
return true;
}
// ---

doNoteOff()
The note-on handler mainly passes the message down to the modules. This also sets the voice state to kNoteOffState but that does not mean the note event is finished, which is only triggered by the expiration of the amp EG object.

// --- note off
bool SynthVoice::doNoteOff(midiEvent& event)
{
// --- lookup MIDI -> pitch value
double midiPitch = midiNoteNumberToOscFrequency(event.midiData1);
MIDINoteEvent noteEvent(midiPitch, event.midiData1, event.midiData2);
// --- components
lfo->doNoteOff(noteEvent);
ampEG->doNoteOff(noteEvent);
wtOsc->doNoteOff(noteEvent);
filter->doNoteOff(noteEvent);
dca->doNoteOff(noteEvent);
// --- set our current state; the ampEG will determine the final state
voiceNoteState = voiceState::kNoteOffState;
return true;
}
// ---

That's all there is to coding the SynthVoice object for note rendering operation. You will see that the example voice objects really only differ by including multiple modules: 2 LFOs, 3 EGs, 2 filters, 4 oscillators, etc... but the code follows the same pattern.


synthlab_4.png