DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Convolver.h
Go to the documentation of this file.
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
40#include "AudioBuffer.h"
41#include "AudioSpec.h"
42#include "FFT.h"
43#include "SimdOps.h"
44
45#include <algorithm>
46#include <cassert>
47#include <memory>
48#include <vector>
49
50namespace dspark {
51
66template <typename T>
68{
69public:
82 void prepare(int blockSize, const T* irData, int irLength)
83 {
84 assert(blockSize >= 2 && (blockSize & (blockSize - 1)) == 0);
85 assert(irLength > 0);
86 assert(irData != nullptr);
87 if (irData == nullptr || irLength <= 0) return;
88
89 // Release-safe normalisation: the FFT needs a power of two >= 4, so
90 // the block needs a power of two >= 2. Round up (never down: rounding
91 // down would shrink the valid overlap-save region below the block).
92 int bs = 2;
93 while (bs < blockSize) bs <<= 1;
94
95 blockSize_ = bs;
96 fftSize_ = bs * 2;
97 numBins_ = fftSize_ + 2; // (N/2 + 1) complex bins, interleaved = N+2 scalars
98
99 // Create FFT processor
100 fft_ = std::make_unique<FFTReal<T>>(fftSize_);
101
102 // Partition the IR into blocks
103 numPartitions_ = (irLength + bs - 1) / bs;
104
105 // Pre-transform each IR partition
106 irPartitions_.resize(static_cast<size_t>(numPartitions_));
107 std::vector<T> paddedBlock(static_cast<size_t>(fftSize_), T(0));
108
109 for (int p = 0; p < numPartitions_; ++p)
110 {
111 int offset = p * bs;
112 int remaining = std::min(bs, irLength - offset);
113
114 // Zero-pad the block to fftSize
115 std::fill(paddedBlock.begin(), paddedBlock.end(), T(0));
116 std::copy_n(irData + offset, remaining, paddedBlock.begin());
117
118 // Forward FFT and store
119 irPartitions_[static_cast<size_t>(p)].resize(static_cast<size_t>(numBins_));
120 fft_->forward(paddedBlock.data(), irPartitions_[static_cast<size_t>(p)].data());
121 }
122
123 // Allocate working buffers
124 inputBuffer_.assign(static_cast<size_t>(fftSize_), T(0));
125 outputBuffer_.assign(static_cast<size_t>(fftSize_), T(0));
126 overlapBuffer_.assign(static_cast<size_t>(blockSize_), T(0));
127 fftAccum_.assign(static_cast<size_t>(numBins_), T(0));
128
129 // Frequency-domain delay line (stores past input FFTs)
130 fdlBuffer_.resize(static_cast<size_t>(numPartitions_));
131 for (auto& fdl : fdlBuffer_)
132 fdl.assign(static_cast<size_t>(numBins_), T(0));
133
134 fdlIndex_ = 0;
135 inputPos_ = 0;
136 }
137
139 void reset() noexcept
140 {
141 std::fill(inputBuffer_.begin(), inputBuffer_.end(), T(0));
142 std::fill(outputBuffer_.begin(), outputBuffer_.end(), T(0));
143 std::fill(overlapBuffer_.begin(), overlapBuffer_.end(), T(0));
144 for (auto& fdl : fdlBuffer_)
145 std::fill(fdl.begin(), fdl.end(), T(0));
146 fdlIndex_ = 0;
147 inputPos_ = 0;
148 }
149
162 void process(const T* input, T* output, int numSamples) noexcept
163 {
164 if (blockSize_ == 0) // not prepared: honest pass-through
165 {
166 if (output != input)
167 std::copy_n(input, numSamples, output);
168 return;
169 }
170
171 // Copy in contiguous runs up to the next block boundary instead of
172 // sample-by-sample (this is the per-channel hot path of Reverb).
173 int i = 0;
174 while (i < numSamples)
175 {
176 const int run = std::min(numSamples - i, blockSize_ - inputPos_);
177
178 // Stage the input run first, then overwrite the caller's samples
179 // with the previous block's result -- this ordering is what makes
180 // full in-place (input == output) legal.
181 std::copy_n(input + i, run,
182 inputBuffer_.begin() + blockSize_ + inputPos_);
183 std::copy_n(overlapBuffer_.begin() + inputPos_, run, output + i);
184
185 inputPos_ += run;
186 i += run;
187
188 if (inputPos_ >= blockSize_)
189 {
190 processPartitionBlock();
191 inputPos_ = 0;
192
193 std::copy_n(outputBuffer_.begin() + blockSize_, static_cast<size_t>(blockSize_),
194 overlapBuffer_.begin());
195 }
196 }
197 }
198
205 void processInPlace(T* data, int numSamples) noexcept
206 {
207 process(data, data, numSamples);
208 }
209
224 void prepare(const AudioSpec& spec, const T* irData, int irLength)
225 {
226 prepare(spec.maxBlockSize, irData, irLength);
227 }
228
238 void processBlock(AudioBufferView<T> buffer) noexcept
239 {
240 if (buffer.getNumChannels() > 0 && buffer.getNumSamples() > 0)
241 processInPlace(buffer.getChannel(0), buffer.getNumSamples());
242 }
243
245 [[nodiscard]] int getLatency() const noexcept { return blockSize_; }
246
248 [[nodiscard]] int getBlockSize() const noexcept { return blockSize_; }
249
251 [[nodiscard]] int getNumPartitions() const noexcept { return numPartitions_; }
252
253private:
263 void processPartitionBlock() noexcept
264 {
265 // Forward FFT of the input buffer [previous block | current block],
266 // directly into this block's FDL slot (no intermediate copy).
267 fft_->forward(inputBuffer_.data(),
268 fdlBuffer_[static_cast<size_t>(fdlIndex_)].data());
269
270 // Complex multiply-accumulate: sum over all partitions
271 std::fill(fftAccum_.begin(), fftAccum_.end(), T(0));
272
273 for (int p = 0; p < numPartitions_; ++p)
274 {
275 int fdlIdx = (fdlIndex_ - p + numPartitions_) % numPartitions_;
276 const auto& irPart = irPartitions_[static_cast<size_t>(p)];
277 const auto& fdlPart = fdlBuffer_[static_cast<size_t>(fdlIdx)];
278
279 // Complex multiplication and accumulation (SIMD-accelerated)
280 int bins = numBins_ / 2;
281 simd::complexMulAccum(fftAccum_.data(), fdlPart.data(), irPart.data(), bins);
282 }
283
284 // Inverse FFT
285 fft_->inverse(fftAccum_.data(), outputBuffer_.data());
286
287 // Shift input buffer: move current block to previous position
288 std::copy_n(inputBuffer_.begin() + blockSize_, static_cast<size_t>(blockSize_),
289 inputBuffer_.begin());
290 std::fill(inputBuffer_.begin() + blockSize_, inputBuffer_.end(), T(0));
291
292 // Advance delay line index
293 fdlIndex_ = (fdlIndex_ + 1) % numPartitions_;
294 }
295
296 int blockSize_ = 0;
297 int fftSize_ = 0;
298 int numBins_ = 0;
299 int numPartitions_ = 0;
300 int inputPos_ = 0;
301 int fdlIndex_ = 0;
302
303 std::unique_ptr<FFTReal<T>> fft_;
304
305 // Pre-transformed IR partitions (frequency domain)
306 std::vector<std::vector<T>> irPartitions_;
307
308 // Frequency-domain delay line (past input FFTs)
309 std::vector<std::vector<T>> fdlBuffer_;
310
311 // Working buffers
312 std::vector<T> inputBuffer_;
313 std::vector<T> outputBuffer_;
314 std::vector<T> overlapBuffer_;
315 std::vector<T> fftAccum_;
316};
317
318} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
Fast Fourier Transform (Cooley-Tukey radix-2) with SIMD acceleration.
SIMD-accelerated buffer operations for real-time audio processing.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Real-time partitioned convolution using overlap-save with FFT.
Definition Convolver.h:68
int getNumPartitions() const noexcept
Returns the number of IR partitions.
Definition Convolver.h:251
void prepare(const AudioSpec &spec, const T *irData, int irLength)
Prepares with AudioSpec and IR data (unified API).
Definition Convolver.h:224
void process(const T *input, T *output, int numSamples) noexcept
Processes a block of audio through the convolver.
Definition Convolver.h:162
int getBlockSize() const noexcept
Returns the block size.
Definition Convolver.h:248
void reset() noexcept
Resets the convolution state (clears delay lines and buffers).
Definition Convolver.h:139
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place (unified API).
Definition Convolver.h:238
int getLatency() const noexcept
Returns the processing latency in samples (= block size).
Definition Convolver.h:245
void prepare(int blockSize, const T *irData, int irLength)
Prepares the convolver with an impulse response.
Definition Convolver.h:82
void processInPlace(T *data, int numSamples) noexcept
Processes audio in-place (output overwrites input).
Definition Convolver.h:205
void complexMulAccum(float *DSPARK_RESTRICT accum, const float *DSPARK_RESTRICT a, const float *DSPARK_RESTRICT b, int bins) noexcept
Complex multiply-accumulate over interleaved [re, im, ...] spectra.
Definition SimdOps.h:1039
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51