Skip to content

V0.997.2


version = "0.997.2" breaking = true applies_if = "cli_version_from < 0.997.2 && cli_version_to >= 0.997.2" summary = "OversamplerT now defaults to the elliptic polyphase IIR lane (better anti-aliasing, cheaper, about 1.7 base samples more delay), and Convolver now renders impulse responses longer than one block correctly"


What changed

pulp::signal::OversamplerT (Oversampler, Oversampler64) constructed without set_kind() now filters with Kind::polyphase_iir instead of Kind::fir_biquad. The polyphase lane is an elliptic half-band design (design_halfband_iir()); Quality::standard is the default.

fir_biquad (old default) polyphase_iir standard (new default)
Alias / image rejection about 7-9 dB 94.8 dB from 0.58 Fs
Passband -1.8 dB at 0.3 Fs flat (< 1e-8 dB) to 0.42 Fs
Cost per sample, x2 / x4, relative to fir_biquad 1 / 1 about 0.17 / 0.26
Round-trip delay near DC, x2 / x4 0.97 / 1.46 base samples 2.66 / 3.49 base samples
Round-trip delay at 0.4 Fs, x2 1.5 base samples 7.6 base samples

Both lanes are minimum phase. latency() still reports a non-constant delay and latency_samples() still returns 0, so host latency compensation is unchanged; the extra delay is uncompensated.

The half-band classes (HalfBandUpsampler2x, HalfBandDownsampler2x) also default to the new design: three sections per path instead of six, rejecting 94.8 dB from 0.29 of their own rate.

Who is affected

Only code that never calls set_kind(). You will hear a brighter top end (the passband no longer droops) and less aliasing. Code that blends the oversampled path with an unprocessed dry path, or aligns it against another signal, sees the extra delay as comb filtering or a timing offset.

Migration steps

To keep the previous sound and timing exactly, select the biquad lane:

oversampler.set_kind(pulp::signal::Oversampler::Kind::fir_biquad);

To keep the new lane and align a dry path, read its delay from the filter instead of hard-coding it:

const double delay = oversampler.group_delay_samples(0.0); // base samples

group_delay_samples(f) is frequency dependent for both minimum-phase lanes; use Kind::linear_phase_fir when a constant, host-compensated latency is required.

Filter kinds, rejection, and delay curves are tabulated in Oversampling filters and the real-input FFT.

Convolver output for impulse responses longer than one block

pulp::signal::ConvolverT (Convolver, Convolver64) accumulated only the most recent block's overlap. When the impulse response was longer than the block size, every tail segment beyond the next block was dropped, so the output was truncated rather than the full convolution. The overlap is now accumulated across blocks and the output is the complete convolution.

The output is still delayed by exactly one block, as before: a block is transformed once it has filled and its result is read out while the next one fills. That delay is now stated in the header.

Who is affected

Code that loads an impulse response longer than its block_size into Convolver. Its output gains the reverb or filter tail it was missing, so it is louder and longer than before. An impulse response no longer than the block size renders as it did, to rounding. PartitionedConvolver, FixedIrConvolver and ZeroLatencyConvolver are separate classes and are not affected.

Migration steps

None for correctness. A test that pinned the old truncated output against a long impulse response needs its expectation regenerated; compare against a direct-form convolution of the same impulse response delayed by one block.