FastLED 3.10.8
Loading...
Searching...
No Matches
equalizer.cpp.hpp
Go to the documentation of this file.
4#include "fl/audio/fft/fft.h"
5#include "fl/math/math.h"
6#include "fl/stl/noexcept.h"
7
8namespace fl {
9namespace audio {
10namespace detector {
11
12namespace {
13// Bin-to-band mapping (WLED style):
14// Bass: bins 0-3 (~90-320 Hz)
15// Mid: bins 4-10 (~320-2560 Hz)
16// Treble: bins 11-15 (~2560-5120 Hz)
17constexpr int kBassStart = 0;
18constexpr int kBassEnd = 3;
19constexpr int kMidStart = 4;
20constexpr int kMidEnd = 10;
21constexpr int kTrebleStart = 11;
22constexpr int kTrebleEnd = 15;
23
25inline float applyScaling(float value, FFTScalingMode mode) FL_NO_EXCEPT {
26 switch (mode) {
28 return fl::sqrtf(value);
30 return fl::logf(1.0f + value);
33 default:
34 return value;
35 }
36}
37
38} // namespace
39
42 mBinMaxFilters.reserve(kNumBins);
43 mBinSmoothers.reserve(kNumBins);
44 for (int i = 0; i < kNumBins; ++i) {
45 mBinMaxFilters.push_back(AttackDecayFilter<float>(mConfig.normAttack, mConfig.normDecay, 0.0f));
47 }
48 mVolumeMax = AttackDecayFilter<float>(mConfig.normAttack, mConfig.normDecay, 0.0f);
49 // Initialize mic gains to 1.0 (no correction)
50 for (int i = 0; i < kNumBins; ++i) {
51 mMicGains[i] = 1.0f;
52 }
54}
55
57
59 mConfig = config;
60 // Rebuild normalization filters
61 mBinMaxFilters.clear();
62 for (int i = 0; i < kNumBins; ++i) {
63 mBinMaxFilters.push_back(AttackDecayFilter<float>(mConfig.normAttack, mConfig.normDecay, 0.0f));
64 }
65 mVolumeMax = AttackDecayFilter<float>(mConfig.normAttack, mConfig.normDecay, 0.0f);
66
67 // Configure output smoothing: attack/decay or simple exponential
68 mUseAttackDecaySmoothing = (mConfig.outputAttack > 0.0f && mConfig.outputDecay > 0.0f);
69 mBinSmoothers.clear();
70 mBinOutputFilters.clear();
72 for (int i = 0; i < kNumBins; ++i) {
73 mBinOutputFilters.push_back(
74 AttackDecayFilter<float>(mConfig.outputAttack, mConfig.outputDecay, 0.0f));
75 }
76 } else {
77 for (int i = 0; i < kNumBins; ++i) {
79 }
80 }
81
82 // Configure spectral equalizer
83 if (mConfig.curve != EqualizationCurve::Flat) {
85 eqConfig.curve = mConfig.curve;
86 eqConfig.numBands = kNumBins;
87 mSpectralEq.configure(eqConfig);
88 }
89
90 // Recompute mic gains if a profile is set (handles freq range changes)
93 }
94
95 // Recompute pink noise gains (freq range may have changed)
97}
98
100 mCurrentMicProfile = profile;
101 MicResponseCurve curve = getMicResponseCurve(profile);
102 mHasMicCorrection = (curve.count > 0);
103 if (mHasMicCorrection) {
104 float binCenters[kNumBins];
105 computeBinCenters(binCenters);
106 downsampleMicResponse(curve, binCenters, kNumBins, mMicGains);
107 } else {
108 for (int i = 0; i < kNumBins; ++i) {
109 mMicGains[i] = 1.0f;
110 }
111 }
112}
113
115 // Log-spaced bin centers matching Bins::binToFreq formula
116 float fmin = mConfig.minFreq;
117 float fmax = mConfig.maxFreq;
118 if (fmax <= fmin) fmax = fmin * 2.0f;
119 float m = fl::logf(fmax / fmin);
120 for (int i = 0; i < kNumBins; ++i) {
121 out[i] = fmin * fl::expf(m * static_cast<float>(i) / static_cast<float>(kNumBins - 1));
122 }
123}
124
130
132 mSampleRate = context->getSampleRate();
133
134 span<const i16> pcm = context->getPCM();
135 if (pcm.size() == 0) return;
136
137 const float dt = computeAudioDt(pcm.size(), mSampleRate);
138
139 // Use Context's cached fft::FFT (shared across detector)
140 mRetainedFFT = context->getFFT(kNumBins, mConfig.minFreq, mConfig.maxFreq);
141 if (!mRetainedFFT) return;
142 const fft::Bins& fftBins = *mRetainedFFT;
143
144 const auto& raw = fftBins.raw();
145 const int numBins = fl::min(static_cast<int>(raw.size()), kNumBins);
146 float scaledBins[kNumBins] = {};
147
148 // Fused loop: Steps 1-4 (Copy, mic correction, pink noise, gain, scaling)
149 // Combined for better cache locality and reduced memory bandwidth
150 const bool applyMicCorrection = mHasMicCorrection;
151 const bool applyScalingMode = (mConfig.scalingMode != FFTScalingMode::None &&
152 mConfig.scalingMode != FFTScalingMode::Linear);
153 const bool applyGain = (mGain != 1.0f);
154
155 for (int i = 0; i < numBins; ++i) {
156 // Step 1: Copy raw and apply fft::FFT downscale
157 float val = raw[i] * kFFTDownscale;
158
159 // Step 2: Microphone correction
160 if (applyMicCorrection) {
161 val *= mMicGains[i];
162 }
163
164 // Step 2.5: Pink noise spectral tilt compensation
165 val *= mPinkNoiseGains[i];
166
167 // Step 3: Apply gain
168 if (applyGain) {
169 val *= mGain;
170 }
171
172 // Step 4: Apply fft::FFT scaling mode
173 if (applyScalingMode) {
174 val = applyScaling(val, mConfig.scalingMode);
175 }
176
177 scaledBins[i] = val;
178 }
179
180 // Step 5: Apply spectral equalization curve (if not flat)
181 if (mConfig.curve != EqualizationCurve::Flat) {
182 span<const float> inSpan(scaledBins, numBins);
183 span<float> outSpan(mEqBuffer, numBins);
184 mSpectralEq.apply(inSpan, outSpan);
185 for (int i = 0; i < numBins; ++i) {
186 scaledBins[i] = mEqBuffer[i];
187 }
188 }
189
190 // Step 6: Smooth → track running max → normalize to 0.0-1.0
191 for (int i = 0; i < numBins; ++i) {
192 float smoothed;
194 smoothed = mBinOutputFilters[i].update(scaledBins[i], dt);
195 } else {
196 smoothed = mBinSmoothers[i].update(scaledBins[i], dt);
197 }
198 float runningMax = mBinMaxFilters[i].update(smoothed, dt);
199 if (runningMax < 0.001f) runningMax = 0.001f;
200 mBins[i] = fl::min(1.0f, smoothed / runningMax);
201 }
202 // Zero any remaining bins
203 for (int i = numBins; i < kNumBins; ++i) {
204 mBins[i] = 0.0f;
205 }
206
207 // Bass = average of bins 0-3
208 float bassSum = 0;
209 for (int i = kBassStart; i <= kBassEnd; ++i) bassSum += mBins[i];
210 mBass = bassSum / static_cast<float>(kBassEnd - kBassStart + 1);
211
212 // Mid = average of bins 4-10
213 float midSum = 0;
214 for (int i = kMidStart; i <= kMidEnd; ++i) midSum += mBins[i];
215 mMid = midSum / static_cast<float>(kMidEnd - kMidStart + 1);
216
217 // Treble = average of bins 11-15
218 float trebleSum = 0;
219 for (int i = kTrebleStart; i <= kTrebleEnd; ++i) trebleSum += mBins[i];
220 mTreble = trebleSum / static_cast<float>(kTrebleEnd - kTrebleStart + 1);
221
222 // Volume = RMS of sample, normalized to 0.0-1.0
223 float rms = context->getRMS();
224 float volumeMax = mVolumeMax.update(rms, dt);
225 if (volumeMax < 0.001f) volumeMax = 0.001f;
226 mVolume = fl::min(1.0f, rms / volumeMax);
227
228 // Volume normalization factor: 1/volumeMax tells the caller how much the
229 // volume was scaled to reach 0-1 range. Higher = quieter input was amplified more.
230 mVolumeNormFactor = (rms > 0.001f) ? (mVolume / rms) : 1.0f;
231
232 // Silence detection: signal is silent when RMS is very low
233 mIsSilence = (rms < mConfig.silenceThreshold);
234
235 // Zero-crossing factor (already 0.0-1.0 from Sample)
236 mZcf = context->getZCF();
237
238 // P2: Dominant frequency detection from pre-normalization data (scaledBins).
239 // Using scaledBins avoids the self-normalization bias where consistently
240 // active bins always read as 1.0 regardless of actual magnitude.
241 int peakBin = 0;
242 float peakVal = 0.0f;
243 float scaledSum = 0.0f;
244 for (int i = 0; i < numBins; ++i) {
245 scaledSum += scaledBins[i];
246 if (scaledBins[i] > peakVal) {
247 peakVal = scaledBins[i];
248 peakBin = i;
249 }
250 }
251 // Magnitude: peak relative to average across all bins (0-1 range).
252 // Reflects how much the peak dominates the spectrum.
253 float scaledAvg = (numBins > 0) ? (scaledSum / static_cast<float>(numBins)) : 0.0f;
254 mDominantMagnitude = (scaledAvg > 0.001f)
255 ? fl::min(1.0f, peakVal / (scaledAvg * static_cast<float>(numBins)))
256 : 0.0f;
257 mDominantFreqHz = fftBins.binToFreq(peakBin);
258
259 // P2: Volume in dB referenced to full-scale i16 (32767.0)
260 constexpr float kFullScale = 32767.0f;
261 if (rms > mConfig.silenceThreshold) {
262 mVolumeDb = 20.0f * fl::logf(rms / kFullScale) / fl::logf(10.0f);
263 if (mVolumeDb < -100.0f) mVolumeDb = -100.0f;
264 } else {
265 mVolumeDb = -100.0f;
266 }
267}
268
270 if (onEqualizer) {
271 Equalizer eq;
272 eq.bass = mBass;
273 eq.mid = mMid;
274 eq.treble = mTreble;
275 eq.volume = mVolume;
276 eq.zcf = mZcf;
280 static_cast<const float*>(mBins), Equalizer::kNumBins);
283 eq.volumeDb = mVolumeDb;
284 onEqualizer(eq);
285 }
286}
287
289 for (int i = 0; i < kNumBins; ++i) {
290 mBins[i] = 0.0f;
291 mBinMaxFilters[i].reset(0.0f);
293 if (i < static_cast<int>(mBinOutputFilters.size())) {
294 mBinOutputFilters[i].reset(0.0f);
295 }
296 } else {
297 if (i < static_cast<int>(mBinSmoothers.size())) {
298 mBinSmoothers[i].reset();
299 }
300 }
301 }
302 mBass = 0;
303 mMid = 0;
304 mTreble = 0;
305 mVolume = 0;
306 mZcf = 0;
307 mVolumeNormFactor = 1.0f;
308 mIsSilence = false;
309 mDominantFreqHz = 0.0f;
310 mDominantMagnitude = 0.0f;
311 mVolumeDb = -100.0f;
312 mVolumeMax.reset(0.0f);
313}
314
316 if (index < 0 || index >= kNumBins) return 0.0f;
317 return mBins[index];
318}
319
320} // namespace detector
321} // namespace audio
322} // namespace fl
float rms(fl::span< const int16_t > data)
Definition simple.h:104
void computeBinCenters(float *out) const FL_NO_EXCEPT
void update(shared_ptr< Context > context) FL_NO_EXCEPT override
vector< AttackDecayFilter< float > > mBinMaxFilters
Definition equalizer.h:138
AttackDecayFilter< float > mVolumeMax
Definition equalizer.h:139
void fireCallbacks() FL_NO_EXCEPT override
static constexpr float kFFTDownscale
Definition equalizer.h:167
vector< AttackDecayFilter< float > > mBinOutputFilters
Definition equalizer.h:144
void configure(const EqualizerConfig &config) FL_NO_EXCEPT
Reconfigure equalizer tuning parameters at runtime.
vector< ExponentialSmoother< float > > mBinSmoothers
Definition equalizer.h:143
shared_ptr< const fft::Bins > mRetainedFFT
Definition equalizer.h:164
~EqualizerDetector() FL_NO_EXCEPT override
void reset() FL_NO_EXCEPT override
function_list< void(const Equalizer &)> onEqualizer
Definition equalizer.h:118
float getBin(int index) const FL_NO_EXCEPT
void setMicProfile(MicProfile profile) FL_NO_EXCEPT
Set microphone correction profile (propagated from Processor).
float binToFreq(int i) const FL_NO_EXCEPT
Definition fft.cpp.hpp:104
fl::span< const float > raw() const FL_NO_EXCEPT
Definition fft.cpp.hpp:63
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
High-resolution microphone frequency response data and utilities.
float applyScaling(float value, FFTScalingMode mode) FL_NO_EXCEPT
Apply fft::FFT scaling mode to a single bin value.
FFTScalingMode
FFT bin scaling mode applied before per-bin normalization.
Definition equalizer.h:17
@ Logarithmic
Log10(1 + magnitude) — compresses high peaks.
Definition equalizer.h:20
@ Linear
Identity (same as None, explicit name for WLED compat)
Definition equalizer.h:21
@ None
Raw magnitudes (no scaling)
Definition equalizer.h:18
@ SquareRoot
Square root of magnitude (WLED-MM default, good perceptual balance)
Definition equalizer.h:19
Configuration for the equalizer detector.
Definition equalizer.h:26
void downsampleMicResponse(const MicResponseCurve &curve, const float *binCenters, int numBins, float *out) FL_NO_EXCEPT
Downsample a high-resolution mic response curve to N output bins.
MicResponseCurve getMicResponseCurve(MicProfile profile) FL_NO_EXCEPT
Get the high-resolution response curve for a given mic profile.
void computePinkNoiseGains(const float *binCenters, int numBins, float *out) FL_NO_EXCEPT
Compute pink noise compensation gains for all bins.
MicProfile
Microphone frequency response correction profile.
@ None
No correction (flat response assumed)
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NO_EXCEPT
Compute the time delta (in seconds) for an audio buffer.
EqualizationCurve curve
Equalization curve type.
int count
Number of data points.
size numBands
Number of frequency bands (must match FrequencyBinMapper output)
Configuration for spectral equalizer.
float logf(float value) FL_NO_EXCEPT
Definition math.h:426
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NO_EXCEPT
Definition math.h:71
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
span< const float, kNumBins > bins
16 bins, each 0.0-1.0
Definition equalizer.h:58
float volumeNormFactor
Volume normalization factor (1/volumeMax). Higher = quieter input was scaled more.
Definition equalizer.h:56
float dominantFreqHz
Frequency of strongest bin (Hz)
Definition equalizer.h:61
float volume
0.0-1.0 (self-normalized RMS)
Definition equalizer.h:54
static constexpr int kNumBins
Definition equalizer.h:50
bool isSilence
True when input signal is effectively silent.
Definition equalizer.h:57
float dominantMagnitude
Magnitude of strongest bin (0.0-1.0, normalized)
Definition equalizer.h:62
float volumeDb
Volume in approximate dB (roughly -100 to 0)
Definition equalizer.h:63
float zcf
0.0-1.0 (zero-crossing factor)
Definition equalizer.h:55
Snapshot of equalizer state, passed to onEqualizer callbacks.
Definition equalizer.h:49