FastLED 3.9.15
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)
17const int kBassStart = 0;
18const int kBassEnd = 3;
19const int kMidStart = 4;
20const int kMidEnd = 10;
21const int kTrebleStart = 11;
22const int kTrebleEnd = 15;
23
25inline float applyScaling(float value, FFTScalingMode mode) {
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
41{
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
126 float binCenters[kNumBins];
127 computeBinCenters(binCenters);
129}
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
315float EqualizerDetector::getBin(int index) const {
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
vector< AttackDecayFilter< float > > mBinMaxFilters
Definition equalizer.h:138
void setMicProfile(MicProfile profile)
Set microphone correction profile (propagated from Processor).
AttackDecayFilter< float > mVolumeMax
Definition equalizer.h:139
~EqualizerDetector() FL_NOEXCEPT override
static constexpr float kFFTDownscale
Definition equalizer.h:167
vector< AttackDecayFilter< float > > mBinOutputFilters
Definition equalizer.h:144
void configure(const EqualizerConfig &config)
Reconfigure equalizer tuning parameters at runtime.
vector< ExponentialSmoother< float > > mBinSmoothers
Definition equalizer.h:143
shared_ptr< const fft::Bins > mRetainedFFT
Definition equalizer.h:164
void update(shared_ptr< Context > context) override
function_list< void(const Equalizer &)> onEqualizer
Definition equalizer.h:118
fl::span< const float > raw() const FL_NOEXCEPT
Definition fft.cpp.hpp:63
float binToFreq(int i) const FL_NOEXCEPT
Definition fft.cpp.hpp:104
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
High-resolution microphone frequency response data and utilities.
float applyScaling(float value, FFTScalingMode mode)
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
MicResponseCurve getMicResponseCurve(MicProfile profile)
Get the high-resolution response curve for a given mic profile.
void downsampleMicResponse(const MicResponseCurve &curve, const float *binCenters, int numBins, float *out)
Downsample a high-resolution mic response curve to N output bins.
MicProfile
Microphone frequency response correction profile.
@ None
No correction (flat response assumed)
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NOEXCEPT
Compute the time delta (in seconds) for an audio buffer.
void computePinkNoiseGains(const float *binCenters, int numBins, float *out)
Compute pink noise compensation gains for all bins.
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.
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NOEXCEPT
Definition math.h:71
float sqrtf(float value) FL_NOEXCEPT
Definition math.h:453
constexpr int type_rank< T >::value
float expf(float value) FL_NOEXCEPT
Definition math.h:398
float logf(float value) FL_NOEXCEPT
Definition math.h:418
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT
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