SynthLab SDK
|
SynthModules are the fundamental synth building blocks: LFOs, EGs, oscillators, filters and amplifiers. These can be broken down into four fundamental types:
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.
The I/O ports connect a module to its input and output sources.
Input Ports
Output Ports
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:
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:
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 |
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.
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:
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.
Figure 1.3: the WTOscillator's interface showing the relationship between module= strings, cores and mod knob strings
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.
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:
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.
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.
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.