DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
EnvelopeGenerator.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
17#include "DspMath.h"
18#include "AudioSpec.h"
19
20#include <algorithm>
21#include <cmath>
22
23namespace dspark {
24
55template <FloatType T>
57{
58public:
59 enum class State { Idle, Attack, Decay, Sustain, Release };
60
61 ADSREnvelope() noexcept { recalculate(); }
62
67 void prepare(double sampleRate) noexcept
68 {
69 if (sampleRate <= 0.0) return;
70 sampleRate_ = sampleRate;
71 recalculate();
72 }
73
78 void prepare(const AudioSpec& spec) noexcept { prepare(spec.sampleRate); }
79
80 // -- Parameters (in milliseconds) ------------------------------------------
81
83 void setAttack(T ms) noexcept { attackMs_ = std::max(ms, T(0.01)); recalculate(); }
84
86 void setDecay(T ms) noexcept { decayMs_ = std::max(ms, T(0.01)); recalculate(); }
87
89 void setSustain(T level) noexcept { sustainLevel_ = std::clamp(level, T(0), T(1)); }
90
92 void setRelease(T ms) noexcept { releaseMs_ = std::max(ms, T(0.01)); recalculate(); }
93
101 void setParameters(T attackMs, T decayMs, T sustain, T releaseMs) noexcept
102 {
103 attackMs_ = std::max(attackMs, T(0.01));
104 decayMs_ = std::max(decayMs, T(0.01));
105 sustainLevel_ = std::clamp(sustain, T(0), T(1));
106 releaseMs_ = std::max(releaseMs, T(0.01));
107 recalculate();
108 }
109
114 void setCurvature(T curve) noexcept
115 {
116 curve = std::clamp(curve, T(0.1), T(10));
117 attackCurve_ = decayCurve_ = releaseCurve_ = curve;
118 recalculate();
119 }
120
131 void setCurvature(T attackCurve, T decayCurve, T releaseCurve) noexcept
132 {
133 attackCurve_ = std::clamp(attackCurve, T(0.1), T(10));
134 decayCurve_ = std::clamp(decayCurve, T(0.1), T(10));
135 releaseCurve_ = std::clamp(releaseCurve, T(0.1), T(10));
136 recalculate();
137 }
138
139 // -- Trigger ---------------------------------------------------------------
140
142 void noteOn() noexcept
143 {
144 state_ = State::Attack;
145 }
146
148 void noteOff() noexcept
149 {
150 if (state_ != State::Idle)
151 state_ = State::Release;
152 }
153
155 void reset() noexcept
156 {
157 state_ = State::Idle;
158 currentValue_ = 0.0;
159 }
160
161 // -- Processing ------------------------------------------------------------
162
167 [[nodiscard]] T getNextValue() noexcept
168 {
169 switch (state_)
170 {
171 case State::Idle:
172 return T(0);
173
174 case State::Attack:
175 currentValue_ += attackRate_ * (1.0 + kAttack_ - currentValue_);
176 if (currentValue_ >= 1.0)
177 {
178 currentValue_ = 1.0;
179 state_ = State::Decay;
180 }
181 break;
182
183 case State::Decay:
184 {
185 const double s = static_cast<double>(sustainLevel_);
186 const double prev = currentValue_;
187 currentValue_ += decayRate_ * (s - (1.0 - s) * kDecay_ - currentValue_);
188 if (currentValue_ <= s)
189 {
190 // Normal completion (crossed from above): land exactly on s.
191 // Sustain raised above the current value mid-decay: keep the
192 // value and let the Sustain chase close the gap click-free.
193 if (prev >= s)
194 currentValue_ = s;
195 state_ = State::Sustain;
196 }
197 break;
198 }
199
200 case State::Sustain:
201 // Chases sustain changes at the decay rate (dynamic modulation).
202 currentValue_ += decayRate_ * (static_cast<double>(sustainLevel_) - currentValue_);
203 break;
204
205 case State::Release:
206 currentValue_ += releaseRate_ * (-kRelease_ - currentValue_);
207 if (currentValue_ <= kSilence)
208 {
209 currentValue_ = 0.0;
210 state_ = State::Idle;
211 }
212 break;
213 }
214
215 return static_cast<T>(currentValue_);
216 }
217
227 void processBlock(T* output, int numSamples) noexcept
228 {
229 // Each stage runs as a tight loop on locals (stage constants hoisted,
230 // recursion state in a register): no state dispatch or member traffic
231 // per sample. Arithmetic is sample-exact with getNextValue().
232 int i = 0;
233 while (i < numSamples)
234 {
235 switch (state_)
236 {
237 case State::Idle:
238 while (i < numSamples)
239 output[i++] = T(0);
240 break;
241
242 case State::Attack:
243 {
244 const double target = 1.0 + kAttack_;
245 const double r = attackRate_;
246 double v = currentValue_;
247 while (i < numSamples)
248 {
249 v += r * (target - v);
250 if (v >= 1.0)
251 {
252 v = 1.0;
253 state_ = State::Decay;
254 output[i++] = T(1);
255 break;
256 }
257 output[i++] = static_cast<T>(v);
258 }
259 currentValue_ = v;
260 break;
261 }
262
263 case State::Decay:
264 {
265 const double s = static_cast<double>(sustainLevel_);
266 const double target = s - (1.0 - s) * kDecay_;
267 const double r = decayRate_;
268 double v = currentValue_;
269 while (i < numSamples)
270 {
271 const double prev = v;
272 v += r * (target - v);
273 if (v <= s)
274 {
275 if (prev >= s)
276 v = s;
277 state_ = State::Sustain;
278 output[i++] = static_cast<T>(v);
279 break;
280 }
281 output[i++] = static_cast<T>(v);
282 }
283 currentValue_ = v;
284 break;
285 }
286
287 case State::Sustain:
288 {
289 const double s = static_cast<double>(sustainLevel_);
290 const double r = decayRate_;
291 double v = currentValue_;
292 while (i < numSamples)
293 {
294 v += r * (s - v);
295 output[i++] = static_cast<T>(v);
296 }
297 currentValue_ = v;
298 break;
299 }
300
301 case State::Release:
302 {
303 const double target = -kRelease_;
304 const double r = releaseRate_;
305 double v = currentValue_;
306 while (i < numSamples)
307 {
308 v += r * (target - v);
309 if (v <= kSilence)
310 {
311 v = 0.0;
312 state_ = State::Idle;
313 output[i++] = T(0);
314 break;
315 }
316 output[i++] = static_cast<T>(v);
317 }
318 currentValue_ = v;
319 break;
320 }
321 }
322 }
323 }
324
325 // -- Getters ---------------------------------------------------------------
326
328 [[nodiscard]] T getCurrentValue() const noexcept { return static_cast<T>(currentValue_); }
329
331 [[nodiscard]] State getState() const noexcept { return state_; }
332
334 [[nodiscard]] bool isActive() const noexcept { return state_ != State::Idle; }
335
336private:
337 // Release ends when the value falls below this floor (about -80 dB);
338 // the strictly negative release target guarantees it is crossed.
339 static constexpr double kSilence = 1e-4;
340
341 void recalculate() noexcept
342 {
343 // Asymptotic one-pole stage design. With ov = exp(-curve), running the
344 // recursion v += rate * (target - v) for exactly `samples` steps
345 // multiplies the distance to the target by ov. Overshooting the stage
346 // endpoint by k = ov / (1 - ov) OF THE STAGE DEPTH makes the endpoint
347 // land exactly on the last step, for any curvature:
348 //
349 // endpoint = target + depth_to_target * ov with target = end + k*depth
350 //
351 // (A first-order shortcut like target = end + ov breaks the timing
352 // contract: at curve 0.1 the stages run ~7x their parameter, and a
353 // fixed absolute overshoot makes decay/release durations drift with
354 // the sustain level.)
355 auto stage = [this](T timeMs, T curve, double& rate, double& k) noexcept {
356 const double c = static_cast<double>(curve);
357 const double ov = std::exp(-c);
358 k = ov / (1.0 - ov);
359 const double samples = std::max(sampleRate_ * 0.001 * static_cast<double>(timeMs), 1.0);
360 rate = 1.0 - std::exp(-c / samples);
361 };
362
363 stage(attackMs_, attackCurve_, attackRate_, kAttack_);
364 stage(decayMs_, decayCurve_, decayRate_, kDecay_);
365 stage(releaseMs_, releaseCurve_, releaseRate_, kRelease_);
366 }
367
368 double sampleRate_ = 48000.0;
369
370 T attackMs_ = T(10);
371 T decayMs_ = T(100);
372 T sustainLevel_ = T(0.7);
373 T releaseMs_ = T(200);
374 T attackCurve_ = T(3.0);
375 T decayCurve_ = T(3.0);
376 T releaseCurve_ = T(3.0);
377
378 // Stage coefficients and overshoot fractions (see recalculate()).
379 double attackRate_ = 0.0;
380 double decayRate_ = 0.0;
381 double releaseRate_ = 0.0;
382 double kAttack_ = 0.0;
383 double kDecay_ = 0.0;
384 double kRelease_ = 0.0;
385
386 double currentValue_ = 0.0;
387 State state_ = State::Idle;
388};
389
390} // namespace dspark
Describes the audio processing environment (sample rate, block size, channels).
Core mathematical utilities for digital signal processing.
Classic ADSR envelope generator with exponential curves.
void setRelease(T ms) noexcept
Sets release time in milliseconds (full scale -> silence).
void setCurvature(T attackCurve, T decayCurve, T releaseCurve) noexcept
Sets independent curvatures per stage.
void noteOn() noexcept
Triggers the attack phase (note on). Legato: continues from the current value.
T getNextValue() noexcept
Returns the next envelope value and advances the state machine.
void setDecay(T ms) noexcept
Sets decay time in milliseconds (1 -> sustain).
void prepare(double sampleRate) noexcept
Prepares the envelope for the given sample rate.
void setCurvature(T curve) noexcept
Sets the curvature of all exponential stages at once.
void setSustain(T level) noexcept
Sets sustain level (0.0 to 1.0).
void setAttack(T ms) noexcept
Sets attack time in milliseconds (0 -> 1).
T getCurrentValue() const noexcept
Returns the current envelope value.
void noteOff() noexcept
Triggers the release phase (note off).
State getState() const noexcept
Returns the current state.
bool isActive() const noexcept
Returns true if the envelope is actively producing output.
void setParameters(T attackMs, T decayMs, T sustain, T releaseMs) noexcept
Sets all ADSR parameters at once.
void reset() noexcept
Resets the envelope to idle with zero output.
void prepare(const AudioSpec &spec) noexcept
Prepares from AudioSpec (unified API).
void processBlock(T *output, int numSamples) noexcept
Fills a buffer with envelope values.
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35