synth.h
The synthesiser side, past one oscillator and one envelope.
#include <sonore/synth.h>- 8types
- Guides: Getting started, MIDI and MPE
On this page
Design notes
dsp.h has a polyBLEP Oscillator, an ADSR, a ladder and a VoiceManager, and every "make me a synth" reached for those four and stopped, because that is where the toolkit stopped. These are the parts the next request needs:
Wavetable + WavetableOscillator any single cycle, band-limited by
mip-mapping, shared between voices
UnisonOscillator the supersaw: N detuned saws, spread
HardSyncOscillator oscillator sync with the reset BLEPped
FmOperator a phase-modulation operator, with
feedback, to stack into algorithms
Dahdsr delay-attack-hold-decay-sustain-release
KarplusStrong a plucked string
ModalResonator a struck body: a bank of ringing modes
Everything is per voice and allocation-free; the Wavetable is the one big object and is designed to be held ONCE and read by every voice.
Included by dsp.h. Uses Oscillator, Fft, DelayLine, OnePole, WhiteNoise.
Types 8
template <size_t Size = 2048, int Mips = 10> class Wavetable
One cycle, stored at every octave of band-limiting.
A table read at a high note aliases exactly as a naive saw does: the cycle holds harmonics up to Size/2, and the ones above Nyquist for the note being played fold back down. The fix is the graphics one -- a mip-map. Mip m keeps the harmonics up to (Size/2) / 2^m, built by transforming the cycle, zeroing what is above the limit, and transforming back. The oscillator picks the first mip whose highest harmonic stays below Nyquist for its frequency; the harmonics between that limit and Nyquist are lost, which is the cost of octave steps and why an oscillator that crosses a mip boundary mid-note can sound its top harmonic come and go. Half-octave steps would halve that and double the memory; octaves are the usual compromise.
SIZE: Mips x Size floats plus the FFT's tables -- 88 KB at the defaults. Hold one per plugin in static storage (the wasm ABI already does), never one per voice and never on the stack.
template <typename Table = Wavetable<>> class WavetableOscillator
Reads a shared Wavetable at a pitch. One per voice; the table is shared.
template <int MaxVoices = 8> class UnisonOscillator
N polyBLEP saws (or squares) spread in pitch and in the stereo field: the supersaw. Detune is the total spread in cents, voices placed evenly across it; spread pans the outer voices out to the sides. Level is normalised by 1/sqrt(N), which keeps the RMS steady as voices are added -- the voices are detuned, so they add as noise, not as a coherent sum.
A slave saw reset every time a master cycle completes. The output has the master's pitch and the slave's harmonic shape, and sweeping the slave's ratio is the sound of "sync".
The reset is a discontinuity like the saw's own wrap, at a moment that is not a sample boundary, and left alone it aliases as badly as a naive saw. It is BLEPped the same way: the master's phase says when the reset will fall inside the next sample (so the sample before can be corrected without a delay) and how far into the current sample it fell (so the sample after can be), and the residual is scaled by the height of the drop, which is however far up the slave had climbed. The unit test compares against the same oscillator with band limiting turned off.
One phase-modulation operator: a sine whose phase is pushed around by whatever is passed in, in RADIANS, plus its own last output scaled by feedback. Stack them as the algorithms do -- modulator.render(0) into carrier.render(index * m) -- and the spectrum follows Bessel: at a modulation index of 2.405 the carrier itself vanishes (J0's first zero), which the unit test asserts, because it is the one FM fact that is easy to check and impossible to fake.
The six-stage envelope: a delay before anything happens, a hold at the peak before the decay, and the four an ADSR has. Same conventions as ADSR: a linear attack, exponential decay and release timed to -60 dB so the numbers on the controls are the numbers a meter reads.
template <int MaxDelay = 16384> class KarplusStrong
A plucked string: a burst of noise circulating in a delay the length of one period, losing a little level and a little brightness on every pass. Decay is the -60 dB time at the fundamental; damping closes a one-pole in the loop so the upper partials die first, as they do on a real string.
The loop's length is the delay PLUS the one-pole's own delay, and the pitch error from forgetting that is a quarter-tone at the top of a guitar. The filter's phase delay is evaluated AT THE STRING'S OWN FUNDAMENTAL from its transfer function (OnePole::phaseDelaySamples), not approximated by its DC value: a dull string sits a good fraction of the way up its damping filter, where the two differ by whole samples. It is subtracted from the line, and the fractional remainder is read with the cubic tap, which is what makes the tuning continuous rather than stepping at every sample boundary. The same filter also LOSES level at the fundamental, and the loop gain is raised by that loss (Jaffe & Smith's compensation) so the declared decay is the measured one at every damping -- capped just under unity, because a loop gain above one at DC is an oscillator.
MaxDelay is the longest period, so it sets the lowest note: the default 16384 holds 20 Hz at every rate up to 327 kHz. It was 4096, which is 47 Hz at 192 kHz -- every bass note below that sounded AT 47 Hz. A note below what the line can hold is now clamped to it in noteOn, where it is visible, rather than in the delay, where it was not.
template <int MaxModes = 8> class ModalResonator
A struck object as a sum of ringing modes -- a bar, a bell, a drum shell. Each mode is a two-pole resonator with its own frequency, decay (-60 dB time) and amplitude, excited by whatever is fed in: an impulse for a strike, a noise burst for a brush, a saw for something stranger.
The recursion y = A sin(w) x + 2r cos(w) y1 - r^2 y2 has the impulse response A r^n sin(w (n+1)): amplitude exactly A, decaying by r per sample, which is what lets the decay be set in seconds rather than tuned.