DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
AutoGain.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
46#include "../Core/AudioBuffer.h"
47#include "../Core/AudioSpec.h"
48#include "../Core/Biquad.h"
49#include "../Core/DspMath.h"
50#include "../Core/StateBlob.h"
51
52#include <algorithm>
53#include <atomic>
54#include <cmath>
55#include <cstddef>
56#include <cstdint>
57#include <vector>
58
59namespace dspark {
60
77template <FloatType T>
79{
80 // Ensure the atomic type won't trigger a hidden mutex lock in the audio thread
81 static_assert(std::atomic<T>::is_always_lock_free,
82 "AutoGain requires a lock-free float type for thread safety in the audio path.");
83
84public:
86 enum class Weighting
87 {
88 KWeighted,
89 Flat
90 };
91
100 void prepare(const AudioSpec& spec)
101 {
102 if (!spec.isValid()) return; // release-safe: keep previous state
103
104 sampleRate_ = spec.sampleRate;
105 numChannels_ = spec.numChannels;
106 shelf_ = BiquadCoeffs::makeKWeightingShelf(sampleRate_);
107 highPass_ = BiquadCoeffs::makeKWeightingHighPass(sampleRate_);
108 refFilters_.assign(static_cast<size_t>(numChannels_), WeightingState {});
109 outFilters_.assign(static_cast<size_t>(numChannels_), WeightingState {});
110 reset();
111 }
112
121 void pushReference(AudioBufferView<T> buffer) noexcept
122 {
123 if (std::min(buffer.getNumChannels(), numChannels_) <= 0 ||
124 buffer.getNumSamples() <= 0)
125 return;
126 integrate(refMeanSquare_, buffer, refFilters_);
127 }
128
134 void compensate(AudioBufferView<T> buffer) noexcept
135 {
136 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
137 const int numSamples = buffer.getNumSamples();
138
139 if (numSamples == 0 || numCh == 0) return;
140
141 integrate(outMeanSquare_, buffer, outFilters_);
142 const T refLevelDb = meanSquareToDb(refMeanSquare_);
143 const T outLevelDb = meanSquareToDb(outMeanSquare_);
144 T targetDb = refLevelDb - outLevelDb;
145
146 // Clamp to safety limits
147 const T maxComp = maxCompensation_.load(std::memory_order_relaxed);
148 targetDb = std::clamp(targetDb, -maxComp, maxComp);
149
150 // Silence bypass (-90 dB threshold)
151 if (refLevelDb < SILENCE_THRESH_DB && outLevelDb < SILENCE_THRESH_DB)
152 targetDb = T(0);
153
154 // Calculate analytical end-state of the one-pole filter for the current block size:
155 // alpha = exp(-N / (Fs * tau))
156 const T smoothSecs = smoothTimeSecs_.load(std::memory_order_relaxed);
157 const T alpha = static_cast<T>(std::exp(-static_cast<double>(numSamples)
158 / (sampleRate_ * static_cast<double>(smoothSecs))));
159 const T endCompensationDb = targetDb + (compensationDb_ - targetDb) * alpha;
160
161 // Convert dB to linear gain for interpolation
162 const T startGain = decibelsToGain(compensationDb_);
163 const T endGain = decibelsToGain(endCompensationDb);
164 const T gainStep = (endGain - startGain) / static_cast<T>(numSamples);
165
166 // Apply linearly interpolated gain.
167 // This loop structure guarantees no loop-carried dependencies, enabling strict SIMD vectorization.
168 for (int ch = 0; ch < numCh; ++ch)
169 {
170 T* data = buffer.getChannel(ch);
171 for (int i = 0; i < numSamples; ++i)
172 {
173 data[i] *= (startGain + static_cast<T>(i) * gainStep);
174 }
175 }
176
177 // Update internal state for the next block
178 compensationDb_ = endCompensationDb;
179 // Publish it for cross-thread metering. The working member stays
180 // audio-thread-private: getCompensationDb() used to read it directly,
181 // which is a plain word written by the audio thread and read by
182 // another -- a data race, not merely an approximate number.
183 publishedCompensationDb_.store(compensationDb_, std::memory_order_relaxed);
184 }
185
189 void reset() noexcept
190 {
191 refMeanSquare_ = 0.0;
192 outMeanSquare_ = 0.0;
193 for (auto& f : refFilters_) f = {};
194 for (auto& f : outFilters_) f = {};
195 compensationDb_ = T(0);
196 publishedCompensationDb_.store(T(0), std::memory_order_relaxed);
197 }
198
200 void setWeighting(Weighting w) noexcept
201 {
202 const int v = std::clamp(static_cast<int>(w), 0, static_cast<int>(Weighting::Flat));
203 weighting_.store(static_cast<Weighting>(v), std::memory_order_relaxed);
204 }
205
207 [[nodiscard]] Weighting getWeighting() const noexcept
208 {
209 return weighting_.load(std::memory_order_relaxed);
210 }
211
221 [[nodiscard]] T getCompensationDb() const noexcept
222 {
223 return publishedCompensationDb_.load(std::memory_order_relaxed);
224 }
225
231 void setMaxCompensation(T dB) noexcept
232 {
233 if (!std::isfinite(dB)) return;
234 maxCompensation_.store(std::abs(dB), std::memory_order_relaxed);
235 }
236
243 void setSmoothingTime(T ms) noexcept
244 {
245 if (!std::isfinite(ms)) return;
246 smoothTimeSecs_.store(std::max<T>(ms * T(0.001), T(0.001)),
247 std::memory_order_relaxed);
248 }
249
251 [[nodiscard]] T getMaxCompensation() const noexcept
252 {
253 return maxCompensation_.load(std::memory_order_relaxed);
254 }
255
257 [[nodiscard]] T getSmoothingTime() const noexcept
258 {
259 return smoothTimeSecs_.load(std::memory_order_relaxed) * T(1000);
260 }
261
263 [[nodiscard]] std::vector<uint8_t> getState() const
264 {
265 // The blob stores float (setState reads float back); the explicit
266 // casts also keep this overload resolvable when T is double.
267 StateWriter w(stateId("AGAN"), 1);
268 w.write("maxComp", static_cast<float>(maxCompensation_.load(std::memory_order_relaxed)));
269 w.write("smoothMs", static_cast<float>(getSmoothingTime()));
270 w.write("weighting", static_cast<int32_t>(getWeighting()));
271 return w.blob();
272 }
273
275 bool setState(const uint8_t* data, size_t size)
276 {
277 StateReader r(data, size);
278 if (!r.isValid() || r.processorId() != stateId("AGAN")) return false;
279 setMaxCompensation(static_cast<T>(r.read("maxComp", 12.0f)));
280 setSmoothingTime(static_cast<T>(r.read("smoothMs", 100.0f)));
281 setWeighting(static_cast<Weighting>(r.read("weighting", 0)));
282 return true;
283 }
284
285private:
287 struct WeightingState { double s1a = 0.0, s2a = 0.0, s1b = 0.0, s2b = 0.0; };
288
289 static inline double tdf2(double x, const BiquadCoeffs& c, double& s1, double& s2) noexcept
290 {
291 const double y = c.b0 * x + s1;
292 s1 = c.b1 * x - c.a1 * y + s2;
293 s2 = c.b2 * x - c.a2 * y;
294 return y;
295 }
296
303 void integrate(double& meanSquare, AudioBufferView<T> buffer,
304 std::vector<WeightingState>& filters) noexcept
305 {
306 const int numCh = std::min({ buffer.getNumChannels(), numChannels_,
307 static_cast<int>(filters.size()) });
308 const int numSamples = buffer.getNumSamples();
309 if (numCh <= 0 || numSamples <= 0) return;
310
311 const bool weighted = weighting_.load(std::memory_order_relaxed) == Weighting::KWeighted;
312 double sumSq = 0.0;
313 for (int ch = 0; ch < numCh; ++ch)
314 {
315 const T* x = buffer.getChannel(ch);
316 WeightingState& f = filters[static_cast<size_t>(ch)];
317 if (weighted)
318 {
319 for (int i = 0; i < numSamples; ++i)
320 {
321 const double y = tdf2(tdf2(static_cast<double>(x[i]), shelf_, f.s1a, f.s2a),
322 highPass_, f.s1b, f.s2b);
323 sumSq += y * y;
324 }
325 }
326 else
327 {
328 for (int i = 0; i < numSamples; ++i)
329 sumSq += static_cast<double>(x[i]) * static_cast<double>(x[i]);
330 }
331 }
332
333 const double blockMs = sumSq / static_cast<double>(numSamples * numCh);
334 if (!std::isfinite(blockMs))
335 {
336 for (auto& state : filters) state = {};
337 return;
338 }
339 const double a = std::exp(-static_cast<double>(numSamples) / (sampleRate_ * kIntegrationSeconds));
340 meanSquare = blockMs + (meanSquare - blockMs) * a;
341 }
342
344 [[nodiscard]] static T meanSquareToDb(double meanSquare) noexcept
345 {
346 return static_cast<T>(10.0 * std::log10(std::max(meanSquare, 1e-15)));
347 }
348
349 static constexpr double kIntegrationSeconds = 0.4;
350 static constexpr T SILENCE_THRESH_DB = T(-90);
351
352 double sampleRate_ = 44100.0;
353 int numChannels_ = 0;
354
355 std::atomic<T> smoothTimeSecs_{ T(0.100) };
356 std::atomic<Weighting> weighting_{ Weighting::KWeighted };
357 double refMeanSquare_ = 0.0;
358 double outMeanSquare_ = 0.0;
359 BiquadCoeffs shelf_ = BiquadCoeffs::makeKWeightingShelf(48000.0);
360 BiquadCoeffs highPass_ = BiquadCoeffs::makeKWeightingHighPass(48000.0);
361 std::vector<WeightingState> refFilters_;
362 std::vector<WeightingState> outFilters_;
363 T compensationDb_ = T(0);
364 std::atomic<T> publishedCompensationDb_{ T(0) };
365
366 std::atomic<T> maxCompensation_{ T(12) };
367};
368
369} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Block-adaptive automatic gain compensation with SIMD-friendly linear interpolation.
Definition AutoGain.h:79
void reset() noexcept
Hard resets the internal state to avoid feedback loops or stale measurements.
Definition AutoGain.h:189
Weighting
Level measurement used for the match.
Definition AutoGain.h:87
@ KWeighted
ITU-R BS.1770 K-weighting (loudness, LUFS filter). Default.
@ Flat
Unweighted mean square (plain RMS).
T getSmoothingTime() const noexcept
Definition AutoGain.h:257
void pushReference(AudioBufferView< T > buffer) noexcept
Snapshots the input level. Must be called BEFORE processing.
Definition AutoGain.h:121
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition AutoGain.h:275
void setWeighting(Weighting w) noexcept
Selects the level measurement (K-weighted loudness by default). RT-safe.
Definition AutoGain.h:200
Weighting getWeighting() const noexcept
Definition AutoGain.h:207
void setSmoothingTime(T ms) noexcept
Sets the smoothing time constant.
Definition AutoGain.h:243
void prepare(const AudioSpec &spec)
Prepares the auto-gain processor.
Definition AutoGain.h:100
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition AutoGain.h:263
void compensate(AudioBufferView< T > buffer) noexcept
Measures output level and applies smoothed gain compensation. Must be called AFTER processing.
Definition AutoGain.h:134
T getMaxCompensation() const noexcept
Definition AutoGain.h:251
T getCompensationDb() const noexcept
Returns the current internal compensation in dB. Useful for UI metering.
Definition AutoGain.h:221
void setMaxCompensation(T dB) noexcept
Thread-safe assignment of the maximum allowed compensation limit.
Definition AutoGain.h:231
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
Main namespace for the DSPark framework.
T decibelsToGain(T dB, T minusInfinityDb=T(-100)) noexcept
Converts a value in decibels to linear gain.
Definition DspMath.h:74
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
static BiquadCoeffs makeKWeightingHighPass(double sampleRate) noexcept
ITU-R BS.1770 K-weighting, stage 2: the RLB high-pass.
Definition Biquad.h:529
static BiquadCoeffs makeKWeightingShelf(double sampleRate) noexcept
ITU-R BS.1770 K-weighting, stage 1: the head-related high shelf.
Definition Biquad.h:504