Convolution reverb¶
Convolution reverb applies a recorded or synthesized impulse response (IR) to audio. Each output sample is the convolution of the input and the IR, so an impulse response can describe a room, a device, or any other linear time-invariant effect.
For long IRs, use convolve_filter. It is a streaming filter that performs partitioned, FFT-based overlap-add convolution. The IR is transformed when the filter is initialized; audio can then be supplied as one buffer or as successive blocks without retaining the complete input signal in memory.
For the shared filter apply interface, reset behavior, and general rules for preserving state across blocks, see Applying Stateful Filters.
Mono reverb¶
The simplest use is in-place processing. The first output sample corresponds to the first input sample, so the filter does not add a signal-alignment latency.
univector<float> audio{ 1.0f, 2.0f, 3.0f };
univector<float> impulse_response{ 0.5f, 1.0f };
convolve_filter<float> reverb(impulse_response);
reverb.apply(audio);
The in-place overload overwrites audio. To retain the dry signal, use a separate output vector instead. An empty output vector is resized to the input length by filter<T>::apply.
univector<float> wet;
reverb.apply(wet, audio);
filter<T>::apply produces one output sample for every input sample. It does not append the part of the response that continues after the input ends. To collect that tail, use filter<T>::apply_zeros to process \(M - 1\) zero-valued input samples after an input whose IR has \(M\) samples:
convolve_filter<float> reverb(impulse_response);
univector<float> wet(audio.size() + impulse_response.size() - 1);
reverb.apply(wet.data(), audio.data(), audio.size());
reverb.apply_zeros(wet.data() + audio.size(), impulse_response.size() - 1);
wet now contains the full linear-convolution result. Do not call convolve_filter<T>::reset before processing the zero-valued input: resetting clears the pending overlap and discards the tail.
Streaming and block size¶
convolve_filter retains its input history and overlap between calls to filter<T>::apply. Keep the same filter object for consecutive blocks from one audio stream:
univector<float> impulse_response{ 0.5f, 1.0f };
std::vector<univector<float>> input_blocks{
{ 1.0f, 2.0f },
{ 3.0f, 4.0f },
};
std::vector<univector<float>> output_blocks;
convolve_filter<float> reverb(impulse_response, 512);
for (const univector<float>& input_block : input_blocks)
{
univector<float> output_block;
reverb.apply(output_block, input_block);
output_blocks.push_back(output_block);
}
univector<float> tail(impulse_response.size() - 1);
reverb.apply_zeros(tail);
output_blocks.push_back(tail);
The optional block-size argument controls the FFT partition size. KFR rounds it up to a power of two; query the actual value with convolve_filter<T>::input_block_size. Processing blocks whose length is a multiple of that value avoids repeated transforms of a partial block and gives the best throughput. Other block lengths are valid and retain the same sample order and filter state.
The final apply_zeros call drains the overlap remaining after the last input block. Do this before resetting the filter or beginning an unrelated stream.
Call convolve_filter<T>::reset before starting an unrelated stream. This clears input history and overlap but keeps the IR and the chosen block size. To replace the IR, call convolve_filter<T>::set_data; replacing it also resets the filter state.
Note
Construct the filter from a non-empty IR, or call set_data before its first apply call. The overload that accepts only an IR length is useful when it is immediately followed by set_data.
True stereo reverb¶
True stereo uses four impulse responses: one for every input-to-output route. The subscripts below name input first, output second:
Each route needs a separate filter because it keeps its own input history and overlap. The following code processes one stereo block. left and right must have equal lengths; repeat the same four calls for every later block.
// h_<input><output>
univector<float> ir_ll{ 1.0f }; // left -> left
univector<float> ir_rl{ 0.5f }; // right -> left
univector<float> ir_lr{ 2.0f }; // left -> right
univector<float> ir_rr{ -1.0f }; // right -> right
convolve_filter<float> ll(ir_ll);
convolve_filter<float> rl(ir_rl);
convolve_filter<float> lr(ir_lr);
convolve_filter<float> rr(ir_rr);
univector<float> left{ 1.0f, 2.0f };
univector<float> right{ 3.0f, 4.0f };
univector<float> left_from_left;
univector<float> left_from_right;
univector<float> right_from_left;
univector<float> right_from_right;
ll.apply(left_from_left, left); // L -> L
rl.apply(left_from_right, right); // R -> L
lr.apply(right_from_left, left); // L -> R
rr.apply(right_from_right, right); // R -> R
left = left_from_left + left_from_right;
right = right_from_left + right_from_right;
For channel-oriented storage, univector2d holds a vector per channel. With channels[0] as left and channels[1] as right, substitute those vectors for left and right in the example.
Choosing a convolution API¶
Use convolve when both finite signals are already available and the complete result is needed at once. It returns \(N + M - 1\) samples for input lengths \(N\) and \(M\). Use convolve_filter for a fixed IR and an incoming stream, or for long IRs where partitioned convolution avoids holding the whole signal in memory.
For more about the overlap-add algorithm and its efficiency criterion, see Convolution filter details.