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