Search guides and the APICtrl K
  1. API reference
  2. DSP

shaping.h

Waveshaping, lookup tables, ballistics and chain composition.

#include <sonore/shaping.h>
On this page

Design notes

The building blocks a generated distortion or dynamics plugin reaches for that were not already in dsp.h. Everything here is real-time safe once prepared: no allocation, no branching on sample values beyond what the maths requires.

Types 9

A sampled function with linear interpolation, for anything too expensive to evaluate per sample.

The table stores ONE EXTRA point past the end so interpolation never reads out of bounds at the top of the range: the classic off-by-one in every hand-rolled lookup, and it corrupts exactly the loudest samples.

A memoryless nonlinearity with optional drive, bias and output compensation.

Bias matters more than it looks: pushing a signal off-centre before an odd shaping curve is what produces EVEN harmonics, which is most of the difference between "transistor" and "valve" in a listener's description. DC introduced by the bias is removed on the way out, or it would offset everything downstream.

Attack/release smoothing with a choice of peak or RMS detection.

EnvFollower in dsp.h is the peak-only version this generalises: RMS detection is what makes a compressor respond to LOUDNESS rather than to transients, and the two sound entirely different on the same source.

template <typename... Stages> class ProcessorChain

Compile-time composition of processors, so a generated plugin can express "filter into saturator into limiter" as a type rather than as five members and five hand-written forwarding calls.

Every stage is expanded at compile time; there is no virtual dispatch and nothing to allocate, which is the whole point: a chain must cost exactly what writing the stages out by hand would have cost.

template <typename MonoProcessor, size_t MaxChannels = 8> class ProcessorDuplicator

Runs one MONO processor per channel, so a design written for a single channel becomes a multichannel one without being rewritten.

Each channel gets its own INSTANCE, which is the whole point: sharing one filter's state across channels is the bug that makes a stereo signal collapse towards mono as soon as the material differs between sides.

Level, applied as a RAMP so a moved fader cannot click.

The ramp is a fixed TIME -- 10 ms, set up for the rate in prepare() -- and carries across blocks. It used to be one host block long, which at 512 frames is 10 ms and at a split block of 16 is a third of a millisecond: a 0 -> 1 move at a block of 1 was a step, and hosts that split blocks at every automation point send blocks that small.

A DC offset. On its own it does nothing audible; its use is BEFORE a waveshaper, where pushing the signal off centre is what asks an odd curve for even harmonics.

template <typename SampleProcessor, size_t MaxChannels = 8> class SampleStage

Turns any PER-SAMPLE processor into a chain stage, one instance per channel.

This is what makes Biquad, SVF, LadderFilter, DcBlocker (anything with a float process(float)) usable inside a ProcessorChain without being rewritten. State stays per channel, for the same reason ProcessorDuplicator exists: one filter shared across channels collapses a stereo image.

Smoothing in the RATIO domain, for anything measured in octaves or decibels.

Smoothing a cutoff linearly from 100 Hz to 10 kHz spends most of the glide above 5 kHz, because equal steps in hertz are wildly unequal steps in pitch. The ear hears ratios, so the smoothing has to work in them: this interpolates the LOGARITHM and the sweep sounds even.

Not to be confused with LogRamp in dsp.h, which is the other half of this pair: that one runs for a stated number of samples and lands exactly on its target, which is what a FADE needs. This one approaches for ever, which is what a knob being turned needs.

Functions 3

↑↓ to move↵ to openEvery public name in the headers, and every guide