FastLED 3.9.15
Loading...
Searching...
No Matches
auto_gain.cpp.hpp
Go to the documentation of this file.
2#include "fl/stl/algorithm.h"
3#include "fl/stl/new.h"
4#include "fl/stl/shared_ptr.h"
5#include "fl/math/math.h"
6#include "fl/stl/noexcept.h"
7
8namespace fl {
9namespace audio {
10
14
16 configure(config);
17}
18
20
21void AutoGain::configure(const AutoGainConfig& config) {
22 mConfig = config;
24 // Initialize peak envelope to targetRMSLevel so initial gain is ~1.0
25 mPeakEnvelope.reset(mConfig.targetRMSLevel);
26 mStats.peakEnvelope = mConfig.targetRMSLevel;
27}
28
30 switch (mConfig.preset) {
32 mPeakDecayTau = 3.3f;
33 mKp = 0.6f;
34 mKi = 1.7f;
35 mGainFollowSlowTau = 12.3f;
36 mGainFollowFastTau = 0.38f;
37 break;
39 mPeakDecayTau = 1.3f;
40 mKp = 1.5f;
41 mKi = 1.85f;
42 mGainFollowSlowTau = 8.2f;
43 mGainFollowFastTau = 0.26f;
44 break;
46 mPeakDecayTau = 6.7f;
47 mKp = 0.65f;
48 mKi = 1.2f;
49 mGainFollowSlowTau = 16.4f;
50 mGainFollowFastTau = 0.51f;
51 break;
53 mPeakDecayTau = mConfig.peakDecayTau;
54 mKp = mConfig.kp;
55 mKi = mConfig.ki;
56 mGainFollowSlowTau = mConfig.gainFollowSlowTau;
57 mGainFollowFastTau = mConfig.gainFollowFastTau;
58 break;
59 }
60 // Reconfigure peak envelope filter with resolved decay tau
61 // Attack is always fast (10ms), decay varies by preset
63}
64
66 mPeakEnvelope.reset(mConfig.targetRMSLevel);
67 mIntegrator = 0.0f;
68 mLastGain = 1.0f;
69 mStats.currentGain = 1.0f;
70 mStats.peakEnvelope = mConfig.targetRMSLevel;
71 mStats.targetGain = 1.0f;
72 mStats.integrator = 0.0f;
73 mStats.inputRMS = 0.0f;
74 mStats.outputRMS = 0.0f;
75 mStats.samplesProcessed = 0;
76}
77
79 // Pass through if disabled
80 if (!mConfig.enabled) {
81 return sample;
82 }
83
84 // Return empty sample if input is invalid or empty
85 if (!sample.isValid() || sample.size() == 0) {
86 return Sample(); // Return invalid sample
87 }
88
89 // Calculate input RMS
90 const float inputRMS = sample.rms();
91 mStats.inputRMS = inputRMS;
92
93 // Compute dt from sample size and sample rate
94 const float dt = (mSampleRate > 0 && sample.size() > 0)
95 ? static_cast<float>(sample.size()) / static_cast<float>(mSampleRate)
96 : 0.023f;
97
98 // Silence detection: spin down integrator when input is essentially silent
99 // (WLED: control_integrated *= 0.91 during silence)
100 const float silenceThreshold = 10.0f;
101 if (inputRMS < silenceThreshold) {
102 mIntegrator *= 0.91f;
103 if (fl::abs(mIntegrator) < 0.01f) mIntegrator = 0.0f;
104 }
105
106 // Step 1: Update peak envelope (fast attack, slow decay)
107 mPeakEnvelope.update(inputRMS, dt);
108 const float peakEnv = mPeakEnvelope.value();
109 mStats.peakEnvelope = peakEnv;
110
111 // Step 2: Compute target gain from peak envelope
112 const float targetGain = computeTargetGain();
113 mStats.targetGain = targetGain;
114
115 // Step 3: PI controller smoothly drives gain toward target
116 const float smoothedGain = updatePIController(targetGain, dt);
117
118 // Step 4: Clamp to configured range
119 const float clampedGain = fl::max(mConfig.minGain,
120 fl::min(mConfig.maxGain, smoothedGain));
121 mStats.currentGain = clampedGain;
122 mLastGain = clampedGain;
123
124 // Step 5: Apply gain to audio
125 const auto& pcm = sample.pcm();
126 mOutputBuffer.clear();
127 mOutputBuffer.reserve(pcm.size());
128 applyGain(pcm, clampedGain, mOutputBuffer);
129
130 // Calculate output RMS for statistics
131 i64 sumSq = 0;
132 for (size i = 0; i < mOutputBuffer.size(); ++i) {
133 i32 val = static_cast<i32>(mOutputBuffer[i]);
134 sumSq += val * val;
135 }
136 mStats.outputRMS = sqrtf(static_cast<float>(sumSq) / static_cast<float>(mOutputBuffer.size()));
137
138 // Update stats
139 mStats.samplesProcessed += sample.size();
140 mStats.integrator = mIntegrator;
141
142 // Create new Sample from amplified PCM
143 SampleImplPtr impl = fl::make_shared<SampleImpl>();
144 impl->assign(mOutputBuffer.begin(), mOutputBuffer.end(), sample.timestamp());
145 return Sample(impl);
146}
147
149 const float peakEnv = mPeakEnvelope.value();
150
151 // Avoid division by very small numbers
152 if (peakEnv < 1.0f) {
153 return mConfig.maxGain; // Signal is essentially silent
154 }
155
156 return mConfig.targetRMSLevel / peakEnv;
157}
158
159float AutoGain::updatePIController(float targetGain, float dt) {
160 const float error = targetGain - mLastGain;
161
162 // Bug 4 fix: Use absolute error threshold when gain is small to avoid
163 // huge errorRatio from dividing by tiny mLastGain
164 const bool largeError = (mLastGain > 0.1f)
165 ? (fl::abs(error) / mLastGain > 0.2f) // Relative: >20% of current gain
166 : (fl::abs(error) > 0.1f); // Absolute: >0.1 when gain is tiny
167 const float tau = largeError ? mGainFollowFastTau : mGainFollowSlowTau;
168
169 // Proportional term
170 const float pTerm = mKp * error;
171
172 // Bug 1 fix: PI target is where we WANT gain to be (not mLastGain + delta + delta)
173 // targetGain + pTerm + integrator = the desired gain level
174 const float piTarget = targetGain + pTerm + mIntegrator;
175
176 // Smooth with exponential filter using adaptive tau
177 float alpha = 1.0f;
178 if (tau > 0.0f && dt > 0.0f) {
179 alpha = 1.0f - fl::exp(-dt / tau);
180 }
181
182 const float unclamped = mLastGain + alpha * (piTarget - mLastGain);
183
184 // Bug 2 fix: Back-calculation anti-windup — only accumulate integrator
185 // when output is NOT clamped. Decay integrator when saturated (WLED: *= 0.91)
186 const bool saturated = (unclamped > mConfig.maxGain) || (unclamped < mConfig.minGain);
187 if (saturated) {
188 mIntegrator *= 0.91f;
189 } else {
190 mIntegrator += mKi * error * dt;
191 }
192 // Clamp integrator magnitude to prevent runaway in pathological cases
193 const float maxIntegral = 4.0f * mConfig.maxGain;
194 mIntegrator = fl::max(-maxIntegral, fl::min(maxIntegral, mIntegrator));
195
196 return unclamped;
197}
198
199void AutoGain::applyGain(const vector<i16>& input, float gain, vector<i16>& output) {
200 output.clear();
201 output.reserve(input.size());
202
203 for (size i = 0; i < input.size(); ++i) {
204 // Multiply sample by gain
205 float amplified = static_cast<float>(input[i]) * gain;
206
207 // Clamp to int16 range to prevent overflow/clipping
208 if (amplified > 32767.0f) amplified = 32767.0f;
209 if (amplified < -32768.0f) amplified = -32768.0f;
210
211 output.push_back(static_cast<i16>(amplified));
212 }
213}
214
215} // namespace audio
216} // namespace fl
void reset()
Reset internal state.
AutoGain() FL_NOEXCEPT
~AutoGain() FL_NOEXCEPT
float mLastGain
Last smoothed gain output.
Definition auto_gain.h:158
AttackDecayFilter< float > mPeakEnvelope
Peak envelope tracker: fast attack (10ms), slow decay (preset-dependent)
Definition auto_gain.h:152
void applyGain(const vector< i16 > &input, float gain, vector< i16 > &output)
Apply gain to audio samples.
vector< i16 > mOutputBuffer
Working buffer (reused to avoid allocations)
Definition auto_gain.h:161
float updatePIController(float targetGain, float dt)
Update PI controller toward target gain.
void configure(const AutoGainConfig &config)
Configure the auto gain controller.
float computeTargetGain()
Compute target gain from peak envelope.
float mIntegrator
PI integrator state.
Definition auto_gain.h:155
AutoGainConfig mConfig
Definition auto_gain.h:140
Sample process(const Sample &sample)
Process audio sample with automatic gain adjustment.
void resolvePreset()
Resolve preset enum into concrete PI tuning parameters.
fl::size size() const FL_NOEXCEPT
@ AGCPreset_Vivid
Faster response: 1.3s peak decay, higher PI gains.
Definition auto_gain.h:15
@ AGCPreset_Normal
Balanced: 3.3s peak decay, moderate PI gains.
Definition auto_gain.h:14
@ AGCPreset_Custom
Use custom PI tuning fields below.
Definition auto_gain.h:17
@ AGCPreset_Lazy
Slower, more stable: 6.7s peak decay, lower PI gains.
Definition auto_gain.h:16
Configuration for automatic gain control.
Definition auto_gain.h:26
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 common_type_t< T, U > max(T a, U b) FL_NOEXCEPT
Definition math.h:75
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode)
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
Definition sample.cpp.hpp:9
fl::i64 i64
Definition s16x16x4.h:222
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
enable_if< is_fixed_point< T >::value, T >::type exp(T x) FL_NOEXCEPT
constexpr enable_if< is_fixed_point< T >::value, T >::type abs(T x) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT