DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
SpectralProcessor.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
43#include "FFT.h"
44#include "WindowFunctions.h"
45#include "AudioSpec.h"
46#include "AudioBuffer.h"
47#include "DenormalGuard.h"
48
49#include <algorithm>
50#include <cassert>
51#include <cmath>
52#include <cstddef>
53#include <memory>
54#include <vector>
55
56namespace dspark {
57
81template <typename T>
83{
84public:
85 // -- Lifecycle -----------------------------------------------------------
86
104 void prepare(const AudioSpec& spec, int fftSize = 2048, int hopSize = 0)
105 {
106 if (!(spec.sampleRate > 0.0) || spec.numChannels < 1) return;
107
108 // Disarm first: if an allocation below throws, the processor is a
109 // safe no-op instead of running half-mutated state (basic guarantee).
110 prepared_ = false;
111 spec_ = spec;
112
113 assert((fftSize & (fftSize - 1)) == 0 && fftSize >= kMinFftSize
114 && "FFT size must be a power of 2");
115 // Release-safe: clamp, then round UP to a power of two (never down,
116 // so a requested resolution is always met). The clamp bounds the
117 // loop; kMaxFftSize is itself a power of two, so no overflow.
118 fftSize = std::clamp(fftSize, kMinFftSize, kMaxFftSize);
119 int pow2 = kMinFftSize;
120 while (pow2 < fftSize) pow2 <<= 1;
121 fftSize_ = pow2;
122 mask_ = fftSize_ - 1;
123
124 hopSize_ = (hopSize > 0) ? hopSize : fftSize_ / 2;
125 hopSize_ = std::clamp(hopSize_, 1, fftSize_ / 2);
126 if (fftSize_ % hopSize_ != 0) hopSize_ = fftSize_ / 2;
127 numBins_ = fftSize_ / 2 + 1;
128
129 fft_ = std::make_unique<FFTReal<T>>(fftSize_);
130
131 // WOLA window: periodic sqrt-Hann (analysis and synthesis).
132 window_.resize(static_cast<std::size_t>(fftSize_));
133 WindowFunctions<T>::hann(window_.data(), fftSize_, true);
134 for (auto& w : window_) w = std::sqrt(w);
135
136 computeWolaNorm();
137
138 const int nCh = spec.numChannels;
139 inputRing_.resize(static_cast<std::size_t>(nCh));
140 outputAccum_.resize(static_cast<std::size_t>(nCh));
141 inputPos_.assign(static_cast<std::size_t>(nCh), 0);
142 outputReadPos_.assign(static_cast<std::size_t>(nCh), 0);
143
144 // The overlap-add accumulator holds twice the FFT size (also pow2).
145 accumMask_ = (fftSize_ * 2) - 1;
146
147 for (int ch = 0; ch < nCh; ++ch)
148 {
149 inputRing_[static_cast<std::size_t>(ch)]
150 .assign(static_cast<std::size_t>(fftSize_), T(0));
151 outputAccum_[static_cast<std::size_t>(ch)]
152 .assign(static_cast<std::size_t>(fftSize_ * 2), T(0));
153 }
154
155 // Plain vectors are fine here: the FFT kernels are unaligned-safe
156 // by design, so no aligned allocator is required.
157 fftIn_.resize(static_cast<std::size_t>(fftSize_));
158 fftOut_.resize(static_cast<std::size_t>(fftSize_ + 2));
159 fftResult_.resize(static_cast<std::size_t>(fftSize_));
160
161 hopCounter_ = 0;
162 prepared_ = true;
163 }
164
178 template <typename Func>
179 void processBlock(AudioBufferView<T> buffer, Func&& processFunc) noexcept
180 {
181 if (!prepared_) return;
182 DenormalGuard guard;
183
184 const int nCh = std::min(buffer.getNumChannels(),
185 static_cast<int>(inputRing_.size()));
186 const int nS = buffer.getNumSamples();
187
188 // Chunked processing between hop boundaries. Order per chunk:
189 // push input, drain output, then fire the hop. Draining BEFORE the
190 // hop is what anchors every frame to its absolute stream position
191 // (the accumulator write position equals sample index m*hop at the
192 // m-th hop no matter how the caller chops the stream); it is also
193 // what makes the pipeline in-place safe (input is read into the
194 // ring before the drain overwrites it).
195 int i = 0;
196 while (i < nS)
197 {
198 const int chunk = std::min(nS - i, hopSize_ - hopCounter_);
199
200 // 1. Push the input chunk into the ring buffers (two contiguous
201 // spans instead of a per-sample masked loop).
202 for (int ch = 0; ch < nCh; ++ch)
203 {
204 const T* data = buffer.getChannel(ch) + i;
205 auto& ring = inputRing_[static_cast<std::size_t>(ch)];
206 const int wp = inputPos_[static_cast<std::size_t>(ch)];
207 const int first = std::min(chunk, fftSize_ - wp);
208 std::copy_n(data, first, ring.data() + wp);
209 std::copy_n(data + first, chunk - first, ring.data());
210 inputPos_[static_cast<std::size_t>(ch)] = (wp + chunk) & mask_;
211 }
212
213 // 2. Drain the overlap-add accumulator for the chunk (read and
214 // clear in one pass, two contiguous spans).
215 for (int ch = 0; ch < nCh; ++ch)
216 {
217 T* data = buffer.getChannel(ch) + i;
218 T* acc = outputAccum_[static_cast<std::size_t>(ch)].data();
219 const int rp = outputReadPos_[static_cast<std::size_t>(ch)];
220 const int first = std::min(chunk, fftSize_ * 2 - rp);
221 for (int k = 0; k < first; ++k)
222 {
223 data[k] = acc[rp + k];
224 acc[rp + k] = T(0);
225 }
226 for (int k = first; k < chunk; ++k)
227 {
228 data[k] = acc[k - first];
229 acc[k - first] = T(0);
230 }
231 outputReadPos_[static_cast<std::size_t>(ch)] = (rp + chunk) & accumMask_;
232 }
233
234 // 3. Fire the STFT hop at the boundary.
235 hopCounter_ += chunk;
236 if (hopCounter_ >= hopSize_)
237 {
238 hopCounter_ = 0;
239 for (int ch = 0; ch < nCh; ++ch)
240 processHop(ch, processFunc);
241 }
242
243 i += chunk;
244 }
245 }
246
247 // -- Queries -------------------------------------------------------------
248
254 [[nodiscard]] int getLatency() const noexcept { return fftSize_; }
255 [[nodiscard]] int getFFTSize() const noexcept { return fftSize_; }
256 [[nodiscard]] int getNumBins() const noexcept { return numBins_; }
257 [[nodiscard]] int getHopSize() const noexcept { return hopSize_; }
258
260 void reset() noexcept
261 {
262 for (auto& ring : inputRing_) std::fill(ring.begin(), ring.end(), T(0));
263 for (auto& acc : outputAccum_) std::fill(acc.begin(), acc.end(), T(0));
264 std::fill(inputPos_.begin(), inputPos_.end(), 0);
265 std::fill(outputReadPos_.begin(), outputReadPos_.end(), 0);
266 hopCounter_ = 0;
267 }
268
269private:
278 template <typename Func>
279 void processHop(int ch, Func& processFunc) noexcept
280 {
281 // 1. Copy from ring buffer with the analysis window, as two
282 // contiguous multiply spans (auto-vectorizable).
283 const T* ring = inputRing_[static_cast<std::size_t>(ch)].data();
284 const T* win = window_.data();
285 const int readPos = inputPos_[static_cast<std::size_t>(ch)];
286 const int head = fftSize_ - readPos;
287 for (int k = 0; k < head; ++k)
288 fftIn_[static_cast<std::size_t>(k)] = ring[readPos + k] * win[k];
289 for (int k = head; k < fftSize_; ++k)
290 fftIn_[static_cast<std::size_t>(k)] = ring[k - head] * win[k];
291
292 // 2. Forward FFT
293 fft_->forward(fftIn_.data(), fftOut_.data());
294
295 // 3. User callback (inlined directly here)
296 processFunc(fftOut_.data(), numBins_);
297
298 // 4. Inverse FFT (FFTReal::inverse already applies the 1/N
299 // normalization, so the analysis->synthesis round-trip is unity
300 // before windowing).
301 fft_->inverse(fftOut_.data(), fftResult_.data());
302
303 // 5. Synthesis window and overlap-add, two contiguous spans.
304 T* acc = outputAccum_[static_cast<std::size_t>(ch)].data();
305 const T* res = fftResult_.data();
306 const int writePos = outputReadPos_[static_cast<std::size_t>(ch)];
307 const int headOut = std::min(fftSize_, fftSize_ * 2 - writePos);
308 for (int k = 0; k < headOut; ++k)
309 acc[writePos + k] += res[k] * win[k] * wolaNorm_;
310 for (int k = headOut; k < fftSize_; ++k)
311 acc[k - headOut] += res[k] * win[k] * wolaNorm_;
312 }
313
322 void computeWolaNorm() noexcept
323 {
324 const int numOverlaps = fftSize_ / hopSize_;
325 T maxSum = T(0);
326 for (int pos = 0; pos < hopSize_; ++pos)
327 {
328 T sumSq = T(0);
329 for (int hop = 0; hop < numOverlaps; ++hop)
330 {
331 const int idx = pos + hop * hopSize_;
332 if (idx < fftSize_)
333 {
334 const T w = window_[static_cast<std::size_t>(idx)];
335 sumSq += w * w;
336 }
337 }
338 if (sumSq > maxSum) maxSum = sumSq;
339 }
340 wolaNorm_ = (maxSum > T(1e-10)) ? (T(1) / maxSum) : T(1);
341 }
342
343 // -- Members -------------------------------------------------------------
344 static constexpr int kMinFftSize = 4;
345 static constexpr int kMaxFftSize = 1 << 20;
346
347 AudioSpec spec_ {};
348 bool prepared_ = false;
349
350 int fftSize_ = 2048;
351 int hopSize_ = 1024;
352 int numBins_ = 1025;
353 int mask_ = 2047; // Bitwise mask for fftSize_ bounds
354 int accumMask_ = 4095; // Bitwise mask for fftSize_*2 bounds
355
356 std::unique_ptr<FFTReal<T>> fft_;
357 std::vector<T> window_;
358 T wolaNorm_ = T(1);
359
360 std::vector<std::vector<T>> inputRing_;
361 std::vector<std::vector<T>> outputAccum_;
362 std::vector<int> inputPos_;
363 std::vector<int> outputReadPos_;
364 int hopCounter_ = 0;
365
366 // Shared work buffers for sequential channel processing
367 std::vector<T> fftIn_, fftOut_, fftResult_;
368};
369
370} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
RAII scope guard that disables denormal (subnormal) float arithmetic.
Fast Fourier Transform (Cooley-Tukey radix-2) with SIMD acceleration.
Standard window functions for spectral analysis and FIR filter design.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
High-performance STFT analysis-modification-synthesis pipeline.
int getNumBins() const noexcept
int getLatency() const noexcept
I/O latency in samples: exactly one FFT frame.
void prepare(const AudioSpec &spec, int fftSize=2048, int hopSize=0)
Allocates buffers and prepares the WOLA processing state.
int getFFTSize() const noexcept
void processBlock(AudioBufferView< T > buffer, Func &&processFunc) noexcept
Processes a block of audio through the STFT pipeline.
void reset() noexcept
Clears all internal delay lines and state variables.
int getHopSize() const noexcept
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static void hann(T *output, int size, bool periodic=true) noexcept
Hann (raised cosine) window.