mirror of
https://codeberg.org/armin/mindball.git
synced 2026-09-01 03:40:49 +02:00
282 lines
8.5 KiB
C++
282 lines
8.5 KiB
C++
#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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
}
|