|
| 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.
|
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.
template<typename SampleType = float>
|
|
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()).
template<typename SampleType = float>
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
-
| ir | IR samples. |
| ir_length | IR length in samples. |
| block_size | Audio callback block size (rounded up to the next power of two for the radix-2 FFT). |
| tail_multiplier | K 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(). |
template<typename SampleType = float>
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.