shaping.h
Waveshaping, lookup tables, ballistics and chain composition.
#include <sonore/shaping.h>- 9types
- 3other declarations
- Guides: Getting started, MIDI and MPE
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
- fn
inline float linear(float y1, float y2, float frac)Two points, straight line. Cheapest, and audibly the worst on modulation.interpolate::linear · shaping.h:211 - fn
inline float lagrange3(float y0, float y1, float y2, float y3, float frac)Third-order Lagrange: the polynomial through all four points. Flat to far higher frequencies than linear at the same cost class.interpolate::lagrange3 · shaping.h:223More
Forwarded, not copied. This lived here as its own arithmetic and again in interpolation.h as a kernel, written months apart by someone who did not look -- which is the exact shape of bug this SDK has been bitten by three times over: a rule in more than one place, with only one copy tested. They were checked against each other over a grid of inputs before being folded together, because two implementations that quietly differ are worse than two that obviously do.
- fn
inline float catmullRom(float y0, float y1, float y2, float y3, float frac)Catmull-Rom spline: matches Lagrange's smoothness but passes through the points with continuous slope, which is what keeps a swept delay from clicking as it crosses sample boundaries.interpolate::catmullRom · shaping.h:234