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 |
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:
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();
|
default |
|
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.
|
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().
|
inline |
|
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.
|
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.
|
inline |
|
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:
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.
|
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.
|
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.
|
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.