SynthLab SDK
Create an Additive Oscillator Core

In this module, we'll create an oscillator core object from scratch using the general purpose Oscillator object. We will use the same simple method of generating sinusoids as we used in the LFO but will use Chebychev polynomials to add harmonics in an additive synth manner. The mod knobs A, B, C, and D will control the amplitudes of the additive oscillator's first 4 harmonics above the fundamental and we will use them as if the user were manipulating a GUI (which of course you may add later using your plugin framework).

The first set of Chebychev polynomials is shown below. We will use T2, T3, T4 adn T5 as waveshapers to generate the harmonics from the fundemantal. I am not going for efficiency here, so please don't complain on your Forums or send me hate-mail. There is a method of using the results of one polynomail to generate the next and so you are welcome to research that and change the code to reflect it.


chyeby.png


There is a general purpose SynthModule object named Oscillator that we will use, along with its companion OscParameters data structure. It will not use wavetables or any databases. We will develop it in stages. Here are a couple of the kinds of additive waveforms it produces under our test conditions:


addosc_0.png


SynthLab Core Template

The SDK contains a pair of source files that are a template for new ModuleCore development and are essentially blank starting points for any kind of core. The files are named synthlabcore.h and synthlabcore.cpp and you need to copy those files into your project and rename them (if you like) as you may be re-using these files many times in your projects.

For the demo project here, I have renamed them addosccore.h and addosccore.cpp and using a text editor, I changed the class name to AddOscCore in both files.

You will need to add the following files to your project:

  • oscillator.h and oscillator.cpp
  • synthlabcore.h and synthlabcore.cpp (rename if you like)

The template files have everything you need to get started.

SynthLab Core .h File

We need to add a timebase and a MIDI pitch variable to the oscillator and we will use the SynthClock modulo counter. Add a static instance to the class declaration. The MIDI pitch value will be set during the note-on handler and will control the timebase phase increment value. Next, copy and add the Chebychev waveshaper function:

// --- member declaration in the class definition
protected:
double sampleRate = 1.0;
double midiPitch = 0.0; //< the midi pitch
// --- timebase
SynthClock oscClock;
enum { ChebyT2, ChebyT3, ChebyT4, ChebyT5 };
// --- NOT the most efficient implementation!!!!!
inline double doChebyWaveshaper(double xn, uint32_t order)
{
double xSquared = xn*xn;
double xCubed = xn*xSquared;
double x4th = xn*xCubed;
double x5th = xn*x4th;
if (order == ChebyT2)
return (2.0 * xSquared) - 1.0;
else if (order == ChebyT3)
return (4.0 * xCubed) - (3.0 * xn);
else if (order == ChebyT4)
return (8.0 * x4th) - (8.0 * xSquared) + 1.0;
else if (order == ChebyT5)
return (16.0 * x5th) - (20.0 * xCubed) + (5.0 * xn);
return xn; // not found
}
//

SynthLab Core .cpp File

Lets hop into the Constructor and setup the waveforms. For this oscillator, we will have 2 waveforms (or patches).

  1. sine
  2. additive

The mod knob labels (which you may use to remember the functionality) are: A. "Harm_2" B. "Harm_3" C. "Harm_4" D. "Harm_5"

Now we will step through the functions in the .cpp file, just as we did for the LFO, adding the code as needed.

Constructor: Waveform Names
Open the .cpp file and change the module type, add a name and alter the waveforms and mod knobs per the plan.

AddOscCore::AddOscCore()
{
moduleType = OSC_MODULE;
moduleName = "Add Osc";
// --- setup your module string here; use empty_string.c_str() for blank (empty) strings
coreData.moduleStrings[0] = "sine"; coreData.moduleStrings[8] = empty_string.c_str();
coreData.moduleStrings[1] = "additive"; coreData.moduleStrings[9] = empty_string.c_str();
coreData.moduleStrings[2] = empty_string.c_str(); coreData.moduleStrings[10] = empty_string.c_str();
coreData.moduleStrings[3] = empty_string.c_str(); coreData.moduleStrings[11] = empty_string.c_str();
coreData.moduleStrings[4] = empty_string.c_str(); coreData.moduleStrings[12] = empty_string.c_str();
coreData.moduleStrings[5] = empty_string.c_str(); coreData.moduleStrings[13] = empty_string.c_str();
coreData.moduleStrings[6] = empty_string.c_str(); coreData.moduleStrings[14] = empty_string.c_str();
coreData.moduleStrings[7] = empty_string.c_str(); coreData.moduleStrings[15] = empty_string.c_str();
// --- modulation control knobs
coreData.modKnobStrings[MOD_KNOB_A] = "Harm_2";
coreData.modKnobStrings[MOD_KNOB_B] = "Harm_3";
coreData.modKnobStrings[MOD_KNOB_C] = "Harm_4";
coreData.modKnobStrings[MOD_KNOB_D] = "Harm_5";
}
//
@endcore
__reset( )__ <br>
For reset, ALWAYS remember to save the sample rate that is required for numerous modules and functions.
- after that, we only need to rest the timebase back to its starting phase of 0.0 (the default argument value)
- you can shift the phase of the clock's starting point with the argument [0.0, 1.0].
@code
bool AddOscCore::reset(CoreProcData& processInfo)
{
// --- save sample rate
sampleRate = processInfo.sampleRate;
// --- RESET OPERATIONS HERE
oscClock.reset();
return true;
}
//

