SynthLab SDK
SynthModules & ModuleCores

SynthModules

SynthModules are the fundamental synth building blocks: LFOs, EGs, oscillators, filters and amplifiers. These can be broken down into four fundamental types:

  1. Modulators: render modulation values into their modulation output arrays in pre-defined slots; many of them render more than one output per render cycle; for block processing they only write an output on the first sample period of the block (see the synth book regarding granulized modulation updates and block processing)
  2. Oscillators: render audio outputs into their AudioBuffer objects
  3. Processors: accept audio input samples and process them into audio output samples via their AudioBuffer objects
  4. Controllers: do not render audio or modulation values but rather manipulate data flowing between modules

Each of these modules implements five (5) functions, plus a constructor, that handle the various aspects of module operation. Figure 3.2 from the synth book shows the SynthModules included in SynthLab.


modules_1.png


SynthModule I/O Ports

The I/O ports connect a module to its input and output sources.

  • Modulation inputs and outputs are arrays of double values with preset slots in the arrays for various modulator types; constants define these slot indexes such as kBipolarMod, kEGMod, and kTriggerMod
  • Audio data is transferred via the AudioBuffer object that has both input and output buffers (arrays of floats)

Input Ports

  • MIDI input is provided via the engine's shared structure; standalone objects synthesize their own MIDI input data structures
  • Modulation input values arrive in the pre-defined modulation input array; there are currently 48 modulation channels (slots for modulation values) and you may easily change this value by changing the MAX_MODULATION_CHANNELS constant
  • FM inputs are audio samples from outputs of other oscillator modules
  • Audio inputs allow you to send external audio data to the module (e.g. from a side chain or vocoder microphone input); these are declared but not used in the SynthLab example projects.

Output Ports

  • Modulation output values are written into the pre-defined slots in the modulation output array
  • FM outputs are identical to the audio output for a given module; all oscillator module output buffers may be used as FM input buffers for other modules
  • Audio output is written into a pre-prepared AudioBuffer object that always has two channels (dual-mono or stereo); note that audio samples are treated differently from modulation outputs, though they may be used as modulation sources (e.g. FM synthesis)
  • A MIDI output structure is provided but not used in the projects; this usually requires special attention in your plugin framework or APIs and not all APIs support all MIDI output messages (e.g. for a MIDI arpeggiator)

ModuleCores

I had used the module appraoch in my classes for more than a decade and generated scores of different modules for various kinds of objects over the years. Each variation on a modular idea became a separate object. For example, there were four different wavetable oscillator objects:

  • normal (static) wavetable
  • dynamic (morphing) wavetable
  • sound effect tables (one shot & loooping)
  • drum wavetables (one shot)

Each of these exposed its own set of oscillator waveforms for the user to choose from, and required setting up specific GUI controls for each oscillator. A synth's "oscillator block" was a set of these modules and ultimately resulted in four different synth plugin binaries - one each for wavetable, morphing wavetable, sound effects and drums. The same was true of other modules - I had analog and digital EG emulations, different kinds of filters (virtual analog, biquad, direct z-plane, etc...) and different LFOs, each packaged as its own module and existing in its own silo.

Around 2018 I began to implement "cores" in my personal synth project modules (and did not use them in class, fearing it would add another layer of complexity or confusion). These module cores each implemented a variation on a main module theme. Now there was only one wavetable oscillator object, but it could load different cores at runtime to change its behavior, and the cores could be mixed - one wavetable oscillator could simultaneously implement different kinds of wavetable synthesis and blend the outputs. When I moved the "guts" of the modules into their own cores, I realized that while it may seem to add complexity, in reality it allowed me to highly compartmentalize the various synth parts and functionalities. And, students could "go deep" on individual synth functions, concentrating on very specific details and only needing to edit one or two C++ source files.

