FastLED 3.9.15
Loading...
Searching...
No Matches
frequency_bands.cpp.hpp
Go to the documentation of this file.
3#include "fl/audio/fft/fft.h"
4#include "fl/math/math.h"
5#include "fl/stl/noexcept.h"
6
7namespace fl {
8namespace audio {
9namespace detector {
10
14
15namespace {
16// Number of fft::FFT bins — higher count gives better frequency resolution
17// and cleaner band separation. With 64 bins, the CQ kernel provides
18// good frequency isolation for all three bands (bass, mid, treble).
19const int kNumBands = 64;
20
21// fft::FFT frequency range covering bass through treble.
22// Range [100, 10000] covers the bass (20-250), mid (250-4000), and treble
23// (4000-20000) ranges with good CQ kernel resolution. The ratio of 100x
24// keeps the highest-frequency kernel window large enough for the 512-sample
25// fft::FFT (N_window = 512 / (10000/100) = 5 samples minimum).
26const float kFFTMinFreq = 100.0f;
27const float kFFTMaxFreq = 10000.0f;
28
29} // namespace
30
32 : mBass(0.0f)
33 , mMid(0.0f)
34 , mTreble(0.0f)
35 , mBassMin(20.0f)
36 , mBassMax(250.0f)
37 , mMidMin(250.0f)
38 , mMidMax(4000.0f)
39 , mTrebleMin(4000.0f)
40 , mTrebleMax(20000.0f)
41{}
42
44
46 // Use sample rate from context if available
47 mSampleRate = context->getSampleRate();
48
49 // Use shared master fft::FFT via context (downsampled to our config)
50 mRetainedFFT = context->getFFT(kNumBands, kFFTMinFreq, kFFTMaxFreq);
51 const fft::Bins& fftBins = *mRetainedFFT;
52 sFrequencyBandsFFTCount++; // Diagnostic counter (no private fft::FFT anymore)
53
54 span<const i16> pcm = context->getPCM();
55
56 // Calculate energy for each band using fractional bin overlap
57 float bassEnergy = calculateBandEnergy(fftBins, mBassMin, mBassMax, kFFTMinFreq, kFFTMaxFreq);
58 float midEnergy = calculateBandEnergy(fftBins, mMidMin, mMidMax, kFFTMinFreq, kFFTMaxFreq);
59 float trebleEnergy = calculateBandEnergy(fftBins, mTrebleMin, mTrebleMax, kFFTMinFreq, kFFTMaxFreq);
60
61 // Compute dt from actual audio buffer duration: pcmSize / sampleRate
62 const float dt = computeAudioDt(pcm.size(), mSampleRate);
63 mBass = mBassSmoother.update(bassEnergy, dt);
64 mMid = mMidSmoother.update(midEnergy, dt);
65 mTreble = mTrebleSmoother.update(trebleEnergy, dt);
66
67 // Per-band normalization — mirrors EnergyAnalyzer pattern
68 auto normalizeBand = [](float val, AttackDecayFilter<float>& filter,
69 float frameDt) -> float {
70 float runningMax = filter.update(val, frameDt);
71 if (runningMax < 0.001f) runningMax = 0.001f;
72 return fl::min(1.0f, val / runningMax);
73 };
74 mBassNorm = normalizeBand(mBass, mBassMaxFilter, dt);
75 mMidNorm = normalizeBand(mMid, mMidMaxFilter, dt);
76 mTrebleNorm = normalizeBand(mTreble, mTrebleMaxFilter, dt);
77}
78
80 if (onLevelsUpdate) {
82 }
83 if (onBassLevel) {
85 }
86 if (onMidLevel) {
88 }
89 if (onTrebleLevel) {
91 }
92}
93
95 mBass = 0.0f;
96 mMid = 0.0f;
97 mTreble = 0.0f;
98 mBassSmoother.reset();
99 mMidSmoother.reset();
100 mTrebleSmoother.reset();
101 mBassMaxFilter.reset(0.0f);
102 mMidMaxFilter.reset(0.0f);
103 mTrebleMaxFilter.reset(0.0f);
104 mBassNorm = 0.0f;
105 mMidNorm = 0.0f;
106 mTrebleNorm = 0.0f;
107}
108
109float FrequencyBands::calculateBandEnergy(const fft::Bins& fft, float minFreq, float maxFreq,
110 float fftMinFreq, float fftMaxFreq) {
111 const int numBins = static_cast<int>(fft.raw().size());
112 if (numBins <= 1) {
113 return 0.0f;
114 }
115
116 float totalEnergy = 0.0f;
117 float totalWeight = 0.0f;
118
119 for (int i = 0; i < numBins; i++) {
120 // Compute the frequency range for this CQ bin using log-spaced
121 // boundaries that match the CQ kernel's actual frequency layout.
122 float binLow;
123 float binHigh;
124 if (i == 0) {
125 binLow = fftMinFreq;
126 } else {
127 binLow = fft.binBoundary(i - 1);
128 }
129 if (i == numBins - 1) {
130 binHigh = fftMaxFreq;
131 } else {
132 binHigh = fft.binBoundary(i);
133 }
134
135 // Calculate fractional overlap between this bin and the target band
136 float overlapMin = fl::max(binLow, minFreq);
137 float overlapMax = fl::min(binHigh, maxFreq);
138
139 if (overlapMax <= overlapMin) {
140 continue; // No overlap
141 }
142
143 float binWidth = binHigh - binLow;
144 float overlapFraction = (overlapMax - overlapMin) / binWidth;
145
146 totalEnergy += fft.raw()[i] * overlapFraction;
147 totalWeight += overlapFraction;
148 }
149
150 // Normalize by total fractional weight to make bands comparable
151 return (totalWeight > 0.0f) ? totalEnergy / totalWeight : 0.0f;
152}
153
154} // namespace detector
155} // namespace audio
156} // namespace fl
~FrequencyBands() FL_NOEXCEPT override
function_list< void(float level)> onBassLevel
shared_ptr< const fft::Bins > mRetainedFFT
AttackDecayFilter< float > mBassMaxFilter
AttackDecayFilter< float > mTrebleMaxFilter
function_list< void(float level)> onTrebleLevel
function_list< void(float level)> onMidLevel
ExponentialSmoother< float > mBassSmoother
void update(shared_ptr< Context > context) override
ExponentialSmoother< float > mTrebleSmoother
float calculateBandEnergy(const fft::Bins &fft, float minFreq, float maxFreq, float fftMinFreq, float fftMaxFreq)
function_list< void(float bass, float mid, float treble)> onLevelsUpdate
AttackDecayFilter< float > mMidMaxFilter
ExponentialSmoother< float > mMidSmoother
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NOEXCEPT
Compute the time delta (in seconds) for an audio buffer.
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NOEXCEPT
Definition math.h:71
constexpr common_type_t< T, U > max(T a, U b) FL_NOEXCEPT
Definition math.h:75
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT