60#include "../Core/AudioBuffer.h"
61#include "../Core/AudioSpec.h"
62#include "../Core/Biquad.h"
63#include "../Core/DenormalGuard.h"
64#include "../Core/DspMath.h"
65#include "../Core/Hysteresis.h"
66#include "../Core/StateBlob.h"
97 prepared_.store(
false, std::memory_order_relaxed);
101 channels_.assign(
static_cast<size_t>(numChannels_), {});
102 for (
auto& ch : channels_)
104 ch.hyst.prepare(sampleRate_);
107 ch.hyst.setParameters(3.5e5, 2.2e4, 1.6e-3, 3.2e4, 0.25);
110 prepared_.store(
true, std::memory_order_relaxed);
111 dirty_.store(
true, std::memory_order_release);
118 if (!prepared_.load(std::memory_order_relaxed))
return;
119 for (
auto& ch : channels_)
126 ch.hpX = ch.hpY = 0.0;
132 currentMix_ = mix_.load(std::memory_order_relaxed);
141 if (!std::isfinite(db))
return;
142 driveDb_.store(std::clamp(db, T(-12), T(24)), std::memory_order_relaxed);
143 dirty_.store(
true, std::memory_order_release);
151 if (!std::isfinite(size))
return;
152 coreSize_.store(std::clamp(size, T(0), T(1)), std::memory_order_relaxed);
153 dirty_.store(
true, std::memory_order_release);
160 if (!std::isfinite(amount))
return;
161 resonance_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
162 dirty_.store(
true, std::memory_order_release);
169 if (!std::isfinite(mix))
return;
170 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
173 [[nodiscard]] T
getDrive() const noexcept {
return driveDb_.load(std::memory_order_relaxed); }
174 [[nodiscard]] T
getCoreSize() const noexcept {
return coreSize_.load(std::memory_order_relaxed); }
175 [[nodiscard]] T
getResonance() const noexcept {
return resonance_.load(std::memory_order_relaxed); }
176 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
179 [[nodiscard]]
static constexpr int getLatency() noexcept {
return 0; }
182 [[nodiscard]] std::vector<uint8_t>
getState()
const
187 w.
write(
"drive",
static_cast<float>(driveDb_.load(std::memory_order_relaxed)));
188 w.
write(
"coreSize",
static_cast<float>(coreSize_.load(std::memory_order_relaxed)));
189 w.
write(
"resonance",
static_cast<float>(resonance_.load(std::memory_order_relaxed)));
190 w.
write(
"mix",
static_cast<float>(mix_.load(std::memory_order_relaxed)));
211 if (!prepared_.load(std::memory_order_relaxed))
return;
214 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
215 const int nS = buffer.getNumSamples();
216 if (nCh == 0 || nS == 0)
return;
220 if (dirty_.load(std::memory_order_relaxed)
221 && dirty_.exchange(
false, std::memory_order_acquire))
228 const T mixTarget = mix_.load(std::memory_order_relaxed);
229 const T mixStart = currentMix_;
230 const T mixStep = (mixTarget - mixStart) /
static_cast<T
>(nS);
240 if (hScaleSm_ <= 0.0) { hScaleSm_ = hScale_; mScaleSm_ = mScale_; }
241 const double kSm = 1.0 - std::exp(-
static_cast<double>(nS) / (0.050 * sampleRate_));
242 const double hEnd = hScaleSm_ * std::pow(hScale_ / hScaleSm_, kSm);
243 const double mEnd = mScaleSm_ * std::pow(mScale_ / mScaleSm_, kSm);
244 const double hRat = std::pow(hEnd / hScaleSm_, 1.0 /
static_cast<double>(nS));
245 const double mRat = std::pow(mEnd / mScaleSm_, 1.0 /
static_cast<double>(nS));
247 for (
int ch = 0; ch < nCh; ++ch)
249 T* d = buffer.getChannel(ch);
250 auto& st = channels_[
static_cast<size_t>(ch)];
251 double hSm = hScaleSm_, mSm = mScaleSm_;
253 for (
int i = 0; i < nS; ++i)
262 double x =
static_cast<double>(d[i]);
263 if (!std::isfinite(x)) { x = 0.0; d[i] = T(0); }
266 const double fluxNew = leak_ * st.flux + halfT_ * (x + st.vPrev);
270 const double m = mSm *
static_cast<double>(
271 st.hyst.processSample(
static_cast<T
>(hSm * fluxNew)));
279 double v = (m - leak_ * st.mPrev) * invHalfT_ - kDiffRho * st.vPrev2;
285 const double hp = hpA_ * (st.hpY + v - st.hpX);
290 const double y = st.bell.process(hp);
292 const T wet =
static_cast<T
>(y);
293 const T mixVal = mixStart + mixStep *
static_cast<T
>(i);
294 d[i] = d[i] + (wet - d[i]) * mixVal;
297 currentMix_ = mixTarget;
303 static constexpr double kDiffRho = 0.974;
307 double b0 = 1.0, b1 = 0.0, b2 = 0.0, a1 = 0.0, a2 = 0.0;
308 double z1 = 0.0, z2 = 0.0;
310 [[nodiscard]]
double process(
double x)
noexcept
312 const double y = b0 * x + z1;
313 z1 = b1 * x - a1 * y + z2;
314 z2 = b2 * x - a2 * y;
326 double hpX = 0.0, hpY = 0.0;
330 void recompute() noexcept
332 const double drive = std::pow(10.0,
static_cast<double>(
333 driveDb_.load(std::memory_order_relaxed)) / 20.0);
334 const double size =
static_cast<double>(coreSize_.load(std::memory_order_relaxed));
335 const double res =
static_cast<double>(resonance_.load(std::memory_order_relaxed));
340 const double cornerHz = 40.0 * std::pow(0.125, size);
341 leak_ = std::exp(-2.0 * std::numbers::pi * cornerHz / sampleRate_);
342 hpA_ = 1.0 - 2.0 * std::numbers::pi * cornerHz / sampleRate_;
343 halfT_ = 0.5 / sampleRate_;
344 invHalfT_ = 2.0 * sampleRate_;
348 const double fluxRef = 1.0 / (2.0 * std::numbers::pi * 30.0);
349 const double headroom = 0.6 + 0.8 * size;
350 hScale_ = drive * 1.1 * 2.2e4 / (fluxRef * headroom);
361 cal.prepare(sampleRate_);
362 cal.setParameters(3.5e5, 2.2e4, 1.6e-3, 3.2e4, 0.25);
363 double flux = 0.0, vPrev = 0.0, mPrev = 0.0, vPrev2 = 0.0;
364 double inSq = 0.0, outSq = 0.0;
365 const int n =
static_cast<int>(0.04 * sampleRate_);
366 for (
int i = 0; i < n; ++i)
368 const double x = 0.25 * std::sin(2.0 * std::numbers::pi * 100.0 * i / sampleRate_);
369 const double fluxNew = leak_ * flux + halfT_ * (x + vPrev);
371 const double m =
static_cast<double>(
372 cal.processSample(
static_cast<T
>(hScale_ * fluxNew)));
373 double v = (m - leak_ * mPrev) * invHalfT_ - kDiffRho * vPrev2;
383 mScale_ = (outSq > 0.0) ? std::sqrt(inSq / outSq) : 1.0;
387 const double bellHz = std::min(12000.0 + 6000.0 * res, 0.42 * sampleRate_);
388 const double bellDb = 2.5 * res;
390 for (
auto& ch : channels_)
392 const double pz1 = ch.bell.z1, pz2 = ch.bell.z2;
393 ch.bell = BellSection { bc.b0, bc.b1, bc.b2, bc.a1, bc.a2, 0.0, 0.0 };
400 double sampleRate_ = 48000.0;
401 int numChannels_ = 0;
402 std::atomic<bool> prepared_ {
false };
404 std::vector<ChannelState> channels_;
406 double leak_ = 0.999;
408 double halfT_ = 0.5 / 48000.0;
409 double invHalfT_ = 96000.0;
410 double hScale_ = 1.0;
411 double mScale_ = 1.0;
412 double hScaleSm_ = -1.0;
413 double mScaleSm_ = -1.0;
414 T currentMix_ = T(1);
416 std::atomic<T> driveDb_ { T(0) };
417 std::atomic<T> coreSize_ { T(0.5) };
418 std::atomic<T> resonance_ { T(0.3) };
419 std::atomic<T> mix_ { T(1) };
420 std::atomic<bool> dirty_ {
true };
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.
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Describes the audio environment for a DSP processor.
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
double sampleRate
Sample rate in Hz.
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.