When working on the 2nd edition synth book, I had a Saturday morning revelation (it's detailed in the book's Preface) and realized that I could make the cores dynamically loadable at run-time and implement them as ultra-lean and very simple DLLs (Windows) or dylibs (MacOS). This allowed me to give students Module Core projects that only required a handful of source files and let the students concentrate on very specific areas of each module as we went over the theory in class. And, these module core projects are not tied to any plugin APIs (AU, AAX, VST or RAFX) nor any frameworks like ASPiK, iPlug2, or JUCE and therefore did not require any special SDKs or libraries. There are some advantages to using this paradigm for SynthLab:

  1. Cores are simple and compact; if you want to focus working on just one type of module and not the entire synth, then you only have a few files to edit, usually just two
  2. Cores follow the idea of C++ encapsulation; each core hides the details of functionality of a concept that is encapsulated; the filter cores both generate filters but in drastically different ways (virtual analog versus biquad) but the user only sees different banks of filters to play with
  3. Cores allow for the concept of program and data "banks" in which each core implements sixteen different variations on its basic theme, each presented to the user in a list each time the core changes; each wavetable oscillator core generates a bank of waveforms, each PCM sample oscillator generates a bank of samples, each filter core creates a set of filters, etc...
  4. Cores (usually) encode a single C++ object – once debugged, that object may be easily dropped into other synth projects, plugin frameworks, and APIs because they are pure C++ and use a very simple data structure for passing arguments.

If you are using my SynthLab pre-compiled plugins, you can build a "core plugin," a plugin that is loaded into the SynthLab plugin at startup time, allowing you to customize each module for yourself. This allows you to go through the book, learning about each module and its parameters, and understanding its inner code and theory of operation. The cores are pure C++ and not tied to any plugin framework, requiring a minimal compiler setup that is so simple, you don't even need CMake. You can also build your own modules in any component flavor, and add them to the existing plugin. This means that my SynthLab plugins are dynamic, and you may modify and change their core operations to suit your own research or interest areas.

This table lists the ModuleCores for each SynthModule. Notice that most modules have less than four cores to play with, and some only have one core. There are plenty of empty cores so that you can add your own in the SynthLab-DM projects (see the homework exercises in the synth book).

SynthModule ModuleCores
SynthLFO LFOCore: all the classic waveforms
FMLFOCore: FM waveforms
EnvelopeGenerator AnalogEGCore: analog EG emulation
DXEGCore: similar to the Yamaha DX synth EGs
LinearEGCore: use as a starting point for your own EG designs; also works well as a morphing wavetable modulator
SynthFilter VAFilterCore: virtual analog filters
BQFilterCore: biquadratic filters
WTOscillator ClassicWTCore: 16 interesting waveforms
MorphWTCore: morphing wavetables
DrumWTCore: wavetables of electronic drum samples
SFXCore: one shot sound effects
FourierWTCore: waveforms using Fourier synthesis, created at load-time
PCMOscillator PCMLegacyCore: PCM samples from the 1st edition synth book
MellotronCore: samples of long analog tape loops from the original Mellotron synth
WavesliceCore: PCM samples taken as slices out of a source WAV file using Aubio
KSOscillator KSOCore: classic Karplus-Strong models for guitar and bass
FMOperator FMOCore: a single sinusoidal waveform, begging for more waveforms
VAOscillator VACore: classic virtual analog oscillator with saw and square waves

Five Operational Phases (plus getParameters())

It is important that you understand early-on that there are really only five functions, plus a constructor at most to call to place the object in each of its 5 operational phases. The constructor will count as phase 0. In addition to these, each object includes a same-named function: getParameters() that returns a shared pointer to its custom parameter structure that is used to manipulate the object, either programmatically or from a GUI. All objects have default values in their parameter structures to produce meaningful results so that you can use the objects straight away without needing a GUI.

Operational Phase SynthModule ModuleCore
0. construction load up to 4 cores set the 16 module strings and the 4 mod knob labels
1. reset call reset( ) on all cores specific core behavior, prepare for note-on
2. update call selectedCore->update( ) update object state with GUI controls and modulations
3. render call this->update( ), selctedCore->update( ) update object state with GUI controls and modulations
4. note-on call handlers on all cores go into note-on state
5. note-off call handlers on all cores go into note-off state

Notice how the SynthModule calls its own update( ) function, which then updates the selected core prior to rendering it. This ensures that the update( ) function is called just before the render( ) operation so that the object is ready to render its output correctly.

Core, Module and Mod Knob Strings

