DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
EnvelopeFollower.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
36#include "../Core/AudioBuffer.h"
37#include "../Core/AudioSpec.h"
38#include "../Core/DspMath.h"
39
40#include <algorithm>
41#include <array>
42#include <atomic>
43#include <cmath>
44#include <cstddef>
45
46namespace dspark {
47
55template <FloatType T, int MaxChannels = 16>
57{
58public:
60 enum class Mode { Peak, RMS };
61
62 EnvelopeFollower() noexcept { updateCoeffs(); }
63
64 // -- Lifecycle ---------------------------------------------------------------
65
73 void prepare(const AudioSpec& spec) noexcept
74 {
75 if (!spec.isValid() || !std::isfinite(spec.sampleRate)) return;
76 sampleRate_.store(spec.sampleRate, std::memory_order_relaxed);
77 numChannels_.store(std::clamp(spec.numChannels, 1, MaxChannels),
78 std::memory_order_relaxed);
79 updateCoeffs();
80 reset();
81 }
82
90 void reset() noexcept
91 {
92 for (auto& s : state_) s = 0.0;
93 for (auto& e : published_) e.store(T(0), std::memory_order_relaxed);
94 }
95
96 // -- Parameters ----------------------------------------------------------------
97
100 void setAttack(T ms) noexcept
101 {
102 const double v = static_cast<double>(ms);
103 if (!std::isfinite(v)) return;
104 attackMs_.store(std::max(v, 0.01), std::memory_order_relaxed);
105 updateCoeffs();
106 }
107
110 void setRelease(T ms) noexcept
111 {
112 const double v = static_cast<double>(ms);
113 if (!std::isfinite(v)) return;
114 releaseMs_.store(std::max(v, 0.01), std::memory_order_relaxed);
115 updateCoeffs();
116 }
117
119 void setMode(Mode m) noexcept
120 {
121 const int v = std::clamp(static_cast<int>(m), 0, 1);
122 mode_.store(static_cast<Mode>(v), std::memory_order_relaxed);
123 }
124
125 // -- Processing -------------------------------------------------------------------
126
133 {
134 const int nCh = std::min(buffer.getNumChannels(),
135 numChannels_.load(std::memory_order_relaxed));
136 const int nS = buffer.getNumSamples();
137 const double aAtt = attackA_.load(std::memory_order_relaxed);
138 const double aRel = releaseA_.load(std::memory_order_relaxed);
139 const bool peak = mode_.load(std::memory_order_relaxed) == Mode::Peak;
140
141 for (int ch = 0; ch < nCh; ++ch)
142 {
143 const T* d = buffer.getChannel(ch);
144 double env = state_[static_cast<size_t>(ch)];
145 if (peak)
146 {
147 for (int i = 0; i < nS; ++i)
148 {
149 const double x = std::abs(static_cast<double>(d[i]));
150 const double a = (x > env) ? aAtt : aRel;
151 env = a * env + (1.0 - a) * x;
152 }
153 }
154 else
155 {
156 for (int i = 0; i < nS; ++i)
157 {
158 const double x = static_cast<double>(d[i]) * d[i];
159 const double a = (x > env) ? aAtt : aRel;
160 env = a * env + (1.0 - a) * x;
161 }
162 }
163 // Publish-time guard: a non-finite sample would park NaN in the
164 // recursion for good (the branch comparison goes false forever),
165 // so the poisoned channel restarts and re-measures on the next
166 // block. The same test flushes fully decayed envelopes to true
167 // zero (~-1000 dBFS) before they walk into double denormals.
168 if (!std::isfinite(env) || env < 1e-100) env = 0.0;
169 state_[static_cast<size_t>(ch)] = env;
170 published_[static_cast<size_t>(ch)].store(readout(env, peak),
171 std::memory_order_relaxed);
172 }
173 }
174
183 [[nodiscard]] T processSample(T input) noexcept
184 {
185 const double aAtt = attackA_.load(std::memory_order_relaxed);
186 const double aRel = releaseA_.load(std::memory_order_relaxed);
187 const bool peak = mode_.load(std::memory_order_relaxed) == Mode::Peak;
188 double env = state_[0];
189 const double x = peak ? std::abs(static_cast<double>(input))
190 : static_cast<double>(input) * input;
191 const double a = (x > env) ? aAtt : aRel;
192 env = a * env + (1.0 - a) * x;
193 if (!std::isfinite(env) || env < 1e-100) env = 0.0;
194 state_[0] = env;
195 const T out = readout(env, peak);
196 published_[0].store(out, std::memory_order_relaxed);
197 return out;
198 }
199
200 // -- Readout (lock-free, any thread) ------------------------------------------------
201
203 [[nodiscard]] T getEnvelope(int channel = 0) const noexcept
204 {
205 const int n = numChannels_.load(std::memory_order_relaxed);
206 channel = std::clamp(channel, 0, n - 1);
207 return published_[static_cast<size_t>(channel)].load(std::memory_order_relaxed);
208 }
209
211 [[nodiscard]] T getEnvelopeMax() const noexcept
212 {
213 const int n = numChannels_.load(std::memory_order_relaxed);
214 T m = T(0);
215 for (int ch = 0; ch < n; ++ch)
216 m = std::max(m, published_[static_cast<size_t>(ch)].load(std::memory_order_relaxed));
217 return m;
218 }
219
221 [[nodiscard]] T getEnvelopeDb(int channel = 0) const noexcept
222 {
223 const double e = static_cast<double>(getEnvelope(channel));
224 return static_cast<T>(20.0 * std::log10(std::max(e, 1e-6)));
225 }
226
228 [[nodiscard]] T getAttack() const noexcept
229 {
230 return static_cast<T>(attackMs_.load(std::memory_order_relaxed));
231 }
232
234 [[nodiscard]] T getRelease() const noexcept
235 {
236 return static_cast<T>(releaseMs_.load(std::memory_order_relaxed));
237 }
238
240 [[nodiscard]] Mode getMode() const noexcept
241 {
242 return mode_.load(std::memory_order_relaxed);
243 }
244
245private:
246 static_assert(MaxChannels >= 1, "EnvelopeFollower needs at least one channel");
247
248 [[nodiscard]] T readout(double env, bool peak) const noexcept
249 {
250 return static_cast<T>(peak ? env : std::sqrt(std::max(env, 0.0)));
251 }
252
253 void updateCoeffs() noexcept
254 {
255 const double fs = sampleRate_.load(std::memory_order_relaxed);
256 attackA_.store(std::exp(-1.0 / (attackMs_.load(std::memory_order_relaxed) * 0.001 * fs)),
257 std::memory_order_relaxed);
258 releaseA_.store(std::exp(-1.0 / (releaseMs_.load(std::memory_order_relaxed) * 0.001 * fs)),
259 std::memory_order_relaxed);
260 }
261
262 std::atomic<double> sampleRate_ { 48000.0 };
263 std::atomic<int> numChannels_ { 1 };
264 std::atomic<double> attackMs_ { 10.0 };
265 std::atomic<double> releaseMs_ { 150.0 };
266 std::atomic<double> attackA_ { 0.99 };
267 std::atomic<double> releaseA_ { 0.999 };
268 std::atomic<Mode> mode_ { Mode::Peak };
269
270 std::array<double, MaxChannels> state_ {};
271 std::array<std::atomic<T>, MaxChannels> published_ {};
272};
273
274} // 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:37