DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
EnvelopeFollower.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
35#include "../Core/AudioBuffer.h"
36#include "../Core/AudioSpec.h"
37#include "../Core/DspMath.h"
38
39#include <algorithm>
40#include <array>
41#include <atomic>
42#include <cmath>
43#include <cstddef>
44
45namespace dspark {
46
54template <FloatType T, int MaxChannels = 16>
56{
57public:
59 enum class Mode { Peak, RMS };
60
61 EnvelopeFollower() noexcept { updateCoeffs(); }
62
63 // -- Lifecycle ---------------------------------------------------------------
64
72 void prepare(const AudioSpec& spec) noexcept
73 {
74 if (!spec.isValid() || !std::isfinite(spec.sampleRate)) return;
75 sampleRate_.store(spec.sampleRate, std::memory_order_relaxed);
76 numChannels_.store(std::clamp(spec.numChannels, 1, MaxChannels),
77 std::memory_order_relaxed);
78 updateCoeffs();
79 reset();
80 }
81
89 void reset() noexcept
90 {
91 for (auto& s : state_) s = 0.0;
92 for (auto& e : published_) e.store(T(0), std::memory_order_relaxed);
93 }
94
95 // -- Parameters ----------------------------------------------------------------
96
99 void setAttack(T ms) noexcept
100 {
101 const double v = static_cast<double>(ms);
102 if (!std::isfinite(v)) return;
103 attackMs_.store(std::max(v, 0.01), std::memory_order_relaxed);
104 updateCoeffs();
105 }
106
109 void setRelease(T ms) noexcept
110 {
111 const double v = static_cast<double>(ms);
112 if (!std::isfinite(v)) return;
113 releaseMs_.store(std::max(v, 0.01), std::memory_order_relaxed);
114 updateCoeffs();
115 }
116
118 void setMode(Mode m) noexcept
119 {
120 const int v = std::clamp(static_cast<int>(m), 0, 1);
121 mode_.store(static_cast<Mode>(v), std::memory_order_relaxed);
122 }
123
124 // -- Processing -------------------------------------------------------------------
125
132 {
133 const int nCh = std::min(buffer.getNumChannels(),
134 numChannels_.load(std::memory_order_relaxed));
135 const int nS = buffer.getNumSamples();
136 const double aAtt = attackA_.load(std::memory_order_relaxed);
137 const double aRel = releaseA_.load(std::memory_order_relaxed);
138 const bool peak = mode_.load(std::memory_order_relaxed) == Mode::Peak;
139
140 for (int ch = 0; ch < nCh; ++ch)
141 {
142 const T* d = buffer.getChannel(ch);
143 double env = state_[static_cast<size_t>(ch)];
144 if (peak)
145 {
146 for (int i = 0; i < nS; ++i)
147 {
148 const double x = std::abs(static_cast<double>(d[i]));
149 const double a = (x > env) ? aAtt : aRel;
150 env = a * env + (1.0 - a) * x;
151 }
152 }
153 else
154 {
155 for (int i = 0; i < nS; ++i)
156 {
157 const double x = static_cast<double>(d[i]) * d[i];
158 const double a = (x > env) ? aAtt : aRel;
159 env = a * env + (1.0 - a) * x;
160 }
161 }
162 // Publish-time guard: a non-finite sample would park NaN in the
163 // recursion for good (the branch comparison goes false forever),
164 // so the poisoned channel restarts and re-measures on the next
165 // block. The same test flushes fully decayed envelopes to true
166 // zero (~-1000 dBFS) before they walk into double denormals.
167 if (!std::isfinite(env) || env < 1e-100) env = 0.0;
168 state_[static_cast<size_t>(ch)] = env;
169 published_[static_cast<size_t>(ch)].store(readout(env, peak),
170 std::memory_order_relaxed);
171 }
172 }
173
182 [[nodiscard]] T processSample(T input) noexcept
183 {
184 const double aAtt = attackA_.load(std::memory_order_relaxed);
185 const double aRel = releaseA_.load(std::memory_order_relaxed);
186 const bool peak = mode_.load(std::memory_order_relaxed) == Mode::Peak;
187 double env = state_[0];
188 const double x = peak ? std::abs(static_cast<double>(input))
189 : static_cast<double>(input) * input;
190 const double a = (x > env) ? aAtt : aRel;
191 env = a * env + (1.0 - a) * x;
192 if (!std::isfinite(env) || env < 1e-100) env = 0.0;
193 state_[0] = env;
194 const T out = readout(env, peak);
195 published_[0].store(out, std::memory_order_relaxed);
196 return out;
197 }
198
199 // -- Readout (lock-free, any thread) ------------------------------------------------
200
202 [[nodiscard]] T getEnvelope(int channel = 0) const noexcept
203 {
204 const int n = numChannels_.load(std::memory_order_relaxed);
205 channel = std::clamp(channel, 0, n - 1);
206 return published_[static_cast<size_t>(channel)].load(std::memory_order_relaxed);
207 }
208
210 [[nodiscard]] T getEnvelopeMax() const noexcept
211 {
212 const int n = numChannels_.load(std::memory_order_relaxed);
213 T m = T(0);
214 for (int ch = 0; ch < n; ++ch)
215 m = std::max(m, published_[static_cast<size_t>(ch)].load(std::memory_order_relaxed));
216 return m;
217 }
218
220 [[nodiscard]] T getEnvelopeDb(int channel = 0) const noexcept
221 {
222 const double e = static_cast<double>(getEnvelope(channel));
223 return static_cast<T>(20.0 * std::log10(std::max(e, 1e-6)));
224 }
225
227 [[nodiscard]] T getAttack() const noexcept
228 {
229 return static_cast<T>(attackMs_.load(std::memory_order_relaxed));
230 }
231
233 [[nodiscard]] T getRelease() const noexcept
234 {
235 return static_cast<T>(releaseMs_.load(std::memory_order_relaxed));
236 }
237
239 [[nodiscard]] Mode getMode() const noexcept
240 {
241 return mode_.load(std::memory_order_relaxed);
242 }
243
244private:
245 static_assert(MaxChannels >= 1, "EnvelopeFollower needs at least one channel");
246
247 [[nodiscard]] T readout(double env, bool peak) const noexcept
248 {
249 return static_cast<T>(peak ? env : std::sqrt(std::max(env, 0.0)));
250 }
251
252 void updateCoeffs() noexcept
253 {
254 const double fs = sampleRate_.load(std::memory_order_relaxed);
255 attackA_.store(std::exp(-1.0 / (attackMs_.load(std::memory_order_relaxed) * 0.001 * fs)),
256 std::memory_order_relaxed);
257 releaseA_.store(std::exp(-1.0 / (releaseMs_.load(std::memory_order_relaxed) * 0.001 * fs)),
258 std::memory_order_relaxed);
259 }
260
261 std::atomic<double> sampleRate_ { 48000.0 };
262 std::atomic<int> numChannels_ { 1 };
263 std::atomic<double> attackMs_ { 10.0 };
264 std::atomic<double> releaseMs_ { 150.0 };
265 std::atomic<double> attackA_ { 0.99 };
266 std::atomic<double> releaseA_ { 0.999 };
267 std::atomic<Mode> mode_ { Mode::Peak };
268
269 std::array<double, MaxChannels> state_ {};
270 std::array<std::atomic<T>, MaxChannels> published_ {};
271};
272
273} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Attack/release envelope detector (Peak or RMS).
T processSample(T input) noexcept
Single-sample path on channel 0 (for embedding in processors).
void setAttack(T ms) noexcept
Attack time in milliseconds (default 10, floor 0.01; non-finite values are ignored).
void setRelease(T ms) noexcept
Release time in milliseconds (default 150, floor 0.01; non-finite values are ignored).
T getAttack() const noexcept
T getEnvelopeDb(int channel=0) const noexcept
T getRelease() const noexcept
void prepare(const AudioSpec &spec) noexcept
Prepares the follower. Allocation-free.
Mode getMode() const noexcept
T getEnvelope(int channel=0) const noexcept
void processBlock(AudioBufferView< const T > buffer) noexcept
Analyzes a block (read-only) and updates per-channel envelopes.
T getEnvelopeMax() const noexcept
void setMode(Mode m) noexcept
Peak (default) or RMS detection. Out-of-range values clamp.
void reset() noexcept
Clears all envelopes.
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35