Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::PartitionedConvolverT< SampleType > Class Template Reference

Uniform partitioned convolution engine. More...

#include <convolver.hpp>

Public Member Functions

 PartitionedConvolverT ()=default
void load_ir (const SampleType *ir, std::size_t ir_length, std::size_t block_size)
 Load an impulse response.
bool try_swap_ir (ConvolverIrSwapperT< SampleType > &swapper)
 Try to consume the most recently staged IR from a swapper.
void set_crossfade (std::size_t samples, TransitionCurve curve=TransitionCurve::Smoothstep)
 Enable a click-free crossfade on IR swaps (opt-in — default 0 = instantaneous swap).
void process (const SampleType *input, SampleType *output, std::size_t num_samples)
 Process a block of audio.
void reset ()
 Drop the convolver's overlap-save input history so the next block starts from silence rather than continuing a previous stream.
std::size_t latency () const
 Returns the algorithmic latency in samples.
std::size_t block_size () const
 The block size process() requires, in samples — the value handed to load_ir() rounded up to the next power of two (the radix-2 FFT needs it).
std::uint64_t block_size_violations () const
 How many times process() was called on a loaded convolver with num_samples != block_size().
std::size_t num_partitions () const
bool is_loaded () const

Detailed Description

template<typename SampleType = float>
class pulp::signal::PartitionedConvolverT< SampleType >

Uniform partitioned convolution engine.

Processes audio through an impulse response using overlap-save with uniform block partitioning. Zero latency (current block output is immediate).

Two ways to load an IR:

  1. load_ir() — synchronous; allocates + FFTs inline. Use at prepare() time, never on the audio thread.
  2. try_swap_ir(ConvolverIrSwapper&) — lock-free, allocation-free; picks up a pre-built IR posted from a worker thread, swaps it in at the next block boundary, and parks the displaced IR for the worker thread to free. Designed for safe live IR swaps during process().

Usage (synchronous): PartitionedConvolver conv; conv.load_ir(ir_data, ir_length, block_size); conv.process(input, output, num_samples);

Usage (background swap): ConvolverIrSwapper swapper; // background thread: swapper.stage_ir(new_ir, new_len, block_size); // audio thread, before/between process() calls: conv.try_swap_ir(swapper); // background thread, periodically: swapper.drain_old();

Constructor & Destructor Documentation

◆ PartitionedConvolverT()

template<typename SampleType = float>
pulp::signal::PartitionedConvolverT< SampleType >::PartitionedConvolverT ( )
default

Member Function Documentation

◆ block_size()

template<typename SampleType = float>
std::size_t pulp::signal::PartitionedConvolverT< SampleType >::block_size ( ) const
inline

The block size process() requires, in samples — the value handed to load_ir() rounded up to the next power of two (the radix-2 FFT needs it).

Zero when no IR is loaded.

Size the caller's blocks from THIS, not from the value passed to load_ir(): load_ir(ir, len, 100) yields a 128-sample convolver, and feeding it 100-sample blocks is a precondition violation.

◆ block_size_violations()

template<typename SampleType = float>
std::uint64_t pulp::signal::PartitionedConvolverT< SampleType >::block_size_violations ( ) const
inline

How many times process() was called on a loaded convolver with num_samples != block_size().

Every one of those blocks was emitted as silence (see process()).

This is a caller bug, not a runtime condition: a nonzero count means the audio host's block size and the size handed to load_ir() disagree. Assert it is zero in tests. Cleared by reset().

◆ is_loaded()

template<typename SampleType = float>
bool pulp::signal::PartitionedConvolverT< SampleType >::is_loaded ( ) const
inline

◆ latency()

template<typename SampleType = float>
std::size_t pulp::signal::PartitionedConvolverT< SampleType >::latency ( ) const
inline

Returns the algorithmic latency in samples.

Overlap-save produces valid output for the current block immediately (partition 0 is applied in the same callback), so latency is 0.

◆ load_ir()

template<typename SampleType = float>
void pulp::signal::PartitionedConvolverT< SampleType >::load_ir ( const SampleType * ir,
std::size_t ir_length,
std::size_t block_size )
inline

