DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
AutoGain.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
31#include "../Core/AudioBuffer.h"
32#include "../Core/AudioSpec.h"
33#include "../Core/DspMath.h"
34#include "../Core/SimdOps.h"
35#include "../Core/StateBlob.h"
36
37#include <algorithm>
38#include <atomic>
39#include <cmath>
40#include <cstddef>
41#include <cstdint>
42#include <vector>
43
44namespace dspark {
45
62template <FloatType T>
64{
65 // Ensure the atomic type won't trigger a hidden mutex lock in the audio thread
66 static_assert(std::atomic<T>::is_always_lock_free,
67 "AutoGain requires a lock-free float type for thread safety in the audio path.");
68
69public:
78 void prepare(const AudioSpec& spec) noexcept
79 {
80 if (!spec.isValid()) return; // release-safe: keep previous state
81
82 sampleRate_ = spec.sampleRate;
83 numChannels_ = spec.numChannels;
84 reset();
85 }
86
95 void pushReference(AudioBufferView<T> buffer) noexcept
96 {
97 if (std::min(buffer.getNumChannels(), numChannels_) <= 0 ||
98 buffer.getNumSamples() <= 0)
99 return;
100 refLevelDb_ = measureRmsDb(buffer);
101 }
102
108 void compensate(AudioBufferView<T> buffer) noexcept
109 {
110 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
111 const int numSamples = buffer.getNumSamples();
112
113 if (numSamples == 0 || numCh == 0) return;
114
115 T outLevelDb = measureRmsDb(buffer);
116 T targetDb = refLevelDb_ - outLevelDb;
117
118 // Safety: Prevent NaN propagation if both levels are -Inf
119 if (std::isnan(targetDb))
120 targetDb = T(0);
121
122 // Clamp to safety limits
123 const T maxComp = maxCompensation_.load(std::memory_order_relaxed);
124 targetDb = std::clamp(targetDb, -maxComp, maxComp);
125
126 // Silence bypass (-90 dB threshold)
127 if (refLevelDb_ < SILENCE_THRESH_DB && outLevelDb < SILENCE_THRESH_DB)
128 targetDb = T(0);
129
130 // Calculate analytical end-state of the one-pole filter for the current block size:
131 // alpha = exp(-N / (Fs * tau))
132 const T smoothSecs = smoothTimeSecs_.load(std::memory_order_relaxed);
133 const T alpha = static_cast<T>(std::exp(-static_cast<double>(numSamples)
134 / (sampleRate_ * static_cast<double>(smoothSecs))));
135 const T endCompensationDb = targetDb + (compensationDb_ - targetDb) * alpha;
136
137 // Convert dB to linear gain for interpolation
138 const T startGain = decibelsToGain(compensationDb_);
139 const T endGain = decibelsToGain(endCompensationDb);
140 const T gainStep = (endGain - startGain) / static_cast<T>(numSamples);
141
142 // Apply linearly interpolated gain.
143 // This loop structure guarantees no loop-carried dependencies, enabling strict SIMD vectorization.
144 for (int ch = 0; ch < numCh; ++ch)
145 {
146 T* data = buffer.getChannel(ch);
147 for (int i = 0; i < numSamples; ++i)
148 {
149 data[i] *= (startGain + static_cast<T>(i) * gainStep);
150 }
151 }
152
153 // Update internal state for the next block
154 compensationDb_ = endCompensationDb;
155 }
156
160 void reset() noexcept
161 {
162 refLevelDb_ = SILENCE_THRESH_DB;
163 compensationDb_ = T(0);
164 }
165
170 [[nodiscard]] T getCompensationDb() const noexcept { return compensationDb_; }
171
177 void setMaxCompensation(T dB) noexcept
178 {
179 if (!std::isfinite(dB)) return;
180 maxCompensation_.store(std::abs(dB), std::memory_order_relaxed);
181 }
182
189 void setSmoothingTime(T ms) noexcept
190 {
191 if (!std::isfinite(ms)) return;
192 smoothTimeSecs_.store(std::max<T>(ms * T(0.001), T(0.001)),
193 std::memory_order_relaxed);
194 }
195
197 [[nodiscard]] T getMaxCompensation() const noexcept
198 {
199 return maxCompensation_.load(std::memory_order_relaxed);
200 }
201
203 [[nodiscard]] T getSmoothingTime() const noexcept
204 {
205 return smoothTimeSecs_.load(std::memory_order_relaxed) * T(1000);
206 }
207
209 [[nodiscard]] std::vector<uint8_t> getState() const
210 {
211 // The blob stores float (setState reads float back); the explicit
212 // casts also keep this overload resolvable when T is double.
213 StateWriter w(stateId("AGAN"), 1);
214 w.write("maxComp", static_cast<float>(maxCompensation_.load(std::memory_order_relaxed)));
215 w.write("smoothMs", static_cast<float>(getSmoothingTime()));
216 return w.blob();
217 }
218
220 bool setState(const uint8_t* data, size_t size)
221 {
222 StateReader r(data, size);
223 if (!r.isValid() || r.processorId() != stateId("AGAN")) return false;
224 setMaxCompensation(static_cast<T>(r.read("maxComp", 12.0f)));
225 setSmoothingTime(static_cast<T>(r.read("smoothMs", 100.0f)));
226 return true;
227 }
228
229private:
235 [[nodiscard]] T measureRmsDb(AudioBufferView<T> buffer) const noexcept
236 {
237 const int numCh = std::min(buffer.getNumChannels(), numChannels_);
238 const int numSamples = buffer.getNumSamples();
239 if (numCh <= 0 || numSamples <= 0) return SILENCE_THRESH_DB;
240
241 T sumSq = T(0);
242 const int totalSamples = numSamples * numCh;
243
244 for (int ch = 0; ch < numCh; ++ch)
245 sumSq += simd::sumOfSquares(buffer.getChannel(ch), numSamples);
246
247 // Apply a strict epsilon floor (approx -150dB) to avoid std::sqrt(0) and gainToDecibels(0) -> -Inf
248 T meanSq = std::max<T>(sumSq / static_cast<T>(totalSamples), T(1e-15));
249 return gainToDecibels(std::sqrt(meanSq));
250 }
251
252 static constexpr T SILENCE_THRESH_DB = T(-90);
253
254 double sampleRate_ = 44100.0;
255 int numChannels_ = 0;
256
257 std::atomic<T> smoothTimeSecs_{ T(0.100) };
258 T refLevelDb_ = SILENCE_THRESH_DB;
259 T compensationDb_ = T(0);
260
261 std::atomic<T> maxCompensation_{ T(12) };
262};
263
264} // 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:64
void reset() noexcept
Hard resets the internal state to avoid feedback loops or stale measurements.
Definition AutoGain.h:160
T getSmoothingTime() const noexcept
Definition AutoGain.h:203
void pushReference(AudioBufferView< T > buffer) noexcept
Snapshots the input level. Must be called BEFORE processing.
Definition AutoGain.h:95
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition AutoGain.h:220
void prepare(const AudioSpec &spec) noexcept
Prepares the auto-gain processor.
Definition AutoGain.h:78
void setSmoothingTime(T ms) noexcept
Sets the smoothing time constant.
Definition AutoGain.h:189
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition AutoGain.h:209
void compensate(AudioBufferView< T > buffer) noexcept
Measures output level and applies smoothed gain compensation. Must be called AFTER processing.
Definition AutoGain.h:108
T getMaxCompensation() const noexcept
Definition AutoGain.h:197
T getCompensationDb() const noexcept
Returns the current internal compensation in dB. Useful for UI metering.
Definition AutoGain.h:170
void setMaxCompensation(T dB) noexcept
Thread-safe assignment of the maximum allowed compensation limit.
Definition AutoGain.h:177
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
float sumOfSquares(const float *DSPARK_RESTRICT data, int count) noexcept
Returns the sum of squared samples (energy).
Definition SimdOps.h:1018
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:65
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:78
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35