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

Two-stage non-uniform partitioned convolver. More...

#include <convolver_non_uniform.hpp>

Public Member Functions

 NonUniformPartitionedConvolverT ()=default
void load_ir (const SampleType *ir, std::size_t ir_length, std::size_t block_size)
 Load an impulse response with the tail multiplier default_tail_multiplier() picks for its length and the block size.
void load_ir (const SampleType *ir, std::size_t ir_length, std::size_t block_size, std::size_t tail_multiplier)
 Load an impulse response into both stages.
void process (const SampleType *input, SampleType *output, std::size_t num_samples)
 Process one audio block.
void reset ()
std::size_t latency () const
 Algorithmic latency in samples.
std::size_t block_size () const
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 head_samples () const
std::size_t tail_block () const
std::size_t tail_multiplier () const
bool is_loaded () const

Static Public Member Functions

static constexpr std::size_t choose_tail_multiplier (std::size_t ir_length, std::size_t block_size) noexcept
 The tail multiplier load_ir() uses when none is given: the head length (K · B samples) that minimises the mean per-frame cost for this IR length and block size.
static constexpr bool table_measured_on (std::string_view cpu_brand) noexcept
 Whether the table was measured on the chip family a CPU brand string (machdep.cpu.brand_string) names: "Apple M1" and "Apple M5", with or without a tier suffix such as "Max".
static constexpr std::size_t default_tail_multiplier_for (std::string_view cpu_brand, std::size_t ir_length, std::size_t block_size) noexcept
 The tail multiplier for a host whose CPU brand string is cpu_brand: choose_tail_multiplier() on a measured chip family, otherwise kDefaultTailMultiplier.
static std::size_t default_tail_multiplier (std::size_t ir_length, std::size_t block_size)
 The tail multiplier the three-argument load_ir() uses: default_tail_multiplier_for() with this host's machdep.cpu.brand_string, read once off the audio thread and cached.

Static Public Attributes

static constexpr std::size_t kDefaultTailMultiplier = 4
 Tail multiplier (K) used where choose_tail_multiplier() has no measurement.

Detailed Description

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

Two-stage non-uniform partitioned convolver.

Block size B + tail multiplier K (default 4). The "head" handles the first head_partitions × B IR samples at block size B; the "tail" handles the remainder at block size K·B.

RT contract: load_ir() and invalidating IR swaps allocate/rebuild FFT storage and are not audio-thread safe. process(), reset(), latency(), and accessors are allocation-free after a valid IR is loaded and callers pass exactly block_size() samples.

Constructor & Destructor Documentation

◆ NonUniformPartitionedConvolverT()

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

Member Function Documentation

◆ block_size()

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

◆ block_size_violations()

template<typename SampleType = float>
std::uint64_t pulp::signal::NonUniformPartitionedConvolverT< 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. A nonzero count is a caller bug — the host's block size and the size handed to load_ir() disagree. Assert it is zero in tests. Cleared by reset().

◆ choose_tail_multiplier()

template<typename SampleType = float>
constexpr std::size_t pulp::signal::NonUniformPartitionedConvolverT< SampleType >::choose_tail_multiplier ( std::size_t ir_length,
std::size_t block_size )
inlinestaticconstexprnoexcept

The tail multiplier load_ir() uses when none is given: the head length (K · B samples) that minimises the mean per-frame cost for this IR length and block size.

A longer head moves more of the IR into the per-block stage, whose multiply-accumulate grows with it; a shorter one fires the tail's large FFT more often. The balance point grows with both the IR and the block: about 1024 samples of head for a one-second IR at 16- to 32-frame blocks, up to 4096 at 512 frames. A fixed K of 4 sits far from it for long IRs (up to 9.75x the cost at 48000-96000 taps).

The table is measured in single precision on Apple Silicon Macs with the Accelerate backend (M1 Max over three runs, M5 Max over five) for IRs of 4096, 16384, 48000 and 96000 taps and blocks of 16 to 512 frames (receipt: docs/validation/nonuniform-head/; a test recomputes it from those files). Each entry is the K whose worst cost ratio to that chip's cheapest K is lowest over the chips, each chip's cost being its median over every round of its runs. An IR length uses the row of the nearest measured length on a log scale. Blocks outside 16-512 frames, other architectures and operating systems, other SIMD backends, and double precision keep kDefaultTailMultiplier until they are measured.

This is the table itself. load_ir() applies it only on the chip families it was measured on (see table_measured_on()).

