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.
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:
MinSynth();
~MinSynth() {}
bool reset(double _sampleRate);
bool update();
const std::shared_ptr<SynthLab::AudioBuffer> render(uint32_t samplesToProcess = 1);
protected:
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()
{
nullptr,
nullptr,
64));
nullptr,
nullptr,
64));
nullptr,
nullptr,
nullptr,
64));
nullptr,
nullptr,
64));
nullptr,
nullptr,
64));
}
bool MinSynth::reset(double _sampleRate)
{
lfo->reset(_sampleRate);
ampEG->reset(_sampleRate);
wtOsc->reset(_sampleRate);
filter->reset(_sampleRate);
dca->reset(_sampleRate);
update();
return true;
}
bool MinSynth::update()
{
std::shared_ptr<SynthLab::LFOParameters> lfoParameters = lfo->getParameters();
if (lfoParameters)
{
lfoParameters->waveformIndex = 8;
lfoParameters->modKnobValue[SynthLab::MOD_KNOB_A] = 0.25;
}
std::shared_ptr<SynthLab::EGParameters> egParameters = ampEG->getParameters();
if (egParameters)
{
egParameters->attackTime_mSec = 50.0;
egParameters->decayTime_mSec = 100.0;
egParameters->sustainLevel = 0.707;
egParameters->releaseTime_mSec = 1000.0;
}
std::shared_ptr<SynthLab::WTOscParameters> wtoParameters = wtOsc->getParameters();
if (wtoParameters)
{
wtoParameters->waveIndex = 1;
wtoParameters->outputAmplitude_dB = -3.0;
}
std::shared_ptr<SynthLab::FilterParameters> filterParameters = filter->getParameters();
if (filterParameters)
{
filterParameters->filterIndex = 4;
filterParameters->fc = 880.0;
filterParameters->Q = 5;
filterParameters->filterOutputGain_dB = -2.0;
filterParameters->modKnobValue[SynthLab::MOD_KNOB_D] = 1.0;
}
std::shared_ptr<SynthLab::DCAParameters> dcaParameters = dca->getParameters();
if (dcaParameters)
{
dcaParameters->gainValue_dB = +3.0;
dcaParameters->panValue = 0.25;
}
return true;
}
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)
{
lfo->render(samplesToProcess);
double lfoNormOut = lfo->getModulationOutput()->getModValue(SynthLab::kLFONormalOutput);
wtOsc->getModulationInput()->setModValue(SynthLab::kBipolarMod, lfoNormOut);
filter->getModulationInput()->setModValue(SynthLab::kBipolarMod, lfoNormOut);
ampEG->render(samplesToProcess);
double egNormOut = ampEG->getModulationOutput()->getModValue(SynthLab::kEGNormalOutput);
dca->getModulationInput()->setModValue(SynthLab::kEGMod, egNormOut);
wtOsc->render(samplesToProcess);
filter->getAudioBuffers(),
SynthLab::STEREO_TO_STEREO, 64);
filter->render(samplesToProcess);
dca->getAudioBuffers(),
SynthLab::STEREO_TO_STEREO, 64);
dca->render(samplesToProcess);
return dca->getAudioBuffers();
}
noteOn( ) and noteOff( )
These two functions are trivial as they simply forward the MIDI event data to the underlying components.
{
lfo->doNoteOn(noteEvent);
ampEG->doNoteOn(noteEvent);
wtOsc->doNoteOn(noteEvent);
filter->doNoteOn(noteEvent);
dca->doNoteOn(noteEvent);
return true;
}
{
lfo->doNoteOff(noteEvent);
ampEG->doNoteOff(noteEvent);
wtOsc->doNoteOff(noteEvent);
filter->doNoteOff(noteEvent);
dca->doNoteOff(noteEvent);
return true;
}
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.
MinSynth miniSynth;
miniSynth.reset(44100.0);
miniSynth.update();
Use the note-on and note-off handlers as normal, from your plugin framework MIDI handler function.
miniSynth.doNoteOn(midiEvent);
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.
update( );
const std::shared_ptr<SynthLab::AudioBuffer> synthBuffer = miniSynth.render(64);
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];
}
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!