SynthLab SDK
SynthVoice Template Code

The SynthVoice template is found in the synthvoice.h and synthvoice.cpp files in the SynthLab_SDK/source/ folder.

SynthVoice Operational Phases

The SynthVoice 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 in the abstract base class (at the end of this section) as these are the main interface functions that the SynthEngine will be calling. In this discussion, I will assume that your SynthVoice object either maintains and uses a set of SynthModules (as in the Minimal Standalone Synth MinSynth) or some other resources for rendering note-events, and that these resources will respond to MIDI events in some way.

Construction Phase

The SynthVoice tempate constructor contains argements that are shared pointers to global synth resources. In the SynthLab paradigm, the SynthEngine will create and own one of each of the resources, then pass a shared pointer to its SynthVoices during construction time.

  • MIDI input (and output) data structures
  • wavetables
  • PCM samples
  • shared voice parameters

Constructor
The constructor may be called in stanalone mode, where all of the shared pointer arguments are nullptrs. In this case, the object will synthesize its own local MIDI and parameter data structures. If using SynthModules, you may choose to have the voice object create these resources, or pass those nullptrs into the modules during he constructor sequence and they will synthesize their own.

// --- note use of shared pointers to resources
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 = 64);
//

Member Module Construction
The constructor __may__be called in stanalone mode, where all of the shared pointer arguments are nullptr. In this case, the object will synthesize its own local version, but this will defeat the purpose of the shared pointers if you use any more than one voice object. We will see examples of SynthModule member construction in the next section. The template constructor code is simple, creating missing resources if needed and setting up the mix buffers to hold the audio output data.

// --- construction phase
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)
{
// --- create if missing, standalone mode
if (!midiInputData)
midiInputData.reset(new (MidiInputData));
if (!midiInputData)
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>();
// --- create your member modules here
// --- create our audio buffers
mixBuffers.reset(new SynthProcessInfo(NO_CHANNELS, STEREO_CHANNELS, blockSize));
// --- more inits here
//
}
// ---

Reset & Intialize Phase

For the voice object the reset phase has two parts. The first is the standard object reset that you've seen in the SynthModule and ModuleCore objects. The second is a separate function named initialize that is called after construction and reset have completed. This secondary intialization function brings path information to the voices. For the SynthLab example synths, this is a path to the folder that contains the DLL or plugin bundle and are listed in PCM Sample Files & Database. However, you may changed this at the engine level, and pass whatever path you like back to the voices, which then trickle them down into the SynthModule memhers. Generally the paths are needed for retrieveing PCM samples from WAV files, but you may use it for other storage/retrieval.

The reset() and initialize() functions are shown below. Note that the path arrives in a const char* datatype, required for loading dynamic ModuleCores (see Creating SynthLab-DM Modules) to survive function calls across the thunk barrier. In the template file, these functions are only sparsely populated. Take notice of:

  1. the currentMIDINote number keeps track of the voice's currently playing MIDI note number, and is used for voice stealing (in the case that the note number plays a role in the stealing heuristic) as well as finding a currently playing voice to receive a note-off message; when this integer is set to -1, it indicates that no note-events have occurred yet.
// --- reset; save sample rate
bool SynthVoice::reset(double _sampleRate)
{
sampleRate = _sampleRate;
currentMIDINote = -1;
// --- initialize sub components for new sample rate
return true;
}
// --- Initialize the voice sub-components; this really only applies to PCM oscillators that need DLL path
bool SynthVoice::initialize(const char* dllPath)
{
// --- initialize all sub components that need the DLL path here
// it is up to you to find that path if you need it
return true;
}
//

Update Phase

The update() function will be called prior to the SynthVoice::render function and for the tempate code, it is empty as there is nothing to do that invovles the template object's few member variables that pertain to voice stealing and voice state. And, since the voice's SynthModule components will be updated later, just prior to render, this function is sparse in the example project code as well. But, it will be called just prior to the SynthVoice::render method if you have any per-block variables that need to be updated or initialized.

