Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
fft.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <cassert>
8#include <cmath>
9#include <complex>
10#include <cstddef>
11#include <cstdint>
12#include <cstring>
13#include <memory>
14#include <numbers>
15#include <type_traits>
16#include <vector>
17
18// Backend selection. Float transforms run on vDSP on Apple and on muFFT
19// everywhere else (the pulp-signal-mufft target defines PULP_FFT_HAS_MUFFT
20// when muFFT compiles for the target; configure falls back without it).
21// FftT<double> keeps the built-in radix-2 transform below on every platform,
22// as do float sizes muFFT does not plan and builds without muFFT.
23// PULP_FFT_FORCE_PORTABLE and PULP_FFT_FORCE_BUILTIN select a backend for one
24// test binary, so every backend is testable on any host; they must be set
25// for a whole binary, never per translation unit.
26#if defined(__APPLE__) && !defined(PULP_FFT_FORCE_PORTABLE)
27#include <Accelerate/Accelerate.h>
28#define PULP_FFT_HAS_VDSP 1
29#endif
30
31#if !defined(PULP_FFT_HAS_VDSP) && defined(PULP_FFT_HAS_MUFFT) && PULP_FFT_HAS_MUFFT && \
32 !defined(PULP_FFT_FORCE_BUILTIN)
33#define PULP_FFT_USE_MUFFT 1
34// The C shim in core/signal/src/fft_mufft.c. A plan owns muFFT's four 1-D
35// plans for one size and two staging buffers of 2n + 2 floats aligned for
36// the kernels. The execute calls need `in` and `out` aligned to
37// pulp_mufft_alignment() and non-overlapping, and allocate nothing. Create
38// returns null for sizes muFFT does not plan (below 16 points, or not a power
39// of two) and on allocation failure. pulp_mufft_allocations() counts every
40// allocation the backend has made, inside muFFT included.
41extern "C" {
42struct pulp_mufft_plan;
43pulp_mufft_plan* pulp_mufft_create(int n);
44void pulp_mufft_destroy(pulp_mufft_plan* plan);
45float* pulp_mufft_in(pulp_mufft_plan* plan);
46float* pulp_mufft_out(pulp_mufft_plan* plan);
47void pulp_mufft_complex(pulp_mufft_plan* plan, int forward, float* out, const float* in);
48void pulp_mufft_real_forward(pulp_mufft_plan* plan, float* out, const float* in);
49void pulp_mufft_real_inverse(pulp_mufft_plan* plan, float* out, const float* in);
50std::size_t pulp_mufft_alignment(void);
51std::uint64_t pulp_mufft_retained_bytes(std::uint64_t n);
52std::uint64_t pulp_mufft_largest_allocation_bytes(std::uint64_t n);
53std::uint64_t pulp_mufft_allocations(void);
54}
55#else
56#define PULP_FFT_USE_MUFFT 0
57#endif
58
59namespace pulp::signal {
60
61namespace detail {
62
63inline constexpr bool is_power_of_two(int value) noexcept {
64 return value > 0 && (value & (value - 1)) == 0;
65}
66
67inline constexpr std::size_t fft_twiddle_index(int phase_index, int stride) noexcept {
68 const auto index = static_cast<std::size_t>(phase_index);
69 const auto step = static_cast<std::size_t>(stride);
70 return index * step;
71}
72
73struct FftRealSplit;
74
75} // namespace detail
76
83inline constexpr std::uint64_t kVdspFftSetupChargeBytesPerPoint = 64;
84
89inline constexpr std::uint64_t kVdspSplitPaddingFloats = 84;
90
91template <typename SampleType>
92inline bool checked_fft_platform_setup_bytes(std::uint64_t fft_size,
93 std::uint64_t target_max_bytes,
94 std::uint64_t& bytes) noexcept {
95#if PULP_FFT_HAS_VDSP
96 if constexpr (std::is_same_v<SampleType, float>)
98 target_max_bytes, bytes);
99#endif
100#if PULP_FFT_USE_MUFFT
101 if constexpr (std::is_same_v<SampleType, float>) {
102 // The plan is several allocations: each must fit the per-allocation
103 // ceiling, and their sum is the retained charge.
104 bytes = pulp_mufft_retained_bytes(fft_size);
105 return pulp_mufft_largest_allocation_bytes(fft_size) <= target_max_bytes;
106 }
107#endif
108 bytes = 0;
109 return true;
110}
111
112namespace detail {
113
117template <typename SampleType>
118inline constexpr bool fft_real_split_needs_scratch(std::uint64_t fft_size) noexcept {
119#if PULP_FFT_HAS_VDSP
120 if constexpr (std::is_same_v<SampleType, float>)
121 return fft_size < 4u;
122#endif
123 static_cast<void>(fft_size);
124 return true;
125}
126
127} // namespace detail
128
132template <typename SampleType>
133inline bool checked_fft_retained_bytes(std::uint64_t fft_size,
134 std::uint64_t target_max_bytes,
135 std::uint64_t& bytes) noexcept {
136 CheckedRetainedByteCharge charge(target_max_bytes);
137 std::uint64_t platform_setup_bytes = 0;
138 if (!charge.add<std::complex<double>>(fft_size / 2u)
139 || !checked_fft_platform_setup_bytes<SampleType>(fft_size, target_max_bytes,
140 platform_setup_bytes)
141 || !charge.add_retained_bytes(platform_setup_bytes))
142 return false;
143#if PULP_FFT_HAS_VDSP
144 if constexpr (std::is_same_v<SampleType, float>) {
145 if (fft_size > (UINT64_MAX - kVdspSplitPaddingFloats) / 2u ||
146 !charge.add<float>(2u * fft_size + kVdspSplitPaddingFloats))
147 return false;
148 }
149#endif
151 !charge.add<std::complex<SampleType>>(fft_size / 2u + 1u))
152 return false;
153 bytes = charge.total();
154 return true;
155}
156
157// Radix-2 FFT — in-place, decimation-in-time.
158// Size must be a power of 2.
159// Float transforms use vDSP on Apple and muFFT elsewhere (see the backend
160// note at the top of this file); the radix-2 code below serves double
161// precision, sizes under 16 points off Apple, and builds without muFFT.
162//
163// Real-input signals have Hermitian spectra, so `forward_real_half()` and
164// `inverse_real_half()` move only the N/2 + 1 independent bins (DC through
165// Nyquist) and run an N/2-point complex transform underneath; a caller-chosen
166// scale is folded into the pass that packs the result, so no separate
167// normalisation sweep runs. `forward_real()` keeps its full mirrored N-bin
168// output for callers that index the upper half.
169//
170// RT contract: construction/destruction allocate or release twiddle/vDSP
171// storage and are prepare/control-thread work. forward(), inverse(),
172// forward_real(), forward_real_half(), inverse_real_half(), magnitude(),
173// magnitude_db(), and size() are allocation-free after construction when callers provide
174// valid input/output buffers.
175template <typename SampleType = float>
176class FftT {
177 static_assert(std::is_floating_point_v<SampleType>,
178 "FftT requires a floating-point sample type");
179public:
180 FftT() = default;
181
182 // Non-copyable (vDSP_setup handle), movable
183 FftT(const FftT&) = delete;
184 FftT& operator=(const FftT&) = delete;
185
186 FftT(FftT&& other) noexcept
187 : size_(other.size_), twiddles_(std::move(other.twiddles_)),
188 real_scratch_(std::move(other.real_scratch_))
189#if PULP_FFT_HAS_VDSP
190 ,
191 log2n_(other.log2n_), vdsp_setup_(other.vdsp_setup_), split_(std::move(other.split_)),
192 split_base_(other.split_base_)
193#endif
194#if PULP_FFT_USE_MUFFT
195 ,
196 mufft_(other.mufft_), mufft_alignment_(other.mufft_alignment_)
197#endif
198 {
199#if PULP_FFT_HAS_VDSP
200 other.vdsp_setup_ = nullptr; // Prevent double-free
201#endif
202#if PULP_FFT_USE_MUFFT
203 other.mufft_ = nullptr;
204#endif
205 other.size_ = 0;
206 }
207
208 FftT& operator=(FftT&& other) noexcept {
209 if (this != &other) {
210#if PULP_FFT_HAS_VDSP
211 if (vdsp_setup_) vDSP_destroy_fftsetup(vdsp_setup_);
212 vdsp_setup_ = other.vdsp_setup_;
213 other.vdsp_setup_ = nullptr;
214 log2n_ = other.log2n_;
215 split_ = std::move(other.split_);
216 split_base_ = other.split_base_;
217#endif
218#if PULP_FFT_USE_MUFFT
219 if (mufft_)
220 pulp_mufft_destroy(mufft_);
221 mufft_ = other.mufft_;
222 mufft_alignment_ = other.mufft_alignment_;
223 other.mufft_ = nullptr;
224#endif
225 size_ = other.size_;
226 other.size_ = 0;
227 twiddles_ = std::move(other.twiddles_);
228 real_scratch_ = std::move(other.real_scratch_);
229 }
230 return *this;
231 }
232
233 explicit FftT(int size) : size_(size) {
234 // Every implementation below is radix-2, including vDSP and muFFT.
235 // Reject invalid geometry before allocating twiddles or backend
236 // storage; otherwise a non-power-of-two size can report ready() and
237 // silently run a truncated log2 transform.
239 size_ = 0;
240 return;
241 }
242#if PULP_FFT_HAS_VDSP
243 // Use vDSP for FFT — much faster for large sizes
244 if constexpr (std::is_same_v<SampleType, float>) {
245 log2n_ = 0;
246 for (int n = size; n > 1; n >>= 1) ++log2n_;
247 split_.resize(2 * static_cast<std::size_t>(size) + kVdspSplitPaddingFloats);
248 // First element on a 64-byte boundary; vector storage moves with
249 // the vector, so the offset stays valid across moves.
250 const auto address = reinterpret_cast<std::uintptr_t>(split_.data());
251 split_base_ = ((64u - address % 64u) % 64u) / sizeof(float);
252 }
253#endif
254 // Pre-compute twiddle factors (used as fallback on non-Apple)
255 twiddles_.resize(size / 2);
256 for (int i = 0; i < size / 2; ++i) {
257 double angle = -2.0 * std::numbers::pi * i / size;
258 twiddles_[i] = {std::cos(angle), std::sin(angle)};
259 }
260 if (size > 0 &&
261 detail::fft_real_split_needs_scratch<SampleType>(static_cast<std::uint64_t>(size)))
262 real_scratch_.resize(static_cast<std::size_t>(size / 2 + 1));
264 if constexpr (std::is_same_v<SampleType, float>) {
265 // Keep raw setup acquisition last: every vector allocation and
266 // twiddle population that can throw has completed, so constructor
267 // unwinding cannot bypass destruction of an acquired setup.
268 vdsp_setup_ = vDSP_create_fftsetup(log2n_, kFFTRadix2);
269 }
270#endif
271#if PULP_FFT_USE_MUFFT
272 // Last, for the same reason as the vDSP setup. A null plan leaves the
273 // transform on the built-in path, which is complete for every size.
274 if constexpr (std::is_same_v<SampleType, float>) {
275 mufft_alignment_ = pulp_mufft_alignment();
276 mufft_ = pulp_mufft_create(size);
277 }
278#endif
279 }
280
282#if PULP_FFT_HAS_VDSP
283 if (vdsp_setup_) vDSP_destroy_fftsetup(vdsp_setup_);
284#endif
285#if PULP_FFT_USE_MUFFT
286 if (mufft_)
287 pulp_mufft_destroy(mufft_);
288#endif
289 }
290
291 int size() const { return size_; }
292
296 [[nodiscard]] bool ready() const noexcept {
297 if (size_ <= 0 || twiddles_.size() != static_cast<std::size_t>(size_ / 2))
298 return false;
299 if (detail::fft_real_split_needs_scratch<SampleType>(static_cast<std::uint64_t>(size_)) &&
300 real_scratch_.size() != static_cast<std::size_t>(size_ / 2 + 1))
301 return false;
302#if PULP_FFT_HAS_VDSP
303 if constexpr (std::is_same_v<SampleType, float>) {
304 return vdsp_setup_ != nullptr &&
305 split_.size() == 2 * static_cast<std::size_t>(size_) + kVdspSplitPaddingFloats;
306 }
307#endif
308 return true;
309 }
310
315 [[nodiscard]] int vdsp_split_plane_gap_floats(bool real_transform) const noexcept {
316#if PULP_FFT_HAS_VDSP
317 if constexpr (std::is_same_v<SampleType, float>) {
318 if (!ready())
319 return -1;
320 const DSPSplitComplex z = real_transform ? real_split() : split();
321 const int half = real_transform ? size_ / 2 : size_;
322 return static_cast<int>(z.imagp - z.realp) - half;
323 }
324#endif
325 static_cast<void>(real_transform);
326 return -1;
327 }
328
334 [[nodiscard]] int vdsp_split_offset_bytes(bool real_transform) const noexcept {
335#if PULP_FFT_HAS_VDSP
336 if constexpr (std::is_same_v<SampleType, float>) {
337 if (!ready())
338 return -1;
339 const DSPSplitComplex z = real_transform ? real_split() : split();
340 const auto re = static_cast<int>(reinterpret_cast<std::uintptr_t>(z.realp) % 64u);
341 const auto im = static_cast<int>(reinterpret_cast<std::uintptr_t>(z.imagp) % 64u);
342 return re == im ? re : -2;
343 }
344#endif
345 static_cast<void>(real_transform);
346 return -1;
347 }
348
349 // Forward FFT (time → frequency) — complex in-place
350 void forward(std::complex<SampleType>* data) const {
352#if PULP_FFT_USE_MUFFT
353 if constexpr (std::is_same_v<SampleType, float>) {
354 if (mufft_) {
355 complex_mufft(data, true, 1.0f);
356 return;
357 }
358 }
359#endif
360#if PULP_FFT_HAS_VDSP
361 if constexpr (std::is_same_v<SampleType, float>) {
362 forward_vdsp(data);
363 } else {
364 forward_fallback(data);
365 }
366#else
367 forward_fallback(data);
368#endif
369 }
370
371 // Inverse FFT (frequency → time) — complex in-place
372 void inverse(std::complex<SampleType>* data) const {
374#if PULP_FFT_USE_MUFFT
375 if constexpr (std::is_same_v<SampleType, float>) {
376 if (mufft_) {
377 complex_mufft(data, false, 1.0f / static_cast<float>(size_));
378 return;
379 }
380 }
381#endif
382#if PULP_FFT_HAS_VDSP
383 if constexpr (std::is_same_v<SampleType, float>) {
384 inverse_vdsp(data);
385 } else {
386 inverse_fallback(data);
387 }
388#else
389 inverse_fallback(data);
390#endif
391 }
392
393 // Real-valued forward FFT: real input → complex output
394 void forward_real(const SampleType* input, std::complex<SampleType>* output) const {
396#if PULP_FFT_USE_MUFFT
397 if constexpr (std::is_same_v<SampleType, float>) {
398 if (mufft_) {
399 forward_real_full_mufft(input, output);
400 return;
401 }
402 }
403#endif
404#if PULP_FFT_HAS_VDSP
405 if constexpr (std::is_same_v<SampleType, float>) {
406 if (size_ >= kVdspMinRealSize) {
407 forward_real_vdsp(input, output);
408 return;
409 }
410 forward_real_fallback(input, output);
411 } else {
412 forward_real_fallback(input, output);
413 }
414#else
415 forward_real_fallback(input, output);
416#endif
417 }
418
420 static constexpr int kMinRealSize = 2;
421
423 int real_bins() const {
424 return size_ / 2 + 1;
425 }
426
433 void forward_real_half(const SampleType* input, std::complex<SampleType>* output,
434 SampleType scale) const {
436 assert(size_ >= kMinRealSize && "real transforms need at least two points");
437#if PULP_FFT_USE_MUFFT
438 if constexpr (std::is_same_v<SampleType, float>) {
439 if (mufft_) {
440 forward_real_half_mufft(input, output, scale);
441 return;
442 }
443 }
444#endif
445#if PULP_FFT_HAS_VDSP
446 if constexpr (std::is_same_v<SampleType, float>) {
447 if (size_ >= kVdspMinRealSize) {
448 forward_real_half_vdsp(input, output, scale);
449 return;
450 }
451 }
452#endif
453 forward_real_half_fallback(input, output, scale);
454 }
455
456 void forward_real_half(const SampleType* input, std::complex<SampleType>* output) const {
457 forward_real_half(input, output, SampleType{1});
458 }
459
469 void inverse_real_half(const std::complex<SampleType>* input, SampleType* output,
470 SampleType scale) const {
472 assert(size_ >= kMinRealSize && "real transforms need at least two points");
473#if PULP_FFT_USE_MUFFT
474 if constexpr (std::is_same_v<SampleType, float>) {
475 if (mufft_) {
476 inverse_real_half_mufft(input, output, scale);
477 return;
478 }
479 }
480#endif
481#if PULP_FFT_HAS_VDSP
482 if constexpr (std::is_same_v<SampleType, float>) {
483 if (size_ >= kVdspMinRealSize) {
484 inverse_real_half_vdsp(input, output, scale);
485 return;
486 }
487 }
488#endif
489 inverse_real_half_fallback(input, output, scale);
490 }
491
492 void inverse_real_half(const std::complex<SampleType>* input, SampleType* output) const {
493 inverse_real_half(input, output, SampleType{1} / static_cast<SampleType>(size_));
494 }
495
496 // Compute magnitude spectrum in dB
497 void magnitude_db(const std::complex<SampleType>* freq,
498 SampleType* out,
499 int num_bins) const {
500 for (int i = 0; i < num_bins; ++i) {
501 SampleType mag = std::abs(freq[i]);
502 out[i] = SampleType{20.0f} *
503 std::log10(std::max(mag, SampleType{1e-10f}));
504 }
505 }
506
507 // Compute magnitude spectrum (linear)
508 void magnitude(const std::complex<SampleType>* freq,
509 SampleType* out,
510 int num_bins) const {
511 for (int i = 0; i < num_bins; ++i) {
512 out[i] = std::abs(freq[i]);
513 }
514 }
515
516 private:
517 friend struct detail::FftRealSplit;
518
525 void forward_real_split(const SampleType* input, SampleType* re, SampleType* im,
526 SampleType scale) const {
528 assert(size_ >= kMinRealSize && "real transforms need at least two points");
529#if PULP_FFT_USE_MUFFT
530 if constexpr (std::is_same_v<SampleType, float>) {
531 if (mufft_) {
532 forward_real_split_mufft(input, re, im, scale);
533 return;
534 }
535 }
536#endif
537#if PULP_FFT_HAS_VDSP
538 if constexpr (std::is_same_v<SampleType, float>) {
539 if (size_ >= kVdspMinRealSize) {
540 forward_real_split_vdsp(input, re, im, scale);
541 return;
542 }
543 }
544#endif
545 forward_real_split_fallback(input, re, im, scale);
546 }
547
553 void inverse_real_split(const SampleType* re, const SampleType* im, SampleType* output,
554 SampleType scale) const {
556 assert(size_ >= kMinRealSize && "real transforms need at least two points");
557#if PULP_FFT_USE_MUFFT
558 if constexpr (std::is_same_v<SampleType, float>) {
559 if (mufft_) {
560 inverse_real_split_mufft(re, im, output, scale);
561 return;
562 }
563 }
564#endif
565#if PULP_FFT_HAS_VDSP
566 if constexpr (std::is_same_v<SampleType, float>) {
567 if (size_ >= kVdspMinRealSize) {
568 inverse_real_split_vdsp(re, im, output, scale);
569 return;
570 }
571 }
572#endif
573 inverse_real_split_fallback(re, im, output, scale);
574 }
575
576 int size_ = 0;
577 std::vector<std::complex<double>> twiddles_;
578 // real_bins() bins for the portable split-spectrum path, which builds
579 // the interleaved half spectrum first; empty where vDSP serves it.
580 mutable std::vector<std::complex<SampleType>> real_scratch_;
581#if PULP_FFT_USE_MUFFT
582 // Owned muFFT plan; null for double precision and for sizes muFFT does
583 // not plan, which take the built-in transform.
584 pulp_mufft_plan* mufft_ = nullptr;
585 std::size_t mufft_alignment_ = 1;
586#endif
587#if PULP_FFT_HAS_VDSP
588 int log2n_ = 0;
589 FFTSetup vdsp_setup_ = nullptr;
590 // One buffer holds both split halves so their placement is fixed rather
591 // than left to the allocator. vDSP takes a separate code path when both
592 // halves of a split complex array sit on 64-byte boundaries, and that
593 // path is the less accurate one: from 512 points up it rounds 0.4-5.3 dB
594 // worse (worst bin error relative to the largest bin, against a
595 // long-double DFT; about -131.6 to -137.6 dB on the boundary against
596 // -135.8 to -141.4 dB off it) on every processor measured: real and
597 // complex transforms on M1 Max, M5 Max and M5 Ultra, real transforms on
598 // M3 Ultra. Both halves therefore always sit 16 bytes past a 64-byte
599 // boundary.
600 //
601 // The cost of that choice depends on the processor. Forward transforms in
602 // this buffer layout, at low priority on shared machines:
603 // - M5 Max and M5 Ultra (paired, interleaved; loaded): off the boundary
604 // is the faster path from 512 to 8192 points, by 1.08-2.16x for real
605 // transforms and 1.15-2.5x for complex ones (3.5-4.4x for complex at
606 // 256); the two are within 0.1% at 256 real points, and at 16384 the
607 // boundary path takes 0.93-1.14x the time.
608 // - M1 Max (paired, interleaved; background QoS): the boundary path is
609 // faster from 2048 real and 1024 complex points, by 1.17-1.75x and
610 // 1.19-2.1x.
611 // - M3 Ultra (best of five, one session, load not recorded): the
612 // boundary path is faster from 1024 real and 512 complex points, by
613 // 1.06-1.42x and 1.18-1.59x.
614 // On M1 Max and M3 Ultra, then, large transforms give up that much speed
615 // for the accuracy above.
616 //
617 // Separate allocations made the same transform's speed and error depend
618 // on where the buffers happened to land.
619 //
620 // The imaginary half starts 64 floats (256 bytes) after the real half
621 // ends, not directly after it. Directly after puts the halves a power of
622 // two apart, and once that distance reaches 64 KB (complex transforms
623 // from 16384 points, real ones from 32768, whose halves are N/2 long)
624 // the two streams contend and the transform slows sharply. With the gap,
625 // forward transforms take 0.53-0.87x the time on M5 Max and 0.46-0.78x on
626 // M1 Max at complex 16384-131072 and real 32768-131072 points (paired,
627 // interleaved; loaded), the complex inverse 0.48-0.88x, with the error
628 // unchanged in every cell. Smaller transforms are unaffected (0.92-1.11x,
629 // within noise). The gap is a whole number of 64-byte lines, so both
630 // halves keep the 16-byte offset above.
631 mutable std::vector<float> split_;
632 std::size_t split_base_ = 0;
633 static constexpr std::size_t kVdspSplitOffsetFloats = 4; // 16 bytes
634 static constexpr std::size_t kVdspSplitPlaneGapFloats = 64; // 256 bytes
635 static_assert(kVdspSplitPlaneGapFloats % (64 / sizeof(float)) == 0,
636 "the plane gap must be whole 64-byte lines to keep both halves' offset");
637 static_assert(kVdspSplitPaddingFloats >=
638 64 / sizeof(float) - 1 + kVdspSplitOffsetFloats + kVdspSplitPlaneGapFloats,
639 "split padding must cover the 64-byte alignment slack, the 16-byte step "
640 "and the gap between the halves");
641#endif
642
643#if PULP_FFT_HAS_VDSP
644 // N-point halves for complex transforms, each 16 bytes past a 64-byte
645 // boundary (N is a power of two and the gap is whole lines, so the second
646 // half keeps the offset), with the plane gap between them.
647 DSPSplitComplex split() const {
648 float* base = split_.data() + split_base_ + kVdspSplitOffsetFloats;
649 return {base, base + size_ + kVdspSplitPlaneGapFloats};
650 }
651
652 // N/2-point halves for real transforms, placed like split()'s.
653 DSPSplitComplex real_split() const {
654 float* base = split_.data() + split_base_ + kVdspSplitOffsetFloats;
655 return {base, base + size_ / 2 + kVdspSplitPlaneGapFloats};
656 }
657
658 void forward_vdsp(std::complex<float>* data) const {
659 DSPSplitComplex z = split();
660 const auto n = static_cast<vDSP_Length>(size_);
661 vDSP_ctoz(reinterpret_cast<const DSPComplex*>(data), 2, &z, 1, n);
662 vDSP_fft_zip(vdsp_setup_, &z, 1, log2n_, kFFTDirection_Forward);
663 vDSP_ztoc(&z, 1, reinterpret_cast<DSPComplex*>(data), 2, n);
664 }
665
666 void inverse_vdsp(std::complex<float>* data) const {
667 DSPSplitComplex z = split();
668 vDSP_ctoz(reinterpret_cast<const DSPComplex*>(data), 2, &z, 1,
669 static_cast<vDSP_Length>(size_));
670 vDSP_fft_zip(vdsp_setup_, &z, 1, log2n_, kFFTDirection_Inverse);
671 // vDSP's inverse is unnormalised; fold 1/N into the interleave.
672 interleave_scaled(z.realp, z.imagp, reinterpret_cast<float*>(data), size_,
673 1.0f / static_cast<float>(size_));
674 }
675
676 // out[2k] = scale * re[k], out[2k + 1] = scale * im[k]: one pass, which
677 // the compiler turns into interleaving vector stores.
678 static void interleave_scaled(const float* re, const float* im, float* out, int count,
679 float scale) {
680 for (int k = 0; k < count; ++k) {
681 out[2 * k] = scale * re[k];
682 out[2 * k + 1] = scale * im[k];
683 }
684 }
685
686 // Packs the real input as N/2 complex pairs (even samples real, odd
687 // imaginary) and runs vDSP's real transform, whose result is twice the
688 // DFT with Nyquist's real part parked in imagp[0].
689 void forward_real_packed_vdsp(const float* input) const {
690 DSPSplitComplex z = real_split();
691 vDSP_ctoz(reinterpret_cast<const DSPComplex*>(input), 2, &z, 1,
692 static_cast<vDSP_Length>(size_ / 2));
693 vDSP_fft_zrip(vdsp_setup_, &z, 1, log2n_, kFFTDirection_Forward);
694 }
695
696 void forward_real_half_vdsp(const float* input, std::complex<float>* output,
697 float scale) const {
698 forward_real_packed_vdsp(input);
699 const int half = size_ / 2;
700 const float s = 0.5f * scale;
701 auto* out = reinterpret_cast<float*>(output);
702 const DSPSplitComplex z = real_split();
703 interleave_scaled(z.realp, z.imagp, out, half, s);
704 out[2 * half] = s * z.imagp[0]; // Nyquist
705 out[2 * half + 1] = 0.0f;
706 out[1] = 0.0f; // DC has no imaginary part
707 }
708
709 void inverse_real_half_vdsp(const std::complex<float>* input, float* output,
710 float scale) const {
711 const int half = size_ / 2;
712 const auto* in = reinterpret_cast<const float*>(input);
713 DSPSplitComplex z = real_split();
714 float* re = z.realp;
715 float* im = z.imagp;
716 for (int k = 0; k < half; ++k) {
717 re[k] = in[2 * k];
718 im[k] = in[2 * k + 1];
719 }
720 im[0] = in[2 * half]; // Nyquist's real part rides in imagp[0]
721 vDSP_fft_zrip(vdsp_setup_, &z, 1, log2n_, kFFTDirection_Inverse);
722 // The packed inverse of the DFT is N times the signal, split as even
723 // samples in realp and odd samples in imagp.
724 interleave_scaled(re, im, output, half, scale);
725 }
726
727 void forward_real_vdsp(const float* input, std::complex<float>* output) const {
728 forward_real_packed_vdsp(input);
729 // vDSP_fft_zrip packs result: realp[0] = DC, imagp[0] = Nyquist, and
730 // carries an implicit 2x scale. Unpack, mirror, and scale in one pass.
731 const int half = size_ / 2;
732 const DSPSplitComplex z = real_split();
733 output[0] = {z.realp[0] * 0.5f, 0.0f};
734 output[half] = {z.imagp[0] * 0.5f, 0.0f};
735 for (int i = 1; i < half; ++i) {
736 const float re = z.realp[i] * 0.5f;
737 const float im = z.imagp[i] * 0.5f;
738 output[i] = {re, im};
739 // Conjugate symmetry: X[N-k] = conj(X[k])
740 output[size_ - i] = {re, -im};
741 }
742 }
743
744 // vDSP's packed real transform already has the split layout; only its 2x
745 // gain is folded into the copy out of the placed buffer.
746 void forward_real_split_vdsp(const float* input, float* re, float* im, float scale) const {
747 forward_real_packed_vdsp(input);
748 const DSPSplitComplex z = real_split();
749 const float s = 0.5f * scale;
750 const auto half = static_cast<vDSP_Length>(size_ / 2);
751 vDSP_vsmul(z.realp, 1, &s, re, 1, half);
752 vDSP_vsmul(z.imagp, 1, &s, im, 1, half);
753 }
754
755 void inverse_real_split_vdsp(const float* re, const float* im, float* output,
756 float scale) const {
757 const int half = size_ / 2;
758 DSPSplitComplex z = real_split();
759 std::copy_n(re, half, z.realp);
760 std::copy_n(im, half, z.imagp);
761 vDSP_fft_zrip(vdsp_setup_, &z, 1, log2n_, kFFTDirection_Inverse);
762 if (scale == 1.0f)
763 vDSP_ztoc(&z, 1, reinterpret_cast<DSPComplex*>(output), 2,
764 static_cast<vDSP_Length>(half));
765 else
766 interleave_scaled(z.realp, z.imagp, output, half, scale);
767 }
768#endif
769
770#if PULP_FFT_USE_MUFFT
771 // muFFT needs aligned, non-overlapping input and output. A caller buffer
772 // that qualifies is handed to it directly; one that does not is staged
773 // through the plan's buffers. The caller's scale is folded into whichever
774 // pass touches the result anyway, so FftT's in-place, any-alignment and
775 // overlapping-output contracts are unchanged.
776 bool mufft_aligned(const void* p) const noexcept {
777 return reinterpret_cast<std::uintptr_t>(p) % mufft_alignment_ == 0;
778 }
779
780 static bool mufft_disjoint(const float* a, std::size_t a_count, const float* b,
781 std::size_t b_count) noexcept {
782 const auto a0 = reinterpret_cast<std::uintptr_t>(a);
783 const auto b0 = reinterpret_cast<std::uintptr_t>(b);
784 return a0 + a_count * sizeof(float) <= b0 || b0 + b_count * sizeof(float) <= a0;
785 }
786
787 static void mufft_scale(float* data, std::size_t count, float scale) noexcept {
788 if (scale != 1.0f)
789 for (std::size_t i = 0; i < count; ++i)
790 data[i] *= scale;
791 }
792
793 // In place: muFFT cannot read and write one buffer, so the input is
794 // always staged; an aligned caller buffer then receives the output.
795 void complex_mufft(std::complex<float>* data, bool forward, float scale) const {
796 const auto floats = 2 * static_cast<std::size_t>(size_);
797 auto* dst = reinterpret_cast<float*>(data);
798 float* in = pulp_mufft_in(mufft_);
799 std::memcpy(in, dst, floats * sizeof(float));
800 if (mufft_aligned(dst)) {
801 pulp_mufft_complex(mufft_, forward ? 1 : 0, dst, in);
802 mufft_scale(dst, floats, scale);
803 return;
804 }
805 const float* out = pulp_mufft_out(mufft_);
806 pulp_mufft_complex(mufft_, forward ? 1 : 0, pulp_mufft_out(mufft_), in);
807 for (std::size_t i = 0; i < floats; ++i)
808 dst[i] = scale * out[i];
809 }
810
811 // The real input, read in place when aligned.
812 const float* mufft_real_input(const float* input) const {
813 if (mufft_aligned(input))
814 return input;
815 float* in = pulp_mufft_in(mufft_);
816 std::memcpy(in, input, static_cast<std::size_t>(size_) * sizeof(float));
817 return in;
818 }
819
820 // Runs the forward real transform, writing bins 0..N/2 interleaved and
821 // unscaled into `out` when it qualifies, otherwise into the plan's output
822 // buffer, and returns where they landed.
823 const float* forward_real_bins_mufft(const float* input, float* out) const {
824 const float* in = mufft_real_input(input);
825 const auto bins = 2 * static_cast<std::size_t>(size_ / 2 + 1);
826 float* dst = (out && mufft_aligned(out) &&
827 mufft_disjoint(out, bins, in, static_cast<std::size_t>(size_)))
828 ? out
829 : pulp_mufft_out(mufft_);
830 pulp_mufft_real_forward(mufft_, dst, in);
831 return dst;
832 }
833
834 void forward_real_full_mufft(const float* input, std::complex<float>* output) const {
835 auto* out = reinterpret_cast<float*>(output);
836 const float* bins = forward_real_bins_mufft(input, out);
837 const int half = size_ / 2;
838 if (bins != out)
839 std::memcpy(out, bins, 2 * static_cast<std::size_t>(half + 1) * sizeof(float));
840 out[1] = 0.0f;
841 out[2 * half + 1] = 0.0f;
842 for (int k = 1; k < half; ++k)
843 output[size_ - k] = std::conj(output[k]);
844 }
845
846 void forward_real_half_mufft(const float* input, std::complex<float>* output,
847 float scale) const {
848 auto* out = reinterpret_cast<float*>(output);
849 const float* bins = forward_real_bins_mufft(input, out);
850 const int half = size_ / 2;
851 const auto count = 2 * static_cast<std::size_t>(half + 1);
852 if (bins == out) {
853 mufft_scale(out, count, scale);
854 } else {
855 for (std::size_t i = 0; i < count; ++i)
856 out[i] = scale * bins[i];
857 }
858 out[1] = 0.0f; // DC has no imaginary part
859 out[2 * half + 1] = 0.0f; // nor does Nyquist
860 }
861
862 // Runs the inverse real transform from `in` (N/2 + 1 interleaved bins,
863 // DC and Nyquist imaginary parts zero) and writes scale times the
864 // unnormalised result to `output`, directly when it qualifies.
865 void inverse_real_from_mufft(const float* in, float* output, float scale) const {
866 const auto n = static_cast<std::size_t>(size_);
867 const auto bins = 2 * static_cast<std::size_t>(size_ / 2 + 1);
868 if (mufft_aligned(output) && mufft_disjoint(output, n, in, bins)) {
869 pulp_mufft_real_inverse(mufft_, output, in);
870 mufft_scale(output, n, scale);
871 return;
872 }
873 const float* out = pulp_mufft_out(mufft_);
874 pulp_mufft_real_inverse(mufft_, pulp_mufft_out(mufft_), in);
875 for (std::size_t i = 0; i < n; ++i)
876 output[i] = scale * out[i];
877 }
878
879 void inverse_real_half_mufft(const std::complex<float>* input, float* output,
880 float scale) const {
881 const int half = size_ / 2;
882 const auto* src = reinterpret_cast<const float*>(input);
883 // The caller's bins are used in place when aligned and already
884 // carrying zero DC and Nyquist imaginary parts, which FftT ignores.
885 if (mufft_aligned(src) && src[1] == 0.0f && src[2 * half + 1] == 0.0f) {
886 inverse_real_from_mufft(src, output, scale);
887 return;
888 }
889 float* in = pulp_mufft_in(mufft_);
890 std::memcpy(in, src, 2 * static_cast<std::size_t>(half + 1) * sizeof(float));
891 in[1] = 0.0f;
892 in[2 * half + 1] = 0.0f;
893 inverse_real_from_mufft(in, output, scale);
894 }
895
896 // The packed split layout is not muFFT's, so its bins always pass
897 // through the plan's buffers on one side.
898 void forward_real_split_mufft(const float* input, float* re, float* im, float scale) const {
899 const float* bins = forward_real_bins_mufft(input, nullptr);
900 const int half = size_ / 2;
901 for (int k = 0; k < half; ++k) {
902 re[k] = scale * bins[2 * k];
903 im[k] = scale * bins[2 * k + 1];
904 }
905 im[0] = scale * bins[2 * half]; // Nyquist rides in im[0]
906 }
907
908 void inverse_real_split_mufft(const float* re, const float* im, float* output,
909 float scale) const {
910 const int half = size_ / 2;
911 float* in = pulp_mufft_in(mufft_);
912 for (int k = 0; k < half; ++k) {
913 in[2 * k] = re[k];
914 in[2 * k + 1] = im[k];
915 }
916 in[1] = 0.0f;
917 in[2 * half] = im[0];
918 in[2 * half + 1] = 0.0f;
919 inverse_real_from_mufft(in, output, scale);
920 }
921#endif
922
923 void forward_fallback(std::complex<SampleType>* data) const {
924 transform_fallback(data, size_);
925 }
926
927 // In-place radix-2 transform of `n` points (n divides size_) using the
928 // size_-point twiddle table at stride size_ / n.
929 void transform_fallback(std::complex<SampleType>* data, int n) const {
930 bit_reverse(data, n);
931 for (int len = 2; len <= n; len <<= 1) {
932 int half = len / 2;
933 int step = size_ / len;
934 for (int i = 0; i < n; i += len) {
935 for (int j = 0; j < half; ++j) {
936 auto w = std::complex<SampleType>(
937 twiddles_[detail::fft_twiddle_index(j, step)]);
938 auto u = data[i + j];
939 auto v = data[i + j + half] * w;
940 data[i + j] = u + v;
941 data[i + j + half] = u - v;
942 }
943 }
944 }
945 }
946
947 void inverse_fallback(std::complex<SampleType>* data) const {
948 for (int i = 0; i < size_; ++i) data[i] = std::conj(data[i]);
949 forward_fallback(data);
950 SampleType scale = SampleType{1.0f} / static_cast<SampleType>(size_);
951 for (int i = 0; i < size_; ++i) data[i] = std::conj(data[i]) * scale;
952 }
953
954 void forward_real_fallback(const SampleType* input,
955 std::complex<SampleType>* output) const {
956 if (size_ < 2) {
957 output[0] = {input[0], SampleType{0}};
958 return;
959 }
960 // The _fallback helper, not the public entry point: forward_real()
961 // already counted this transform once.
962 forward_real_half_fallback(input, output, SampleType{1});
963 for (int i = 1; i < size_ / 2; ++i)
964 output[size_ - i] = std::conj(output[i]);
965 }
966
967 // Real transform through an N/2-point complex one: pack z[k] =
968 // x[2k] + i x[2k+1], transform, then separate the even/odd spectra
969 // E, O pairwise and combine X[k] = E[k] + W^k O[k],
970 // X[N/2 - k] = conj(E[k] - W^k O[k]).
971 // vDSP's packed real transform needs two complex points; smaller real
972 // transforms take the portable path.
973 static constexpr int kVdspMinRealSize = 4;
974
975 void forward_real_half_fallback(const SampleType* input, std::complex<SampleType>* output,
976 SampleType scale) const {
977 assert(size_ >= kMinRealSize);
978 const int half = size_ / 2;
979 for (int k = 0; k < half; ++k)
980 output[k] = {input[2 * k], input[2 * k + 1]};
981 transform_fallback(output, half);
982 const SampleType h{0.5f};
983 const auto z0 = output[0];
984 for (int k = 1; k <= half / 2; ++k) {
985 const auto a = output[k];
986 const auto b = std::conj(output[half - k]);
987 const std::complex<SampleType> e = h * (a + b);
988 const std::complex<SampleType> d = h * (a - b);
989 const std::complex<SampleType> o{d.imag(), -d.real()}; // d / i
990 const std::complex<SampleType> w(twiddles_[static_cast<std::size_t>(k)]);
991 const auto wo = w * o;
992 output[k] = scale * (e + wo);
993 if (k != half - k)
994 output[half - k] = scale * std::conj(e - wo);
995 }
996 output[0] = {scale * (z0.real() + z0.imag()), SampleType{0}};
997 output[half] = {scale * (z0.real() - z0.imag()), SampleType{0}};
998 }
999
1000 // Inverse of the above: rebuild Z[k] = E[k] + i O[k] (scaled by 2 * scale,
1001 // which is what turns the N/2-point inverse into scale times the N-point
1002 // one), transform, and read the even/odd samples back out of the complex
1003 // result. Each pair (k, N/2 - k) reads and writes only its own two
1004 // slots, so the spectrum may share storage with the output.
1005 void inverse_real_half_fallback(const std::complex<SampleType>* input, SampleType* output,
1006 SampleType scale) const {
1007 assert(size_ >= kMinRealSize);
1008 const int half = size_ / 2;
1009 const SampleType nyquist = input[half].real();
1010 const SampleType dc = input[0].real();
1011 // N real samples are N/2 complex values in the same storage
1012 // ([complex.numbers] guarantees std::complex<T> is an array of two T),
1013 // which is what lets the N/2-point transform run in the output.
1014 auto* z = reinterpret_cast<std::complex<SampleType>*>(output);
1015 // Build conj(Z) so the forward transform below acts as the inverse.
1016 for (int k = 1; k <= half / 2; ++k) {
1017 const auto xk = input[k];
1018 const auto xm = input[half - k];
1019 const std::complex<SampleType> w(twiddles_[static_cast<std::size_t>(k)]);
1020 // E = X[k] + conj(X[h-k]), O = (X[k] - conj(X[h-k])) conj(W^k);
1021 // the partner uses conj(W^(h-k)) = -W^k.
1022 const auto ek = xk + std::conj(xm);
1023 const auto ok = (xk - std::conj(xm)) * std::conj(w);
1024 const auto em = xm + std::conj(xk);
1025 const auto om = (xm - std::conj(xk)) * (-w);
1026 const std::complex<SampleType> i{SampleType{0}, SampleType{1}};
1027 z[k] = std::conj(scale * (ek + i * ok));
1028 if (k != half - k)
1029 z[half - k] = std::conj(scale * (em + i * om));
1030 }
1031 z[0] = std::conj(std::complex<SampleType>(scale * (dc + nyquist), scale * (dc - nyquist)));
1032 transform_fallback(z, half);
1033 for (int k = 0; k < half; ++k)
1034 z[k] = std::conj(z[k]);
1035 }
1036
1037 void forward_real_split_fallback(const SampleType* input, SampleType* re, SampleType* im,
1038 SampleType scale) const {
1039 std::complex<SampleType>* bins = real_scratch_.data();
1040 forward_real_half_fallback(input, bins, scale);
1041 const int half = size_ / 2;
1042 for (int k = 0; k < half; ++k) {
1043 re[k] = bins[k].real();
1044 im[k] = bins[k].imag();
1045 }
1046 im[0] = bins[half].real();
1047 }
1048
1049 void inverse_real_split_fallback(const SampleType* re, const SampleType* im, SampleType* output,
1050 SampleType scale) const {
1051 std::complex<SampleType>* bins = real_scratch_.data();
1052 const int half = size_ / 2;
1053 for (int k = 0; k < half; ++k)
1054 bins[k] = {re[k], im[k]};
1055 bins[0] = {re[0], SampleType{0}};
1056 bins[half] = {im[0], SampleType{0}};
1057 inverse_real_half_fallback(bins, output, scale);
1058 }
1059
1060 void bit_reverse(std::complex<SampleType>* data) const {
1061 bit_reverse(data, size_);
1062 }
1063
1064 void bit_reverse(std::complex<SampleType>* data, int count) const {
1065 int bits = 0;
1066 for (int n = count; n > 1; n >>= 1)
1067 ++bits;
1068
1069 for (int i = 0; i < count; ++i) {
1070 int j = 0;
1071 for (int b = 0; b < bits; ++b)
1072 if (i & (1 << b)) j |= 1 << (bits - 1 - b);
1073 if (i < j) std::swap(data[i], data[j]);
1074 }
1075 }
1076};
1077
1080
1081namespace detail {
1082
1092 template <typename SampleType>
1093 static void forward(const FftT<SampleType>& fft, const SampleType* input, SampleType* re,
1094 SampleType* im, SampleType scale) {
1095 fft.forward_real_split(input, re, im, scale);
1096 }
1097
1098 template <typename SampleType>
1099 static void inverse(const FftT<SampleType>& fft, const SampleType* re, const SampleType* im,
1100 SampleType* output, SampleType scale) {
1101 fft.inverse_real_split(re, im, output, scale);
1102 }
1103};
1104
1110template <typename SampleType>
1111inline void multiply_packed_split(const SampleType* a_re, const SampleType* a_im,
1112 const SampleType* b_re, const SampleType* b_im,
1113 SampleType* out_re, SampleType* out_im, int half) noexcept {
1114 if (half <= 0)
1115 return;
1116 const SampleType dc = a_re[0] * b_re[0];
1117 const SampleType nyquist = a_im[0] * b_im[0];
1118 for (int k = 0; k < half; ++k) {
1119 const SampleType re = a_re[k] * b_re[k] - a_im[k] * b_im[k];
1120 const SampleType im = a_re[k] * b_im[k] + a_im[k] * b_re[k];
1121 out_re[k] = re;
1122 out_im[k] = im;
1123 }
1124 out_re[0] = dc;
1125 out_im[0] = nyquist;
1126}
1127
1131template <typename SampleType>
1132inline void multiply_accumulate_packed_split(const SampleType* a_re, const SampleType* a_im,
1133 const SampleType* b_re, const SampleType* b_im,
1134 SampleType* acc_re, SampleType* acc_im,
1135 int half) noexcept {
1136 if (half <= 0)
1137 return;
1138 const SampleType dc = acc_re[0] + a_re[0] * b_re[0];
1139 const SampleType nyquist = acc_im[0] + a_im[0] * b_im[0];
1140 for (int k = 0; k < half; ++k) {
1141 acc_re[k] += a_re[k] * b_re[k] - a_im[k] * b_im[k];
1142 acc_im[k] += a_re[k] * b_im[k] + a_im[k] * b_re[k];
1143 }
1144 acc_re[0] = dc;
1145 acc_im[0] = nyquist;
1146}
1147
1148} // namespace detail
1149
1150} // namespace pulp::signal
1151
1152// ConvolverT is declared in its own header; it stays reachable through this
1153// one for code that has always included fft.hpp for it.
Definition checked_allocation.hpp:57
bool add(std::uint64_t elements) noexcept
Definition checked_allocation.hpp:63
std::uint64_t total() const noexcept
Definition checked_allocation.hpp:83
bool add_retained_bytes(std::uint64_t bytes) noexcept
Definition checked_allocation.hpp:78
Definition fft.hpp:176
FftT & operator=(FftT &&other) noexcept
Definition fft.hpp:208
void magnitude(const std::complex< SampleType > *freq, SampleType *out, int num_bins) const
Definition fft.hpp:508
int size() const
Definition fft.hpp:291
~FftT()
Definition fft.hpp:281
void magnitude_db(const std::complex< SampleType > *freq, SampleType *out, int num_bins) const
Definition fft.hpp:497
void forward_real(const SampleType *input, std::complex< SampleType > *output) const
Definition fft.hpp:394
static constexpr int kMinRealSize
Definition fft.hpp:420
void forward_real_half(const SampleType *input, std::complex< SampleType > *output, SampleType scale) const
Real-input forward transform.
Definition fft.hpp:433
void inverse(std::complex< SampleType > *data) const
Definition fft.hpp:372
int real_bins() const
Independent bins of a real transform of size(): DC through Nyquist.
Definition fft.hpp:423
int vdsp_split_offset_bytes(bool real_transform) const noexcept
Byte offset past a 64-byte boundary of the real and imaginary halves the vDSP transforms use,...
Definition fft.hpp:334
void forward(std::complex< SampleType > *data) const
Definition fft.hpp:350
void inverse_real_half(const std::complex< SampleType > *input, SampleType *output, SampleType scale) const
Inverse of forward_real_half().
Definition fft.hpp:469
FftT & operator=(const FftT &)=delete
void forward_real_half(const SampleType *input, std::complex< SampleType > *output) const
Definition fft.hpp:456
void inverse_real_half(const std::complex< SampleType > *input, SampleType *output) const
Definition fft.hpp:492
FftT(FftT &&other) noexcept
Definition fft.hpp:186
bool ready() const noexcept
True when every backend resource required by forward()/inverse() is available.
Definition fft.hpp:296
FftT(const FftT &)=delete
FftT(int size)
Definition fft.hpp:233
int vdsp_split_plane_gap_floats(bool real_transform) const noexcept
Floats between the end of the real half and the start of the imaginary half the vDSP transforms use (...
Definition fft.hpp:315
#define PULP_FFT_HAS_VDSP
Definition fft.hpp:28
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
@ d
Definition pitch.hpp:22
@ b
Definition pitch.hpp:31
Round-2 effect-wave vocabulary.
Definition analysis_frontends.hpp:50
void multiply_packed_split(const SampleType *a_re, const SampleType *a_im, const SampleType *b_re, const SampleType *b_im, SampleType *out_re, SampleType *out_im, int half) noexcept
Product of two packed split spectra of half = size() / 2 slots each (see FftRealSplit): bin k of out ...
Definition fft.hpp:1111
void multiply_accumulate_packed_split(const SampleType *a_re, const SampleType *a_im, const SampleType *b_re, const SampleType *b_im, SampleType *acc_re, SampleType *acc_im, int half) noexcept
acc += a * b over packed split spectra, with the product as in multiply_packed_split().
Definition fft.hpp:1132
constexpr std::size_t fft_twiddle_index(int phase_index, int stride) noexcept
Definition fft.hpp:67
constexpr bool is_power_of_two(int value) noexcept
Definition fft.hpp:63
constexpr bool fft_real_split_needs_scratch(std::uint64_t fft_size) noexcept
Whether a FftT of fft_size points keeps a real_bins()-long complex scratch for the split-spectrum pat...
Definition fft.hpp:118
void count_fft() noexcept
Record one FFT execution.
Definition rt_work_counter.hpp:84
@ half
Definition units.hpp:290
Definition additive_bank.hpp:218
FftT< double > Fft64
Definition fft.hpp:1079
bool checked_fft_retained_bytes(std::uint64_t fft_size, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Complete retained-storage contract for one prepared FftT.
Definition fft.hpp:133
FftT< float > Fft
Definition fft.hpp:1078
constexpr std::uint64_t kVdspFftSetupChargeBytesPerPoint
Conservative policy charge for Accelerate's opaque float FFT setup.
Definition fft.hpp:83
constexpr std::uint64_t kVdspSplitPaddingFloats
Floats a float FftT's vDSP split buffer holds beyond its two N-point halves: up to 15 to reach a 64-b...
Definition fft.hpp:89
bool checked_capacity_product(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &product) noexcept
Definition checked_allocation.hpp:12
bool checked_fft_platform_setup_bytes(std::uint64_t fft_size, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Definition fft.hpp:92
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ o
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ ok
Definition breakpoint_envelope.hpp:36
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ j
Definition input_events.hpp:40
@ n
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ v
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
@ z
Definition input_events.hpp:42
Single-partition overlap-add convolution on the real-input FFT.
Deterministic operation counters for audio-callback cost gates.
The packed split-complex form of a real transform, for Pulp's own frequency-domain stages (the overla...
Definition fft.hpp:1091
static void inverse(const FftT< SampleType > &fft, const SampleType *re, const SampleType *im, SampleType *output, SampleType scale)
Definition fft.hpp:1099
static void forward(const FftT< SampleType > &fft, const SampleType *input, SampleType *re, SampleType *im, SampleType scale)
Definition fft.hpp:1093