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