DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
FFT.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
28#if defined(_M_AMD64) || defined(_M_X64) || defined(__x86_64__) || defined(__amd64__)
29 #define DSPARK_FFT_SSE 1
30 #include <pmmintrin.h> // SSE3 _mm_addsub_* (guarded below) + SSE2 baseline
31#elif defined(__aarch64__) || defined(_M_ARM64)
32 #define DSPARK_FFT_NEON 1
33 #include <arm_neon.h>
34#endif
35
36#include <cassert>
37#include <cmath>
38#include <cstdint>
39#include <cstring>
40#include <numbers>
41#include <type_traits>
42#include <utility>
43#include <vector>
44
45// --- Exception policy --------------------------------------------------------
46// By default invalid FFT sizes throw std::invalid_argument. For embedded and
47// plugin builds compiled without exception support, define DSPARK_NO_EXCEPTIONS
48// (auto-detected from -fno-exceptions / /EHs-c-) -- invalid sizes then assert in
49// debug and degrade to a safe minimal size in release instead of throwing.
50#if !defined(DSPARK_NO_EXCEPTIONS)
51 #if (defined(__GNUC__) || defined(__clang__)) && !defined(__EXCEPTIONS)
52 #define DSPARK_NO_EXCEPTIONS 1
53 #elif defined(_MSC_VER) && !defined(_CPPUNWIND)
54 #define DSPARK_NO_EXCEPTIONS 1
55 #endif
56#endif
57
58#if !defined(DSPARK_NO_EXCEPTIONS)
59 #include <stdexcept>
60#endif
61
62namespace dspark {
63
64// ============================================================================
65// FFTComplex
66// ============================================================================
67
76template <typename T>
78{
79public:
86 explicit FFTComplex(size_t size)
87 : size_(size)
88 {
89 if (size < 2 || (size & (size - 1)) != 0)
90 {
91#if defined(DSPARK_NO_EXCEPTIONS)
92 assert(false && "FFTComplex size must be a power of two >= 2");
93 size_ = 2; // degrade to the minimal valid size
94#else
95 throw std::invalid_argument("FFTComplex size must be a power of two >= 2");
96#endif
97 }
98
99 computeTwiddles();
100 computeBitReversalTable();
101 }
102
107 [[nodiscard]] size_t getSize() const noexcept { return size_; }
108
118 void forward(T* data) noexcept
119 {
120 bitReverse(data);
121 butterflyPass(data, false);
122 }
123
129 void inverse(T* data) noexcept
130 {
131 bitReverse(data);
132 butterflyPass(data, true);
133
134 const T invN = T(1) / static_cast<T>(size_);
135 const size_t total = size_ * 2;
136 for (size_t i = 0; i < total; ++i)
137 data[i] *= invN;
138 }
139
140private:
141#ifdef DSPARK_FFT_SSE
142 // _mm_addsub_* is an SSE3 instruction; baseline x86-64 (the Linux/GCC
143 // default) is SSE2 only. MSVC exposes the intrinsic unconditionally; on
144 // other compilers without -msse3 fall back to negate-even-lanes + add,
145 // which is bit-identical (IEEE negation is exact).
146 static inline __m128 addsubPs(__m128 a, __m128 b) noexcept
147 {
148#if defined(__SSE3__) || defined(_MSC_VER)
149 return _mm_addsub_ps(a, b);
150#else
151 return _mm_add_ps(a, _mm_xor_ps(b, _mm_setr_ps(-0.0f, 0.0f, -0.0f, 0.0f)));
152#endif
153 }
154 static inline __m128d addsubPd(__m128d a, __m128d b) noexcept
155 {
156#if defined(__SSE3__) || defined(_MSC_VER)
157 return _mm_addsub_pd(a, b);
158#else
159 return _mm_add_pd(a, _mm_xor_pd(b, _mm_setr_pd(-0.0, 0.0)));
160#endif
161 }
162#endif // DSPARK_FFT_SSE
163
164 void computeTwiddles()
165 {
166 twiddles_.clear();
167 twiddles_.reserve((size_ - 1) * 2);
168 size_t numStages = 0;
169 for (size_t s = size_; s > 1; s >>= 1) ++numStages;
170
171 // Stage 1 (stride 2, k = 0) contributes the pair (1, -0). butterflyPass
172 // runs that stage as a dedicated multiply-free loop, but the pair is
173 // still stored so every later stage keeps its natural offset.
174 size_t stride = 2;
175 for (size_t stage = 0; stage < numStages; ++stage)
176 {
177 size_t halfStride = stride / 2;
178 for (size_t k = 0; k < halfStride; ++k)
179 {
180 double angle = -2.0 * std::numbers::pi_v<double> * static_cast<double>(k)
181 / static_cast<double>(stride);
182 twiddles_.push_back(static_cast<T>(std::cos(angle)));
183 twiddles_.push_back(static_cast<T>(std::sin(angle)));
184 }
185 stride *= 2;
186 }
187 }
188
189 void computeBitReversalTable()
190 {
191 bitrev_.resize(size_);
192 size_t bits = 0;
193 for (size_t s = size_; s > 1; s >>= 1) ++bits;
194
195 for (size_t i = 0; i < size_; ++i)
196 {
197 size_t rev = 0;
198 size_t val = i;
199 for (size_t b = 0; b < bits; ++b)
200 {
201 rev = (rev << 1) | (val & 1);
202 val >>= 1;
203 }
204 bitrev_[i] = rev;
205 }
206 }
207
208 void bitReverse(T* data) const noexcept
209 {
210 for (size_t i = 0; i < size_; ++i)
211 {
212 size_t j = bitrev_[i];
213 if (i < j)
214 {
215 std::swap(data[2 * i], data[2 * j]);
216 std::swap(data[2 * i + 1], data[2 * j + 1]);
217 }
218 }
219 }
220
221 void butterflyPass(T* data, bool isInverse) const noexcept
222 {
223 // --- Stage 1 (stride 2): twiddle is exactly (1, -0), so t = o. A
224 // dedicated pass removes one complex multiply per butterfly from the
225 // stage with the most butterflies (N/2). Identical for the inverse
226 // (conj(1) == 1). The adjacent add/sub pattern with contiguous loads
227 // auto-vectorises (it is not a reduction).
228 for (size_t group = 0; group < size_; group += 2)
229 {
230 const size_t eIdx = 2 * group;
231 const size_t oIdx = eIdx + 2;
232
233 const T tr = data[oIdx];
234 const T ti = data[oIdx + 1];
235 data[oIdx] = data[eIdx] - tr;
236 data[oIdx + 1] = data[eIdx + 1] - ti;
237 data[eIdx] += tr;
238 data[eIdx + 1] += ti;
239 }
240
241 // --- Remaining stages (stride 4 .. N), with real twiddle multiplies.
242 size_t twiddleOffset = 2; // skip stage 1's stored (1, -0) pair
243 size_t stride = 4;
244
245 while (stride <= size_)
246 {
247 size_t halfStride = stride / 2;
248
249 for (size_t group = 0; group < size_; group += stride)
250 {
251 size_t k = 0;
252
253 // --- SIMD path: float, 2 butterflies at a time ----------------
254#ifdef DSPARK_FFT_SSE
255 if constexpr (std::is_same_v<T, float>)
256 {
257 __m128 invTwMask = _mm_setzero_ps();
258 if (isInverse)
259 {
260 alignas(16) static constexpr float kInvTw[4] = { 0.0f, -0.0f, 0.0f, -0.0f };
261 invTwMask = _mm_load_ps(kInvTw); // safe: alignas(16)
262 }
263
264 for (; k + 1 < halfStride; k += 2)
265 {
266 const size_t twIdx = twiddleOffset + k * 2;
267 const size_t eIdx = 2 * (group + k);
268 const size_t oIdx = 2 * (group + k + halfStride);
269
270 // Unaligned loads -- `data` and `twiddles_` come from
271 // std::vector<float>, whose data pointer is only
272 // guaranteed 4-byte aligned for float. Aligned loads
273 // here would fault; _mm_loadu_ps costs the same as the
274 // aligned form on Sandy Bridge and newer.
275 __m128 e = _mm_loadu_ps(&data[eIdx]);
276 __m128 o = _mm_loadu_ps(&data[oIdx]);
277 __m128 w = _mm_xor_ps(_mm_loadu_ps(&twiddles_[twIdx]), invTwMask);
278
279 __m128 o_re = _mm_shuffle_ps(o, o, _MM_SHUFFLE(2,2,0,0));
280 __m128 o_im = _mm_shuffle_ps(o, o, _MM_SHUFFLE(3,3,1,1));
281 __m128 w_sw = _mm_shuffle_ps(w, w, _MM_SHUFFLE(2,3,0,1));
282
283 __m128 p1 = _mm_mul_ps(w, o_re);
284 __m128 p2 = _mm_mul_ps(w_sw, o_im);
285
286 // addsub processes the complex multiply:
287 // Even: p1 - p2 (Real part) | Odd: p1 + p2 (Imaginary part)
288 __m128 t = addsubPs(p1, p2);
289
290 _mm_storeu_ps(&data[eIdx], _mm_add_ps(e, t));
291 _mm_storeu_ps(&data[oIdx], _mm_sub_ps(e, t));
292 }
293 }
294
295 // Double path: one complex value per __m128d vector. The same
296 // addsub trick as the float path, ~1.5x over scalar.
297 if constexpr (std::is_same_v<T, double>)
298 {
299 __m128d invTwMaskD = _mm_setzero_pd();
300 if (isInverse)
301 {
302 alignas(16) static constexpr double kInvTwD[2] = { 0.0, -0.0 };
303 invTwMaskD = _mm_load_pd(kInvTwD);
304 }
305
306 for (; k < halfStride; ++k)
307 {
308 const size_t twIdx = twiddleOffset + k * 2;
309 const size_t eIdx = 2 * (group + k);
310 const size_t oIdx = 2 * (group + k + halfStride);
311
312 __m128d e = _mm_loadu_pd(&data[eIdx]);
313 __m128d o = _mm_loadu_pd(&data[oIdx]);
314 __m128d w = _mm_xor_pd(_mm_loadu_pd(&twiddles_[twIdx]), invTwMaskD);
315
316 __m128d w_re = _mm_unpacklo_pd(w, w); // [wr, wr]
317 __m128d w_im = _mm_unpackhi_pd(w, w); // [wi, wi]
318 __m128d o_sw = _mm_shuffle_pd(o, o, 1); // [oi, or]
319
320 __m128d p1 = _mm_mul_pd(w_re, o); // [wr*or, wr*oi]
321 __m128d p2 = _mm_mul_pd(w_im, o_sw); // [wi*oi, wi*or]
322 __m128d t = addsubPd(p1, p2); // [Re, Im] of w*o
323
324 _mm_storeu_pd(&data[eIdx], _mm_add_pd(e, t));
325 _mm_storeu_pd(&data[oIdx], _mm_sub_pd(e, t));
326 }
327 }
328#endif // DSPARK_FFT_SSE
329
330#ifdef DSPARK_FFT_NEON
331 if constexpr (std::is_same_v<T, float>)
332 {
333 static constexpr uint32_t kAddSub[4] = { 0x80000000u, 0, 0x80000000u, 0 };
334 const uint32x4_t addsubMask = vld1q_u32(kAddSub);
335
336 static constexpr uint32_t kInvTw[4] = { 0, 0x80000000u, 0, 0x80000000u };
337 const uint32x4_t invTwMask = isInverse ? vld1q_u32(kInvTw) : vdupq_n_u32(0);
338
339 for (; k + 1 < halfStride; k += 2)
340 {
341 const size_t twIdx = twiddleOffset + k * 2;
342 const size_t eIdx = 2 * (group + k);
343 const size_t oIdx = 2 * (group + k + halfStride);
344
345 float32x4_t e = vld1q_f32(&data[eIdx]);
346 float32x4_t o = vld1q_f32(&data[oIdx]);
347 float32x4_t w = vreinterpretq_f32_u32(veorq_u32(
348 vreinterpretq_u32_f32(vld1q_f32(&twiddles_[twIdx])), invTwMask));
349
350 float32x4_t o_re = vtrn1q_f32(o, o);
351 float32x4_t o_im = vtrn2q_f32(o, o);
352 float32x4_t w_sw = vrev64q_f32(w);
353 float32x4_t p1 = vmulq_f32(w, o_re);
354 float32x4_t p2 = vmulq_f32(w_sw, o_im);
355 float32x4_t t = vaddq_f32(p1,
356 vreinterpretq_f32_u32(veorq_u32(vreinterpretq_u32_f32(p2), addsubMask)));
357
358 vst1q_f32(&data[eIdx], vaddq_f32(e, t));
359 vst1q_f32(&data[oIdx], vsubq_f32(e, t));
360 }
361 }
362#endif // DSPARK_FFT_NEON
363
364 // --- Scalar path: remainder + double + non-SIMD platforms -----
365 for (; k < halfStride; ++k)
366 {
367 size_t idx = twiddleOffset + k * 2;
368 T wr = twiddles_[idx];
369 T wi = twiddles_[idx + 1];
370
371 if (isInverse) wi = -wi;
372
373 size_t evenIdx = 2 * (group + k);
374 size_t oddIdx = 2 * (group + k + halfStride);
375
376 T tr = wr * data[oddIdx] - wi * data[oddIdx + 1];
377 T ti = wr * data[oddIdx + 1] + wi * data[oddIdx];
378
379 data[oddIdx] = data[evenIdx] - tr;
380 data[oddIdx + 1] = data[evenIdx + 1] - ti;
381 data[evenIdx] += tr;
382 data[evenIdx + 1] += ti;
383 }
384 }
385
386 twiddleOffset += halfStride * 2;
387 stride *= 2;
388 }
389 }
390
391 size_t size_;
392 std::vector<T> twiddles_;
393 std::vector<size_t> bitrev_;
394};
395
396// ============================================================================
397// FFTReal
398// ============================================================================
399
417template <typename T>
419{
420public:
427 explicit FFTReal(size_t size)
428 : realSize_(validateSize(size)) // validates BEFORE complexFFT_ is built
429 , halfSize_(realSize_ / 2) // derived from the VALIDATED size, so the
430 , complexFFT_(realSize_ / 2) // trio stays coherent when size degrades
431 {
432 computePostTwiddles();
433 workBuffer_.resize(realSize_);
434 }
435
437 [[nodiscard]] size_t getSize() const noexcept { return realSize_; }
438
440 [[nodiscard]] size_t getFrequencyDomainSize() const noexcept { return realSize_ + 2; }
441
443 [[nodiscard]] size_t getNumBins() const noexcept { return halfSize_ + 1; }
444
450 void forward(const T* timeData, T* freqData) noexcept
451 {
452 T* work = workBuffer_.data();
453 // Consecutive (even, odd) sample pairs feed the half-size complex FFT
454 // as (re, im) -- a straight contiguous copy of N elements.
455 std::memcpy(work, timeData, realSize_ * sizeof(T));
456
457 complexFFT_.forward(work);
458 unpackForward(work, freqData);
459 }
460
466 void inverse(const T* freqData, T* timeData) noexcept
467 {
468 T* work = workBuffer_.data();
469 packInverse(freqData, work);
470 complexFFT_.inverse(work);
471
472 std::memcpy(timeData, work, realSize_ * sizeof(T));
473 }
474
480 void computeMagnitudes(const T* freqData, T* magnitudes) const noexcept
481 {
482 for (size_t k = 0; k <= halfSize_; ++k)
483 {
484 T re = freqData[2 * k];
485 T im = freqData[2 * k + 1];
486 magnitudes[k] = std::sqrt(re * re + im * im);
487 }
488 }
489
495 void computePhases(const T* freqData, T* phases) const noexcept
496 {
497 for (size_t k = 0; k <= halfSize_; ++k)
498 {
499 T re = freqData[2 * k];
500 T im = freqData[2 * k + 1];
501 phases[k] = std::atan2(im, re);
502 }
503 }
504
510 void computePowerSpectrum(const T* freqData, T* power) const noexcept
511 {
512 for (size_t k = 0; k <= halfSize_; ++k)
513 {
514 T re = freqData[2 * k];
515 T im = freqData[2 * k + 1];
516 power[k] = re * re + im * im;
517 }
518 }
519
527 [[nodiscard]] static T binToFrequency(size_t binIndex, double sampleRate, size_t fftSize) noexcept
528 {
529 return static_cast<T>(static_cast<double>(binIndex) * sampleRate / static_cast<double>(fftSize));
530 }
531
539 [[nodiscard]] static size_t frequencyToBin(double frequency, double sampleRate, size_t fftSize) noexcept
540 {
541 return static_cast<size_t>(std::round(frequency * static_cast<double>(fftSize) / sampleRate));
542 }
543
544private:
549 static size_t validateSize(size_t size)
550 {
551 if (size < 4 || (size & (size - 1)) != 0)
552 {
553#if defined(DSPARK_NO_EXCEPTIONS)
554 assert(false && "FFTReal size must be a power of two >= 4");
555 return 4;
556#else
557 throw std::invalid_argument("FFTReal size must be a power of two >= 4");
558#endif
559 }
560 return size;
561 }
562
563 void computePostTwiddles()
564 {
565 postTwiddles_.resize(halfSize_ * 2);
566 for (size_t k = 0; k < halfSize_; ++k)
567 {
568 double angle = -2.0 * std::numbers::pi_v<double> * static_cast<double>(k)
569 / static_cast<double>(realSize_);
570 postTwiddles_[2 * k] = static_cast<T>(std::cos(angle));
571 postTwiddles_[2 * k + 1] = static_cast<T>(std::sin(angle));
572 }
573 }
574
575 void unpackForward(const T* halfFFT, T* fullSpectrum) const noexcept
576 {
577 const size_t N2 = halfSize_;
578
579 T dcRe = halfFFT[0] + halfFFT[1];
580 T nyRe = halfFFT[0] - halfFFT[1];
581
582 fullSpectrum[0] = dcRe;
583 fullSpectrum[1] = T(0);
584 fullSpectrum[2 * N2] = nyRe;
585 fullSpectrum[2 * N2 + 1] = T(0);
586
587 for (size_t k = 1; k < N2; ++k)
588 {
589 size_t kConj = N2 - k;
590
591 T hkRe = halfFFT[2 * k];
592 T hkIm = halfFFT[2 * k + 1];
593 T hcRe = halfFFT[2 * kConj];
594 T hcIm = halfFFT[2 * kConj + 1];
595
596 T xeRe = T(0.5) * (hkRe + hcRe);
597 T xeIm = T(0.5) * (hkIm - hcIm);
598
599 T xoRe = T(0.5) * (hkRe - hcRe);
600 T xoIm = T(0.5) * (hkIm + hcIm);
601
602 T wr = postTwiddles_[2 * k];
603 T wi = postTwiddles_[2 * k + 1];
604
605 T joRe = xoIm;
606 T joIm = -xoRe;
607
608 T twRe = wr * joRe - wi * joIm;
609 T twIm = wr * joIm + wi * joRe;
610
611 fullSpectrum[2 * k] = xeRe + twRe;
612 fullSpectrum[2 * k + 1] = xeIm + twIm;
613 }
614 }
615
616 void packInverse(const T* fullSpectrum, T* halfFFT) const noexcept
617 {
618 const size_t N2 = halfSize_;
619
620 T dcRe = fullSpectrum[0];
621 T nyRe = fullSpectrum[2 * N2];
622
623 halfFFT[0] = T(0.5) * (dcRe + nyRe);
624 halfFFT[1] = T(0.5) * (dcRe - nyRe);
625
626 for (size_t k = 1; k < N2; ++k)
627 {
628 size_t kConj = N2 - k;
629
630 T xkRe = fullSpectrum[2 * k];
631 T xkIm = fullSpectrum[2 * k + 1];
632 T xcRe = fullSpectrum[2 * kConj];
633 T xcIm = fullSpectrum[2 * kConj + 1];
634
635 T xeRe = T(0.5) * (xkRe + xcRe);
636 T xeIm = T(0.5) * (xkIm - xcIm);
637
638 T diffRe = T(0.5) * (xkRe - xcRe);
639 T diffIm = T(0.5) * (xkIm + xcIm);
640
641 T wr = postTwiddles_[2 * k];
642 T wi = -postTwiddles_[2 * k + 1];
643
644 T twRe = wr * diffRe - wi * diffIm;
645 T twIm = wr * diffIm + wi * diffRe;
646
647 T xoRe = -twIm;
648 T xoIm = twRe;
649
650 halfFFT[2 * k] = xeRe + xoRe;
651 halfFFT[2 * k + 1] = xeIm + xoIm;
652 }
653 }
654
655 size_t realSize_;
656 size_t halfSize_;
657 FFTComplex<T> complexFFT_;
658 std::vector<T> postTwiddles_;
659 std::vector<T> workBuffer_;
660};
661
662} // namespace dspark
In-place Cooley-Tukey radix-2 DIT FFT for complex data.
Definition FFT.h:78
FFTComplex(size_t size)
Constructs an FFT processor for the given size.
Definition FFT.h:86
size_t getSize() const noexcept
Returns the FFT size (number of complex points).
Definition FFT.h:107
void forward(T *data) noexcept
Performs a forward (time->frequency) FFT in-place.
Definition FFT.h:118
void inverse(T *data) noexcept
Performs an inverse (frequency->time) FFT in-place.
Definition FFT.h:129
FFT optimised for real-valued input signals (the common audio case).
Definition FFT.h:419
static size_t frequencyToBin(double frequency, double sampleRate, size_t fftSize) noexcept
Returns the bin index closest to a given frequency.
Definition FFT.h:539
void computePhases(const T *freqData, T *phases) const noexcept
Computes the phase angle of each frequency bin.
Definition FFT.h:495
size_t getNumBins() const noexcept
Returns the number of frequency bins (N/2 + 1) including DC and Nyquist.
Definition FFT.h:443
void computePowerSpectrum(const T *freqData, T *power) const noexcept
Computes the power spectrum (magnitude squared) of each bin.
Definition FFT.h:510
void forward(const T *timeData, T *freqData) noexcept
Forward transform: real time-domain -> complex frequency-domain.
Definition FFT.h:450
FFTReal(size_t size)
Constructs a real FFT processor.
Definition FFT.h:427
void inverse(const T *freqData, T *timeData) noexcept
Inverse transform: complex frequency-domain -> real time-domain.
Definition FFT.h:466
size_t getFrequencyDomainSize() const noexcept
Returns the frequency-domain buffer size in elements (N + 2).
Definition FFT.h:440
static T binToFrequency(size_t binIndex, double sampleRate, size_t fftSize) noexcept
Returns the frequency in Hz corresponding to a given bin index.
Definition FFT.h:527
size_t getSize() const noexcept
Returns the number of real input samples (N).
Definition FFT.h:437
void computeMagnitudes(const T *freqData, T *magnitudes) const noexcept
Computes the magnitude of each frequency bin.
Definition FFT.h:480
Main namespace for the DSPark framework.