mindball/Source/DelayEngine.h

289 lines
8.8 KiB
C
Raw Normal View History

2026-08-14 21:14:18 +02:00
#pragma once
#include "DelayLine.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
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<float> (sampleRate);
dl.prepare (static_cast<float> (maxDelaySeconds * sampleRate));
dr.prepare (static_cast<float> (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<float> (divisionBeats[static_cast<std::size_t> (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<float> (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;
}
}
2026-08-15 13:54:42 +02:00
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<float> (divisionBeats[static_cast<std::size_t> (idx)] * secondsPerBeat * 1000.0);
}
2026-08-14 21:14:18 +02:00
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<float, numDivisions> 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;
};
}