◆ default_tail_multiplier()

template<typename SampleType = float>
std::size_t pulp::signal::NonUniformPartitionedConvolverT< SampleType >::default_tail_multiplier ( std::size_t ir_length,
std::size_t block_size )
inlinestatic

The tail multiplier the three-argument load_ir() uses: default_tail_multiplier_for() with this host's machdep.cpu.brand_string, read once off the audio thread and cached.

Builds without the measured table, and hosts whose brand cannot be read, keep kDefaultTailMultiplier.

◆ default_tail_multiplier_for()

template<typename SampleType = float>
constexpr std::size_t pulp::signal::NonUniformPartitionedConvolverT< SampleType >::default_tail_multiplier_for ( std::string_view cpu_brand,
std::size_t ir_length,
std::size_t block_size )
inlinestaticconstexprnoexcept

The tail multiplier for a host whose CPU brand string is cpu_brand: choose_tail_multiplier() on a measured chip family, otherwise kDefaultTailMultiplier.

Pure, so the family rule is testable on any host; default_tail_multiplier() applies it to the running host.

◆ head_samples()

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

◆ is_loaded()

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

◆ latency()

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

Algorithmic latency in samples.

Always 0 — head stage produces the current block immediately (overlap-save).

◆ load_ir() [1/2]

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

Load an impulse response with the tail multiplier default_tail_multiplier() picks for its length and the block size.

Allocates and FFTs inline — call off the audio thread.

◆ load_ir() [2/2]

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

Load an impulse response into both stages.

Allocates and FFTs inline — call off the audio thread.

Architectural invariant: head covers the first tail_multiplier × block_size IR samples. This makes the buffering delay of the tail stage (K small blocks) coincide with the IR-tap offset of the tail (K · B samples), so the tail FFT output streams in with zero net latency. Callers may override tail_multiplier but cannot decouple head length from it — that's the whole point of the non-uniform layout.

Parameters
irIR samples.
ir_lengthIR length in samples.
block_sizeAudio callback block size (rounded up to the next power of two for the radix-2 FFT).
tail_multiplierK factor for the tail-stage block size (rounded up to power of 2; K=1 degenerates to uniform partitioning). The three-argument overload picks it with default_tail_multiplier().

◆ process()

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

Process one audio block.

input and output may be the same buffer (in-place processing).

Precondition: num_samples == block_size() whenever an IR is loaded. The head/tail stages advance interlocked cursors sized to block_size(); a block of any other length desynchronizes them.

Fails closed on violation, exactly as PartitionedConvolver does: an unloaded convolver passes through (it is a wire), but a loaded one handed the wrong block size emits silence, bumps block_size_violations(), and marks its history torn so the next correctly-sized block resets both stages. It never passes through, because a pass-through is audibly indistinguishable from a working convolution and would hide the caller's block size bug.

RT-safe: no allocation, no blocking, no logging, no abort. Pure compute on the frequency-domain accumulators of both stages.

Tail-stage timing: Accumulate K small blocks of input into tail_input_buf_, then fire one large-block tail.process() that emits K·B samples of tail contribution. Those samples stream out over the NEXT K small blocks, mixed into the head output. The K-block buffering delay coincides with the K·B IR-tap offset of the tail slice, so the sum is sample-aligned with the true linear convolution.

◆ reset()

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

◆ table_measured_on()

template<typename SampleType = float>
constexpr bool pulp::signal::NonUniformPartitionedConvolverT< SampleType >::table_measured_on ( std::string_view cpu_brand)
inlinestaticconstexprnoexcept

Whether the table was measured on the chip family a CPU brand string (machdep.cpu.brand_string) names: "Apple M1" and "Apple M5", with or without a tier suffix such as "Max".

Other families can rank the K arms differently for the same Accelerate transforms, so they keep kDefaultTailMultiplier until they are measured.

◆ tail_block()

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

◆ tail_multiplier()

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

Member Data Documentation

◆ kDefaultTailMultiplier

template<typename SampleType = float>
std::size_t pulp::signal::NonUniformPartitionedConvolverT< SampleType >::kDefaultTailMultiplier = 4
staticconstexpr

Tail multiplier (K) used where choose_tail_multiplier() has no measurement.

The head stage covers the first K small-block partitions of the IR so that the tail stage's input-buffering latency (K small blocks) exactly cancels the IR-tap offset, producing seamless alignment.


The documentation for this class was generated from the following file: