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

resample.h

Rate conversion.

#include <sonore/resample.h>
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

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