#pragma once #include "DelayLine.h" #include #include #include #include namespace mindball { class DelayEngine { public: enum Mode { normal = 0, invert, pingpong, superpong }; struct Params { float feedback = 0.35f; float loCutHz = 100.0f; float hiCutHz = 12000.0f; float dryWet = 0.4f; float timeMs = 300.0f; int timeSyncIndex = 0; int mode = normal; bool sync = false; bool doubleTap = false; bool center = false; bool autoPan = false; float panDepth = 1.0f; double bpm = 120.0; }; void prepare (double sampleRate, double maxDelaySeconds) { sr = static_cast (sampleRate); dl.prepare (static_cast (maxDelaySeconds * sampleRate)); dr.prepare (static_cast (maxDelaySeconds * sampleRate)); clear(); sFeedback = 0.0f; sLoCut = 20.0f; sHiCut = 18000.0f; sDryWet = 0.0f; head1 = 1.0f; head2 = 1.0f; blend = 0.0f; lfoPhase = 0.0f; } void clear() { dl.clear(); dr.clear(); lpL.reset(); hpL.reset(); lpR.reset(); hpR.reset(); lfoPhase = 0.0f; } void process (const float* inL, const float* inR, float* outL, float* outR, int numSamples, const Params& p, float* wetLOut = nullptr, float* wetROut = nullptr) { constexpr float pi = 3.14159265358979f; const float kPar = 1.0f - std::exp (-1.0f / (0.02f * sr)); const float fadeInc = 1.0f / (0.012f * sr); float targetTimeSamples; if (p.sync) { const int idx = std::clamp (p.timeSyncIndex, 0, numDivisions - 1); const double secondsPerBeat = 60.0 / std::max (1.0, p.bpm); targetTimeSamples = static_cast (divisionBeats[static_cast (idx)] * secondsPerBeat * sr); } else { targetTimeSamples = p.timeMs * 0.001f * sr; } targetTimeSamples = std::max (2.0f, targetTimeSamples); const float targetFeedback = p.feedback; const float targetLoCut = p.loCutHz; const float targetHiCut = p.hiCutHz; const float targetDryWet = p.dryWet; const bool dbl = p.doubleTap; const bool ctr = p.center; const bool needB = dbl || (ctr && (p.mode == pingpong || p.mode == superpong)); const float lfoRate = p.autoPan ? (p.sync ? static_cast (p.bpm / 240.0) : 0.25f) : 0.0f; for (int i = 0; i < numSamples; ++i) { sFeedback += (targetFeedback - sFeedback) * kPar; sLoCut += (targetLoCut - sLoCut) * kPar; sHiCut += (targetHiCut - sHiCut) * kPar; sDryWet += (targetDryWet - sDryWet) * kPar; // Delay time changes are done with two stationary read heads and a // short crossfade, never by gliding a read position. This avoids the // pitch-bend "sped up / slowed down" artifacts that come from a // moving read head. When the target moves, snap head2 to it and fade // between the two fixed heads; once blended, head2 takes over. if (blend == 0.0f && std::fabs (targetTimeSamples - head1) > 0.5f) head2 = targetTimeSamples; if (blend < 1.0f) { blend += fadeInc; if (blend >= 1.0f) { head1 = head2; blend = 0.0f; } } lpL.setFc (sHiCut, sr); hpL.setFc (sLoCut, sr); lpR.setFc (sHiCut, sr); hpR.setFc (sLoCut, sr); const float h1L = dl.read (head1); const float h2L = dl.read (head2); const float h1R = dr.read (head1); const float h2R = dr.read (head2); const float aL = h1L + (h2L - h1L) * blend; const float aR = h1R + (h2R - h1R) * blend; float bL = 0.0f, bR = 0.0f; if (needB) { const float two1 = 2.0f * head1; const float two2 = 2.0f * head2; bL = dl.read (two1) + (dl.read (two2) - dl.read (two1)) * blend; bR = dr.read (two1) + (dr.read (two2) - dr.read (two1)) * blend; } float fbL = 0.0f, fbR = 0.0f; float wetL = 0.0f, wetR = 0.0f; switch (p.mode) { case normal: fbL = aL; fbR = aR; wetL = aL + bL; wetR = aR + bR; break; case invert: fbL = aL; fbR = -aR; wetL = aL + bL; wetR = -(aR + bR); break; case pingpong: fbL = aR; fbR = aL; wetL = aL + bL; wetR = aR + bR; break; case superpong: { const float sumA = aL + aR; const float sumAB = sumA + (needB ? bL + bR : 0.0f); fbL = 0.5f * sumA; fbR = 0.5f * sumA; wetL = sumAB * 0.5f; wetR = sumAB * 0.5f; break; } } if (ctr && (p.mode == pingpong || p.mode == superpong)) { const float c = 0.5f * (aL + aR); wetL = c + (needB ? bL : 0.0f); wetR = c + (needB ? bR : 0.0f); } if (p.autoPan) { const float pan = std::sin (2.0f * pi * lfoPhase); lfoPhase += lfoRate / sr; if (lfoPhase >= 1.0f) lfoPhase -= std::floor (lfoPhase); const float s = std::clamp (p.panDepth, 0.0f, 1.0f); const float angle = (1.0f - s) * pi * 0.25f + s * (pan + 1.0f) * pi * 0.25f; const float gL = std::cos (angle); const float gR = std::sin (angle); wetL *= gL; wetR *= gR; } const float g = sFeedback * (1.0f + 0.002f * std::pow (sFeedback, 100.0f)); dl.write (inL[i] + softClip (lpL.processLP (hpL.processHP (fbL * g)))); dr.write (inR[i] + softClip (lpR.processLP (hpR.processHP (fbR * g)))); const float w = sDryWet; float dryGain, wetGain; if (w <= 0.5f) { dryGain = 1.0f; wetGain = 2.0f * w; } else { dryGain = 2.0f * (1.0f - w); wetGain = 1.0f; } outL[i] = inL[i] * dryGain + wetL * wetGain; outR[i] = inR[i] * dryGain + wetR * wetGain; if (wetLOut != nullptr) wetLOut[i] = wetL * wetGain; if (wetROut != nullptr) wetROut[i] = wetR * wetGain; } } static float syncTimeMs (int index, double bpm) { const int idx = std::clamp (index, 0, numDivisions - 1); const double secondsPerBeat = 60.0 / std::max (1.0, bpm); return static_cast (divisionBeats[static_cast (idx)] * secondsPerBeat * 1000.0); } private: static float softClip (float x) { const float ax = std::fabs (x); if (ax < 0.8f) return x; return std::copysign (0.8f + 0.2f * std::tanh ((ax - 0.8f) / 0.2f), x); } struct OnePole { float alpha = 0.0f; float state = 0.0f; float xPrev = 0.0f; void reset() { state = 0.0f; xPrev = 0.0f; } void setFc (float fc, float sr) { constexpr float pi = 3.14159265358979f; alpha = 1.0f - std::exp (-2.0f * pi * fc / sr); } float processLP (float x) { state += alpha * (x - state); return state; } float processHP (float x) { state = x - xPrev + (1.0f - alpha) * state; xPrev = x; return state; } }; static constexpr int numDivisions = 12; static constexpr std::array divisionBeats = { 1.0f / 16.0f, 1.0f / 8.0f, 1.0f / 4.0f, 1.0f / 3.0f, 1.0f / 2.0f, 3.0f / 4.0f, 1.0f, 3.0f / 2.0f, 2.0f, 3.0f, 4.0f, 8.0f }; DelayLine dl, dr; OnePole lpL, hpL, lpR, hpR; float sr = 48000.0f; float lfoPhase = 0.0f; float sFeedback = 0.0f; float sLoCut = 0.0f; float sHiCut = 0.0f; float sDryWet = 0.0f; float head1 = 1.0f; float head2 = 1.0f; float blend = 0.0f; }; }