DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
TransientDesigner.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
36#include "../Core/DspMath.h"
37#include "../Core/AudioSpec.h"
38#include "../Core/AudioBuffer.h"
39#include "../Core/DenormalGuard.h"
40#include "../Core/StateBlob.h"
41
42#include <algorithm>
43#include <array>
44#include <atomic>
45#include <cmath>
46#include <cstddef>
47#include <cstdint>
48#include <vector>
49
50namespace dspark {
51
62template <FloatType T>
64{
65public:
66 ~TransientDesigner() = default; // non-virtual: leaf class (no virtual dispatch)
67
68 // -- Lifecycle -----------------------------------------------------------
69
74 void prepare(const AudioSpec& spec) noexcept
75 {
76 if (!spec.isValid()) return;
77 prepare(spec.sampleRate);
78 }
79
87 void prepare(double sampleRate) noexcept
88 {
89 if (!std::isfinite(sampleRate) || sampleRate <= 0.0) return;
90 sampleRate_ = sampleRate;
91 updateCoefficients();
92 reset();
93 }
94
99 void processBlock(AudioBufferView<T> buffer) noexcept
100 {
101 DenormalGuard guard;
102 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
103 const int nS = buffer.getNumSamples();
104
105 const T attAmt = attackAmount_.load(std::memory_order_relaxed);
106 const T susAmt = sustainAmount_.load(std::memory_order_relaxed);
107 const bool odr = outputDepRecovery_.load(std::memory_order_relaxed);
108
109 // Constants for VCA log-domain emulation
110 constexpr T noiseFloor = T(1e-5); // -100 dB floor avoids log(0) and denormals
111 constexpr T maxGainLog = T(2.77258); // approx +24dB max gain change
112
113 // Outer loop: channels (state stays in registers)
114 for (int ch = 0; ch < nCh; ++ch)
115 {
116 T* const channelData = buffer.getChannel(ch);
117 T fast = envFast_[ch];
118 T slow = envSlow_[ch];
119 T lastOut = lastOutput_[ch];
120
121 // Inner loop: serial envelope recursion per sample
122 for (int i = 0; i < nS; ++i)
123 {
124 T sample = channelData[i];
125 T absSample = std::abs(sample) + noiseFloor;
126
127 // 1. Fast envelope (Peak)
128 T fastCoeff = (absSample > fast) ? fastAttackCoeff_ : fastReleaseCoeff_;
129 fast += fastCoeff * (absSample - fast);
130
131 // 2. Slow envelope (Sustain/RMS tracker)
132 T currentSlowRelCoeff = slowReleaseCoeff_;
133
134 if (odr)
135 {
136 // Corrected ODR: Higher output = LARGER coefficient = Faster release
137 T modifier = T(1) + std::abs(lastOut) * T(2.0);
138 currentSlowRelCoeff = std::min(fastReleaseCoeff_, currentSlowRelCoeff * modifier);
139 }
140
141 T slowCoeff = (absSample > slow) ? slowAttackCoeff_ : currentSlowRelCoeff;
142 slow += slowCoeff * (absSample - slow);
143
144 // 3. Log-domain VCA computation (fastLog/fastExp: relative
145 // error < 2e-7 - far below audibility, ~3x faster than libm)
146 T diffLog = fastLog(fast) - fastLog(slow);
147
148 // Attack kicks in when fast > slow (diffLog > 0). Sustain when slow > fast (diffLog < 0)
149 T gainLog = (diffLog > T(0)) ? (diffLog * attAmt) : (-diffLog * susAmt);
150
151 gainLog = std::clamp(gainLog, -maxGainLog, maxGainLog);
152
153 T gain = fastExp(gainLog);
154
155 lastOut = sample * gain;
156 channelData[i] = lastOut;
157 }
158
159 // Save state
160 envFast_[ch] = fast;
161 envSlow_[ch] = slow;
162 lastOutput_[ch] = lastOut;
163 }
164 }
165
166 // -- Parameters ----------------------------------------------------------
167 // All setters ignore non-finite values: a NaN amount used to storm the
168 // output for as long as it was published, and under output-dependent
169 // recovery the NaN last-output silently switched the slow envelope to the
170 // fast release, leaving the envelope history diverged after recovery.
171
176 void setAttack(T amount) noexcept
177 {
178 if (!std::isfinite(amount)) return;
179 attackAmount_.store(std::clamp(amount, T(-100), T(100)) / T(100), std::memory_order_relaxed);
180 }
181
186 void setSustain(T amount) noexcept
187 {
188 if (!std::isfinite(amount)) return;
189 sustainAmount_.store(std::clamp(amount, T(-100), T(100)) / T(100), std::memory_order_relaxed);
190 }
191
196 void setOutputDepRecovery(bool enabled) noexcept
197 {
198 outputDepRecovery_.store(enabled, std::memory_order_relaxed);
199 }
200
205 void setCharacter(T amount) noexcept
206 {
207 if (!std::isfinite(amount)) return;
208 T c = std::clamp(amount, T(-1), T(1));
209 attackAmount_.store(c, std::memory_order_relaxed);
210 sustainAmount_.store(-c * T(0.5), std::memory_order_relaxed);
211 }
212
216 void reset() noexcept
217 {
218 envFast_.fill(T(1e-5)); // Init to noise floor
219 envSlow_.fill(T(1e-5));
220 lastOutput_.fill(T(0));
221 }
222
223
225 [[nodiscard]] std::vector<uint8_t> getState() const
226 {
227 StateWriter w(stateId("TDES"), 1);
228 // static_cast<float>: the blob stores float (percent units), and
229 // StateWriter's overload set is ambiguous for a double argument.
230 w.write("attack", static_cast<float>(attackAmount_.load(std::memory_order_relaxed)) * 100.0f);
231 w.write("sustain", static_cast<float>(sustainAmount_.load(std::memory_order_relaxed)) * 100.0f);
232 w.write("outputDep", outputDepRecovery_.load(std::memory_order_relaxed));
233 return w.blob();
234 }
235
237 bool setState(const uint8_t* data, size_t size)
238 {
239 StateReader r(data, size);
240 if (!r.isValid() || r.processorId() != stateId("TDES")) return false;
241 setAttack(static_cast<T>(r.read("attack", 0.0f)));
242 setSustain(static_cast<T>(r.read("sustain", 0.0f)));
243 setOutputDepRecovery(r.read("outputDep", false));
244 return true;
245 }
246
247private:
248 static constexpr int kMaxChannels = 16;
249
250 void updateCoefficients() noexcept
251 {
252 if (!(sampleRate_ > 0.0)) return;
253 T fs = static_cast<T>(sampleRate_);
254 fastAttackCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.0001)));
255 fastReleaseCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.005)));
256 slowAttackCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.020)));
257 slowReleaseCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.200)));
258 }
259
260 double sampleRate_ = 48000.0;
261
262 // Atomic parameters (Lock-free thread safety)
263 std::atomic<T> attackAmount_ { T(0) };
264 std::atomic<T> sustainAmount_ { T(0) };
265 std::atomic<bool> outputDepRecovery_ { false };
266
267 // Envelope coefficients (written by prepare/updateCoefficients on the
268 // setup thread, read by the audio thread)
269 T fastAttackCoeff_ = T(0), fastReleaseCoeff_ = T(0);
270 T slowAttackCoeff_ = T(0), slowReleaseCoeff_ = T(0);
271
272 // Per-channel state (scalar recursion; no SIMD kernel touches these)
273 std::array<T, kMaxChannels> envFast_ {};
274 std::array<T, kMaxChannels> envSlow_ {};
275 std::array<T, kMaxChannels> lastOutput_ {};
276};
277
278} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
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
Zero-allocation, thread-safe transient shaper.
void reset() noexcept
Clears all internal buffers and state. Must be lock-free.
void setSustain(T amount) noexcept
Sets sustain (body) emphasis.
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
void setOutputDepRecovery(bool enabled) noexcept
Enables output-dependent recovery (ODR).
void prepare(double sampleRate) noexcept
Prepares the processor with a specific sample rate.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
void prepare(const AudioSpec &spec) noexcept
Prepares the processor with the current audio specification.
void setAttack(T amount) noexcept
Sets attack (transient) emphasis.
void setCharacter(T amount) noexcept
Sets character as a single macro-knob (overwrites attack AND sustain).
Main namespace for the DSPark framework.
T fastLog(T x) noexcept
Fast natural logarithm approximation.
Definition DspMath.h:261
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
T fastExp(T x) noexcept
Fast approximation of e^x via std::exp2 (~2x faster than std::exp on MSVC).
Definition DspMath.h:167
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35