update( )
For the update phase, we only need to transfer the MIDI pitch value that arrived during the note-on event to set the oscillator timbase frequency. We do this here (rather than doNoteOn) because we will change this in the next module to add pitch modulation and GUI control manipulation.

bool AddOscCore::update(CoreProcData& processInfo)
{
// --- Get Parameters (will use in next module)
//
OscParameters* parameters = static_cast<OscParameters*>(processInfo.moduleParameters);
// --- UPDATE OPERATIONS HERE
double pitchShift = 1.0; // prepare for modulation later
// --- calculate the moduated pitch value
double oscillatorFrequency = midiPitch*pitchShift;
// --- BOUND the value to our range - in theory, we would bound this to any NYQUIST
boundValue(oscillatorFrequency, OSC_FMIN, OSC_FMAX);
// --- phase inc = fo/fs this sets it
oscClock.setFrequency(oscillatorFrequency, sampleRate);
return true;
}
//

render( )
For the render function, we need to grab the audio output buffers and go into a loop to generate the block of samples requested.

  • when the user selects the "additive" patch, note how I bring down the overall amplitude of the waveform to make up for the fact that the user could max out all of the harmonic amplitude controls at 1.0
  • notice how the mod knob values are transferred from the parameter structure; this would come from a GUI control

Step 1: get the audio buffer pointers and go into the block processing loop:

bool AddOscCore::render(CoreProcData& processInfo)
{
// --- Get Parameters
//
OscParameters* parameters = static_cast<OscParameters*>(processInfo.moduleParameters);
// --- get output buffers
float* leftOutBuffer = processInfo.outputBuffers[LEFT_CHANNEL];
float* rightOutBuffer = processInfo.outputBuffers[RIGHT_CHANNEL];
// --- render additive signal
for (uint32_t i = 0; i < processInfo.samplesToProcess; i++)
{
//

Step 2: Start the decision tree that will decode the waveform index and generate the fundamental wavform which is needed for the others. Refer back to the LFO tutorial regarding the timebase and 2pi multiplication.

double oscOutput = 0.0;
// --- generate the fundamental
double fund = sin(oscClock.mcounter * kTwoPi);
if (parameters->waveIndex == 0) // fundamental only
oscOutput = fund;
else if (parameters->waveIndex == 1)// additive
{
//

Step 3: For the additive code, I am first grabbing the harmonic amplitudes fromt the parameter structure; we're using the mod knobs to test with. Then, create the output for this waveform.

// --- get the harmonic amplitudes from mod-knobs (or other GUI controls)
// note that the value is already on the range we desire, from 0.0 to 1.0 so there
// is no need to use the helper functions
double h2Amp = parameters->modKnobValue[MOD_KNOB_A];
double h3Amp = parameters->modKnobValue[MOD_KNOB_B];
double h4Amp = parameters->modKnobValue[MOD_KNOB_C];
double h5Amp = parameters->modKnobValue[MOD_KNOB_D];
// --- additive signal
oscOutput = fund + (h2Amp * doChebyWaveshaper(fund, ChebyT2))
+ (h3Amp * doChebyWaveshaper(fund, ChebyT3))
+ (h4Amp * doChebyWaveshaper(fund, ChebyT4))
+ (h5Amp * doChebyWaveshaper(fund, ChebyT5));
// --- scale by -12dB to prevent clipping
oscOutput *= 0.25;
}
//

Now that the output is rendered, write to the output buffers, outside of the decision tree but inside of the block processing loop, and then advance the clock.

// --- write out
leftOutBuffer[i] = oscOutput;
rightOutBuffer[i] = oscOutput;
// --- advance clock and wrap if needed
oscClock.advanceWrapClock();
}
return true;
}
//

doNoteOn( )
For this message handler we only need to store the MIDI pitch that arrives in the MIDI event structure and reset the oscillator timebase in the case that this is not the first time a note has been played. You may move this reset() function call to the note-off handler if that makes more sense to you. Note that the MIDI structure also includes the MIDI note number and velocity values [0, 127].

//
bool AddOscCore::doNoteOn(CoreProcData& processInfo)
{
// --- parameters
midiPitch = processInfo.noteEvent.midiPitch;
// --- reset timebase
oscClock.reset();
return true;
}
//

Modify the Oscillator Object

Now that the core is complete, we need to modify the Oscillator object to load this core at construction time. This only requires modifying the constructor to load the core. A comment has been left in the Oscillator constructor for you.

