DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
ZeroLatencyConvolver.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
47#include "AudioBuffer.h"
48#include "Convolver.h"
49#include "FFT.h"
50#include "SimdOps.h"
51
52#include <algorithm>
53#include <cstdint>
54#include <cstring>
55#include <memory>
56#include <vector>
57
58namespace dspark {
59
66template <typename T>
68{
69public:
79 void prepare(const T* irData, int irLength, int headSize = 128)
80 {
81 prepared_ = false;
82 if (irData == nullptr || irLength <= 0) return;
83
84 headSize_ = 32;
85 while (headSize_ < std::clamp(headSize, 32, 512)) headSize_ <<= 1;
86
87 // --- head: ir[0, headLen) stored reversed for a linear dot product ---
88 headLen_ = std::min(irLength, headSize_);
89 headRev_.assign(static_cast<size_t>(headLen_), T(0));
90 for (int i = 0; i < headLen_; ++i)
91 headRev_[static_cast<size_t>(i)] = irData[headLen_ - 1 - i];
92
93 // --- mid: uniform partitioned convolver, block == headSize ----------
94 // Its block latency equals this region's offset, so its (delayed)
95 // output is exactly the contribution of ir[headSize, tailStart).
96 const int midEnd = std::min(irLength, kTailStart);
97 hasMid_ = irLength > headSize_;
98 if (hasMid_)
99 {
100 mid_.prepare(headSize_, irData + headSize_, midEnd - headSize_);
101 midScratch_.assign(static_cast<size_t>(headSize_), T(0));
102 }
103
104 // --- tail: time-distributed partitions of kTailBlock ----------------
105 hasTail_ = irLength > kTailStart;
106 numTailParts_ = 0;
107 if (hasTail_)
108 {
109 const int tailLen = irLength - kTailStart;
110 numTailParts_ = (tailLen + kTailBlock - 1) / kTailBlock;
111 totalUnits_ = numTailParts_ + 3; // FFT + K MACs + IFFT + scatter
112
113 tailFft_ = std::make_unique<FFTReal<T>>(static_cast<size_t>(2 * kTailBlock));
114
115 tailIR_.assign(static_cast<size_t>(numTailParts_) * kTailSpec, T(0));
116 tailFdl_.assign(static_cast<size_t>(numTailParts_) * kTailSpec, T(0));
117 tailAccum_.assign(static_cast<size_t>(kTailSpec), T(0));
118 tailScratch_.assign(static_cast<size_t>(2 * kTailBlock), T(0));
119 tailOut_.assign(static_cast<size_t>(kTailOutSize), T(0));
120
121 std::vector<T> padded(static_cast<size_t>(2 * kTailBlock), T(0));
122 for (int p = 0; p < numTailParts_; ++p)
123 {
124 const int offset = kTailStart + p * kTailBlock;
125 const int len = std::min(kTailBlock, irLength - offset);
126 std::fill(padded.begin(), padded.end(), T(0));
127 std::copy_n(irData + offset, len, padded.begin());
128 tailFft_->forward(padded.data(), &tailIR_[static_cast<size_t>(p) * kTailSpec]);
129 }
130 }
131
132 // --- shared input history ring ---------------------------------------
133 int ringSize = 2 * headSize_;
134 if (hasTail_) ringSize = std::max(ringSize, 4 * kTailBlock);
135 int pow2 = 1;
136 while (pow2 < ringSize) pow2 <<= 1;
137 inRing_.assign(static_cast<size_t>(pow2), T(0));
138 inMask_ = pow2 - 1;
139
140 prepared_ = true;
141 reset();
142 }
143
145 void reset() noexcept
146 {
147 if (!prepared_) return;
148 std::fill(inRing_.begin(), inRing_.end(), T(0));
149 std::fill(tailFdl_.begin(), tailFdl_.end(), T(0));
150 std::fill(tailOut_.begin(), tailOut_.end(), T(0));
151 if (hasMid_) mid_.reset();
152 absPos_ = static_cast<int64_t>(inRing_.size()); // keep indices positive
153 cyclePos_ = 0;
154 fdlIndex_ = 0;
155 taskPhase_ = totalUnits_; // idle
156 taskStart_ = 0;
157 }
158
165 void process(const T* input, T* output, int numSamples) noexcept
166 {
167 if (!prepared_)
168 {
169 if (output != input)
170 std::memmove(output, input, static_cast<size_t>(numSamples) * sizeof(T));
171 return;
172 }
173
174 int i = 0;
175 while (i < numSamples)
176 {
177 int chunk = std::min(numSamples - i, headSize_);
178 if (hasTail_)
179 chunk = std::min(chunk, kTailBlock - cyclePos_);
180
181 // Mid level reads the pristine input before output overwrites it.
182 if (hasMid_)
183 mid_.process(input + i, midScratch_.data(), chunk);
184
185 for (int k = 0; k < chunk; ++k)
186 {
187 inRing_[static_cast<size_t>(absPos_ & inMask_)] = input[i + k];
188
189 T y = headDot();
190 if (hasMid_) y += midScratch_[static_cast<size_t>(k)];
191 if (hasTail_)
192 {
193 auto& slot = tailOut_[static_cast<size_t>(absPos_ & kTailOutMask)];
194 y += slot;
195 slot = T(0); // clear-on-read keeps the ring self-cleaning
196 }
197 output[i + k] = y;
198 ++absPos_;
199 }
200
201 if (hasTail_)
202 {
203 cyclePos_ += chunk;
204 // Execute this cycle's proportional share of the pending task.
205 // 64-bit product: totalUnits_ grows with the partition count,
206 // and totalUnits_ * cyclePos_ would wrap int for hours-long IRs.
207 runTaskUnits(static_cast<int>(
208 (static_cast<int64_t>(totalUnits_) * cyclePos_ + kTailBlock - 1) / kTailBlock));
209 if (cyclePos_ >= kTailBlock)
210 {
211 cyclePos_ = 0; // task is fully drained (quota == total)
212 launchTask();
213 }
214 }
215 i += chunk;
216 }
217 }
218
220 void processInPlace(T* data, int numSamples) noexcept
221 {
222 process(data, data, numSamples);
223 }
224
229 void processBlock(AudioBufferView<T> buffer) noexcept
230 {
231 if (buffer.getNumChannels() > 0 && buffer.getNumSamples() > 0)
232 processInPlace(buffer.getChannel(0), buffer.getNumSamples());
233 }
234
236 [[nodiscard]] static constexpr int getLatency() noexcept { return 0; }
237
239 [[nodiscard]] int getHeadSize() const noexcept { return headSize_; }
240
242 [[nodiscard]] int getNumTailPartitions() const noexcept { return numTailParts_; }
243
244private:
245 static constexpr int kTailBlock = 1024;
246 static constexpr int kTailStart = 2 * kTailBlock;
247 static constexpr int kTailSpec = 2 * kTailBlock + 2;
248 static constexpr int kTailOutSize = 8 * kTailBlock;
249 static constexpr int kTailOutMask = kTailOutSize - 1;
250
252 [[nodiscard]] T headDot() const noexcept
253 {
254 const int64_t start = absPos_ - headLen_ + 1;
255 const int s0 = static_cast<int>(start & inMask_);
256 const int ringSize = inMask_ + 1;
257 const int first = std::min(headLen_, ringSize - s0);
258
259 T y = simd::dotProduct(headRev_.data(), &inRing_[static_cast<size_t>(s0)], first);
260 if (first < headLen_)
261 y += simd::dotProduct(headRev_.data() + first, inRing_.data(), headLen_ - first);
262 return y;
263 }
264
266 void launchTask() noexcept
267 {
268 taskStart_ = absPos_;
269 fdlIndex_ = (fdlIndex_ + 1) % numTailParts_;
270 taskPhase_ = 0;
271 }
272
281 void runTaskUnits(int target) noexcept
282 {
283 target = std::min(target, totalUnits_);
284 while (taskPhase_ < target)
285 {
286 const int unit = taskPhase_++;
287 if (unit == 0)
288 {
289 const int64_t blockStart = taskStart_ - kTailBlock;
290 const int s0 = static_cast<int>(blockStart & inMask_);
291 const int ringSize = inMask_ + 1;
292 const int first = std::min(kTailBlock, ringSize - s0);
293 std::copy_n(&inRing_[static_cast<size_t>(s0)], first, tailScratch_.begin());
294 if (first < kTailBlock)
295 std::copy_n(inRing_.data(), kTailBlock - first, tailScratch_.begin() + first);
296 std::fill(tailScratch_.begin() + kTailBlock, tailScratch_.end(), T(0));
297
298 tailFft_->forward(tailScratch_.data(),
299 &tailFdl_[static_cast<size_t>(fdlIndex_) * kTailSpec]);
300 std::fill(tailAccum_.begin(), tailAccum_.end(), T(0));
301 }
302 else if (unit <= numTailParts_)
303 {
304 const int p = unit - 1;
305 const int slot = (fdlIndex_ - p + numTailParts_) % numTailParts_;
306 simd::complexMulAccum(tailAccum_.data(),
307 &tailFdl_[static_cast<size_t>(slot) * kTailSpec],
308 &tailIR_[static_cast<size_t>(p) * kTailSpec],
309 kTailBlock + 1);
310 }
311 else if (unit == numTailParts_ + 1)
312 {
313 tailFft_->inverse(tailAccum_.data(), tailScratch_.data());
314 }
315 else
316 {
317 const int64_t dst = taskStart_ + kTailBlock;
318 for (int m = 0; m < 2 * kTailBlock; ++m)
319 tailOut_[static_cast<size_t>((dst + m) & kTailOutMask)]
320 += tailScratch_[static_cast<size_t>(m)];
321 }
322 }
323 }
324
325 // -- Members -----------------------------------------------------------------
326 bool prepared_ = false;
327
328 int headSize_ = 128;
329 int headLen_ = 0;
330 std::vector<T> headRev_;
331
332 bool hasMid_ = false;
333 Convolver<T> mid_;
334 std::vector<T> midScratch_;
335
336 bool hasTail_ = false;
337 int numTailParts_ = 0;
338 int totalUnits_ = 0;
339 std::unique_ptr<FFTReal<T>> tailFft_;
340 std::vector<T> tailIR_;
341 std::vector<T> tailFdl_;
342 std::vector<T> tailAccum_;
343 std::vector<T> tailScratch_;
344 std::vector<T> tailOut_;
345
346 std::vector<T> inRing_;
347 int inMask_ = 0;
348 int64_t absPos_ = 0;
349
350 int cyclePos_ = 0;
351 int fdlIndex_ = 0;
352 int64_t taskStart_ = 0;
353 int taskPhase_ = 0;
354};
355
356} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
FFT-based partitioned convolution for long impulse responses.
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
Gardner-style non-uniform partitioned convolver (zero latency, flat CPU).
void processBlock(AudioBufferView< T > buffer) noexcept
Processes channel 0 of a buffer in-place (unified API). Mono engine – use one instance per channel fo...
int getHeadSize() const noexcept
Resolved direct-head length in samples.
void process(const T *input, T *output, int numSamples) noexcept
Convolves out-of-place. Works with any numSamples, even 1.
int getNumTailPartitions() const noexcept
Number of FFT partitions in the time-distributed tail.
void prepare(const T *irData, int irLength, int headSize=128)
Prepares the convolver with an impulse response (allocates).
static constexpr int getLatency() noexcept
Zero – that is the point.
void processInPlace(T *data, int numSamples) noexcept
Convolves in-place.
void reset() noexcept
Clears all signal state (keeps the IR). Safe on the audio thread.
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
float dotProduct(const float *DSPARK_RESTRICT a, const float *DSPARK_RESTRICT b, int count) noexcept
Computes the dot product of two arrays.
Definition SimdOps.h:419
Main namespace for the DSPark framework.