interpolation.h
Reading a stream at a rate that is not one sample per sample.
#include <sonore/interpolation.h>- 6types
- 6other declarations
- Guides: Getting started, MIDI and MPE
On this page
Design notes
Every varispeed thing in audio is this: a tape effect, a pitch shifter, a sampler transposing, a chorus whose delay is moving, a session running at a rate the material was not recorded at. The SDK could already do the whole- buffer case (resample.h) and the sampler had a Catmull-Rom reader welded into it, and there was nothing in between: nothing that takes a STREAM, block after block, remembering what it had from last time.
That memory is the entire difficulty. An interpolator needs samples on both sides of the point it is reading, and at a block boundary the ones on the left belong to the previous call. Get that wrong and the result is a click every block, at exactly the block rate, which people diagnose as anything except an interpolator.
Four points about the kernels, since choosing between them is the reason there is more than one:
Zero-order hold no cost, no delay, and audible aliasing. Real use: a
control signal, or a deliberate lo-fi effect.
Linear a lowpass whose corner moves with the fraction, which is
why a linearly-interpolated delay sounds duller as it
sweeps.
Catmull-Rom four points, C1 continuous, passes through its points.
Lagrange (3rd) four points, the polynomial that fits them exactly.
Windowed sinc what the maths actually says, at the cost of a table and
a wide window; the only one whose error keeps falling as
you spend more on it.
Every kernel declares its LATENCY, in samples, and the test measures that rather than believing it. A latency that is documented and wrong is worse than one that is undocumented, because the host compensates by it.
Types 6
No interpolation at all: the newest sample, held.
Straight line between the two samples the point falls between.
Catmull-Rom: a cubic through the middle two points, with the outer two setting the slopes. Continuous in value and first derivative, which is why a swept delay using it does not buzz.
Third-order Lagrange: the unique cubic through all four points.
Not the same thing as Catmull-Rom, which only passes through the middle two. Fitting all four exactly costs some smoothness at the joins, and which of the two sounds better depends on the material -- hence both.
template <int HalfWidth = 8> struct WindowedSincKernel
Windowed sinc, from a table.
The weights depend only on the fraction, so they are computed ONCE for a grid of fractions and looked up. Computing them per sample would be correct and would put sixteen sin() calls in an audio callback, which is the kind of correct that gets a plugin uninstalled.
Between grid points the two neighbouring filters are blended. That is an approximation of an approximation, and it is why the grid is 512 wide: at that spacing the blend error is far below the window's own stopband.
template <typename Kernel> class Interpolator
Reads a stream at speedRatio input samples per output sample, carrying the samples it needs across block boundaries.
Ratio above 1 reads faster than real time (pitch up, shorter); below 1 is slower. Nothing here band-limits on the way DOWN in rate: reading at 2x without a lowpass first will alias, and it will alias whatever kernel is chosen, because the aliasing happened before the interpolation did. For a whole buffer at a fixed ratio, resample.h does it properly.
Functions 1
- fn
template <typename Kernel> inline float sampleAt(const float* buffer, size_t length, double position)Read a buffer at a fractional index, with no state.interpolation.h:391More
The other half of the problem: a sampler does not walk forwards at a fixed ratio, it JUMPS -- a loop point, a new note, a reversed playback. A stream reader cannot express that and this can.
Out of range reads as silence rather than wrapping or clamping. Clamping would smear the last sample into a DC step at the end of every one-shot, and wrapping would splice the start of the file onto its end.
Aliases 5
- using
using ZeroOrderHoldInterpolator = Interpolator<ZeroOrderHoldKernel>No interpolation and no latency: for a control signal, or lo-fi on purpose.interpolation.h:365 - using
using LinearInterpolator = Interpolator<LinearKernel>Two points, one sample of latency; duller as the fraction moves.interpolation.h:367 - using
using CatmullRomInterpolator = Interpolator<CatmullRomKernel>Four points, two samples of latency, continuous in slope.interpolation.h:369 - using
using LagrangeInterpolator = Interpolator<Lagrange3Kernel>Four points, two samples of latency, the cubic through all of them.interpolation.h:371 - using
template <int HalfWidth = 8> using WindowedSincInterpolator = Interpolator<WindowedSincKernel<HalfWidth>>2 x HalfWidth taps and HalfWidth samples of latency; the weight table is shared by every interpolator of the same width.interpolation.h:375