// --- update prior to synth voice render
bool SynthVoice::update()
{
// --- update your sub-components here based on the GUI parameters (if you used them)
return true;
}

Render Phase

Referring back to the MinSynth C++ object (see Minimal Standalone Synth) you can see that the render() function is perhaps the most important as this is where the modules are manipulated to fill audio buffers with freshly synthesized data. The template code below follows the same pattern as the example synths:

  1. flush the mix buffers old data from the last render() call
  2. render audio: the missing code here would normally
  • render the modulators
  • render the mod matrix (if desired)
  • render the oscillator's into the mix buffer
  • push the audio data down the chain of processing modules and then into the DCA
  1. copy the mix buffer outputs (or DCA output buffers) to the SynthProcessInfo structure's buffers
  2. check to see if the voice note-event has expired

The last item in the list is very important. As the synth book discusses, the final amp EG play a cricital role in the lifecycle of a note event. The last part of the render() function contains the code to detect if the EG has expired; if so the note event is done, if not more render operations will necessarily be called.

// --- voice render template code
bool SynthVoice::render(SynthProcessInfo& synthProcessInfo)
{
// --- modules require knowing the block size to render
uint32_t samplesToProcess = synthProcessInfo.getSamplesInBlock();
// --- clear for accumulation
mixBuffers->flushBuffers();
// --- render your objects into the mix buffer output
// --- THE SYNTHESIS IS HERE --- //
// --- to mains
copyOutputToOutput(mixBuffers, synthProcessInfo, STEREO_TO_STEREO, samplesToProcess);
// --- check for note off condition
if (voiceIsActive)
{
// --- check to see if voice has expired (final output EG has terminated)
/* if(EG is complete)
voiceIsActive = false; */
}
return true;
}
//

Render Helper Functions

The render operation will be manipulating AudioBuffer objects that are the outputs of oscillators and the inputs and outputs of processors. The template voice conatins an AudioBuffer to be used as a temporary mix buffer, or splitting buffer, or however you wish. The template code contains two helper functions for moving data to the mix buffers or to the final output buffers for the Block Audio Processing SynthProcessInfo structure. One of the functions accumulates into the destination, while the other writes over the audio in the destination.

// --- acculumulate: used when mixing the outputs of parallel oscillators
void SynthVoice::accumulateToMixBuffer(std::shared_ptr<AudioBuffer> oscBuffers, uint32_t samplesInBlock, double scaling)
// --- overwrite: for moving audio serially between modules
void SynthVoice::writeToMixBuffer(std::shared_ptr<AudioBuffer> oscBuffers, uint32_t samplesInBlock, double scaling = 1.0)
//

Note On and Note Off Phase

The SynthVoice shares identically prototyped functions as the SynthModules for the note-on and note-off message handlers.

doNoteOn()
In this function, the MIDI note pitch is calculated for the incoming MIDI note number and the voice state member variables are reset in a ready-to-render state. In this function, you will call the same doNoteOn() functions on all of the SynthModules. For oscillators, this is also where you setup glide (portamento) modulation because the GlideModulator object will need to be started prior to rendering audio from the oscillator.

// --- note on
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);
// --- send NOTE ON message to your sub objects here
// --- call doNoteOn() with midiNoteEvents (a sub-set of midiEvents)
// --- 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 majority of this function is for calling the doNoteOff() handlers on the SynthModule objects. Not much else happens here. However it is critical to call the amp EG's note off handler from this function as it dictates the lifecycle of the event.

// --- note off
bool SynthVoice::doNoteOff(midiEvent& event)
{
// --- lookup MIDI -> pitch value
double midiPitch = midiNoteNumberToOscFrequency(event.midiData1);
MIDINoteEvent noteEvent(midiPitch, event.midiData1, event.midiData2);
// --- send NOTE OFF message to your sub objects here
// --- set our current state; the ampEG will determine the final state
voiceNoteState = voiceState::kNoteOffState;
return true;
}
// ---