To maximize flexibility, the SynthLab example synth projects use a dynamic GUI interface that allows loading string lists and GUI labels on the fly. The sizes of the lists are fixed to allow proper handling of automation and DAW state save and restore operations. Note that this is an optional behaviour and very much tied to the framework's GUI capabilities. You may choose to not include this behavior and hardwire your GUI controls; this is explained in the sample code documentation.

Figure 1.3 shows a typical GUI implementation for WTOscillator. On the right side, there are four "mod knob" controls named A, B, C and D that are specific to each module core. Most cores have at least one unassigned mod knob for you to experiment with. Examine Figure 1.3 a. and b. and notice how the GUI controls connect to the module and its cores:

  1. The GUI exposes the module core names that the WTOscillator provides in a list for the user; Classic WT, etc...
  2. When the user selects a core, the module strings are dynamically loaded into the next control named "Waveform" here (or "Filter Type" for the filters, or "EG Contour" for the EGs, etc...)
  3. In addition, the mod knob labels (A, B, C, and D) are re-named for that particular core to show the functionality; un-assigned knobs show only the alphabetical letter
  4. Each object includes four hard-wired controls that are specific to that module, for example in the oscillator object, these are tuning, output and pan controls while for the EG object, these are attack, decay, sustain and release

You will see that almost all of the synth modules follow this paradigm and include exactly 10 GUI controls per module, the exceptions are the sequencer, mod matrix, FM Operator and DCA that are either too complex to shoehorn into this format, or too simple to require multiple cores and GUI controls.

modules_2.png


Figure 1.3: the WTOscillator's interface showing the relationship between module= strings, cores and mod knob strings

GUI Parameter Updates

The SynthModules and their cores will usually be connected to a GUI and you use a custom GUI parameter structure to pass the GUI control information to the object. All SynthModules have the same-named function: getParameters( ) however the shared pointer that is returned is always specific to that kind of module. LFOs use a LFOParamter structure, while wavetable oscillators use the WTOscParameter structure and so on. Regardless of how you implement the object (stand-alone or not) you obtain the parameter structure the same way. So, for each object you will also want to look at the custom parameter structure to see what values you can manipulate and what GUI controls you can present to the user.

  • all GUI parameters are tranmitted in their native ranges and types (i.e. not normalized); frequencies are in Hz and within the GUI ranges setup in the synthlabparams.h file
  • the four mod knob controls, customizable for each Core, transmit normalized values and there are helper functions to allow you to easily map those unipolar values on to linear, log, or anti-log ranges of other values
  • the first mod knob (MOD_KNOB_A) always defaults to the center value of 0.5; the other three default to 0.0
  • you should generally only use the custom parameter structure for GUI control manipulation, or for storing locally calculated (or cooked) parameters; do not use this structure for modulation, use the ModulationInput array instead
  • getParameter( ) always returns a shared pointer and you may adjust the values directly; there is NO setParameters( ) function as you are setting them directy

Getting Started
For each of the synth modules, the first thing to do is look at the main documentation for the class definition. You will find several pieces of information about the module that you need to know to configure, update and use it. We will do this one module at a time in the standalone object operation section. The first thing to establish is which ModuleCores the module will load at startup. From the documentation you will find:

  1. a list of the ModuleCore object names; you can then go to each Core's documentation for further information(we'll do that soon)
  2. the name of the custom data structure used to pass GUI and control information to the module
  3. function names to get and set modulation values
  4. function names to get access to audio buffers (for objects that render or process audio)
  5. information about stand-alone mode, and how to construct the object that way
  6. the final location of the output of this object, either the output modulation array (modulators) or the AudioBuffer outputs (oscillators and filters)

Default ModuleCore
SynthModules may contain up to 4 ModuleCores; not all of the SynthLab examples use all four cores but all have at least one.

  • The first core is the default core and is always loaded at construction time. You may also select other (existing) cores after construction
  • The core will load its first module string (waveform, filter, etc...) by default

To select a core, you pass in the zero-indexed core index on the range of [0, 3]. The function will return TRUE if a core was selected and FALSE if the core does not exist.

// --- SynthModule member function to select a core
bool selectModuleCore(uint32_t index);
//

When using a SynthModule in stand-alone mode, you will want to know about the default cores, waveforms, and other details from the documentation and synth book.


synthlab_4.png