SynthLab SDK
SynthLab Examples

The SynthLab SDK includes SynthLab projects that are described in detail in the synth book .Figure 1 shows the block digrams for the SynthEngine and SynthVoice Objects used in all six of the synth examples. All of these projects, as well as the example project from the programming guide here (described in SynthVoice Example and SynthEngine Example) encode the entire synth product inside of four files:

  • synthengine.h and synthengine.cpp
  • synthvoice.h and synthvoice.cpp

SynthEngine is Designed for Re-Use

The SynthEngine was designed to be reused from the very beginning. Its role in the synth is easily pre-defined and hard-codable. Of the two objects (voice and engine), the SynthEngine is the less specific in nature, and other than instantiating and managing its array of SynthVoice objects, it is really only involved in a couple of direct synthesis tasks:

  • it assigns note-on and note-off events according to a pre-defined algorithm:
  • always use voice[0] for mono mode operation
  • always use voices[0] - [3] for unison mode operation
  • always use the built-in heuristic for voice stealing (steal the oldest playing note)
  • it accumulates the outputs of the voice objects, then routes them through audio effects, if enabled
  • the SynthLab example synths feature a built-in ping-pong delay placed on the final stereo output buss
  • the SynthEngine applies the final global volume control (named "master volume" in the MIDI spec)

This means that once you get your SynthEngine up and running the way you like (and that may be right out-of-the-box), you may keep re-using the same object and plugin framework interface code without modification, over and over. Figure 1 below shows the block diagram for the SynthEngine object, as coded in the example voice and engine files.

SynthVoice Defines the Synth Architecture

In each of these projects, the SynthEngine object stores an array of SynthVoice objects that are identical. This makes the engine a mono timbral synth engine. In these examples:

  • the engine responds to incoming MIDI messages on all channels alike
  • each voice renders notes from the same "patch" that is the connection of SynthModules via the modulation matrix
  • the voice controls the digital audio signal flow through its modules

This means that a complete synth can be encoded in a single SynthVoice object because the engine object does not discern between voices in its array. This means that once you have a working synth:

  • you may create a centralized SynthLab SDK location, and then freely edit the SynthModules and ModuleCores within the SDK
  • as long as you adhere to the base class construction rules, the engine's databases and parameters will be safely shared via smart pointers
  • you may alter the interface objects without needing to modify any of the SDK modules, voices or engine

Fun fact: all six of the SynthLab example synths share the same source files, including the same synthvoice.h and synthvoice.cpp code as a testament to how reusable the code really is

  • this is why you only see four files in the sample code folder, two for the engine and two for the voice
  • the six example synths are discerned by a single #define statement within the compiler project; you can build all of them from the same source code files with only one line of code modfied, the #define statement

Multi-Timbral SynthEngine
You can build multi-timbral synths with the engine and voice objects as well, to mimic a Korg synth in "Combi" mode where a different patch plays on each MIDI channel, up to 16 at a time. In this case, you need to:

  • make the SynthVoice object a base class for your "patch" voices
  • define different SynthVoice objects for each "patch" in your combination; one for piano, one for drums, etc...
  • change the engine object to maintain instances of each kind of voice, but still maintain a stack of SynthVoice (base class) pointers
  • change the engine's MIDI message decoding to assign different MIDI channels to different voices (patches)
  • run the rest of the engine as normal; if your voice objects adhere to the new SynthVoice base class, then the engine does not need to know anything else about them, and will call their base class functions (already declared as virtual specefically for this) as with the others
  • multi-timbral synths pose daunting GUI coding issues so this will be a major part of the project

Dynamic String Support

The SynthLab projects exploit the SynthModle <-> ModuleCore relationship that is outlined and detailed in the SynthModule Progamming and ModuleCore Progamming sections. This means that each module loads up to four cores, each of which implements a variation on the functionality of the module. The wavetable oscillator object WTOscillator (the SynthModule) fills up all four of its module core slots with its variations that include five ModuleCore objects: traditional wavetable, morphing wavetable, drum tables, sound FX tables, and Fourier synthesized tables.

You have two choices when using the SynthModules in combination with ModuleCores (remember that SynthModules may also be implemented without cores, which simplifies the GUI programming a bit but still involves the same effort of designing and implementing the operational phase methods).

  1. assign the ModuleCores at compile time so that they are fixed; SynthModules may still expose different kinds of cores to the user but they will be hard-wired
  2. use the dynamic string capabilities discussed in SynthModules & ModuleCores

Hardwired Modules & Cores

All of the SynthModules that load cores (which is every module except the DCA) have code written in their constructor that loads at least one core to use as the default. Some modules load more than one, adding them in sequence to their list of cores. If you leave this code as-is, then the synth modules and cores are already hardwired. Once set a module can't load a different core. This means for example that once you commit an oscillator to be a drum wavetable oscillator, it must remain rendering that data for the rest of the life of the synth. And, this is very much the way hardware synths operate as well.

  • this means that you know the names of all of the waveforms and mod knob labels apriori while you are developing your GUI
  • you can programmatically select one of the pre-loaded cores using SynthModule::selectCore( ) at construction time
  • core[0] is the first in the list and the default
  • you can then "program" your architecture at construction time, but once set it can't change

Dynamic Modules & Cores

SynthLab was designed for dynamic ModuleCore loading from the very beginning. This means that you may select different cores into their owning modules at run-time, for example changing an oscillator from drum wavetables to morphing wavetables to sound effect tables. When the core changes, two other pieces of information also change:

  • the module strings, which display the waveform list (oscillators), filter types (filters), or EG contours (EGs) for the user
  • the four Mod Knob labels that adorn the GUI controls, letting you know how these new controls have been assigned.

The synth book details all of this, and provides the theory that underlies the modules, voice, and engine. Figure 1.3 (from the book) 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

If you choose to use the dynamic string version, then you will need to make calls to the engine that will return the module core strings and mod knob labels for you to apply as a result of the user changing cores on the GUI (or automation). You will also need to know how your plugin framework handles dynamic swapping of string lists and GUI control labels. Even if you don't use ASPiK or RackAFX you can still examine the function calls and code in the sample projects available at www.willpirkle.com to see how I am dynamically changing strings around.

SynthLab Examples


synthlab_4.png