FastLED 3.10.8
Loading...
Searching...
No Matches
audio_reactive.cpp.hpp
Go to the documentation of this file.
3#include "fl/stl/assert.h"
7#include "fl/math/math.h"
8#include "fl/stl/span.h"
9#include "fl/stl/int.h"
10#include "fl/stl/unique_ptr.h" // For unique_ptr
11#include "fl/stl/noexcept.h"
12
13namespace fl {
14namespace audio {
15
17 FL_NO_EXCEPT : mConfig{}, mContext(fl::make_shared<Context>(Sample())), mFFTBins(16) // Initialize with 16 frequency bins
18{
19 // Initialize enhanced beat detection components
20 mSpectralFluxDetector = fl::make_unique<SpectralFluxDetector>();
21 mPerceptualWeighting = fl::make_unique<PerceptualWeighting>();
22
23 // Initialize previous magnitudes array to zero
24 for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
25 mPreviousMagnitudes[i] = 0.0f;
26 }
27
28 // Configure spectral silence envelopes — tau=0.2s chosen because FFT noise
29 // floor is brittle; these metrics should snap to zero quickly in silence.
30 SilenceEnvelope::Config envCfg;
31 envCfg.decayTauSeconds = 0.2f;
32 envCfg.targetValue = 0.0f;
33 mDominantFrequencyEnvelope.configure(envCfg);
34 mMagnitudeEnvelope.configure(envCfg);
35 mSpectralFluxEnvelope.configure(envCfg);
36}
37
39
41 setConfig(config);
42
43 // Reset state
46 mLastBeatTime = 0;
47 mPreviousVolume = 0.0f;
48
49 // Configure signal conditioning components
51 scConfig.enableDCRemoval = config.enableSignalConditioning;
52 scConfig.enableSpikeFilter = config.enableSignalConditioning;
53 scConfig.enableNoiseGate = config.noiseGate && config.enableSignalConditioning;
54 mSignalConditioner.configure(scConfig);
55 mSignalConditioner.reset();
56
58 nfConfig.enabled = config.enableNoiseFloorTracking;
59 mNoiseFloorTracker.configure(nfConfig);
60 mNoiseFloorTracker.reset();
61
62 // Configure frequency bin mapper (obligatory - fixes hardcoded sample rate)
65 fbmConfig.sampleRate = config.sampleRate;
66 fbmConfig.useLogSpacing = config.enableLogBinSpacing;
67 fbmConfig.minFrequency = 20.0f;
68 fbmConfig.maxFrequency = static_cast<float>(config.sampleRate) / 2.0f; // Nyquist
69 // fftBinCount will be set when we know the fft::FFT size (after first processSample)
70 fbmConfig.fftBinCount = 256; // Default, overridden when actual fft::FFT size known
71 mFrequencyBinMapper.configure(fbmConfig);
72
73 // Compute pink noise compensation gains from log-spaced CQ bin centers.
74 // CQ bins are log-spaced from fmin to fmax (default: 90-14080 Hz),
75 // matching Bins::binToFreq().
76 {
77 const float fmin = fft::Args::DefaultMinFrequency();
78 const float fmax = fft::Args::DefaultMaxFrequency();
79 const float m = fl::logf(fmax / fmin);
80 float binCenters[16];
81 for (int i = 0; i < 16; ++i) {
82 binCenters[i] = fmin * fl::expf(m * static_cast<float>(i) / 15.0f);
83 }
85 mPinkNoiseComputed = true;
86 }
87
88 // Reset enhanced beat detection components
90 mSpectralFluxDetector->reset();
91 mSpectralFluxDetector->setThreshold(config.spectralFluxThreshold);
92 }
93
94 // Configure musical beat detection (Phase 3 middleware - lazy creation)
95 if (config.enableMusicalBeatDetection) {
98 }
100 mbdConfig.minBPM = config.musicalBeatMinBPM;
101 mbdConfig.maxBPM = config.musicalBeatMaxBPM;
102 mbdConfig.minBeatConfidence = config.musicalBeatConfidence;
103 mbdConfig.sampleRate = config.sampleRate;
104 mbdConfig.samplesPerFrame = 512; // Typical fft::FFT frame size
105 mMusicalBeatDetector->configure(mbdConfig);
106 mMusicalBeatDetector->reset();
107 } else {
108 mMusicalBeatDetector.reset();
109 }
110
111 if (config.enableMultiBandBeats) {
114 }
116 mbbdConfig.bassThreshold = config.bassThreshold;
117 mbbdConfig.midThreshold = config.midThreshold;
118 mbbdConfig.trebleThreshold = config.trebleThreshold;
119 mMultiBandBeatDetector->configure(mbbdConfig);
120 mMultiBandBeatDetector->reset();
121 } else {
123 }
124
125 // Configure spectral equalizer (optional - lazy creation)
126 if (config.enableSpectralEqualizer) {
127 if (!mSpectralEqualizer) {
129 }
132 seConfig.numBands = 16;
133 mSpectralEqualizer->configure(seConfig);
134 } else {
135 mSpectralEqualizer.reset();
136 }
137
138 // Reset previous magnitudes
139 for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
140 mPreviousMagnitudes[i] = 0.0f;
141 }
142
143 // Reset silence envelopes for spectral metrics.
144 mDominantFrequencyEnvelope.reset(0.0f);
145 mMagnitudeEnvelope.reset(0.0f);
146 mSpectralFluxEnvelope.reset(0.0f);
147
148 // Update Context sample rate
149 mContext->setSampleRate(config.sampleRate);
150
151 // Ensure internal Processor exists and is configured.
152 // Eager creation so processSample() can always source volume from it.
154 mAudioProcessor->setSampleRate(config.sampleRate);
155 mAudioProcessor->reset();
156 // Force EnergyAnalyzer detector registration so updateFromContext() updates it
157 mAudioProcessor->getEnergy();
158}
159
161 mConfig = config;
162}
163
165 if (!sample.isValid()) {
166 return; // Invalid sample, ignore
167 }
168
169 // Extract timestamp from the Sample
170 fl::u32 currentTimeMs = sample.timestamp();
171
172 // Phase 1: Signal conditioning pipeline
173 Sample processedSample = sample;
174
175 // Step 1: Signal conditioning (DC removal, spike filtering, noise gate)
176 if (mConfig.enableSignalConditioning) {
177 processedSample = mSignalConditioner.processSample(processedSample);
178 if (!processedSample.isValid()) {
179 return; // Signal was completely filtered out
180 }
181 }
182
183 // Step 2: Noise floor tracking (update tracker, but don't modify signal)
184 if (mConfig.enableNoiseFloorTracking) {
185 float rms = processedSample.rms();
186 mNoiseFloorTracker.update(rms);
187 }
188
189 // Set conditioned sample on shared Context (clears per-frame FFT cache)
190 mContext->setSample(processedSample);
191
192 // Populate silence flag after setSample() resets it — detectors run via
193 // updateFromContext() below and will read context->isSilent().
194 //
195 // Uses an absolute RMS threshold (not the adaptive noise floor). The
196 // adaptive floor's first-sample init matches the current level, which
197 // causes isAboveFloor() to be stuck at false for any steady signal —
198 // wrongly flagging loud constant tones as silent. Absolute RMS is the
199 // right primitive for "no signal present": a steady loud tone has
200 // large RMS and is not silent regardless of noise-floor adaptation.
201 if (mConfig.enableNoiseFloorTracking) {
202 constexpr float kSilenceRmsThreshold = 10.0f;
203 mContext->setSilent(processedSample.rms() < kSilenceRmsThreshold);
204 }
205
206 // Process the conditioned Sample - timing is gated by sample availability
207 processFFT(processedSample);
208
209 // Update internal Processor BEFORE populating Data fields,
210 // so updateVolumeAndPeak() can source normalized volume.
212 mAudioProcessor->updateFromContext(mContext);
213
214 updateVolumeAndPeak(processedSample);
215
216 // Enhanced processing pipeline
219
220 // Apply pink noise compensation AFTER band energy calculation
221 // so that bassEnergy/midEnergy/trebleEnergy reflect actual spectral content
222 if (mPinkNoiseComputed) {
223 for (int i = 0; i < 16; ++i) {
224 mCurrentData.frequencyBins[i] *= mPinkNoiseGains[i];
225 }
226 }
227
228 // Apply A-weighting BEFORE spectral flux and beat detection so that
229 // high-frequency attenuation is visible to onset/beat algorithms.
230 // Previously this ran after beat detection, causing bin 15 to appear
231 // disproportionately active (boosted by pink noise but not yet attenuated).
233
235
236 // Silence gate for spectral metrics (FastLED#2253).
237 // During audio the envelopes are pass-through; during silence they
238 // exponentially decay (tau=0.2s) the raw argmax / flux outputs toward
239 // zero so the FFT noise floor cannot lock onto arbitrary bins.
240 // dt is the exact audio duration of this PCM buffer (frame-rate
241 // independent). When enableNoiseFloorTracking is false, isSilent()
242 // is always false, making this a strict pass-through (no behavior
243 // change for users who haven't opted in).
244 {
245 const bool silent = mContext->isSilent();
246 const float dt = computeAudioDt(processedSample.pcm().size(),
247 mConfig.sampleRate);
248 mCurrentData.dominantFrequency = mDominantFrequencyEnvelope.update(
249 silent, mCurrentData.dominantFrequency, dt);
250 mCurrentData.magnitude = mMagnitudeEnvelope.update(
251 silent, mCurrentData.magnitude, dt);
252 mCurrentData.spectralFlux = mSpectralFluxEnvelope.update(
253 silent, mCurrentData.spectralFlux, dt);
254 }
255
256 // Enhanced beat detection (includes original)
257 detectBeat(currentTimeMs);
258 detectEnhancedBeats(currentTimeMs);
259
260 // Loudness compensation stays after beat detection — it is a global
261 // level adjustment that should not affect relative frequency balance.
263
264 applyGain();
265 applyScaling();
267
268 mCurrentData.timestamp = currentTimeMs;
269
270 // Processor was already updated earlier in this method (before updateVolumeAndPeak).
271}
272
273void Reactive::update(fl::u32 currentTimeMs) FL_NO_EXCEPT {
274 // This method handles updates without new sample data
275 // Just apply smoothing and update timestamp
277 mCurrentData.timestamp = currentTimeMs;
278}
279
281 // Get PCM data from Sample
282 const auto& pcmData = sample.pcm();
283 if (pcmData.empty()) return;
284
285 // Use shared Context for cached FFT (avoids recomputation if Processor also needs it)
286 auto cachedBins = mContext->getFFT16();
287 mFFTBins = *cachedBins;
288
289 // Map fft::FFT bins to frequency channels using WLED-compatible mapping
291}
292
294 // Sample::fft() returns CQ-kernel bins that are already
295 // frequency-mapped (linearly spaced from fmin to fmax). Copy them
296 // directly instead of re-mapping through FrequencyBinMapper, which
297 // incorrectly treats CQ bins as raw DFT bins.
298 fl::span<const float> rawBins = mFFTBins.raw();
299 if (rawBins.empty()) {
300 for (int i = 0; i < 16; ++i) {
301 mCurrentData.frequencyBins[i] = 0.0f;
302 }
303 return;
304 }
305
306 // Copy CQ bins directly to frequency bins (already frequency-mapped)
307 for (int i = 0; i < 16; ++i) {
308 if (i < static_cast<int>(rawBins.size())) {
309 mCurrentData.frequencyBins[i] = rawBins[i];
310 } else {
311 mCurrentData.frequencyBins[i] = 0.0f;
312 }
313 }
314
315 // Note: Pink noise compensation is applied later in processSample(),
316 // AFTER band energies are calculated from the raw CQ bins.
317 // This ensures bassEnergy/midEnergy/trebleEnergy reflect actual
318 // spectral content, not display-oriented compensation.
319
320 // Find dominant frequency bin
321 float maxMagnitude = 0.0f;
322 int maxBin = 0;
323 for (int i = 0; i < 16; ++i) {
324 if (mCurrentData.frequencyBins[i] > maxMagnitude) {
325 maxMagnitude = mCurrentData.frequencyBins[i];
326 maxBin = i;
327 }
328 }
329
330 // CQ bins are log-spaced — use the same formula as Bins::binToFreq().
331 mCurrentData.dominantFrequency = mFFTBins.binToFreq(maxBin);
332 mCurrentData.magnitude = maxMagnitude;
333}
334
336 // Get PCM data from Sample
337 const auto& pcmData = sample.pcm();
338 if (pcmData.empty()) {
339 mCurrentData.volume = 0.0f;
340 mCurrentData.volumeRaw = 0.0f;
341 mCurrentData.peak = 0.0f;
342 return;
343 }
344
345 // Raw volume: simple bounded normalization (no adaptive smoothing)
346 // 23170 ≈ max RMS of a full-scale 16-bit sine wave (32767 / √2).
347 // Clamp so clipped/square-wave input stays at 1.0.
348 float rms = sample.rms();
349 float raw = rms / 23170.0f;
350 mCurrentData.volumeRaw = (raw > 1.0f) ? 1.0f : raw;
351
352 // Normalized volume from Processor's EnergyAnalyzer (adaptive 0.0-1.0).
353 // mAudioProcessor is guaranteed non-null: ensureAudioProcessor() is called
354 // in processSample() immediately before this method.
355 FL_ASSERT(mAudioProcessor, "mAudioProcessor is null — was begin() called before processSample()?");
356 mCurrentData.volume = mAudioProcessor->getEnergy();
357
358 // Peak: simple bounded normalization
359 float maxSample = 0.0f;
360 for (fl::i16 pcmSample : pcmData) {
361 float absSample = (pcmSample < 0) ? -static_cast<float>(pcmSample) : static_cast<float>(pcmSample);
362 maxSample = (maxSample > absSample) ? maxSample : absSample;
363 }
364 mCurrentData.peak = maxSample / 32768.0f;
365}
366
367void Reactive::detectBeat(fl::u32 currentTimeMs) FL_NO_EXCEPT {
368 // Need minimum time since last beat
369 if (currentTimeMs - mLastBeatTime < BEAT_COOLDOWN) {
370 mCurrentData.beatDetected = false;
371 return;
372 }
373
374 // Use raw volume for beat detection — it is proportional to amplitude.
375 // Adaptive volume (mCurrentData.volume) converges toward a steady state,
376 // masking the transient jumps that indicate beats.
377 // NOTE: this runs BEFORE applyGain(), so beat detection is gain-independent.
378 float currentVolume = mCurrentData.volumeRaw;
379
380 // Beat detected if volume significantly increased
381 if (currentVolume > mPreviousVolume + mVolumeThreshold &&
382 currentVolume > 0.02f) { // Minimum volume threshold
383 mCurrentData.beatDetected = true;
384 mLastBeatTime = currentTimeMs;
385 } else {
386 mCurrentData.beatDetected = false;
387 }
388
389 // Update previous volume for next comparison using attack/decay
390 float beatAttackRate = mConfig.attack / 255.0f * 0.5f + 0.1f; // 0.1 to 0.6
391 float beatDecayRate = mConfig.decay / 255.0f * 0.3f + 0.05f; // 0.05 to 0.35
392
393 if (currentVolume > mPreviousVolume) {
394 // Rising volume - use attack rate (faster tracking)
395 mPreviousVolume = mPreviousVolume * (1.0f - beatAttackRate) + currentVolume * beatAttackRate;
396 } else {
397 // Falling volume - use decay rate (slower tracking)
398 mPreviousVolume = mPreviousVolume * (1.0f - beatDecayRate) + currentVolume * beatDecayRate;
399 }
400}
401
403 // Apply gain setting (0-255 maps to 0.0-2.0 multiplier)
404 float gainMultiplier = static_cast<float>(mConfig.gain) / 128.0f;
405
406 // Don't apply gain to adaptive volume — it's self-normalizing (converges
407 // to 1.0 regardless of amplitude), so scaling it is meaningless.
408 mCurrentData.volumeRaw *= gainMultiplier;
409 mCurrentData.peak *= gainMultiplier;
410
411 // Clamp to documented 0.0-1.0 range after gain
412 if (mCurrentData.volumeRaw > 1.0f) mCurrentData.volumeRaw = 1.0f;
413 if (mCurrentData.peak > 1.0f) mCurrentData.peak = 1.0f;
414
415 for (int i = 0; i < 16; ++i) {
416 mCurrentData.frequencyBins[i] *= gainMultiplier;
417 }
418}
419
421 // Apply scaling mode to frequency bins
422 for (int i = 0; i < 16; ++i) {
423 float value = mCurrentData.frequencyBins[i];
424
425 switch (mConfig.scalingMode) {
426 case 1: // Logarithmic scaling
427 if (value > 1.0f) {
428 value = logf(value) * 20.0f; // Scale factor
429 } else {
430 value = 0.0f;
431 }
432 break;
433
434 case 2: // Linear scaling (no change)
435 // value remains as-is
436 break;
437
438 case 3: // Square root scaling
439 if (value > 0.0f) {
440 value = sqrtf(value) * 8.0f; // Scale factor
441 } else {
442 value = 0.0f;
443 }
444 break;
445
446 case 0: // No scaling
447 default:
448 // value remains as-is
449 break;
450 }
451
452 mCurrentData.frequencyBins[i] = value;
453 }
454}
455
457 // Attack/decay smoothing - different rates for rising vs falling values
458 // Convert attack/decay times to smoothing factors
459 // Shorter times = less smoothing (faster response)
460 float attackFactor = 1.0f - (mConfig.attack / 255.0f * 0.9f); // Range: 0.1 to 1.0
461 float decayFactor = 1.0f - (mConfig.decay / 255.0f * 0.95f); // Range: 0.05 to 1.0
462
463 // volume is already adaptively smoothed by EnergyAnalyzer — copy directly
464 // to avoid double-smoothing which would make it sluggish.
465 mSmoothedData.volume = mCurrentData.volume;
466
467 // Apply attack/decay smoothing to volumeRaw
468 if (mCurrentData.volumeRaw > mSmoothedData.volumeRaw) {
469 mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - attackFactor) +
470 mCurrentData.volumeRaw * attackFactor;
471 } else {
472 mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - decayFactor) +
473 mCurrentData.volumeRaw * decayFactor;
474 }
475
476 // Apply attack/decay smoothing to peak
477 if (mCurrentData.peak > mSmoothedData.peak) {
478 mSmoothedData.peak = mSmoothedData.peak * (1.0f - attackFactor) +
479 mCurrentData.peak * attackFactor;
480 } else {
481 mSmoothedData.peak = mSmoothedData.peak * (1.0f - decayFactor) +
482 mCurrentData.peak * decayFactor;
483 }
484
485 // Apply attack/decay smoothing to frequency bins
486 for (int i = 0; i < 16; ++i) {
487 if (mCurrentData.frequencyBins[i] > mSmoothedData.frequencyBins[i]) {
488 // Rising - use attack time
489 mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - attackFactor) +
490 mCurrentData.frequencyBins[i] * attackFactor;
491 } else {
492 // Falling - use decay time
493 mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - decayFactor) +
494 mCurrentData.frequencyBins[i] * decayFactor;
495 }
496 }
497
498 // Copy non-smoothed values
499 mSmoothedData.beatDetected = mCurrentData.beatDetected;
500 mSmoothedData.dominantFrequency = mCurrentData.dominantFrequency;
501 mSmoothedData.magnitude = mCurrentData.magnitude;
502 mSmoothedData.timestamp = mCurrentData.timestamp;
503
504 // Copy enhanced beat detection fields
505 mSmoothedData.bassBeatDetected = mCurrentData.bassBeatDetected;
506 mSmoothedData.midBeatDetected = mCurrentData.midBeatDetected;
507 mSmoothedData.trebleBeatDetected = mCurrentData.trebleBeatDetected;
508 mSmoothedData.spectralFlux = mCurrentData.spectralFlux;
509 mSmoothedData.bassEnergy = mCurrentData.bassEnergy;
510 mSmoothedData.midEnergy = mCurrentData.midEnergy;
511 mSmoothedData.trebleEnergy = mCurrentData.trebleEnergy;
512}
513
515 return mCurrentData;
516}
517
521
523 return mCurrentData.volume;
524}
525
527 // Average of bins 0-1 (sub-bass and bass)
528 return (mCurrentData.frequencyBins[0] + mCurrentData.frequencyBins[1]) / 2.0f;
529}
530
532 // Average of bins 6-7 (midrange around 1kHz)
533 return (mCurrentData.frequencyBins[6] + mCurrentData.frequencyBins[7]) / 2.0f;
534}
535
537 // Average of bins 14-15 (high frequencies)
538 return (mCurrentData.frequencyBins[14] + mCurrentData.frequencyBins[15]) / 2.0f;
539}
540
542 return mCurrentData.beatDetected;
543}
544
546 return mCurrentData.bassBeatDetected;
547}
548
550 return mCurrentData.midBeatDetected;
551}
552
554 return mCurrentData.trebleBeatDetected;
555}
556
558 return mCurrentData.spectralFlux;
559}
560
562 return mCurrentData.bassEnergy;
563}
564
566 return mCurrentData.midEnergy;
567}
568
570 return mCurrentData.trebleEnergy;
571}
572
574 float scaled = mCurrentData.volume * 255.0f;
575 if (scaled < 0.0f) scaled = 0.0f;
576 if (scaled > 255.0f) scaled = 255.0f;
577 return static_cast<fl::u8>(scaled);
578}
579
580CRGB Reactive::volumeToColor(const CRGBPalette16& /* palette */) const FL_NO_EXCEPT {
581 fl::u8 index = volumeToScale255();
582 // Simplified color palette lookup
583 return CRGB(index, index, index); // For now, return grayscale
584}
585
587 if (binIndex >= 16) return 0;
588 // Bin values have no fixed upper bound (depend on FFT output, scaling
589 // mode, gain, and equalization). Best-effort clamp to 0-255.
590 float value = mCurrentData.frequencyBins[binIndex];
591 if (value < 0.0f) value = 0.0f;
592 if (value > 255.0f) value = 255.0f;
593 return static_cast<fl::u8>(value);
594}
595
596// Enhanced beat detection methods
598 span<const float> bins(mCurrentData.frequencyBins, 16);
599 mCurrentData.bassEnergy = mFrequencyBinMapper.getBassEnergy(bins);
600 mCurrentData.midEnergy = mFrequencyBinMapper.getMidEnergy(bins);
601 mCurrentData.trebleEnergy = mFrequencyBinMapper.getTrebleEnergy(bins);
602}
603
605 if (!mConfig.enableSpectralEqualizer) {
606 return;
607 }
608
609 // Apply spectral EQ in-place on the frequency bins
610 float equalizedBins[16];
611 span<const float> inputSpan(mCurrentData.frequencyBins, 16);
612 span<float> outputSpan(equalizedBins, 16);
613 mSpectralEqualizer->apply(inputSpan, outputSpan);
614
615 // Copy back
616 for (int i = 0; i < 16; ++i) {
617 mCurrentData.frequencyBins[i] = equalizedBins[i];
618 }
619}
620
622 // Compute spectral flux from Reactive's own previous magnitudes.
623 //
624 // IMPORTANT ordering contract (called from processSample):
625 // Step 7: updateSpectralFlux() — uses & updates Reactive::mPreviousMagnitudes
626 // Step 9: detectEnhancedBeats() — calls SpectralFluxDetector::detectOnset()
627 // which uses & updates SpectralFluxDetector::mPreviousMagnitudes
628 //
629 // Both arrays converge to the same values (both set to mCurrentData.frequencyBins
630 // each frame), but they are separate to avoid a state-ordering bug: calling
631 // mSpectralFluxDetector->calculateSpectralFlux() here would update the
632 // detector's internal state and cause detectOnset() to see zero flux.
633 float flux = 0.0f;
634 for (int i = 0; i < 16; ++i) {
635 float diff = mCurrentData.frequencyBins[i] - mPreviousMagnitudes[i];
636 if (diff > 0.0f) {
637 flux += diff;
638 }
639 }
640 mCurrentData.spectralFlux = flux;
641
642 // Update previous magnitudes for next frame
643 for (int i = 0; i < 16; ++i) {
644 mPreviousMagnitudes[i] = mCurrentData.frequencyBins[i];
645 }
646}
647
648void Reactive::detectEnhancedBeats(fl::u32 currentTimeMs) FL_NO_EXCEPT {
649 // Reset beat flags
650 mCurrentData.bassBeatDetected = false;
651 mCurrentData.midBeatDetected = false;
652 mCurrentData.trebleBeatDetected = false;
653
654 // Skip if enhanced beat detection is disabled
655 if (!mConfig.enableSpectralFlux && !mConfig.enableMultiBand &&
656 !mConfig.enableMusicalBeatDetection && !mConfig.enableMultiBandBeats) {
657 return;
658 }
659
660 // Need minimum time since last beat for enhanced detection too
661 if (currentTimeMs - mLastBeatTime < BEAT_COOLDOWN) {
662 return;
663 }
664
665 // Spectral flux-based onset detection (preliminary)
666 bool onsetDetected = false;
667 float onsetStrength = 0.0f;
668
669 if (mConfig.enableSpectralFlux && mSpectralFluxDetector) {
670 onsetDetected = mSpectralFluxDetector->detectOnset(
671 mCurrentData.frequencyBins
672 );
673
674 // Use already-computed spectral flux for onset strength.
675 // Do NOT call calculateSpectralFlux() again — detectOnset() already
676 // updated the detector's internal state, so a second call would see
677 // current == previous and return 0.
678 if (onsetDetected) {
679 onsetStrength = mCurrentData.spectralFlux;
680 }
681 }
682
683 // Phase 3: Musical beat detection - validates onsets as true musical beats
684 if (mConfig.enableMusicalBeatDetection) {
685 mMusicalBeatDetector->processSample(onsetDetected, onsetStrength);
686
687 if (mMusicalBeatDetector->isBeat()) {
688 // This is a validated musical beat, not just a random onset
689 mCurrentData.beatDetected = true;
690 mLastBeatTime = currentTimeMs;
691 }
692 } else if (onsetDetected) {
693 // Fall back to simple onset detection if musical beat detection disabled
694 mCurrentData.beatDetected = true;
695 mLastBeatTime = currentTimeMs;
696 }
697
698 // Phase 3: Multi-band beat detection - per-frequency beat tracking
699 if (mConfig.enableMultiBandBeats) {
700 mMultiBandBeatDetector->detectBeats(mCurrentData.frequencyBins);
701
702 mCurrentData.bassBeatDetected = mMultiBandBeatDetector->isBassBeat();
703 mCurrentData.midBeatDetected = mMultiBandBeatDetector->isMidBeat();
704 mCurrentData.trebleBeatDetected = mMultiBandBeatDetector->isTrebleBeat();
705 } else if (mConfig.enableMultiBand) {
706 // Fall back to simple threshold-based detection if multi-band disabled
707 // Bass beat detection (bins 0-1)
708 if (mCurrentData.bassEnergy > mConfig.bassThreshold) {
709 mCurrentData.bassBeatDetected = true;
710 }
711
712 // Mid beat detection (bins 6-7)
713 if (mCurrentData.midEnergy > mConfig.midThreshold) {
714 mCurrentData.midBeatDetected = true;
715 }
716
717 // Treble beat detection (bins 14-15)
718 if (mCurrentData.trebleEnergy > mConfig.trebleThreshold) {
719 mCurrentData.trebleBeatDetected = true;
720 }
721 }
722}
723
729
732 mPerceptualWeighting->applyLoudnessCompensation(mCurrentData, 0.28f);
733 }
734}
735
736// Helper methods
737float Reactive::mapFrequencyBin(int fromBin, int toBin) FL_NO_EXCEPT {
738 if (fromBin < 0 || toBin >= static_cast<int>(mFFTBins.bands()) || fromBin > toBin) {
739 return 0.0f;
740 }
741
742 float sum = 0.0f;
743 for (int i = fromBin; i <= toBin; ++i) {
744 if (i < static_cast<int>(mFFTBins.raw().size())) {
745 sum += mFFTBins.raw()[i];
746 }
747 }
748
749 return sum / static_cast<float>(toBin - fromBin + 1);
750}
751
753 if (samples.empty()) return 0.0f;
754
755 float sumSquares = 0.0f;
756 for (const auto& sample : samples) {
757 float f = static_cast<float>(sample);
758 sumSquares += f * f;
759 }
760
761 return sqrtf(sumSquares / samples.size());
762}
763
764// SpectralFluxDetector implementation
767#if SKETCH_HAS_LARGE_MEMORY
768 , mHistoryIndex(0)
769#endif
770{
771 // Initialize previous magnitudes to zero
772 for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
773 mPreviousMagnitudes[i] = 0.0f;
774 }
775
776#if SKETCH_HAS_LARGE_MEMORY
777 // Initialize flux history to zero
778 for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
779 mFluxHistory[i] = 0.0f;
780 }
781#endif
782}
783
785
787 for (fl::size i = 0; i < mPreviousMagnitudes.size(); ++i) {
788 mPreviousMagnitudes[i] = 0.0f;
789 }
790
791#if SKETCH_HAS_LARGE_MEMORY
792 for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
793 mFluxHistory[i] = 0.0f;
794 }
795 mHistoryIndex = 0;
796#endif
797}
798
800 float flux = calculateSpectralFlux(currentBins, span<const float, 16>(mPreviousMagnitudes.data(), 16));
801
802#if SKETCH_HAS_LARGE_MEMORY
803 // Store flux in history for adaptive threshold calculation
804 mFluxHistory[mHistoryIndex] = flux;
805 mHistoryIndex = (mHistoryIndex + 1) % mFluxHistory.size();
806
807 float adaptiveThreshold = calculateAdaptiveThreshold();
808 return flux > adaptiveThreshold;
809#else
810 // Simple fixed threshold for memory-constrained platforms
811 return flux > mFluxThreshold;
812#endif
813}
814
816 float flux = 0.0f;
817
818 // Calculate spectral flux as sum of positive differences
819 for (int i = 0; i < 16; ++i) {
820 float diff = currentBins[i] - previousBins[i];
821 if (diff > 0.0f) {
822 flux += diff;
823 }
824 }
825
826 // Update previous magnitudes for next calculation
827 for (int i = 0; i < 16; ++i) {
828 mPreviousMagnitudes[i] = currentBins[i];
829 }
830
831 return flux;
832}
833
835 mFluxThreshold = threshold;
836}
837
841
842#if SKETCH_HAS_LARGE_MEMORY
843float SpectralFluxDetector::calculateAdaptiveThreshold() FL_NO_EXCEPT {
844 // Calculate moving average of flux history
845 float sum = 0.0f;
846 for (fl::size i = 0; i < mFluxHistory.size(); ++i) {
847 sum += mFluxHistory[i];
848 }
849 float average = sum / mFluxHistory.size();
850
851 // Adaptive threshold is base threshold plus some multiple of recent average
852 return mFluxThreshold + (average * 0.5f);
853}
854#endif
855
856// BeatDetectors implementation
862
864
866#if SKETCH_HAS_LARGE_MEMORY
867 bass.reset();
868 mid.reset();
869 treble.reset();
870#else
871 combined.reset();
872#endif
873
874 mBassEnergy = 0.0f;
875 mMidEnergy = 0.0f;
876 mTrebleEnergy = 0.0f;
877 mPreviousBassEnergy = 0.0f;
878 mPreviousMidEnergy = 0.0f;
880}
881
883 // Calculate current band energies
884 mBassEnergy = (frequencyBins[0] + frequencyBins[1]) / 2.0f;
885 mMidEnergy = (frequencyBins[6] + frequencyBins[7]) / 2.0f;
886 mTrebleEnergy = (frequencyBins[14] + frequencyBins[15]) / 2.0f;
887
888 // Simple threshold-based detection using per-band energy deltas.
889 audioData.bassBeatDetected = (mBassEnergy > mPreviousBassEnergy * 1.3f) && (mBassEnergy > 0.1f);
890 audioData.midBeatDetected = (mMidEnergy > mPreviousMidEnergy * 1.25f) && (mMidEnergy > 0.08f);
891 audioData.trebleBeatDetected = (mTrebleEnergy > mPreviousTrebleEnergy * 1.2f) && (mTrebleEnergy > 0.05f);
892
893 // Update previous energies
897}
898
899void BeatDetectors::setThresholds(float bassThresh, float midThresh, float trebleThresh) FL_NO_EXCEPT {
900#if SKETCH_HAS_LARGE_MEMORY
901 bass.setThreshold(bassThresh);
902 mid.setThreshold(midThresh);
903 treble.setThreshold(trebleThresh);
904#else
905 combined.setThreshold((bassThresh + midThresh + trebleThresh) / 3.0f);
906#endif
907}
908
909// PerceptualWeighting implementation
911#if SKETCH_HAS_LARGE_MEMORY
912 : mHistoryIndex(0)
913#endif
914{
915#if SKETCH_HAS_LARGE_MEMORY
916 // Initialize loudness history to zero
917 for (fl::size i = 0; i < mLoudnessHistory.size(); ++i) {
918 mLoudnessHistory[i] = 0.0f;
919 }
920 // Suppress unused warning until mHistoryIndex is implemented
921 (void)mHistoryIndex;
922#endif
923}
924
926
928 // Apply A-weighting coefficients to frequency bins
929 for (int i = 0; i < 16; ++i) {
930 data.frequencyBins[i] *= A_WEIGHTING_COEFFS[i];
931 }
932}
933
934void PerceptualWeighting::applyLoudnessCompensation(Data& data, float referenceLevel) const FL_NO_EXCEPT {
935 // Calculate current loudness level from raw (non-adaptive) volume.
936 // data.volume is adaptive (converges to ~1.0) and cannot distinguish
937 // quiet from loud signals. volumeRaw preserves actual amplitude.
938 float currentLoudness = data.volumeRaw;
939
940 // Calculate compensation factor based on difference from reference
941 float compensationFactor = 1.0f;
942 if (currentLoudness < referenceLevel) {
943 // Boost quiet signals
944 compensationFactor = 1.0f + (referenceLevel - currentLoudness) / referenceLevel * 0.3f;
945 } else if (currentLoudness > referenceLevel * 1.5f) {
946 // Slightly reduce very loud signals
947 compensationFactor = 1.0f - (currentLoudness - referenceLevel * 1.5f) / (referenceLevel * 2.0f) * 0.2f;
948 }
949
950 // Apply compensation to frequency bins
951 for (int i = 0; i < 16; ++i) {
952 data.frequencyBins[i] *= compensationFactor;
953 }
954
955#if SKETCH_HAS_LARGE_MEMORY
956 // Store in history for future adaptive compensation (not implemented yet)
957 // This would be used for more sophisticated dynamic range compensation
958#endif
959}
960
962 ensureAudioProcessor().setGain(gain);
963}
964
966 if (mAudioProcessor) {
967 return mAudioProcessor->getGain();
968 }
969 return 1.0f;
970}
971
972// Signal conditioning stats accessors
976
980
982 return mConfig.enableSpectralEqualizer;
983}
984
986 if (!mSpectralEqualizer) {
987 static const SpectralEqualizer::Stats kEmpty{};
988 return kEmpty;
989 }
990 return mSpectralEqualizer->getStats();
991}
992
993// ----- Polling Getter Forwarding (via internal Processor) -----
994
1002
1040
1041} // namespace audio
1042} // namespace fl
float rms(fl::span< const int16_t > data)
Definition simple.h:104
bool detectBeat(float energy)
Definition advanced.h:123
#define FL_ASSERT(x, MSG)
Definition assert.h:6
Get current statistics (for monitoring/debugging)
void applyAWeighting(Data &data) const FL_NO_EXCEPT
static constexpr float A_WEIGHTING_COEFFS[16]
~PerceptualWeighting() FL_NO_EXCEPT
void applyLoudnessCompensation(Data &data, float referenceLevel) const FL_NO_EXCEPT
float getBPM() FL_NO_EXCEPT
float getChordConfidence() FL_NO_EXCEPT
float getVocalConfidence() FL_NO_EXCEPT
float getPitch() FL_NO_EXCEPT
float getMoodArousal() FL_NO_EXCEPT
float getPitchConfidence() FL_NO_EXCEPT
float getNoteVelocity() FL_NO_EXCEPT
float getMidLevel() FL_NO_EXCEPT
float getTempoBPM() FL_NO_EXCEPT
float getDropImpact() FL_NO_EXCEPT
float getDownbeatConfidence() FL_NO_EXCEPT
float getDynamicTrend() FL_NO_EXCEPT
bool isSnare() FL_NO_EXCEPT
float getTrebleLevel() FL_NO_EXCEPT
bool isDiminuendo() FL_NO_EXCEPT
float getMeasurePhase() FL_NO_EXCEPT
float getTransientStrength() FL_NO_EXCEPT
float getNoteConfidence() FL_NO_EXCEPT
float getBassLevel() FL_NO_EXCEPT
bool isKick() FL_NO_EXCEPT
float getKeyConfidence() FL_NO_EXCEPT
float getBackbeatConfidence() FL_NO_EXCEPT
float getPeakLevel() FL_NO_EXCEPT
u32 getSilenceDuration() FL_NO_EXCEPT
u8 getCurrentBeatNumber() FL_NO_EXCEPT
bool isCrescendo() FL_NO_EXCEPT
float getTempoConfidence() FL_NO_EXCEPT
float getEnergy() FL_NO_EXCEPT
float getBuildupProgress() FL_NO_EXCEPT
bool isSilent() FL_NO_EXCEPT
bool isTom() FL_NO_EXCEPT
float getBackbeatStrength() FL_NO_EXCEPT
float getBeatConfidence() FL_NO_EXCEPT
float getMoodValence() FL_NO_EXCEPT
bool isHiHat() FL_NO_EXCEPT
float getBuildupIntensity() FL_NO_EXCEPT
u8 getCurrentNote() FL_NO_EXCEPT
float getBassLevel() FL_NO_EXCEPT
bool isTrebleBeat() const FL_NO_EXCEPT
float getMoodValence() FL_NO_EXCEPT
float getVocalConfidence() FL_NO_EXCEPT
void processSample(const Sample &sample) FL_NO_EXCEPT
void mapFFTBinsToFrequencyChannels() FL_NO_EXCEPT
float getTrebleLevel() FL_NO_EXCEPT
bool isTom() FL_NO_EXCEPT
float getTreble() const FL_NO_EXCEPT
float getBass() const FL_NO_EXCEPT
float getTrebleEnergy() const FL_NO_EXCEPT
bool isBeat() const FL_NO_EXCEPT
float getMeasurePhase() FL_NO_EXCEPT
const Data & getData() const FL_NO_EXCEPT
bool isHiHat() FL_NO_EXCEPT
void calculateBandEnergies() FL_NO_EXCEPT
void updateSpectralFlux() FL_NO_EXCEPT
void detectBeat(fl::u32 currentTimeMs) FL_NO_EXCEPT
void setConfig(const ReactiveConfig &config) FL_NO_EXCEPT
u8 getCurrentNote() FL_NO_EXCEPT
float getBackbeatConfidence() FL_NO_EXCEPT
float getPitchHz() FL_NO_EXCEPT
bool isBassBeat() const FL_NO_EXCEPT
void applyLoudnessCompensation() FL_NO_EXCEPT
void setGain(float gain) FL_NO_EXCEPT
fl::unique_ptr< SpectralFluxDetector > mSpectralFluxDetector
bool isDiminuendo() FL_NO_EXCEPT
void applySpectralEqualization() FL_NO_EXCEPT
SilenceEnvelope mDominantFrequencyEnvelope
fl::unique_ptr< SpectralEqualizer > mSpectralEqualizer
bool isCrescendo() FL_NO_EXCEPT
CRGB volumeToColor(const CRGBPalette16 &palette) const FL_NO_EXCEPT
fl::unique_ptr< detector::MultiBandBeat > mMultiBandBeatDetector
const SpectralEqualizer::Stats & getSpectralEqualizerStats() const FL_NO_EXCEPT
shared_ptr< Context > mContext
fl::unique_ptr< Processor > mAudioProcessor
float getMoodArousal() FL_NO_EXCEPT
void applyAWeighting() FL_NO_EXCEPT
u32 getSilenceDuration() FL_NO_EXCEPT
float getBuildupIntensity() FL_NO_EXCEPT
SilenceEnvelope mMagnitudeEnvelope
float getBassEnergy() const FL_NO_EXCEPT
float getPitchConfidence() FL_NO_EXCEPT
float getBPM() FL_NO_EXCEPT
void applyGain() FL_NO_EXCEPT
float getMid() const FL_NO_EXCEPT
ReactiveConfig mConfig
float getTempoBPM() FL_NO_EXCEPT
float getBuildupProgress() FL_NO_EXCEPT
void updateVolumeAndPeak(const Sample &sample) FL_NO_EXCEPT
const NoiseFloorTracker::Stats & getNoiseFloorStats() const FL_NO_EXCEPT
float getNoteConfidence() FL_NO_EXCEPT
u8 getCurrentBeatNumber() FL_NO_EXCEPT
bool isSilent() FL_NO_EXCEPT
SignalConditioner mSignalConditioner
float computeRMS(const fl::vector< fl::i16 > &samples) FL_NO_EXCEPT
float getDownbeatConfidence() FL_NO_EXCEPT
NoiseFloorTracker mNoiseFloorTracker
bool isSpectralEqualizerEnabled() const FL_NO_EXCEPT
~Reactive() FL_NO_EXCEPT
const SignalConditioner::Stats & getSignalConditionerStats() const FL_NO_EXCEPT
bool isSnare() FL_NO_EXCEPT
const Data & getSmoothedData() const FL_NO_EXCEPT
float getNoteVelocity() FL_NO_EXCEPT
float getKeyConfidence() FL_NO_EXCEPT
fl::unique_ptr< detector::MusicalBeat > mMusicalBeatDetector
float getBeatConfidence() FL_NO_EXCEPT
float getPeakLevel() FL_NO_EXCEPT
float getMidEnergy() const FL_NO_EXCEPT
void applyScaling() FL_NO_EXCEPT
float getTempoConfidence() FL_NO_EXCEPT
float getDynamicTrend() FL_NO_EXCEPT
float getGain() const FL_NO_EXCEPT
float getDropImpact() FL_NO_EXCEPT
float getBackbeatStrength() FL_NO_EXCEPT
void detectEnhancedBeats(fl::u32 currentTimeMs) FL_NO_EXCEPT
fl::unique_ptr< PerceptualWeighting > mPerceptualWeighting
void begin(const ReactiveConfig &config=ReactiveConfig{}) FL_NO_EXCEPT
fl::array< float, 16 > mPreviousMagnitudes
Processor & ensureAudioProcessor() FL_NO_EXCEPT
fl::u8 volumeToScale255() const FL_NO_EXCEPT
void update(fl::u32 currentTimeMs) FL_NO_EXCEPT
float getVolume() const FL_NO_EXCEPT
float mapFrequencyBin(int fromBin, int toBin) FL_NO_EXCEPT
float getMidLevel() FL_NO_EXCEPT
static constexpr fl::u32 BEAT_COOLDOWN
void smoothResults() FL_NO_EXCEPT
float getChordConfidence() FL_NO_EXCEPT
fl::u8 frequencyToScale255(fl::u8 binIndex) const FL_NO_EXCEPT
bool isMidBeat() const FL_NO_EXCEPT
float getEnergyLevel() FL_NO_EXCEPT
bool isKick() FL_NO_EXCEPT
float getTransientStrength() FL_NO_EXCEPT
SilenceEnvelope mSpectralFluxEnvelope
FrequencyBinMapper mFrequencyBinMapper
float getSpectralFlux() const FL_NO_EXCEPT
void processFFT(const Sample &sample) FL_NO_EXCEPT
const VectorPCM & pcm() const FL_NO_EXCEPT
Definition audio.cpp.hpp:78
bool isValid() const FL_NO_EXCEPT
Definition audio.h:35
float rms() const FL_NO_EXCEPT
Get current statistics (for debugging/monitoring)
Get statistics (for debugging/monitoring)
~SpectralFluxDetector() FL_NO_EXCEPT
bool detectOnset(span< const float, 16 > currentBins) FL_NO_EXCEPT
float calculateSpectralFlux(span< const float, 16 > currentBins, span< const float, 16 > previousBins) FL_NO_EXCEPT
float getThreshold() const FL_NO_EXCEPT
void setThreshold(float threshold) FL_NO_EXCEPT
fl::array< float, 16 > mPreviousMagnitudes
constexpr bool empty() const FL_NO_EXCEPT
Definition span.h:535
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
fl::size size() const FL_NO_EXCEPT
High-resolution microphone frequency response data and utilities.
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
Definition unique_ptr.h:263
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
u32 samplesPerFrame
Samples per frame - used for timing calculations.
float midThreshold
Mid beat threshold (0.0-1.0) Energy increase required to trigger mid beat.
u32 sampleRate
Sample rate (Hz) - used for timing calculations.
float maxBPM
Maximum BPM to detect (default: 250 BPM)
float trebleThreshold
Treble beat threshold (0.0-1.0) Energy increase required to trigger treble beat.
float minBeatConfidence
Minimum beat confidence to report a beat (0.0-1.0) Higher values = fewer false positives,...
float minBPM
Minimum BPM to detect (default: 50 BPM)
float bassThreshold
Bass beat threshold (0.0-1.0) Energy increase required to trigger bass beat.
Configuration for musical beat detection.
Configuration for multi-band beat detection.
void computePinkNoiseGains(const float *binCenters, int numBins, float *out) FL_NO_EXCEPT
Compute pink noise compensation gains for all bins.
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NO_EXCEPT
Compute the time delta (in seconds) for an audio buffer.
bool enabled
Enable noise floor tracking.
bool enableDCRemoval
Enable DC offset removal (running average high-pass filter)
bool enableNoiseGate
Enable noise gate with hysteresis.
EqualizationCurve curve
Equalization curve type.
bool useLogSpacing
Use logarithmic spacing (recommended for audio) Logarithmic spacing provides better bass/mid/treble s...
u32 fftBinCount
Number of FFT bins available from FFT output For 512-sample FFT at 22050 Hz: 256 bins (512/2)
bool enableSpikeFilter
Enable spike filtering for I2S glitches.
float minFrequency
Minimum frequency (Hz) - default 20 Hz (bass)
float maxFrequency
Maximum frequency (Hz) - default 16000 Hz (treble)
u32 sampleRate
Sample rate (Hz) - must match FFT sample rate.
size numBands
Number of frequency bands (must match FrequencyBinMapper output)
FrequencyBinMode mode
Number of output frequency bins (16 or 32)
Configuration for frequency bin mapping.
Configuration for signal conditioning pipeline.
Configuration for noise floor tracking.
Configuration for spectral equalizer.
float logf(float value) FL_NO_EXCEPT
Definition math.h:426
unsigned char u8
Definition stdint.h:131
fl::CRGB CRGB
Definition video.h:18
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
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 sqrtf(float value) FL_NO_EXCEPT
Definition math.h:461
float expf(float value) FL_NO_EXCEPT
Definition math.h:406
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NO_EXCEPT
Representation of an 8-bit RGB pixel (Red, Green, Blue)
Definition crgb.h:38
void setThresholds(float bassThresh, float midThresh, float trebleThresh) FL_NO_EXCEPT
~BeatDetectors() FL_NO_EXCEPT
void detectBeats(span< const float, 16 > frequencyBins, Data &audioData) FL_NO_EXCEPT
SpectralFluxDetector combined
static float DefaultMinFrequency() FL_NO_EXCEPT
Definition fft.h:127
static float DefaultMaxFrequency() FL_NO_EXCEPT
Definition fft.h:128