processMIDIEvent( )
The SynthEngine will normally be the MIDI message decoder. In SynthLab, the engine splits out note-on and note-off messages and directs them to a SynthVoice object that it has chosen, either a dormant object for a new note-on event, or the currently playing voice for a note-off event. The engine will call the voice's processMIDIEvent passing note-on or note-off messages. The template function simply decodes the message and calls the proper handler on the object. For non-note events, the engine places all desired MIDI information, from CCs to global tuning values, into its MIDI input data arrays that are shared with every voice and every SynthModule so that every part of the synth has access to the MIDI input data via the shared pointer used at construction. This means that you do not ever need to pass non-note information around the objects as they all have instant access to it. In the template fuction below, note that the clearTimestamp() function is called to reset the voice timestamps; this is used in the voice stealing heuristics in the synth book.

// --- MIDI proc function
bool SynthVoice::processMIDIEvent(midiEvent& event)
{
// --- the voice only needs to process note on and off
// Other MIDI info such as CC can be found in global midi table via our midiData interface
if (event.midiMessage == NOTE_ON)
{
// --- clear timestamp
clearTimestamp();
// --- call the subfunction
doNoteOn(event);
}
else if (event.midiMessage == NOTE_OFF)
{
// --- call the subfunction
doNoteOff(event);
}
return true;
}
// ---

This completes the tour of the template object. In the next section, we will convert the MinSynth C++ object into a SynthVoice, to be used with a mono SynthEngine we will design and implement in the SynthEngine Object.

Abstract SynthVoice Base Class

If you want the functionality of the voice within the SynthEngine paradigm, but you want to write your own implementations, use this abstract base class version to subclass your object. You will need to implement the abstract virtual functions. The remaining functions and variables handle queries from the SynthEngine's MIDI message handler. If you do not intend on using the Engine's MIDI messaging, the you may delete them.

class SynthVoice
{
public:
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 = 64);
virtual ~SynthVoice() {}
// --- abstract base class overrides
virtual bool reset(double _sampleRate) = 0;
virtual bool update() = 0;
virtual bool render(SynthProcessInfo& synthProcessInfo) = 0;
virtual bool processMIDIEvent(midiEvent& event) = 0;
virtual bool initialize(const char* dllPath = nullptr) = 0;
virtual bool doNoteOn(midiEvent& event) = 0;
virtual bool doNoteOff(midiEvent& event) = 0;
// --- functions that the engine calls, but refer only to local attributes
bool isVoiceActive() { return voiceIsActive; }
voiceState getVoiceState() { return voiceNoteState; }
uint32_t getTimestamp() { return timestamp; } // < the higher the value, the longer the voice has been running
void incrementTimestamp() { timestamp++; } // < increment timestamp when a new note is triggered
void clearTimestamp() { timestamp = 0; } // < reset timestamp after voice is turned off
unsigned int getMIDINoteNumber() { return voiceMIDIEvent.midiData1; } // < note is data byte 1, velocity is byte 2
unsigned int getStealMIDINoteNumber() { return voiceStealMIDIEvent.midiData1; } // < note is data byte 1, velocity is byte 2
bool voiceIsStealing() { return stealPending; }
protected:
// --- standalone operation only
std::shared_ptr<SynthVoiceParameters> parameters = nullptr;
// --- voice timestamp, for knowing the age of a voice
uint32_t timestamp = 0;
int32_t currentMIDINote = -1;
// --- note message state
voiceState voiceNoteState = voiceState::kNoteOffState;
// --- per-voice stuff
bool voiceIsActive = false;
midiEvent voiceMIDIEvent;
// --- for voice stealing
bool stealPending = false;
midiEvent voiceStealMIDIEvent;
};
//


synthlab_4.png