Load an impulse response.

block_size should match your audio callback size. block_size must be a power of two (for the radix-2 FFT); if not, it is rounded up to the next one.

Allocates and FFTs inline — call off the audio thread.

◆ num_partitions()

template<typename SampleType = float>
std::size_t pulp::signal::PartitionedConvolverT< SampleType >::num_partitions ( ) const
inline

◆ process()

template<typename SampleType = float>
void pulp::signal::PartitionedConvolverT< SampleType >::process ( const SampleType * input,
SampleType * output,
std::size_t num_samples )
inline

Process a block of audio.

Precondition: num_samples == block_size() whenever an IR is loaded. Overlap-save folds each block into a partition history sized to block_size(); a block of any other length cannot be folded into that history coherently.

The three states are distinct, and only one of them passes audio through:

  • No IR loaded (or an empty IR). The convolver is a wire: input is copied to output. This is the correct neutral behavior.
  • IR loaded, num_samples == block_size(). Normal convolution.
  • IR loaded, wrong num_samples. A precondition violation. It fails closed: the block is emitted as silence, block_size_violations() increments, and the history is marked torn so the next correctly-sized block resets rather than continuing from a partially-advanced state.

    It deliberately does NOT pass through. A pass-through here is audibly indistinguishable from a working convolution with a near-unity IR, so it turns a caller's block size bug into audio that merely sounds wrong — the hardest kind of bug to find. Silence plus a counter is a bug you can see, and block_size_violations() == 0 is an invariant a test can assert.

RT-safe in every branch: the violation path bumps a counter and fills a buffer. It does not log, allocate, throw, or abort on the audio thread — a convolver must never take down its host over a caller bug.

◆ reset()

template<typename SampleType = float>
void pulp::signal::PartitionedConvolverT< SampleType >::reset ( )
inline

Drop the convolver's overlap-save input history so the next block starts from silence rather than continuing a previous stream.

There is one history for every IR the convolver renders, so this clears a crossfade's fade-out side along with the live one. Also clears the torn-history flag and the block-size violation count.

◆ set_crossfade()

template<typename SampleType = float>
void pulp::signal::PartitionedConvolverT< SampleType >::set_crossfade ( std::size_t samples,
TransitionCurve curve = TransitionCurve::Smoothstep )
inline

Enable a click-free crossfade on IR swaps (opt-in — default 0 = instantaneous swap).

When set, a swap keeps the OLD IR rendering in parallel with the new for samples and blends old→new via the shared TransitionMixer, so an IR change is audibly continuous rather than a hard cut. Call off the audio thread AFTER an IR is loaded (it sizes the parallel-render scratch from the block size).

Both sides of the fade render from the convolver's single shared input history, so both are convolving against the same real past: the outgoing IR fades out from its tail and the incoming one fades in already warm. A fade is therefore continuous at any fade length — under the default Smoothstep curve, whose gain law is equal-gain, swapping in a copy of the IR already loaded is inaudible — and the choice of samples is purely a taste question about how fast the timbre should change. (EqualPower holds POWER rather than amplitude constant, so it bulges on a correlated pair by design; pick it when the two IRs are decorrelated.)

A fade shorter than the IR is fine and costs nothing in continuity. Note only that a fade blends the outputs of two DIFFERENT impulse responses, which for a pair with opposing phase can dip in level mid-fade — that is inherent to blending, not a history artifact.

◆ try_swap_ir()

template<typename SampleType = float>
bool pulp::signal::PartitionedConvolverT< SampleType >::try_swap_ir ( ConvolverIrSwapperT< SampleType > & swapper)
inline

Try to consume the most recently staged IR from a swapper.

If one is available, the current IR is parked on the swapper for the worker thread to free and the new IR is installed.

Allocation-free, lock-free, RT-safe. Returns true if a swap happened on this call.

Must be called at a block boundary (between process() calls) so the in-flight overlap buffers don't pick up midway through.


The documentation for this class was generated from the following file:
  • /home/runner/work/pulp/pulp/core/signal/include/pulp/signal/convolver.hpp