resample.h
Rate conversion.
#include <sonore/resample.h>- 4types
- 12other declarations
- Guides: Getting started, MIDI and MPE
On this page
Design notes
Two jobs that share one piece of maths:
Oversampler<Stages> runs a nonlinearity at 2×, 4×, 8×… the host's rate, so
the harmonics it creates land above Nyquist and get
filtered out instead of folding back into the audio as
inharmonic grit. This is the difference between a
saturator that measures clean on the aliasing probe
and one that does not.
Resampler arbitrary-ratio conversion, for material that arrives
at the wrong rate: an impulse response recorded at
44.1 kHz loaded into a 96 kHz session, a wavetable
shipped at one rate and played at another.
The kernels are COMPUTED, not hand-typed. A windowed-sinc table pasted into source is a set of magic numbers nobody can check; computing it from the formula is both verifiable and lets the length be a parameter.
Types 4
template <int Taps = 33> class HalfBandFilter
A half-band FIR: passes everything below fs/4, stops everything above.
Half-band is the right filter for power-of-two rate changes because HALF ITS TAPS ARE EXACTLY ZERO: every even tap either side of the centre vanishes, so the filter costs half of what its length suggests. Taps must be odd.
The state is a circular buffer rather than a shifted array: shifting is O(n) per sample and shows up immediately in a plugin running eight of these.
template <int Order = 5> class PolyphaseIirHalfBand
A half-band built from two ALLPASS chains instead of an FIR.
The idea is standard (a polyphase IIR half-band, as in Valimaki's work and every serious oversampler): a half-band response can be written as
H(z) = ½·( A₀(z²) + z⁻¹·A₁(z²) )
where A₀ and A₁ are cascades of second-order allpass sections. Because allpass sections are unit-gain by construction, the filtering costs ONE multiply per section per sample (roughly a quarter of the FIR's work for the same stopband), and the phase response is minimum-phase, so the delay is a few samples, varying with frequency, rather than the FIR's sixteen (see latency()).
The trade against the FIR path is real and worth stating: phase is NOT linear, so this is the wrong choice when a plugin is summed against a dry path that has to stay phase-coherent. The FIR is the default for that reason; this is the option for a plugin that wants latency-free saturation.
The coefficients are DERIVED, not tabulated: a Butterworth half-band's poles are purely imaginary in the z-plane, and squaring them gives the allpass coefficients directly. That means the order is a parameter rather than a number of magic constants nobody can check, and the test measures the resulting response instead of trusting it.
template <int Stages, typename Filter> class OversamplerT
Oversampling by 2^Stages, built from cascaded half-band stages.
Usage is one sample in, one sample out. The shaper simply runs 2^Stages times in between:
float y = os.process(x, [drive](float s) { return std::tanh(s * drive); });
LATENCY IS REAL and must be declared to the host: the half-band filters delay the signal, and a plugin that reports zero smears every parallel mix it sits in. latencySamples() is the number to hand back from the DSP's own latencySamples().
With Taps = 33 that latency is a whole number of samples at every factor: 32·(1 − 2^−Stages), which is why 33 is the default rather than a rounder looking 32 or 31.
Arbitrary-ratio resampling, for material that arrives at the wrong rate.
Windowed-sinc interpolation: for each output position, sum the input samples around it weighted by a shifted sinc. When DOWN-sampling the sinc is widened to the output's Nyquist, because otherwise the conversion aliases: the most common mistake in a hand-rolled resampler, and it sounds like the material has grit that was never in it.
Offline by design: this is for prepare()-time work such as fitting an impulse response to the host's rate, not for the audio thread. It reads and writes caller-owned buffers and allocates nothing.
Aliases 12
- using
template <int Stages = 1, int Taps = 33> using Oversampler = OversamplerT<Stages, HalfBandFilter<Taps>>The two flavours, and the choice between them is a real one:resample.h:759 - using
template <int Stages = 1, int Order = 9> using OversamplerIir = OversamplerT<Stages, PolyphaseIirHalfBand<Order>>The minimum-phase flavour: 2^Stages oversampling through polyphase IIR half-bands ofresample.h:764Order(9 by default), latency reported as 0. - using
using Oversampler2x = Oversampler<1>2x, linear phase, 33 taps: 16 samples of latency at the base rate.resample.h:767 - using
using Oversampler4x = Oversampler<2>4x, linear phase: 24 samples of latency at the base rate.resample.h:769 - using
using Oversampler8x = Oversampler<3>8x, linear phase: 28 samples of latency at the base rate.resample.h:771 - using
using Oversampler16x = Oversampler<4>16x. Worth it only for something violently nonlinear (a hard clipper, a fold-back) where even 8x leaves audible folded product. It costs about twice 8x for a few dB, so it is a deliberate choice, not a default.resample.h:775 - using
using Oversampler32x = Oversampler<5>32x. The ceiling, and past the point of audible return: measured gain over 8x is a handful of dB for four times the work. Here because being unable to do it would be a limitation; not because it is a good default.resample.h:779 - using
using OversamplerIir2x = OversamplerIir<1>2x, minimum phase: reports 0, delays about three samples (measured).resample.h:781 - using
using OversamplerIir4x = OversamplerIir<2>4x, minimum phase: reports 0, delays more than the 2x does.resample.h:783 - using
using OversamplerIir8x = OversamplerIir<3>8x, minimum phase: reports 0; the real delay grows with the stages.resample.h:785 - using
using OversamplerIir16x = OversamplerIir<4>16x, minimum phase: reports 0; the real delay grows with the stages.resample.h:787 - using
using OversamplerIir32x = OversamplerIir<5>32x, minimum phase: reports 0; the real delay grows with the stages.resample.h:789