SynthLab SDK
SynthEngine Example

The fabulous news here is that you only barely need to modify the template engine code to get the final engine that is used across all SynthLab examples. You can use the SynthEngine template code verbatim, but with the addition of update code (to handle unison mode) and MIDI message decoding. In this section we will wrap up the engine programming guide by adding the last bits of code. For this engine, we will use the following:

  • for MONO mode, we will use the first voice in the array for all MIDI note messages
  • for UNISON mode, we will use the first four voices in the array with slightly different detuning, and oscillator start phases for a thick unison sound
  • for POLY mode, we will use the voice stealing heuristics detailed in the synth book and added in the MIDI code in this section

Update Phase

We need to modify the setParameters() function to add the unison mode detuning. Check out the bit of code added here that applies the voice-level UNISON mode detuning to the first four voice objects in the array.

// --- 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;
for (unsigned int i = 0; i < MAX_VOICES; i++)
{
// --- needed for modules
synthVoices[i]->update();
if (synthVoices[i]->isVoiceActive())
{
// -- note the special handling for unison mode - you could probably
// clean this up
if (parameters->synthModeIndex == enumToInt(SynthMode::kUnison) ||
parameters->synthModeIndex == enumToInt(SynthMode::kUnisonLegato))
{
if (i == 0)
{
parameters->voiceParameters->unisonDetuneCents = 0.0;
parameters->voiceParameters->unisonStartPhase = 0.0;
}
else if (i == 1)
{
parameters->voiceParameters->unisonDetuneCents = parameters->globalUnisonDetune_Cents;
parameters->voiceParameters->unisonStartPhase = 13.0;
}
else if (i == 2)
{
parameters->voiceParameters->unisonDetuneCents = -parameters->globalUnisonDetune_Cents;
parameters->voiceParameters->unisonStartPhase = -13.0;
}
else if (i == 3)
{
parameters->voiceParameters->unisonDetuneCents = 0.707*parameters->globalUnisonDetune_Cents;
parameters->voiceParameters->unisonStartPhase = 37.0;
}
}
else
{
parameters->voiceParameters->unisonStartPhase = 0.0;
parameters->voiceParameters->unisonDetuneCents = 0.0;
}
}
}
}
// ---

Processing MIDI

The code here will make this the longest function in our example and will save you some time in dealing with basic MIDI events.

The code here adds the following functionailty to the template code:

  1. decodes the MIDI message; note events are separated from CC events which are separeated from all other events
  2. finds a voice to send the note-on or note-off message to; this depenes on the mode of operation
  • note events (on and off) are sent to the voices for processing
  • CC events are saved in the MIDI input CC data array
  • other global MIDI data is stored; a basic set of data is defined and you may add as much more as you like
  • for MONO operation, the first voice in the array is used for all note events, no exceptions
  • for UNISON operation, the first four voices in the array are used, and voice-level detune, and oscillator start phases may be optionally applied
  • for POLY operation, the engine tries to find a free voice; if none are avialable it steals a voice (note that this requires extra code in the voice object; see the example synths for more information)
// --- note on
bool SynthEngine::processMIDIEvent(midiEvent& event)
{
if (parameters->enableMIDINoteEvents && event.midiMessage == NOTE_ON)
{
// --- set current MIDI data
midiInputData->setGlobalMIDIData(kCurrentMIDINoteNumber, event.midiData1);
midiInputData->setGlobalMIDIData(kCurrentMIDINoteVelocity, event.midiData2);
// --- mono mode
if (parameters->synthModeIndex == enumToInt(SynthMode::kMono) ||
parameters->synthModeIndex == enumToInt(SynthMode::kLegato))
{
// --- just use voice 0 and do the note EG variables will handle the rest
synthVoices[0]->processMIDIEvent(event);
}
else if (parameters->synthModeIndex == enumToInt(SynthMode::kUnison) ||
parameters->synthModeIndex == enumToInt(SynthMode::kUnisonLegato))
{
// --- UNISON mode is heavily dependent on the manufacturer's
// implementation and decision
// for the synth core, we will use 4 voices
synthVoices[0]->processMIDIEvent(event);
synthVoices[1]->processMIDIEvent(event);
synthVoices[2]->processMIDIEvent(event);
synthVoices[3]->processMIDIEvent(event);
}
else if (parameters->synthModeIndex == enumToInt(SynthMode::kPoly))
{
// --- get index of the next available voice (for note on events)
int voiceIndex = getFreeVoiceIndex();
if (voiceIndex < 0)
{
voiceIndex = getVoiceIndexToSteal();
}
// --- trigger next available note
if (voiceIndex >= 0)
{
synthVoices[voiceIndex]->processMIDIEvent(event);
}
// --- increment all timestamps for note-on voices
for (int i = 0; i < MAX_VOICES; i++)
{
if (synthVoices[i]->isVoiceActive())
synthVoices[i]->incrementTimestamp();
}
}
// --- need to store these for things like portamento
// --- store global data for note ON event: set previous note-on data
midiInputData->setGlobalMIDIData(kLastMIDINoteNumber, event.midiData1);
midiInputData->setGlobalMIDIData(kLastMIDINoteVelocity, event.midiData2);
}
else if (parameters->enableMIDINoteEvents && event.midiMessage == NOTE_OFF)
{
// --- for mono, we only use one voice, number [0]
if (parameters->synthModeIndex == enumToInt(SynthMode::kMono) ||
parameters->synthModeIndex == enumToInt(SynthMode::kLegato))
{
if (synthVoices[0]->isVoiceActive())
{
synthVoices[0]->processMIDIEvent(event);
return true;
}
}
else if (parameters->synthModeIndex == enumToInt(SynthMode::kPoly))
{
// --- find the note with this MIDI number (this implies that note numbers and voices are exclusive to each other)
int voiceIndex = getActiveVoiceIndexInNoteOn(event.midiData1);
if (voiceIndex < 0)
{
voiceIndex = getStealingVoiceIndexInNoteOn(event.midiData1);
}
if (voiceIndex >= 0)
{
synthVoices[voiceIndex]->processMIDIEvent(event);
}
return true;
}
else if (parameters->synthModeIndex == enumToInt(SynthMode::kUnison) ||
parameters->synthModeIndex == enumToInt(SynthMode::kUnisonLegato))
{
// --- this will get complicated with voice stealing.
synthVoices[0]->processMIDIEvent(event);
synthVoices[1]->processMIDIEvent(event);
synthVoices[2]->processMIDIEvent(event);
synthVoices[3]->processMIDIEvent(event);
return true;
}
}
else // --- non-note stuff here!
{
// --- store the data in our arrays; sub-components have access to all data via safe IMIDIData pointer
if (event.midiMessage == PITCH_BEND)
{
midiInputData->setGlobalMIDIData(kMIDIPitchBendDataLSB, event.midiData1);
midiInputData->setGlobalMIDIData(kMIDIPitchBendDataMSB, event.midiData2);
}
if (event.midiMessage == CONTROL_CHANGE)
{
// --- store CC event in globally shared array
midiInputData->setCCMIDIData(event.midiData1, event.midiData2);
}
// --- NOTE: this synth has GUI controls for items that may also be transmitted via SYSEX-MIDI
//
// If you want your synth plugin to support these messages, you need to add the code here
// to handle the MIDI. See any website with MIDI specs details or
// http://www.somascape.org/midi/tech/spec.html
}
return true;
}
//

That's it! The engine object is ready to be used in a plugin framework. In the next section we will look at the client-side code that instantiates and deals with the engine to complete the synth projects.


synthlab_4.png