// --- need the core.h file
#include "addosccore.h"
Oscillator::Oscillator(std::shared_ptr<MidiInputData> _midiInputData,
std::shared_ptr<OscParameters> _parameters,
uint32_t blockSize)
: SynthModule(_midiInputData)
, parameters(_parameters)
{
// --- standalone ONLY: parameters
if(!parameters)
parameters.reset(new OscParameters);
// --- create our audio buffers
audioBuffers.reset(new SynthProcessInfo(OSC_INPUTS, OSC_OUTPUTS, blockSize));
// --- setup the core processing structure for dynamic cores
coreProcessData.inputBuffers = getAudioBuffers()->getInputBuffers();
coreProcessData.outputBuffers = getAudioBuffers()->getOutputBuffers();
coreProcessData.modulationInputs = modulationInput->getModulatorPtr();
coreProcessData.modulationOutputs = modulationOutput->getModulatorPtr();
coreProcessData.moduleParameters = parameters.get();
coreProcessData.midiInputData = midiInputData->getIMIDIInputData();
// --- setup the cores
// Core 0:
std::shared_ptr<AddOscCore> additiveCore = std::make_shared<AddOscCore>();
addModuleCore(std::static_pointer_cast<ModuleCore>(additiveCore));
// --- selects first core
selectDefaultModuleCore();
}
/* C-TOR */

Testing the Oscillator and Core

You have several options here and if you went through the MinSynth tutorial then you already have an obeject to test with. Regardless of how you test it, you will still need to follow the same steps. And remember that for testing I am using the mod knobs as fake GUI parameter controls for the harmonic amplitudes (another reason that they exist). Testing follows the same pattern as with the individual standalone objects, or the MinSynth voice object.

Create the Oscillator Instance

#include "oscillator.h"
// --- an additive oscillator
std::unique_ptr<SynthLab::Oscillator> addOsc = nullptr;
uint32_t blockSize = 64; //< make this 1 for processing single samples if that is easier for you
// --- create the smart pointer (this "reset" function has no relationship to the SynthModule::reset() method)
addOsc.reset(new SynthLab::Oscillator(nullptr, /* MIDI input data */
nullptr, /* parameters */
blockSize)); /* process blocks (block size = 1 for processing samples instead)*/
// --- reset the object with the SynthModule method:
addOsc->reset(44100.0); //< get fs from your framework
// --- select Core [0]
addOsc->selectModuleCore(0);
//

Update the object for Waveform[0]
There is no need to manipulate the mod knobs yet.

// --- setup the first test of waveform 0 (fundamental)
// --- get parameters
std::shared_ptr<SynthLab::OscParameters> oscParameters = addOsc->getParameters();
if (oscParameters) // should never fail
{
// --- set the variable
oscParameters->waveIndex = 0;
// --- call the update function (only needs to be done once per render cycle)
addOsc->update();
}
//

Send a Note-On Event
You can get this from a live MIDI source or fake it to test as done here.

// --- prepare a MIDI event for note-on
midiEvent.midiNoteNumber = 60;
midiEvent.midiNoteVelocity = 127;
// --- send the message
addOsc->doNoteOn(midiEvent);
//

Render the Oscillator
Render the oscillator and loop over the audio block for the output values. Remember that we set the blockSize variable at construction time.

// --- render output
addOsc->render();
// --- get buffers
float* leftOutBuffer = addOsc->getAudioBuffers()->getOutputBuffer(SynthLab::LEFT_CHANNEL);
float* rightOutBuffer = addOsc->getAudioBuffers()->getOutputBuffer(SynthLab::RIGHT_CHANNEL);
// --- loop
for (uint32_t i = 0; i < blockSize; i++)
{
float leftSample = leftOutBuffer[i];
float rightSample = rightOutBuffer[i];
// --- send samples to your output buffer
}
//

Update the object for Waveform [1]
After testing and hearing a bland sinusoid, try the 2nd waveform that is additive. Here, I am using the mod knobs for harmonic amplitudes.

std::shared_ptr<SynthLab::OscParameters> oscParameters = addOsc->getParameters();
if (oscParameters) // should never fail
{
// --- set the variable
oscParameters->waveIndex = 1; //<--- additive
// --- adjust harmonic amplitudes here
oscParameters->modKnobValue[SynthLab::MOD_KNOB_A] = 0.75;
oscParameters->modKnobValue[SynthLab::MOD_KNOB_B] = 0.5;
oscParameters->modKnobValue[SynthLab::MOD_KNOB_C] = 0.25;
oscParameters->modKnobValue[SynthLab::MOD_KNOB_D] = 0.5;
// --- call the update function (only needs to be done once per render cycle)
addOsc->update();
}
//

Testing Waveform [1]
Test this oscillator waveform and you should see something like Figure 2 in your oscilloscope and spectrum analyzer showing the fundamental plus four harmonics with various amplitudes.


addosc_1.png


Go back and adjust the mod knobs to see the image in Figure 3.:

  • A = 1.0
  • B = 0.0
  • C = 0.0
  • D = 1.0


    addosc_2.png


Add Pitch Modulation Code

In the next section, we will add the pitch modulation code. You may test the oscillator connected to your LFO in the MinSynth object if you've been following the tutorials.


synthlab_4.png