interpolators.lib
A library to handle interpolation. Its official prefix is it.
This library provides interpolation algorithms for signal and control processing. It includes linear, polynomial, spline, and higher-order interpolation methods used in delay lines, envelope shaping, resampling, and parameter smoothing.
The Interpolators library is organized into 7 sections:
- Two points interpolation functions
- Four points interpolation functions
- Two points interpolators
- Four points interpolators
- Generic piecewise linear interpolation
- Lagrange based interpolators
- Misc functions
The first four sections provide several basic interpolation functions, as well as interpolators
taking a gen circuit of N outputs producing values to be interpolated, triggered
by a idv read index signal. Two points and four points interpolations are implemented.
The idv parameter is to be used as a read index. In -single (= singleprecision) mode,
a technique based on 2 signals with the pure integer index and a fractional part in the [0,1]
range is used to avoid accumulating errors. In -double (= doubleprecision) or -quad (= quadprecision) modes,
a standard implementation with a single fractional index signal is used. Three functions int_part, frac_part and mak_idv are available to manipulate the read index signal.
Here is a use-case with waveform. Here the signal given to interpolator_XXX uses the idv model.
waveform_interpolator(wf, step, interp) = interp(gen, idv)
with {
gen(idx) = wf, (idx:max(0):min(size-1)) : rdtable with { size = wf:(_,!); }; /* waveform size */
index = (+(step)~_)-step; /* starting from 0 */
idv = it.make_idv(index); /* build the signal for interpolation in a generic way */
};
waveform_linear(wf, step) = waveform_interpolator(wf, step, it.interpolator_linear);
waveform_cosine(wf, step) = waveform_interpolator(wf, step, it.interpolator_cosine);
waveform_cubic(wf, step) = waveform_interpolator(wf, step, it.interpolator_cubic);
waveform_interp(wf, step, selector) = waveform_interpolator(wf, step, interp_select(selector))
with {
/* adapts the argument order */
interp_select(sel, gen, idv) = it.interpolator_select(gen, idv, sel);
};
waveform and index
waveform_interpolator1(wf, idv, interp) = interp(gen, idv)
with {
gen(idx) = wf, (idx:max(0):min(size-1)) : rdtable with { size = wf:(_,!); }; /* waveform size */
};
waveform_linear1(wf, idv) = waveform_interpolator1(wf, idv, it.interpolator_linear);
waveform_cosine1(wf, idv) = waveform_interpolator1(wf, idv, it.interpolator_cosine);
waveform_cubic1(wf, idv) = waveform_interpolator1(wf, idv, it.interpolator_cubic);
waveform_interp1(wf, idv, selector) = waveform_interpolator1(wf, idv, interp_select(selector))
with {
/* adapts the argument order */
interp_select(sel, gen, idv) = it.interpolator_select(gen, idv, sel);
};
Some tests here:
wf = waveform {0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 0.0};
process = waveform_linear(wf, step), waveform_cosine(wf, step), waveform_cubic(wf, step) with { step = 0.25; };
process = waveform_interp(wf, 0.25, nentry("algo", 0, 0, 3, 1));
process = waveform_interp1(wf, idv, nentry("algo", 0, 0, 3, 1))
with {
step = 0.1;
idv_aux = (+(step)~_)-step; /* starting from 0 */
idv = it.make_idv(idv_aux); /* build the signal for interpolation in a generic way */
};
/* Test linear interpolation between 2 samples with a `(idx,dv)` signal built using a waveform */
linear_test = (idx,dv), it.interpolator_linear(gen, (idx,dv))
with {
/* signal to interpolate (only 2 points here) */
gen(id) = waveform {3.0, -1.0}, (id:max(0)) : rdtable;
dv = waveform {0.0, 0.25, 0.50, 0.75, 1.0}, index : rdtable;
idx = 0;
/* test index signal */
index = (+(1)~_)-1; /* starting from 0 */
};
/* Test cosine interpolation between 2 samples with a `(idx,dv)` signal built using a waveform */
cosine_test = (idx,dv), it.interpolator_cosine(gen, (idx,dv))
with {
/* signal to interpolate (only 2 points here) */
gen(id) = waveform {3.0, -1.0}, (id:max(0)) : rdtable;
dv = waveform {0.0, 0.25, 0.50, 0.75, 1.0}, index : rdtable;
idx = 0;
/* test index signal */
index = (+(1)~_)-1; /* starting from 0 */
};
/* Test cubic interpolation between 4 samples with a `(idx,dv)` signal built using a waveform */
cubic_test = (idx,dv), it.interpolator_cubic(gen, (idx,dv))
with {
/* signal to interpolate (only 4 points here) */
gen(id) = waveform {-1.0, 2.0, 1.0, 4.0}, (id:max(0)) : rdtable;
dv = waveform {0.0, 0.25, 0.50, 0.75, 1.0}, index : rdtable;
idx = 0;
/* test index signal */
index = (+(1)~_)-1; /* starting from 0 */
};
References
Two points interpolation functions
(it.)interpolate_linear
Linear interpolation between 2 values.
Usage
interpolate_linear(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_linear_test = it.interpolate_linear(0.5, 0.0, 1.0);
References
(it.)interpolate_cosine
Cosine interpolation between 2 values.
Usage
interpolate_cosine(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_cosine_test = it.interpolate_cosine(0.5, 0.0, 1.0);
References
(it.)interpolate_logarithmic
Logarithmic (exponential) interpolation between 2 values.
The output traces a constant-ratio (geometric) curve from v0 to v1, so it is
the natural choice for perceptually even sweeps of quantities such as frequency or
amplitude. Both bounds must be strictly positive and have the same sign.
Used for pitch and frequency sweeps and for octave-spaced oscillator or filter
banks, where equal steps should mean equal pitch ratios rather than equal Hz.
Usage
interpolate_logarithmic(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first value (same sign asv1, != 0)v1: is the second value (same sign asv0, != 0)
Test
it = library("interpolators.lib");
interpolate_logarithmic_test = it.interpolate_logarithmic(0.5, 100.0, 10000.0);
(it.)interpolate_power
Power (skewed) interpolation between 2 values.
Linear interpolation shaped by raising the fraction to the power p: p = 1 is
linear, p > 1 biases the curve toward v0, and 0 < p < 1 biases it toward v1.
Unlike interpolate_logarithmic, the bounds may be any values (including 0 and
opposite signs). p is the first argument so it can be partially applied to obtain a
(dv,v0,v1) interpolator.
Used for fader and volume taper laws (an audio-taper pot is roughly p = 2..3) and
for velocity- or expression-to-amplitude curves.
Usage
interpolate_power(p,dv,v0,v1) : _
Where:
p: the skew exponent (p > 0); 1 is linear, > 1 biases towardv0, < 1 towardv1dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_power_test = it.interpolate_power(2.0, 0.5, 0.0, 1.0);
(it.)interpolate_exponential
Exponential (constant-rate) interpolation between 2 values.
Shapes the fraction through (exp(k*dv)-1)/(exp(k)-1) and then interpolates
linearly between v0 and v1 by that shaped fraction. k sets the curvature:
k > 0 clusters the values near v0, k < 0 clusters them near v1, and as
k -> 0 the curve approaches interpolate_linear (guarded so exactly 0 does not
divide by 0). Because it shapes the fraction rather than the ratio, the bounds may
be any values (including 0 and opposite signs), unlike interpolate_logarithmic.
k is the first argument so it can be partially applied to obtain a (dv,v0,v1)
interpolator.
Used for natural-sounding envelope segments: the curve matches RC capacitor
charge/discharge, giving analog-style attack and decay/release shapes.
Usage
interpolate_exponential(k,dv,v0,v1) : _
Where:
k: the curvature; > 0 clusters nearv0, < 0 nearv1, -> 0 approaches lineardv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_exponential_test = it.interpolate_exponential(3.0, 0.5, 0.0, 1.0);
(it.)interpolate_smoothstep
Smoothstep (3rd-order, C1) interpolation between 2 values.
Eases in and out with zero slope at both ends, using the classic polynomial
3*dv^2 - 2*dv^3. Similar in spirit to interpolate_cosine but polynomial rather
than trigonometric, with slightly gentler shoulders. This is the curve most people
mean by "ease in/out".
Used to de-zipper control signals (gain, pan, cutoff) without clicks: the zero slope
at each end removes the discontinuity a linear ramp leaves at its corners.
Usage
interpolate_smoothstep(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_smoothstep_test = it.interpolate_smoothstep(0.5, 0.0, 1.0);
(it.)interpolate_smootherstep
Smootherstep (5th-order, C2) interpolation between 2 values.
Like interpolate_smoothstep but with zero first AND second derivative at both
ends, using 6*dv^5 - 15*dv^4 + 10*dv^3. The smoothest of the symmetric S-curves;
use it when even the rate of change should have no visible kink at the bounds.
Used for morphs or long automation moves where smoothstep is not enough: the
continuous acceleration keeps a slow filter or crossfade from audibly snapping into
motion or to a stop.
Usage
interpolate_smootherstep(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second value
Test
it = library("interpolators.lib");
interpolate_smootherstep_test = it.interpolate_smootherstep(0.5, 0.0, 1.0);
(it.)interpolate_mel
Mel-scale (perceptual pitch) interpolation between 2 frequencies.
Unlike the other interpolators, v0 and v1 are frequencies in Hz: they are
converted to the mel scale with ba.hz2mel, interpolated linearly there, and
converted back with ba.mel2hz. This spreads frequencies the way the ear hears
pitch, which is the right spacing for filterbank or oscillator-bank center
frequencies.
Used to choose band center frequencies for a filterbank or vocoder, or bin
groupings for spectral analysis, so the bands track perceived pitch.
Usage
interpolate_mel(dv,v0,v1) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first frequency in Hzv1: is the second frequency in Hz
Test
it = library("interpolators.lib");
interpolate_mel_test = it.interpolate_mel(0.5, 100.0, 8000.0);
Four points interpolation functions
(it.)interpolate_cubic
Cubic interpolation between 4 values.
Usage
interpolate_cubic(dv,v0,v1,v2,v3) : _
Where:
dv: in the fractional value in [0..1] rangev0: is the first valuev1: is the second valuev2: is the third valuev3: is the fourth value
Test
it = library("interpolators.lib");
interpolate_cubic_test = it.interpolate_cubic(0.5, -1.0, 2.0, 1.0, 4.0);
References
Two points interpolators
(it.)interpolator_two_points
Generic interpolator on two points (current and next index), assuming an increasing index.
Usage
interpolator_two_points(gen, idv, interpolate_two_points) : si.bus(outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pairinterpolate_two_points: a two points interpolation function
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_two_points_test = it.interpolator_two_points(gen, idv, it.interpolate_linear)
with {
gen(idx) = waveform {0.0, 1.0, 4.0, 9.0, 16.0}, int(ma.modulo(idx, 5)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 4.0);
idv = it.make_idv(idxFloat);
};
(it.)interpolator_linear
Linear interpolator for a 'gen' circuit triggered by an 'idv' input to generate values.
Usage
interpolator_linear(gen, idv) : si.bus(outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pair
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_linear_test = it.interpolator_linear(gen, idv)
with {
gen(idx) = waveform {0.0, 1.0, 4.0, 9.0, 16.0}, int(ma.modulo(idx, 5)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 4.0);
idv = it.make_idv(idxFloat);
};
(it.)interpolator_cosine
Cosine interpolator for a 'gen' circuit triggered by an 'idv' input to generate values.
Usage
interpolator_cosine(gen, idv) : si.bus(outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pair
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_cosine_test = it.interpolator_cosine(gen, idv)
with {
gen(idx) = waveform {0.0, 1.0, 4.0, 9.0, 16.0}, int(ma.modulo(idx, 5)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 4.0);
idv = it.make_idv(idxFloat);
};
Four points interpolators
(it.)interpolator_four_points
Generic interpolator on interpolator_four_points points (previous, current and two next indexes), assuming an increasing index.
Usage
interpolator_four_points(gen, idv, interpolate_four_points) : si.bus(outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pairinterpolate_four_points: a four points interpolation function
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_four_points_test = it.interpolator_four_points(gen, idv, it.interpolate_cubic)
with {
gen(idx) = waveform {-1.0, 2.0, 1.0, 4.0, 7.0, 3.0}, int(ma.modulo(idx, 6)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 5.0);
idv = it.make_idv(idxFloat);
};
(it.)interpolator_cubic
Cubic interpolator for a 'gen' circuit triggered by an 'idv' input to generate values.
Usage
interpolator_cubic(gen, idv) : si.bus(outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pair
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_cubic_test = it.interpolator_cubic(gen, idv)
with {
gen(idx) = waveform {-1.0, 2.0, 1.0, 4.0, 7.0, 3.0}, int(ma.modulo(idx, 6)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 5.0);
idv = it.make_idv(idxFloat);
};
(it.)interpolator_select
Generic configurable interpolator (with selector between in [0..3]). The value 3 is used for no interpolation.
Usage
interpolator_select(gen, idv, sel) : _,_... (equal to N = outputs(gen))
Where:
gen: a circuit with an 'idv' reader input that produces N outputsidv: a fractional read index expressed as a float value, or a (int,frac) pairsel: an interpolation algorithm selector in [0..3] (0 = linear, 1 = cosine, 2 = cubic, 3 = nointerp)
Test
it = library("interpolators.lib");
ma = library("maths.lib");
interpolator_select_test = it.interpolator_select(gen, idv, 2)
with {
gen(idx) = waveform {-1.0, 2.0, 1.0, 4.0, 7.0, 3.0}, int(ma.modulo(idx, 6)) : rdtable;
step = 0.25;
idxFloat = ma.modulo((+(step)~_) - step, 5.0);
idv = it.make_idv(idxFloat);
};
Generic piecewise linear interpolation
(it.)lerp
Linear interpolation between two points.
Usage
lerp(x0, x1, y0, y1, x) : si.bus(1);
Where:
x0: x-coordinate originx1: x-coordinate destinationy0: y-coordinate originy1: y-coordinate destinationx: x-coordinate input
Test
it = library("interpolators.lib");
lerp_test = it.lerp(0.0, 10.0, -5.0, 5.0, 2.5);
(it.)piecewise
Linear piecewise interpolation between N points.
Usage
piecewise(xList, yList, x) : si.bus(1);
Where:
xList: x-coordinates listyList: y-coordinates listx: x-coordinate input
Example test program
The code below will output the values of linear segments going through the y coordinates as the input goes from -5 to 5:
x = hslider("x", -5, -5.0, 5.0, .001);
process = it.piecewise((-5, -3, 0, 3, 5), (2, 0, 3, -3, -2), x);
Test
it = library("interpolators.lib");
os = library("oscillators.lib");
piecewise_test = it.piecewise((-5, -2, 0, 3), (1, 0, 4, -1), os.osc(0.1));
Lagrange based interpolators
(it.)lagrangeCoeffs
This is a function to generate N + 1 coefficients for an Nth-order Lagrange basis polynomial with arbitrary spacing of the points.
Usage
lagrangeCoeffs(N, xCoordsList, x) : si.bus(N + 1)
Where:
N: order of the interpolation filter, known at compile-timexCoordsList: a list of N + 1 elements determining the x-axis coordinates of N + 1 values, known at compile-timex: a fractional position on the x-axis to obtain the interpolated y-value
Test
it = library("interpolators.lib");
lagrangeCoeffs_test = it.lagrangeCoeffs(2, (0.0, 0.5, 1.0), 0.25);
References
- https://ccrma.stanford.edu/~jos/pasp/Lagrange_Interpolation.html
- https://en.wikipedia.org/wiki/Lagrange_polynomial
(it.)lagrangeInterpolation
Nth-order Lagrange interpolator to interpolate between a set of arbitrarily spaced N + 1 points.
Usage
x , yCoords : lagrangeInterpolation(N, xCoordsList) : _
Where:
N: order of the interpolator, known at compile-timexCoordsList: a list of N + 1 elements determining the x-axis spacing of the points, known at compile-timex: an x-axis position to interpolate between the y-valuesyCoords: N + 1 elements determining the values of the interpolation points
Example: find the centre position of a four-point set using an order-3 Lagrange function fitting the equally-spaced points [2, 5, -1, 3]:
N = 3;
xCoordsList = (0, 1, 2, 3);
x = N / 2.0;
yCoords = 2, 5, -1, 3;
process = x, yCoords : it.lagrangeInterpolation(N, xCoordsList);
which outputs ~1.938.
- Example: output the dashed curve showed on the Wikipedia page (top figure in https://en.wikipedia.org/wiki/Lagrange_polynomial):
N = 3;
xCoordsList = (-9, -4, -1, 7);
x = os.phasor(16, 1) - 9;
yCoords = 5, 2, -2, 9;
process = x, yCoords : it.lagrangeInterpolation(N, xCoordsList);
Test
it = library("interpolators.lib");
lagrangeInterpolation_test =
(lagrange_x, lagrange_y0, lagrange_y1, lagrange_y2, lagrange_y3)
: it.lagrangeInterpolation(3, (0, 1, 2, 3))
with {
lagrange_x = 1.5;
lagrange_y0 = 2.0;
lagrange_y1 = 5.0;
lagrange_y2 = -1.0;
lagrange_y3 = 3.0;
};
References
- https://ccrma.stanford.edu/~jos/pasp/Lagrange_Interpolation.html
- Sanfilippo and Parker 2021, "Combining zeroth and firstāorder analysis with Lagrange polynomials to reduce artefacts in live concatenative granular processing." Proceedings of the DAFx conference 2021, Vienna, Austria.
- https://dafx2020.mdw.ac.at/proceedings/papers/DAFx20in21_paper_38.pdf
(it.)frdtable
Look-up circular table with Nth-order Lagrange interpolation for fractional indexes. The index is wrapped-around and the table is cycles for an index span of size S, which is the table size in samples.
Usage
frdtable(N, S, init, idx) : _
Where:
N: Lagrange interpolation order, known at compile-timeS: table size in samples, known at compile-timeinit: the initial table content, known at compile-timeidx: fractional index wrapped-around 0 and S
Example test program
Test the effectiveness of the 5th-order interpolation scheme by creating a table look-up oscillator using only 16 points of a sinewave; compare the result with a non-interpolated version:
N = 5;
S = 16;
index = os.phasor(S, 1000);
process = rdtable(S, os.sinwaveform(S), int(index)) ,
it.frdtable(N, S, os.sinwaveform(S), index);
Test
it = library("interpolators.lib");
os = library("oscillators.lib");
frdtable_test = it.frdtable(3, 16, os.sinwaveform(16), os.phasor(16, 200));
(it.)frwtable
Look-up updatable circular table with Nth-order Lagrange interpolation for fractional indexes. The index is wrapped-around and the table is circular indexes ranging from 0 to S, which is the table size in samples.
Usage
frwtable(N, S, init, w_idx, x, r_idx) : _
Where:
N: Lagrange interpolation order, known at compile-timeS: table size in samples, known at compile-timeinit: the initial table content, known at compile-timew_idx: it should be an INT between 0 and S - 1x: input signal written on the w_idx positionsr_idx: fractional index wrapped-around 0 and S
Example test program
Test the effectiveness of the 5th-order interpolation scheme by creating a table look-up oscillator using only 16 points of a sinewave; compare the result with a non-interpolated version:
N = 5;
S = 16;
rIdx = os.phasor(S, 300);
wIdx = ba.period(S);
process = rwtable(S, os.sinwaveform(S), wIdx, os.sinwaveform(S), int(rIdx)) ,
it.frwtable(N, S, os.sinwaveform(S), wIdx, os.sinwaveform(S), rIdx);
Test
it = library("interpolators.lib");
os = library("oscillators.lib");
ba = library("basics.lib");
frwtable_test = it.frwtable(3, 16, os.sinwaveform(16), ba.period(16), os.osc(220), os.phasor(16, 150));
Misc functions
(it.)remap
Linearly map from an input domain to an output range.
Usage
_ : remap(from1, from2, to1, to2) : _
Where:
from1: the domain's lower bound.from2: the domain's upper bound.to1: the range's lower bound.to2: the range's upper bound.
Note that having from1 == from2 in the mapping will cause a division by zero that has to be taken in account.
Example test program
An oscillator remapped from [-1., 1.] to [100., 1000.]:
os.osc(440) : it.remap(-1., 1., 100., 1000.)
Test
it = library("interpolators.lib");
os = library("oscillators.lib");
remap_test = it.remap(-1.0, 1.0, 100.0, 1000.0, os.osc(0.5));