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.
void SynthEngine::setParameters(std::shared_ptr<SynthEngineParameters>& _parameters)
{
parameters = _parameters;
parameters->voiceParameters->synthModeIndex = parameters->synthModeIndex;
{
synthVoices[i]->update();
if (synthVoices[i]->isVoiceActive())
{
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:
- decodes the MIDI message; note events are separated from CC events which are separeated from all other events
- 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)
bool SynthEngine::processMIDIEvent(midiEvent& event)
{
if (parameters->enableMIDINoteEvents && event.midiMessage == NOTE_ON)
{
midiInputData->setGlobalMIDIData(kCurrentMIDINoteNumber, event.midiData1);
midiInputData->setGlobalMIDIData(kCurrentMIDINoteVelocity, event.midiData2);
if (parameters->synthModeIndex ==
enumToInt(SynthMode::kMono) ||
parameters->synthModeIndex ==
enumToInt(SynthMode::kLegato))
{
synthVoices[0]->processMIDIEvent(event);
}
else if (parameters->synthModeIndex ==
enumToInt(SynthMode::kUnison) ||
parameters->synthModeIndex ==
enumToInt(SynthMode::kUnisonLegato))
{
synthVoices[0]->processMIDIEvent(event);
synthVoices[1]->processMIDIEvent(event);
synthVoices[2]->processMIDIEvent(event);
synthVoices[3]->processMIDIEvent(event);
}
else if (parameters->synthModeIndex ==
enumToInt(SynthMode::kPoly))
{
int voiceIndex = getFreeVoiceIndex();
if (voiceIndex < 0)
{
voiceIndex = getVoiceIndexToSteal();
}
if (voiceIndex >= 0)
{
synthVoices[voiceIndex]->processMIDIEvent(event);
}
{
if (synthVoices[i]->isVoiceActive())
synthVoices[i]->incrementTimestamp();
}
}
midiInputData->setGlobalMIDIData(kLastMIDINoteNumber, event.midiData1);
midiInputData->setGlobalMIDIData(kLastMIDINoteVelocity, event.midiData2);
}
else if (parameters->enableMIDINoteEvents && event.midiMessage ==
NOTE_OFF)
{
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))
{
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))
{
synthVoices[0]->processMIDIEvent(event);
synthVoices[1]->processMIDIEvent(event);
synthVoices[2]->processMIDIEvent(event);
synthVoices[3]->processMIDIEvent(event);
return true;
}
}
else
{
if (event.midiMessage == PITCH_BEND)
{
midiInputData->setGlobalMIDIData(kMIDIPitchBendDataLSB, event.midiData1);
midiInputData->setGlobalMIDIData(kMIDIPitchBendDataMSB, event.midiData2);
}
if (event.midiMessage == CONTROL_CHANGE)
{
midiInputData->setCCMIDIData(event.midiData1, event.midiData2);
}
}
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.