44#include "../Core/DspMath.h"
45#include "../Core/AudioSpec.h"
46#include "../Core/AudioBuffer.h"
47#include "../Core/DenormalGuard.h"
48#include "../Core/Hilbert.h"
49#include "../Core/StateBlob.h"
85 if (!spec.isValid())
return;
98 if (!std::isfinite(sampleRate) || sampleRate <= 0.0)
return;
99 sampleRate_ = sampleRate;
100 updateCoefficients();
111 const int nCh = std::min(buffer.getNumChannels(), kMaxChannels);
112 const int nS = buffer.getNumSamples();
114 const T attAmt = attackAmount_.load(std::memory_order_relaxed);
115 const T susAmt = sustainAmount_.load(std::memory_order_relaxed);
116 const bool odr = outputDepRecovery_.load(std::memory_order_relaxed);
119 constexpr T noiseFloor = T(1e-5);
120 constexpr T maxGainLog = T(2.77258);
123 for (
int ch = 0; ch < nCh; ++ch)
125 T*
const channelData = buffer.getChannel(ch);
126 auto& analytic = analytic_[
static_cast<size_t>(ch)];
127 T fast = envFast_[ch];
128 T slow = envSlow_[ch];
129 T lastOut = lastOutput_[ch];
133 for (
int i = 0; i < nS; ++i)
135 if (i % kDetChunk == 0)
137 const int n = std::min(kDetChunk, nS - i);
138 for (
int j = 0; j < n; ++j) detIn_[static_cast<size_t>(j)] =
static_cast<double>(channelData[i + j]);
139 analytic.magnitudeBlock(detIn_.data(), detMag_.data(), n);
141 T sample = channelData[i];
142 const T envelopeLevel = std::max(
143 std::abs(sample),
static_cast<T
>(detMag_[
static_cast<size_t>(i % kDetChunk)]));
144 T absSample = envelopeLevel + noiseFloor;
147 T fastCoeff = (absSample > fast) ? fastAttackCoeff_ : fastReleaseCoeff_;
148 fast += fastCoeff * (absSample - fast);
151 T currentSlowRelCoeff = slowReleaseCoeff_;
156 T modifier = T(1) + std::abs(lastOut) * T(2.0);
157 currentSlowRelCoeff = std::min(fastReleaseCoeff_, currentSlowRelCoeff * modifier);
160 T slowCoeff = (absSample > slow) ? slowAttackCoeff_ : currentSlowRelCoeff;
161 slow += slowCoeff * (absSample - slow);
168 T gainLog = (diffLog > T(0)) ? (diffLog * attAmt) : (-diffLog * susAmt);
170 gainLog = std::clamp(gainLog, -maxGainLog, maxGainLog);
174 lastOut = sample * gain;
175 channelData[i] = lastOut;
181 lastOutput_[ch] = lastOut;
197 if (!std::isfinite(amount))
return;
198 attackAmount_.store(std::clamp(amount, T(-100), T(100)) / T(100), std::memory_order_relaxed);
207 if (!std::isfinite(amount))
return;
208 sustainAmount_.store(std::clamp(amount, T(-100), T(100)) / T(100), std::memory_order_relaxed);
217 outputDepRecovery_.store(enabled, std::memory_order_relaxed);
226 if (!std::isfinite(amount))
return;
227 T c = std::clamp(amount, T(-1), T(1));
228 attackAmount_.store(c, std::memory_order_relaxed);
229 sustainAmount_.store(-c * T(0.5), std::memory_order_relaxed);
237 envFast_.fill(T(1e-5));
238 envSlow_.fill(T(1e-5));
239 lastOutput_.fill(T(0));
240 for (
auto& a : analytic_) a.reset();
245 [[nodiscard]] std::vector<uint8_t>
getState()
const
250 w.
write(
"attack",
static_cast<float>(attackAmount_.load(std::memory_order_relaxed)) * 100.0f);
251 w.
write(
"sustain",
static_cast<float>(sustainAmount_.load(std::memory_order_relaxed)) * 100.0f);
252 w.
write(
"outputDep", outputDepRecovery_.load(std::memory_order_relaxed));
268 static constexpr int kMaxChannels = 16;
270 void updateCoefficients() noexcept
272 if (!(sampleRate_ > 0.0))
return;
273 T fs =
static_cast<T
>(sampleRate_);
274 fastAttackCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.0001)));
275 fastReleaseCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.005)));
276 slowAttackCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.020)));
277 slowReleaseCoeff_ = T(1) - std::exp(T(-1) / (fs * T(0.200)));
280 double sampleRate_ = 48000.0;
283 std::atomic<T> attackAmount_ { T(0) };
284 std::atomic<T> sustainAmount_ { T(0) };
285 std::atomic<bool> outputDepRecovery_ {
false };
289 T fastAttackCoeff_ = T(0), fastReleaseCoeff_ = T(0);
290 T slowAttackCoeff_ = T(0), slowReleaseCoeff_ = T(0);
293 std::array<T, kMaxChannels> envFast_ {};
294 std::array<T, kMaxChannels> envSlow_ {};
295 std::array<T, kMaxChannels> lastOutput_ {};
296 std::array<HilbertIIR<double>, kMaxChannels> analytic_ {};
297 static constexpr int kDetChunk = 64;
298 std::array<double, kDetChunk> detIn_ {};
299 std::array<double, kDetChunk> detMag_ {};
Non-owning view over audio channel data.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
bool isValid() const noexcept
uint32_t processorId() const noexcept
Serializes key/value parameters into a versioned blob.
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
void write(const char *key, float value)
Writes a float parameter.
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.
~TransientDesigner()=default
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.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
T fastExp(T x) noexcept
Fast approximation of e^x via std::exp2 (~2x faster than std::exp on MSVC).
Describes the audio environment for a DSP processor.