FastLED 3.9.15
Loading...
Searching...
No Matches
fft_impl.cpp.hpp
Go to the documentation of this file.
1#ifndef FASTLED_INTERNAL
2#define FASTLED_INTERNAL
3#endif
4
5#include "fl/system/fastled.h"
6
7// IWYU pragma: begin_keep
11
12// IWYU pragma: end_keep
13#include "fl/stl/alloca.h"
14#include "fl/stl/array.h"
15#include "fl/audio/audio.h"
16#include "fl/audio/fft/fft.h"
18#include "fl/stl/string.h"
20#include "fl/math/fixed_point.h"
21#include "fl/stl/vector.h"
22#include "fl/math/alpha.h"
23#include "fl/log/log.h"
24
25#include "fl/stl/cstring.h"
26#include "fl/stl/singleton.h"
27#include "fl/stl/noexcept.h"
28
29#ifndef FL_AUDIO_SAMPLE_RATE
30#define FL_AUDIO_SAMPLE_RATE 44100
31#endif
32
33#ifndef FL_FFT_SAMPLE_RATE
34#define FL_FFT_SAMPLE_RATE FL_AUDIO_SAMPLE_RATE
35#endif
36
37#ifndef FL_FFT_SAMPLES
38#define FL_FFT_SAMPLES 512
39#endif
40
41#ifndef FL_FFT_BANDS
42#define FL_FFT_BANDS 16
43#endif
44
45#ifndef FL_FFT_MIN_VAL
46#define FL_FFT_MIN_VAL 5000 // Equivalent to 0.15 in Q15
47#endif
48
49#ifndef FL_FFT_PRINT_HEADER
50#define FL_FFT_PRINT_HEADER 1
51#endif
52
53namespace fl {
54namespace audio {
55namespace fft {
56
57 // Delegates to Args::resolve() — single source of truth.
58
59class Context {
60 public:
61 Context(int samples, int bands, float fmin, float fmax, int sample_rate,
62 Mode mode, Window window)
63 : mFftrCfg(nullptr), mInputSamples(samples),
64 mKernels(nullptr),
65 mMode(mode), mTotalBands(bands), mFmin(fmin), mFmax(fmax),
66 mSampleRate(sample_rate), mWindow(window) {
67 Args::resolveModeEnums(mMode, mWindow, bands, samples, fmin, fmax);
68 fl::memset(&mCqCfg, 0, sizeof(mCqCfg));
69
70 mFftrCfg = kiss_fftr_alloc(samples, 0, nullptr, nullptr);
71 if (!mFftrCfg) {
72 FL_WARN("Failed to allocate Impl context");
73 return;
74 }
75
76 switch (mMode) {
77 case Mode::LOG_REBIN:
79 break;
80 case Mode::CQ_NAIVE:
81 initNaive(samples, bands, fmin, fmax, sample_rate);
82 break;
83 case Mode::CQ_HYBRID:
84 initHybrid(samples, bands, fmin, fmax, sample_rate);
85 break;
86 case Mode::CQ_OCTAVE:
87 initOctaveWise(samples, bands, fmin, fmax, sample_rate);
88 break;
89 case Mode::AUTO:
90 FL_WARN("Mode::AUTO should have been resolved");
91 break;
92 }
93 }
94
96 if (mFftrCfg) {
98 }
99 if (mKernels) {
101 }
102 for (int i = 0; i < static_cast<int>(mOctaves.size()); i++) {
103 if (mOctaves[i].kernels) {
104 free_kernels(mOctaves[i].kernels, mOctaves[i].cfg);
105 }
106 }
107 if (mHybridSmallFft) {
109 }
110 if (mHybridMidFft) {
112 }
113 }
114
115 fl::size sampleSize() const { return mInputSamples; }
116
117 void run(span<const i16> buffer, Bins *out) {
118 switch (mMode) {
119 case Mode::LOG_REBIN:
120 runLogRebin(buffer, out);
121 break;
122 case Mode::CQ_NAIVE:
123 runNaive(buffer, out);
124 break;
125 case Mode::CQ_OCTAVE:
126 runOctaveWise(buffer, out);
127 break;
128 case Mode::CQ_HYBRID:
129 runHybrid(buffer, out);
130 break;
131 case Mode::AUTO:
132 FL_WARN("Mode::AUTO should have been resolved");
133 break;
134 }
135 }
136
137 fl::string info() const {
138 Bins tmp(mTotalBands);
140 for (int i = 0; i < mTotalBands; ++i) {
141 tmp.raw_mut().push_back(0.0f);
142 }
143
144 fl::sstream ss;
145 ss << "Impl Frequency Bands (CQ log-spaced): ";
146 for (int i = 0; i < mTotalBands; ++i) {
147 float f_low = (i == 0) ? mFmin : tmp.binBoundary(i - 1);
148 float f_high =
149 (i == mTotalBands - 1) ? mFmax : tmp.binBoundary(i);
150 ss << f_low << "Hz-" << f_high << "Hz, ";
151 }
152 return ss.str();
153 }
154
155 private:
156 // Thread-local scratch buffers to avoid stack overflow on ESP32 tasks
157 // with limited stack (~4-8 KB). Buffers persist across calls and are
158 // resized on demand.
167
171
172 // ---- Log-rebin path (fast, no CQ kernels) ----
173 //
174 // Single 512-point FFT, then group the linear FFT bins into
175 // geometrically-spaced output bins. Same approach as WLED.
176 // Cost: ~0.15ms on ESP32-S3. No kernel memory.
177
179 // Pre-compute bin edges aligned with CQ center frequencies.
180 // CQ center[i] = fmin * exp(logRatio * i / (bands-1)), so
181 // binToFreq(i) returns these centers for both modes.
182 //
183 // Edges are placed at the geometric mean of adjacent centers:
184 // edge[i] = sqrt(center[i-1] * center[i])
185 // = fmin * exp(logRatio * (2*i - 1) / (2*(bands-1)))
186 //
187 // edge[0] and edge[bands] extend half a bin beyond fmin/fmax.
188 const int bands = mTotalBands;
189 mLogBinEdges.resize(bands + 1);
190 float logRatio = logf(mFmax / mFmin);
191 if (bands <= 1) {
192 mLogBinEdges[0] = mFmin;
193 mLogBinEdges[1] = mFmax;
194 } else {
195 float denom = 2.0f * static_cast<float>(bands - 1);
196 // Edge below first center (half-bin below fmin)
197 mLogBinEdges[0] =
198 mFmin * expf(-logRatio / denom);
199 // Intermediate edges: geometric mean of adjacent CQ centers
200 for (int i = 1; i < bands; i++) {
201 mLogBinEdges[i] =
202 mFmin *
203 expf(logRatio * (2.0f * static_cast<float>(i) - 1.0f) /
204 denom);
205 }
206 // Edge above last center (half-bin above fmax)
207 mLogBinEdges[bands] =
208 mFmax * expf(logRatio / denom);
209 }
210
211 // Clamp top bin edge to Nyquist — prevents incomplete bin coverage
212 // and bad normalization when fmax is close to Nyquist.
213 float nyquist = static_cast<float>(mSampleRate) / 2.0f;
214 if (mLogBinEdges[bands] > nyquist) {
215 mLogBinEdges[bands] = nyquist;
216 }
217
219
220 // Pre-compute bin mapping LUTs
222 static_cast<float>(mSampleRate), 0, mTotalBands);
224
225 // Pre-compute window as Q15 integer coefficients
227
228 // Pre-compute bin-width normalization factors for LOG_REBIN.
229 // Without normalization, wider high-frequency bins accumulate more
230 // sidelobe energy than narrow low-frequency bins, creating visible
231 // "aliasing" artifacts during a tone sweep.
233 mInputSamples, static_cast<float>(mSampleRate),
234 0, mTotalBands);
235 }
236
237 void runLogRebin(span<const i16> buffer, Bins *out) {
239 const int N = mInputSamples;
240 const int bands = mTotalBands;
241 const int numRawBins = N / 2 + 1;
242
243 // Use thread-local scratch buffers to avoid stack overflow on
244 // ESP32 tasks with limited stack (~4-8 KB).
245 FftScratch &s = scratch();
246 s.windowed.resize(N);
247 s.fftOut.resize(N);
248 s.re.resize(numRawBins);
249 s.im.resize(numRawBins);
250 s.mag.resize(numRawBins);
251 s.rawBinsI.resize(bands);
252
253 applyWindow(buffer.data(), mWindowBuf.data(), s.windowed.data(), N);
255
256 // Deinterleave AoS → SoA and batch-compute magnitudes
257 deinterleave(s.fftOut.data(), s.re.data(), s.im.data(), numRawBins);
258 batchMag(s.re.data(), s.im.data(), s.mag.data(), numRawBins);
259
260 // Linear bins (same as other paths)
261 computeLinearBins(s.mag.data(), N, out);
262
263 // Group FFT bins into log-spaced output bins (integer accumulation)
264 fl::memset(s.rawBinsI.data(), 0, sizeof(u32) * bands);
265 logRebinRange(s.mag.data(), N, static_cast<float>(mSampleRate),
266 0, bands, s.rawBinsI.data(), mLogBinLut);
267
268 // Store raw magnitudes (dB computed lazily by Bins::db())
269 fl::vector<float> &rawBins = out->raw_mut();
270 rawBins.resize(bands);
271 for (int i = 0; i < bands; ++i) {
272 rawBins[i] = static_cast<float>(s.rawBinsI[i]);
273 }
274
275 // Store bin-width normalization factors so consumers can optionally
276 // normalize (e.g. for equalization display). Raw output is unchanged.
278 }
279
280 // ---- Naive single-FFT path (narrow frequency ranges) ----
281
282 void initNaive(int samples, int bands, float fmin, float fmax, int sr) {
283 mCqCfg.samples = samples;
284 mCqCfg.bands = bands;
285 mCqCfg.fmin = fmin;
286 mCqCfg.fmax = fmax;
287 mCqCfg.fs = sr;
288 mCqCfg.min_val = FL_FFT_MIN_VAL;
291 // Note: CQ kernels already apply Hamming windowing in frequency domain.
292 // Adding time-domain Hanning would double-window and over-attenuate.
293 }
294
295 void runNaive(span<const i16> buffer, Bins *out) {
297 const int fftSize = mInputSamples;
298 const int numRawBins = fftSize / 2 + 1;
299
300 FftScratch &s = scratch();
301 s.fftOut.resize(fftSize);
302 s.re.resize(numRawBins);
303 s.im.resize(numRawBins);
304 s.mag.resize(numRawBins);
305
307
308 // Deinterleave AoS → SoA and batch-compute magnitudes
309 deinterleave(s.fftOut.data(), s.re.data(), s.im.data(), numRawBins);
310 batchMag(s.re.data(), s.im.data(), s.mag.data(), numRawBins);
311
312 computeLinearBins(s.mag.data(), fftSize, out);
313
316
317 const int bands = mCqCfg.bands;
318 fl::vector<float> &rawBins = out->raw_mut();
319 rawBins.resize(bands);
320 for (int i = 0; i < bands; ++i) {
321 i32 real = cq[i].r;
322 i32 imag = cq[i].i;
323#ifdef FIXED_POINT
324 rawBins[i] = static_cast<float>(fastMag(real, imag));
325#else
326 float r2 = float(real * real);
327 float i2 = float(imag * imag);
328 rawBins[i] = sqrt(r2 + i2);
329#endif
330 }
331 }
332
333 // Fast integer magnitude: max(|re|,|im|) + 0.40625*min(|re|,|im|)
334 // Max error ~3.5% vs exact sqrt(re²+im²). No float, no division.
335 // 0.40625 = 13/32, exactly representable → bit-exact with original.
336 static inline u16 fastMag(i32 re, i32 im) {
337 u32 a = (re >= 0) ? static_cast<u32>(re) : static_cast<u32>(-re);
338 u32 b = (im >= 0) ? static_cast<u32>(im) : static_cast<u32>(-im);
339 u32 mx = (a > b) ? a : b;
340 u32 mn = (a <= b) ? a : b;
341 static constexpr u16x16 kMinWeight(0.40625f);
342 return static_cast<u16>(mx + (kMinWeight * static_cast<i32>(mn)).to_int());
343 }
344
345 // Fast integer dB conversion: 20 * log10(x) = 6.02060 * log2(x).
346 // Uses MSB position for integer part, corrected linear interpolation
347 // for fractional part. Max error ~0.05 dB — imperceptible for audio
348 // visualization. No log10f, no division.
349 // Internally uses fixed-point u16x16 in FIXED16 mode, converts to float at output.
350 static inline float fastDb(u32 x) {
351 if (x == 0) return 0.0f;
352
353 // Find highest set bit (integer part of log2)
354 int msb = 0;
355 {
356 u32 v = x;
357 if (v >= 0x10000u) { v >>= 16; msb += 16; }
358 if (v >= 0x100u) { v >>= 8; msb += 8; }
359 if (v >= 0x10u) { v >>= 4; msb += 4; }
360 if (v >= 0x4u) { v >>= 2; msb += 2; }
361 if (v >= 0x2u) { msb += 1; }
362 }
363
364 // Normalized mantissa in [0, 1) as u16x16
365 u32 t_raw;
366 if (msb >= 16) {
367 t_raw = (x >> (msb - 16)) - 65536u;
368 } else {
369 t_raw = (x << (16 - msb)) - 65536u;
370 }
371 u16x16 t = u16x16::from_raw(t_raw);
372
373 // log2(1+t) ~= t + 0.345 * t * (1-t) [max error ~0.008]
374 // 22610 = 0.345 * 65536 (from_raw preserves exact original constant)
375 static constexpr u16x16 one(1.0f);
376 static constexpr u16x16 kCorrection = u16x16::from_raw(22610u);
377 u16x16 complement = one - t;
378 u16x16 prod = t * complement;
379 u16x16 correction = prod * kCorrection;
380 u16x16 frac = t + correction;
381
382 // log2(x) = msb + frac, assembled as u16x16
383 u16x16 log2_val = u16x16::from_raw(
384 (static_cast<u32>(msb) << 16) + frac.raw());
385
386 // 20*log10(x) = 6.02060 * log2(x)
387#if FASTLED_FFT_PRECISION == FASTLED_FFT_FIXED16
388 // 394593 = 6.02060 * 65536 (from_raw preserves exact original constant)
389 static constexpr u16x16 kDbScale = u16x16::from_raw(394593u);
390 u16x16 db = log2_val * kDbScale;
391 return db.to_float();
392#else
393 return 6.02060f * log2_val.to_float();
394#endif
395 }
396
397 // Deinterleave AoS kiss_fft_cpx into SoA re[]/im[] arrays.
398 // Enables auto-vectorization of the subsequent batchMag loop.
399 static void deinterleave(const kiss_fft_cpx *cpx,
401 int n) {
402 for (int i = 0; i < n; ++i) {
403 re[i] = cpx[i].r;
404 im[i] = cpx[i].i;
405 }
406 }
407
408 // Batch magnitude: compute fastMag for contiguous SoA re[]/im[] → mag[].
409 // Contiguous layout allows compiler to auto-vectorize (4-8 mags per SIMD).
410 static void batchMag(const kiss_fft_scalar *re,
411 const kiss_fft_scalar *im,
412 u16 *mag, int n) {
413 for (int i = 0; i < n; ++i) {
414 mag[i] = fastMag(re[i], im[i]);
415 }
416 }
417
418 // Compute Q16.16 bin edges from float bin edges for integer inner loops.
420 int n = static_cast<int>(mLogBinEdges.size());
421 mLogBinEdgesQ16.resize(n);
422 for (int i = 0; i < n; ++i) {
424 }
425 }
426
427 // Build log-bin LUT: for each FFT bin k, pre-compute which output bin
428 // it maps to. Moves the binary search from runtime to init.
429 void buildLogBinLut(fl::vector<u8>& lut, int fftN, float fs,
430 int binStart, int binEnd) {
431 const int numRawBins = fftN / 2 + 1;
432 lut.resize(numRawBins);
433 const u16x16 rawBinHz(fs / static_cast<float>(fftN));
434
435 for (int k = 0; k < numRawBins; ++k) {
436 u16x16 freq = rawBinHz * static_cast<u32>(k);
437
438 int lo = binStart, hi = binEnd - 1;
439 while (lo < hi) {
440 int mid = (lo + hi + 1) / 2;
441 if (mLogBinEdgesQ16[mid] <= freq)
442 lo = mid;
443 else
444 hi = mid - 1;
445 }
446 lut[k] = static_cast<u8>(lo);
447 }
448 }
449
450 // Build linear-bin LUT: for each FFT bin k, pre-compute which linear
451 // output bin it maps to. Moves the u16x16 division from runtime to init.
452 void buildLinearBinLut(fl::vector<u8>& lut, int fftN) {
453 const int numRawBins = fftN / 2 + 1;
454 const int numLinearBins = mTotalBands;
455 lut.resize(numRawBins);
456
457 const u16x16 rawBinHz(static_cast<float>(mSampleRate) /
458 static_cast<float>(fftN));
459 const u16x16 halfBin = rawBinHz >> 1;
460 const u16x16 fminFP(mFmin);
461 const u16x16 fmaxFP(mFmax);
462 const u16x16 linearBinHz(
463 (mFmax - mFmin) / static_cast<float>(numLinearBins));
464
465 // Pre-compute loop bounds (stored for runtime use)
466 mLinearKStart = 0;
467 if (fminFP > halfBin) {
468 mLinearKStart = static_cast<int>(
469 u16x16::ceil((fminFP - halfBin) / rawBinHz).to_int());
470 }
471 mLinearKEnd = static_cast<int>(
472 u16x16::ceil((fmaxFP + halfBin) / rawBinHz).to_int());
473 if (mLinearKEnd > numRawBins) mLinearKEnd = numRawBins;
474
475 for (int k = 0; k < numRawBins; ++k) {
476 u16x16 freq = rawBinHz * static_cast<u32>(k);
477
478 if (freq < fminFP) {
479 lut[k] = 0;
480 continue;
481 }
482 int linIdx = static_cast<int>(
483 ((freq - fminFP) / linearBinHz).to_int());
484 if (linIdx >= numLinearBins)
485 linIdx = numLinearBins - 1;
486 lut[k] = static_cast<u8>(linIdx);
487 }
488 }
489
490 // Compute window function as alpha16 (UNORM16) coefficients in [0, 1].
491 // Uses fixed-point arithmetic throughout to avoid float on MCUs.
492 static void computeWindow(fl::vector<alpha16> &win, int N, Window type) {
493 using FP = fl::fixed_point<16, 16>;
494 win.resize(N);
495
496 // NONE: rectangular window (all coefficients = 1.0)
497 if (type == Window::NONE) {
498 for (int n = 0; n < N; ++n) {
499 win[n] = alpha16(65535);
500 }
501 return;
502 }
503
504 const FP two_pi(6.2831853f); // 2π
505 const FP invNm1 = FP(1) / FP(N - 1);
506 const FP phase_step = two_pi * invNm1;
507
508 // Blackman-Harris coefficients
509 constexpr FP bh_a0(0.35875f);
510 constexpr FP bh_a1(0.48829f);
511 constexpr FP bh_a2(0.14128f);
512 constexpr FP bh_a3(0.01168f);
513 // Hanning coefficients
514 constexpr FP half(0.5f);
515 constexpr FP one(1.0f);
516
517 FP phase(0.0f);
518 for (int n = 0; n < N; ++n) {
519 FP w;
520 switch (type) {
522 // 4-term Blackman-Harris: -92 dB sidelobe rejection
523 FP phase2 = phase + phase;
524 FP phase3 = phase2 + phase;
525 w = bh_a0 - bh_a1 * FP::cos(phase)
526 + bh_a2 * FP::cos(phase2)
527 - bh_a3 * FP::cos(phase3);
528 break;
529 }
530 case Window::HANNING:
531 default:
532 w = half * (one - FP::cos(phase));
533 break;
534 }
535 i32 raw = w.raw();
536 if (raw < 0) raw = 0; // guard against FP rounding
537 if (raw > 65535) raw = 65535; // 1.0 in s16x16 = 65536, clamp for u16
538 win[n] = alpha16(static_cast<unsigned short>(raw));
539 phase = phase + phase_step;
540 }
541 }
542
543 // Apply window: out[i] = sample[i] * win[i]
544 // Window coefficients are alpha16 (UNORM16 [0,1]); uses scale_signed().
545 static void applyWindow(const kiss_fft_scalar *samples,
546 const alpha16 *win, kiss_fft_scalar *out, int N) {
547 for (int i = 0; i < N; ++i) {
548 out[i] = static_cast<kiss_fft_scalar>(
549 win[i].scale_signed(static_cast<int>(samples[i])));
550 }
551 }
552
556
557 // ---- Octave-wise CQT path (wide frequency ranges) ----
558 //
559 // Instead of one massive FFT + degenerate kernels, split the frequency
560 // range into octaves. Each octave uses the same N-point FFT (512) with
561 // well-conditioned CQ kernels (N_window >= N/2). The signal is decimated
562 // by 2x between octaves via a halfband filter.
563 //
564 // Memory: ~25KB total vs ~830KB for zero-padding approach.
565
572
573 void initOctaveWise(int samples, int bands, float fmin, float fmax,
574 int sr) {
575
576
577 // Use floor so the top octave covers the remaining frequency range
578 // rather than creating a tiny sliver octave with 1 bin.
579 int numOctaves = static_cast<int>(floorf(log2f(fmax / fmin)));
580 if (numOctaves < 1)
581 numOctaves = 1;
582
583 // Log-spaced center frequencies for all bins
584 float logRatio = logf(fmax / fmin);
585 FASTLED_STACK_ARRAY(float, centerFreqs, bands);
586 for (int i = 0; i < bands; i++) {
587 centerFreqs[i] =
588 fmin *
589 expf(logRatio * static_cast<float>(i) /
590 static_cast<float>(bands - 1));
591 }
592
593 // Assign each bin to an octave: oct j spans [fmin*2^j, fmin*2^(j+1))
594 FASTLED_STACK_ARRAY(int, binOctave, bands);
595 for (int i = 0; i < bands; i++) {
596 int oct =
597 static_cast<int>(floorf(log2f(centerFreqs[i] / fmin)));
598 if (oct < 0)
599 oct = 0;
600 if (oct >= numOctaves)
601 oct = numOctaves - 1;
602 binOctave[i] = oct;
603 }
604
605 // Build per-octave CQ kernel sets
606 mOctaves.resize(numOctaves);
608 for (int oct = 0; oct < numOctaves; oct++) {
609 int first = -1, last = -1;
610 for (int i = 0; i < bands; i++) {
611 if (binOctave[i] == oct) {
612 if (first < 0)
613 first = i;
614 last = i;
615 }
616 }
617
618 OctaveInfo &oi = mOctaves[oct];
619 fl::memset(&oi.cfg, 0, sizeof(oi.cfg));
620 oi.kernels = nullptr;
621 if (first < 0) {
622 oi.firstBin = 0;
623 oi.numBins = 0;
624 continue;
625 }
626
627 oi.firstBin = first;
628 oi.numBins = last - first + 1;
629 if (oi.numBins > mMaxBinsPerOctave) {
631 }
632
633 // Decimation: top octave (numOctaves-1) uses original sample rate.
634 // Each lower octave halves the effective sample rate.
635 int decimExp = numOctaves - 1 - oct;
636 float effectiveFs =
637 static_cast<float>(sr) /
638 static_cast<float>(1 << decimExp);
639
640 oi.cfg.samples = samples;
641 oi.cfg.bands = oi.numBins;
642 oi.cfg.fmin = centerFreqs[first];
643 oi.cfg.fmax = (oi.numBins > 1) ? centerFreqs[last]
644 : centerFreqs[first] * 2.0f;
645 oi.cfg.fs = effectiveFs;
647
649 }
650
651 // Pre-allocate reusable buffers
652 mWorkBuf.resize(samples);
653 mFftOut.resize(samples);
654
656 // Note: CQ kernels already apply Hamming windowing in frequency domain.
657 // Adding time-domain Hanning would double-window and over-attenuate.
658 }
659
660 void runOctaveWise(span<const i16> buffer, Bins *out) {
661 const int N = mInputSamples;
662 const int numOctaves = static_cast<int>(mOctaves.size());
663 const int numRawBins = N / 2 + 1;
664
666
667 // Copy input to working buffer
668 int workLen = N;
669 for (int i = 0; i < N; i++) {
670 mWorkBuf[i] =
671 (i < static_cast<int>(buffer.size())) ? buffer[i] : 0;
672 }
673
674 // FFT at full sample rate (for linear bins + top octave CQ)
676
677 // Deinterleave AoS → SoA and batch-compute magnitudes
678 FftScratch &s = scratch();
679 s.re.resize(numRawBins);
680 s.im.resize(numRawBins);
681 s.mag.resize(numRawBins);
682 deinterleave(mFftOut.data(), s.re.data(), s.im.data(), numRawBins);
683 batchMag(s.re.data(), s.im.data(), s.mag.data(), numRawBins);
684
685 computeLinearBins(s.mag.data(), N, out);
686
687 // Prepare CQ output bins
688 fl::vector<float> &rawBins = out->raw_mut();
689 rawBins.resize(mTotalBands);
690 for (int i = 0; i < mTotalBands; i++) {
691 rawBins[i] = 0.0f;
692 }
693
694 // Pre-allocate CQ accumulator once (avoids alloca in loop)
696
697 // Process octaves from top (highest freq) to bottom (lowest freq).
698 // Top octave uses the FFT already computed above.
699 // Each lower octave: decimate signal by 2x, then FFT + CQ.
700 for (int oct = numOctaves - 1; oct >= 0; oct--) {
701 const OctaveInfo &oi = mOctaves[oct];
702 if (oi.numBins <= 0 || !oi.kernels)
703 continue;
704
705 if (oct != numOctaves - 1) {
706 decimateBy2(mWorkBuf.data(), workLen);
707 workLen = workLen / 2;
708 // Zero-pad remainder so FFT sees clean input
709 for (int i = workLen; i < N; i++)
710 mWorkBuf[i] = 0;
712 }
713
714 // Zero the CQ accumulator and apply kernels
715 fl::memset(cq, 0, sizeof(kiss_fft_cpx) * oi.numBins);
716 apply_kernels(mFftOut.data(), cq, oi.kernels, oi.cfg);
717
718 for (int i = 0; i < oi.numBins; i++) {
719 int binIdx = oi.firstBin + i;
720 i32 real = cq[i].r;
721 i32 imag = cq[i].i;
722#ifdef FIXED_POINT
723 rawBins[binIdx] = static_cast<float>(fastMag(real, imag));
724#else
725 float r2 = float(real * real);
726 float i2 = float(imag * imag);
727 rawBins[binIdx] = sqrt(r2 + i2);
728#endif
729 }
730 }
731 }
732
733 // ---- Hybrid path: dual LOG_REBIN (full-rate upper + decimated bass) ----
734 //
735 // Two FFT passes, both using LOG_REBIN (no CQ kernels):
736 // 1. Full-rate windowed 512-point FFT → LOG_REBIN for upper bins
737 // 2. Decimated small FFT (e.g. 64-point) → LOG_REBIN for bass bins
738 //
739 // The small FFT is much faster than 512-point, and the anti-alias
740 // decimation filter removes high-frequency content before bass analysis.
741
742 void initHybrid(int samples, int bands, float fmin, float fmax, int sr) {
743 float logRatio = logf(fmax / fmin);
744
745 // Log-spaced center frequencies for all bins
746 FASTLED_STACK_ARRAY(float, centerFreqs, bands);
747 for (int i = 0; i < bands; i++) {
748 centerFreqs[i] =
749 fmin * expf(logRatio * static_cast<float>(i) /
750 static_cast<float>(bands - 1));
751 }
752
753 // 3-tier split at octave boundaries:
754 // bass/mid at fmin*4 (~698 Hz, 2 octaves above fmin)
755 // mid/upper at fmin*8 (~1397 Hz, 3 octaves above fmin)
756 float bassMidFreq = fmin * 4.0f;
757 float midUpperFreq = fmin * 8.0f;
758
759 // Clamp splits to valid range
760 if (midUpperFreq >= fmax) midUpperFreq = fmax * 0.5f;
761 if (bassMidFreq >= midUpperFreq) bassMidFreq = midUpperFreq * 0.5f;
762
763 // Find split bin indices
764 mHybridSplitBin = 0;
765 for (int i = 0; i < bands; i++) {
766 if (centerFreqs[i] < bassMidFreq)
767 mHybridSplitBin = i + 1;
768 }
770 for (int i = mHybridSplitBin; i < bands; i++) {
771 if (centerFreqs[i] < midUpperFreq)
772 mHybridMidSplitBin = i + 1;
773 }
774
775 // Ensure each tier has at least 1 bin
776 if (mHybridSplitBin < 1) mHybridSplitBin = 1;
779 if (mHybridMidSplitBin >= bands)
780 mHybridMidSplitBin = bands - 1;
781
782 // LOG_REBIN bin edges (shared by all three tiers)
783 mLogBinEdges.resize(bands + 1);
784 if (bands <= 1) {
785 mLogBinEdges[0] = fmin;
786 mLogBinEdges[1] = fmax;
787 } else {
788 float denom = 2.0f * static_cast<float>(bands - 1);
789 mLogBinEdges[0] = fmin * expf(-logRatio / denom);
790 for (int i = 1; i < bands; i++) {
791 mLogBinEdges[i] =
792 fmin * expf(logRatio *
793 (2.0f * static_cast<float>(i) - 1.0f) / denom);
794 }
795 mLogBinEdges[bands] = fmax * expf(logRatio / denom);
796 }
797
798 // Clamp top bin edge to Nyquist
799 float nyquist = static_cast<float>(sr) / 2.0f;
800 if (mLogBinEdges[bands] > nyquist) {
801 mLogBinEdges[bands] = nyquist;
802 }
803
805
806 // Window for the full-rate 512pt FFT (upper tier)
807 initWindow();
808
809 // Mid-tier: 2 decimation steps → samples/4 at sr/4
810 // Zero-pad 2x: 128 real samples → 256pt FFT → 43 Hz bins
811 mHybridMidN = samples / 4;
812 mHybridMidFs = static_cast<float>(sr) / 4.0f;
813 mHybridMidFft = kiss_fftr_alloc(mHybridMidN * 2, 0, nullptr, nullptr);
814 mHybridMidFftOut.resize(mHybridMidN * 2);
816
817 // Bass-tier: 3 decimation steps → samples/8 at sr/8
818 mHybridSmallN = samples / 8;
819 mHybridSmallFs = static_cast<float>(sr) / 8.0f;
821 kiss_fftr_alloc(mHybridSmallN, 0, nullptr, nullptr);
824
825 // Work buffer for decimation (reused across phases)
826 mWorkBuf.resize(samples);
827 mFftOut.resize(samples);
828
829 // Pre-computed bin mapping LUTs for each tier
830 buildLogBinLut(mLogBinLut, samples,
831 static_cast<float>(sr),
832 mHybridMidSplitBin, bands);
838 0, mHybridSplitBin);
840
841 // Pre-compute normalization factors for each hybrid tier
843 samples, static_cast<float>(sr),
844 mHybridMidSplitBin, bands);
850 0, mHybridSplitBin);
851
852 // Pre-compute merged norm factors (avoids per-frame allocation)
853 mHybridMergedNorm.resize(bands);
854 for (int i = 0; i < bands; ++i) {
855 if (i >= mHybridMidSplitBin && i < static_cast<int>(mHybridNormUpper.size())) {
857 } else if (i >= mHybridSplitBin && i < static_cast<int>(mHybridNormMid.size())) {
859 } else if (i < static_cast<int>(mHybridNormBass.size())) {
861 } else {
862 mHybridMergedNorm[i] = 1.0f;
863 }
864 }
865 }
866
867 void runHybrid(span<const i16> buffer, Bins *out) {
868 const int N = mInputSamples;
869 const int numRawBins = N / 2 + 1;
870
872
873 // Use thread-local scratch buffers
874 FftScratch &s = scratch();
875 s.re.resize(numRawBins);
876 s.im.resize(numRawBins);
877 s.mag.resize(numRawBins);
878 s.windowed.resize(N);
880
881 // Phase 1: Windowed 512pt FFT → LOG_REBIN for upper bins
882 applyWindow(buffer.data(), mWindowBuf.data(), s.windowed.data(), N);
883
885
886 deinterleave(mFftOut.data(), s.re.data(), s.im.data(), numRawBins);
887 batchMag(s.re.data(), s.im.data(), s.mag.data(), numRawBins);
888
889 computeLinearBins(s.mag.data(), N, out);
890
891 // Integer accumulation for log-rebin
892 fl::memset(s.rawBinsI.data(), 0, sizeof(u32) * mTotalBands);
893
894 // Upper tier: LOG_REBIN for bins [mHybridMidSplitBin, mTotalBands)
895 logRebinRange(s.mag.data(), N,
896 static_cast<float>(mSampleRate),
898 mLogBinLut);
899
900 // Decimate signal: 512 → 256 → 128 (2 steps for mid tier)
901 int workLen = N;
902 for (int i = 0; i < N; i++) {
903 mWorkBuf[i] =
904 (i < static_cast<int>(buffer.size())) ? buffer[i] : 0;
905 }
906 decimateBy2(mWorkBuf.data(), workLen);
907 workLen /= 2;
908 decimateBy2(mWorkBuf.data(), workLen);
909 workLen /= 2;
910
911 // Phase 2: Zero-padded 256pt FFT (128 windowed + 128 zeros) → LOG_REBIN for mid bins
913 int midFftN = mHybridMidN * 2;
914 int midRawBins = midFftN / 2 + 1;
915 s.windowed.resize(midFftN);
918 for (int i = mHybridMidN; i < midFftN; ++i) {
919 s.windowed[i] = 0;
920 }
922 s.windowed.data(),
923 mHybridMidFftOut.data());
924 deinterleave(mHybridMidFftOut.data(), s.re.data(), s.im.data(), midRawBins);
925 batchMag(s.re.data(), s.im.data(), s.mag.data(), midRawBins);
926
927 logRebinRange(s.mag.data(), midFftN,
931 }
932
933 // Decimate 1 more step: 128 → 64 (reuse unwindowed workBuf)
934 decimateBy2(mWorkBuf.data(), workLen);
935 workLen /= 2;
936
937 // Phase 3: Windowed 64pt FFT → LOG_REBIN for bass bins
938 if (mHybridSplitBin > 0 && mHybridSmallFft) {
939 int bassRawBins = mHybridSmallN / 2 + 1;
944 s.windowed.data(),
945 mHybridSmallFftOut.data());
946 deinterleave(mHybridSmallFftOut.data(), s.re.data(), s.im.data(), bassRawBins);
947 batchMag(s.re.data(), s.im.data(), s.mag.data(), bassRawBins);
952 }
953
954 // Store raw magnitudes (dB computed lazily by Bins::db())
955 fl::vector<float> &rawBins = out->raw_mut();
956 rawBins.resize(mTotalBands);
957 for (int i = 0; i < mTotalBands; ++i) {
958 rawBins[i] = static_cast<float>(s.rawBinsI[i]);
959 }
960
961 // Use pre-computed merged norm factors (no per-frame allocation)
963 }
964
965 // ---- Shared utilities ----
966
967 // Compute bin-width normalization factors for LOG_REBIN.
968 // Counts how many FFT bins map to each output bin via the LUT,
969 // then stores 1/count as the normalization factor. Bins with 0 FFT bins
970 // get factor 1.0 (no scaling).
972 const fl::vector<u8>& lut,
973 int fftN, float fs,
974 int binStart, int binEnd) {
975 int bands = binEnd - binStart;
976 normFactors.resize(binEnd);
977 for (int i = 0; i < binEnd; ++i) {
978 normFactors[i] = 1.0f;
979 }
980
981 // Count FFT bins mapping to each output bin using same bounds as logRebinRange
982 const int numRawBins = fftN / 2 + 1;
983 const u16x16 rawBinHz(fs / static_cast<float>(fftN));
984 const u16x16 halfBin = rawBinHz >> 1;
985 const u16x16 loEdge(mLogBinEdges[binStart]);
986 const u16x16 hiEdge(mLogBinEdges[binEnd]);
987
988 int kStart = 0;
989 if (loEdge > halfBin) {
990 kStart = static_cast<int>(
991 u16x16::ceil((loEdge - halfBin) / rawBinHz).to_int());
992 }
993 int kEnd = static_cast<int>(
994 u16x16::ceil((hiEdge + halfBin) / rawBinHz).to_int());
995 if (kEnd > numRawBins) kEnd = numRawBins;
996
997 FASTLED_STACK_ARRAY(float, counts, binEnd);
998 for (int k = kStart; k < kEnd; ++k) {
999 counts[lut[k]] += 1.0f;
1000 }
1001
1002 for (int i = binStart; i < binEnd; ++i) {
1003 normFactors[i] = (counts[i] > 0.0f) ? 1.0f / counts[i] : 1.0f;
1004 }
1005 (void)bands;
1006 }
1007
1008 // LOG_REBIN helper: group FFT bins into CQ output bins [binStart, binEnd)
1009 // Uses pre-computed LUT for O(1) bin mapping per FFT bin.
1010 void logRebinRange(const u16 *mag, int fftN, float fs,
1011 int binStart, int binEnd,
1012 u32 *rawBinsI, const fl::vector<u8>& lut) {
1013 const int numRawBins = fftN / 2 + 1;
1014 // Compute loop bounds to skip out-of-range FFT bins
1015 const u16x16 rawBinHz(fs / static_cast<float>(fftN));
1016 const u16x16 halfBin = rawBinHz >> 1;
1017 const u16x16 loEdge(mLogBinEdges[binStart]);
1018 const u16x16 hiEdge(mLogBinEdges[binEnd]);
1019
1020 int kStart = 0;
1021 if (loEdge > halfBin) {
1022 kStart = static_cast<int>(
1023 u16x16::ceil((loEdge - halfBin) / rawBinHz).to_int());
1024 }
1025 int kEnd = static_cast<int>(
1026 u16x16::ceil((hiEdge + halfBin) / rawBinHz).to_int());
1027 if (kEnd > numRawBins) kEnd = numRawBins;
1028
1029 for (int k = kStart; k < kEnd; ++k) {
1030 rawBinsI[lut[k]] += static_cast<u32>(mag[k]);
1031 }
1032 }
1033
1034 void computeLinearBins(const u16 *mag, int /*nfft*/, Bins *out) {
1035 const int numLinearBins = mTotalBands;
1036
1037 fl::vector<float> &linBins = out->linear_mut();
1038 linBins.resize(numLinearBins);
1040
1041 // Integer accumulation using pre-computed LUT
1042 FASTLED_STACK_ARRAY(u32, linBinsI, numLinearBins);
1043 for (int i = 0; i < numLinearBins; ++i) {
1044 linBinsI[i] = 0;
1045 }
1046
1047 for (int k = mLinearKStart; k < mLinearKEnd; ++k) {
1048 linBinsI[mLinearBinLut[k]] += static_cast<u32>(mag[k]);
1049 }
1050
1051 // Convert to float
1052 for (int i = 0; i < numLinearBins; ++i) {
1053 linBins[i] = static_cast<float>(linBinsI[i]);
1054 }
1055 }
1056
1057 // 7-tap halfband filter + decimate by 2.
1058 // h = [-1/32, 0, 9/32, 1/2, 9/32, 0, -1/32]
1059 // Stopband rejection: ~-45dB per stage (vs -13dB for old 3-tap).
1060 // Total rejection with 3 decimation stages: ~-135dB.
1061 static void decimateBy2(kiss_fft_scalar *buf, int len) {
1062 int outLen = len / 2;
1063 for (int i = 0; i < outLen; i++) {
1064 int idx = i * 2;
1065 auto s = [&](int offset) -> i32 {
1066 int j = idx + offset;
1067 if (j < 0) j = 0;
1068 if (j >= len) j = len - 1;
1069 return buf[j];
1070 };
1071 i32 val = -s(-3) + 9*s(-1) + 16*s(0) + 9*s(1) - s(3);
1072 buf[i] = static_cast<kiss_fft_scalar>(val / 32);
1073 }
1074 }
1075
1076 // ---- Member variables ----
1077
1078 // Shared
1081
1082 // Naive CQ path only
1085
1086 // Log-rebin path only
1087 fl::vector<float> mLogBinEdges; // bands+1 geometric bin edges (float)
1088 fl::vector<u16x16> mLogBinEdgesQ16; // bands+1 geometric bin edges (Q16.16)
1089 fl::vector<alpha16> mWindowBuf; // N pre-computed window coefficients (UNORM16 [0,1])
1090 fl::vector<float> mLogBinNormFactors; // Per-bin width normalization (1/count)
1091
1092 // Octave-wise CQ path (also used by Hybrid)
1095 fl::vector<kiss_fft_scalar> mWorkBuf; // reusable decimation buffer
1096 fl::vector<kiss_fft_cpx> mFftOut; // reusable FFT output buffer
1098
1099 // Hybrid 3-tier path
1100 int mHybridSplitBin = 0; // bins < this use bass fft::FFT
1101 int mHybridSmallN = 0; // bass FFT size (samples/8)
1102 float mHybridSmallFs = 0.0f; // bass sample rate (sr/8)
1105 // Mid-tier (3-tier hybrid)
1106 int mHybridMidN = 0; // mid FFT size (samples/4)
1107 float mHybridMidFs = 0.0f; // mid sample rate (sr/4)
1110 int mHybridMidSplitBin = 0; // bins >= this use upper fft::FFT
1111 fl::vector<alpha16> mHybridBassWindow; // UNORM16 window for bass
1112 fl::vector<alpha16> mHybridMidWindow; // UNORM16 window for mid
1113 fl::vector<float> mHybridNormUpper; // Normalization factors for upper tier
1114 fl::vector<float> mHybridNormMid; // Normalization factors for mid tier
1115 fl::vector<float> mHybridNormBass; // Normalization factors for bass tier
1116 fl::vector<float> mHybridMergedNorm; // Pre-computed merged norm factors
1117
1118 // Pre-computed bin mapping LUTs (built at init, used at runtime)
1119 fl::vector<u8> mLogBinLut; // FFT bin k → log-bin index (primary fft::FFT)
1120 fl::vector<u8> mLinearBinLut; // FFT bin k → linear-bin index (primary fft::FFT)
1121 fl::vector<u8> mLogBinLutMid; // HYBRID mid-tier LUT (256pt)
1122 fl::vector<u8> mLogBinLutBass; // HYBRID bass-tier LUT (64pt)
1123 int mLinearKStart = 0; // Pre-computed linear bin loop bounds
1125
1126 // Used by both paths
1128 float mFmin, mFmax;
1131};
1132
1134 mContext = fl::make_unique<Context>(args.samples, args.bands, args.fmin,
1135 args.fmax, args.sample_rate,
1136 args.mode, args.window);
1137}
1138
1140
1142 if (mContext) {
1143 return mContext->info();
1144 } else {
1145 FL_WARN("Impl context is not initialized");
1146 return fl::string();
1147 }
1148}
1149
1150fl::size Impl::sampleSize() const {
1151 if (mContext) {
1152 return mContext->sampleSize();
1153 }
1154 return 0;
1155}
1156
1158 auto &audio_sample = sample.pcm();
1159 span<const i16> slice(audio_sample);
1160 return run(slice, out);
1161}
1162
1164 if (!mContext) {
1165 return Impl::Result(false, "Impl context is not initialized");
1166 }
1167 if (sample.size() != mContext->sampleSize()) {
1168 FL_WARN("Impl sample size mismatch");
1169 return Impl::Result(false, "Impl sample size mismatch");
1170 }
1171 mContext->run(sample, out);
1172 return Impl::Result(true, "");
1173}
1174
1175} // namespace fft
1176} // namespace audio
1177} // namespace fl
fl::UIAudio audio("Audio Input")
AudioAnalyzeFFT1024 fft
#define FASTLED_STACK_ARRAY(TYPE, NAME, SIZE)
Stack-allocated array with automatic zero-initialization.
Definition alloca.h:32
Unsigned alpha types with UNORM semantics (GPU industry standard).
static T & instance() FL_NOEXCEPT
Definition singleton.h:133
float binBoundary(int i) const FL_NOEXCEPT
Definition fft.cpp.hpp:124
fl::vector< float > & raw_mut() FL_NOEXCEPT
Definition fft.cpp.hpp:130
void setParams(float fmin, float fmax, int sampleRate) FL_NOEXCEPT
Definition fft.cpp.hpp:146
fl::vector< float > & linear_mut() FL_NOEXCEPT
Definition fft.cpp.hpp:139
void setNormFactors(const fl::vector< float > &factors) FL_NOEXCEPT
Definition fft.cpp.hpp:157
void setLinearParams(float linearFmin, float linearFmax) FL_NOEXCEPT
Definition fft.cpp.hpp:152
static float fastDb(u32 x)
void computeLogRebinNormFactors(fl::vector< float > &normFactors, const fl::vector< u8 > &lut, int fftN, float fs, int binStart, int binEnd)
fl::vector< alpha16 > mHybridBassWindow
static void decimateBy2(kiss_fft_scalar *buf, int len)
static void applyWindow(const kiss_fft_scalar *samples, const alpha16 *win, kiss_fft_scalar *out, int N)
void initHybrid(int samples, int bands, float fmin, float fmax, int sr)
static void deinterleave(const kiss_fft_cpx *cpx, kiss_fft_scalar *re, kiss_fft_scalar *im, int n)
fl::string info() const
void buildLogBinLut(fl::vector< u8 > &lut, int fftN, float fs, int binStart, int binEnd)
fl::vector< kiss_fft_scalar > mWorkBuf
void runHybrid(span< const i16 > buffer, Bins *out)
void runLogRebin(span< const i16 > buffer, Bins *out)
fl::vector< alpha16 > mWindowBuf
fl::vector< u8 > mLinearBinLut
static void batchMag(const kiss_fft_scalar *re, const kiss_fft_scalar *im, u16 *mag, int n)
fl::vector< alpha16 > mHybridMidWindow
fl::vector< kiss_fft_cpx > mHybridMidFftOut
fl::vector< float > mLogBinEdges
fl::vector< u8 > mLogBinLutMid
static FftScratch & scratch()
static u16 fastMag(i32 re, i32 im)
fl::vector< kiss_fft_cpx > mHybridSmallFftOut
void initNaive(int samples, int bands, float fmin, float fmax, int sr)
void buildLinearBinLut(fl::vector< u8 > &lut, int fftN)
fl::vector< float > mHybridNormUpper
fl::vector< u16x16 > mLogBinEdgesQ16
fl::vector< u8 > mLogBinLutBass
fl::vector< OctaveInfo > mOctaves
Context(int samples, int bands, float fmin, float fmax, int sample_rate, Mode mode, Window window)
fl::vector< u8 > mLogBinLut
fl::size sampleSize() const
fl::vector< kiss_fft_cpx > mFftOut
fl::vector< float > mHybridNormBass
void logRebinRange(const u16 *mag, int fftN, float fs, int binStart, int binEnd, u32 *rawBinsI, const fl::vector< u8 > &lut)
void initOctaveWise(int samples, int bands, float fmin, float fmax, int sr)
fl::vector< float > mLogBinNormFactors
void computeLinearBins(const u16 *mag, int, Bins *out)
fl::vector< float > mHybridMergedNorm
void runNaive(span< const i16 > buffer, Bins *out)
void run(span< const i16 > buffer, Bins *out)
fl::vector< float > mHybridNormMid
static void computeWindow(fl::vector< alpha16 > &win, int N, Window type)
void runOctaveWise(span< const i16 > buffer, Bins *out)
fl::vector< kiss_fft_scalar > re
fl::vector< kiss_fft_scalar > im
fl::vector< kiss_fft_cpx > fftOut
fl::vector< kiss_fft_scalar > windowed
Result run(const Sample &sample, Bins *out)
Impl(const Args &args)
fl::unique_ptr< Context > mContext
Definition fft_impl.h:51
fl::size sampleSize() const
fl::string info() const
constexpr i32 raw() const FL_NOEXCEPT
Definition s16x16.h:60
const T * data() const FL_NOEXCEPT
Definition span.h:461
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
string str() const FL_NOEXCEPT
Definition strstream.h:43
constexpr u32 raw() const FL_NOEXCEPT
Definition u16x16.h:59
static constexpr FASTLED_FORCE_INLINE u16x16 ceil(u16x16 x) FL_NOEXCEPT
Definition u16x16.h:133
constexpr float to_float() const FL_NOEXCEPT
Definition u16x16.h:61
constexpr u32 to_int() const FL_NOEXCEPT
Definition u16x16.h:60
static constexpr FASTLED_FORCE_INLINE u16x16 from_raw(u32 raw) FL_NOEXCEPT
Definition u16x16.h:53
T * data() FL_NOEXCEPT
Definition vector.h:619
void push_back(const T &value) FL_NOEXCEPT
Definition vector.h:624
void resize(fl::size n) FL_NOEXCEPT
Definition vector.h:593
void apply_kernels(kiss_fft_cpx fft[], kiss_fft_cpx cq[], struct sparse_arr kernels[], struct cq_kernel_cfg cfg) FL_NOEXCEPT
struct sparse_arr * generate_kernels(struct cq_kernel_cfg cfg) FL_NOEXCEPT
void free_kernels(struct sparse_arr *kernels, struct cq_kernel_cfg cfg) FL_NOEXCEPT
struct sparse_arr * cq_kernels_t
Definition cq_kernel.h:59
kiss_fft_scalar min_val
Definition cq_kernel.h:43
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
#define FL_FFT_MIN_VAL
Internal FastLED header for implementation files.
#define kiss_fft_scalar
Definition kiss_fft.h:69
kiss_fft_scalar r
Definition kiss_fft.h:85
kiss_fft_scalar i
Definition kiss_fft.h:86
kiss_fftr_cfg kiss_fftr_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem) FL_NOEXCEPT
#define kiss_fftr_free
Definition kiss_fftr.h:49
struct kiss_fftr_state * kiss_fftr_cfg
Definition kiss_fftr.h:26
#define FL_WARN(X)
Definition log.h:276
Centralized logging categories for FastLED hardware interfaces and subsystems.
void fl_fft_real_forward(kiss_fftr_cfg cfg, int N, const kiss_fft_scalar *in, kiss_fft_cpx *out) FL_NOEXCEPT
Forward real-to-complex FFT.
unsigned char u8
Definition stdint.h:131
float expf(float value) FL_NOEXCEPT
Definition math.h:398
float floorf(float value) FL_NOEXCEPT
Definition math.h:304
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NOEXCEPT
Definition unique_ptr.h:261
constexpr enable_if< is_fixed_point< T >::value, T >::type sqrt(T x) FL_NOEXCEPT
void * memset(void *s, int c, size_t n) FL_NOEXCEPT
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode)
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
Definition sample.cpp.hpp:9
const oct_t oct
Definition ios.cpp.hpp:8
float log2f(float value) FL_NOEXCEPT
Definition math.h:430
float logf(float value) FL_NOEXCEPT
Definition math.h:418
Base definition for an LED controller.
Definition crgb.hpp:179
corkscrew_args args
Definition old.h:149
#define FL_NOEXCEPT
constexpr T scale_signed(T v) const FL_NOEXCEPT
Scale a signed integer by this alpha (UNORM16 semantics).
Definition alpha.h:123
Unsigned 16-bit alpha / brightness — UNORM16.
Definition alpha.h:87
static void resolveModeEnums(Mode &mode, Window &window, int bands, int samples, float fmin, float fmax) FL_NOEXCEPT
Definition fft.cpp.hpp:265