Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
1264 lines (1088 sloc) 31 KB
// madronalib: a C++ framework for DSP applications.
// Copyright (c) 2020 Madrona Labs LLC. http://www.madronalabs.com
// Distributed under the MIT license: http://madrona-labs.mit-license.org/
// DSP filters: functor objects implementing an operator()(DSPVector input, ...).
// All these filters have some state, otherwise they would be DSPOps.
//
// These objects are for building fixed DSP graphs in a functional style. The
// compiler should have many opportunities to optimize these graphs. For dynamic
// graphs changeable at runtime, see MLProcs. In general MLProcs will be written
// using DSPGens, DSPOps, DSPFilters.
//
// Filter cutoffs are set by a parameter omega, equal to frequency / sample
// rate. This lets filter objects be unaware of the sample rate, resulting in
// less code overall. For all filters, k is a damping parameter equal to 1/Q
// where Q is the analog filter "quality." For bell and shelf filters, gain is
// specified as an output / input ratio A.
#pragma once
#include <vector>
#include "MLDSPGens.h"
namespace ml
{
// use this, not dBToAmp for calculating filter gain parameter A.
inline float dBToGain(float dB) { return powf(10.f, dB / 40.f); }
// from a coefficients start array and a coefficients end array, make a
// DSPVectorArray with each coefficient interpolated over time.
template <size_t COEFFS_SIZE>
DSPVectorArray<COEFFS_SIZE> interpolateCoeffsLinear(const std::array<float, COEFFS_SIZE> c0,
const std::array<float, COEFFS_SIZE> c1)
{
DSPVectorArray<COEFFS_SIZE> vy;
for (int i = 0; i < COEFFS_SIZE; ++i)
{
vy.row(i) = interpolateDSPVectorLinear(c0[i], c1[i]);
}
return vy;
}
// --------------------------------------------------------------------------------
// utility filters implemented as SVF variations
// Thanks to Andrew Simper [www.cytomic.com] for sharing his work over the
// years.
class Lopass
{
struct _coeffs
{
float g0, g1, g2;
};
float ic1eq{0};
float ic2eq{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega, float k)
{
float piOmega = kPi * omega;
float s1 = sinf(piOmega);
float s2 = sinf(2.0f * piOmega);
float nrm = 1.0f / (2.f + k * s2);
float g0 = s2 * nrm;
float g1 = (-2.f * s1 * s1 - k * s2) * nrm;
float g2 = (2.0f * s1 * s1) * nrm;
return {g0, g1, g2};
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float t0 = v0 - ic2eq;
float t1 = mCoeffs.g0 * t0 + mCoeffs.g1 * ic1eq;
float t2 = mCoeffs.g2 * t0 + mCoeffs.g0 * ic1eq;
float v2 = t2 + ic2eq;
ic1eq += 2.0f * t1;
ic2eq += 2.0f * t2;
vy[n] = v2;
}
return vy;
}
};
class Hipass
{
struct _coeffs
{
float g0, g1, g2, k;
};
float ic1eq{0};
float ic2eq{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega, float k)
{
float piOmega = kPi * omega;
float s1 = sinf(piOmega);
float s2 = sinf(2.0f * piOmega);
float nrm = 1.0f / (2.f + k * s2);
float g0 = s2 * nrm;
float g1 = (-2.f * s1 * s1 - k * s2) * nrm;
float g2 = (2.0f * s1 * s1) * nrm;
return {g0, g1, g2, k};
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float t0 = v0 - ic2eq;
float t1 = mCoeffs.g0 * t0 + mCoeffs.g1 * ic1eq;
float t2 = mCoeffs.g2 * t0 + mCoeffs.g0 * ic1eq;
float v1 = t1 + ic1eq;
float v2 = t2 + ic2eq;
ic1eq += 2.0f * t1;
ic2eq += 2.0f * t2;
vy[n] = v0 - mCoeffs.k * v1 - v2;
}
return vy;
}
};
class Bandpass
{
struct _coeffs
{
float g0, g1, g2;
};
float ic1eq{0};
float ic2eq{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega, float k)
{
float piOmega = kPi * omega;
float s1 = sinf(piOmega);
float s2 = sinf(2.0f * piOmega);
float nrm = 1.0f / (2.f + k * s2);
float g0 = s2 * nrm;
float g1 = (-2.f * s1 * s1 - k * s2) * nrm;
float g2 = (2.0f * s1 * s1) * nrm;
return {g0, g1, g2};
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float t0 = v0 - ic2eq;
float t1 = mCoeffs.g0 * t0 + mCoeffs.g1 * ic1eq;
float t2 = mCoeffs.g2 * t0 + mCoeffs.g0 * ic1eq;
float v1 = t1 + ic1eq;
ic1eq += 2.0f * t1;
ic2eq += 2.0f * t2;
vy[n] = v1;
}
return vy;
}
};
class LoShelf
{
enum coeffNames
{
a1,
a2,
a3,
m1,
m2,
COEFFS_SIZE
};
typedef std::array<float, COEFFS_SIZE> _coeffs;
typedef DSPVectorArray<COEFFS_SIZE> _vcoeffs;
float ic1eq{0};
float ic2eq{0};
public:
enum paramNames
{
omega,
k,
A,
PARAMS_SIZE
};
typedef std::array<float, PARAMS_SIZE> params;
_coeffs mCoeffs{};
static _coeffs coeffs(params p)
{
_coeffs r;
float piOmega = kPi * p[omega];
float g = tanf(piOmega) / sqrtf(p[A]);
r[a1] = 1.f / (1.f + g * (g + p[k]));
r[a2] = g * r[a1];
r[a3] = g * r[a2];
r[m1] = p[k] * (p[A] - 1.f);
r[m2] = (p[A] * p[A] - 1.f);
return r;
}
static _vcoeffs vcoeffs(const params p0, const params p1)
{
return interpolateCoeffsLinear(coeffs(p0), coeffs(p1));
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float v3 = v0 - ic2eq;
float v1 = mCoeffs[a1] * ic1eq + mCoeffs[a2] * v3;
float v2 = ic2eq + mCoeffs[a2] * ic1eq + mCoeffs[a3] * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
vy[n] = v0 + mCoeffs[m1] * v1 + mCoeffs[m2] * v2;
}
return vy;
}
inline DSPVector operator()(const DSPVector vx, const _vcoeffs vc)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float v3 = v0 - ic2eq;
float v1 = vc.constRow(a1)[n] * ic1eq + vc.constRow(a2)[n] * v3;
float v2 = ic2eq + vc.constRow(a2)[n] * ic1eq + vc.constRow(a3)[n] * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
vy[n] = v0 + vc.constRow(m1)[n] * v1 + vc.constRow(m2)[n] * v2;
}
return vy;
}
};
class HiShelf
{
enum coeffnames
{
a1,
a2,
a3,
m0,
m1,
m2,
COEFFS_SIZE
};
typedef std::array<float, COEFFS_SIZE> _coeffs;
typedef DSPVectorArray<COEFFS_SIZE> _vcoeffs;
float ic1eq{0};
float ic2eq{0};
public:
enum paramnames
{
omega,
k,
A,
PARAMS_SIZE
};
typedef std::array<float, PARAMS_SIZE> params;
_coeffs mCoeffs{};
static _coeffs coeffs(params p)
{
_coeffs r;
float piOmega = kPi * p[omega];
float g = tanf(piOmega) * sqrtf(p[A]);
r[a1] = 1.f / (1.f + g * (g + p[k]));
r[a2] = g * r[a1];
r[a3] = g * r[a2];
r[m0] = p[A] * p[A];
r[m1] = p[k] * (1.f - p[A]) * p[A];
r[m2] = (1.f - p[A] * p[A]);
return r;
}
static _vcoeffs vcoeffs(const params p0, const params p1)
{
return interpolateCoeffsLinear(coeffs(p0), coeffs(p1));
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float v3 = v0 - ic2eq;
float v1 = mCoeffs[a1] * ic1eq + mCoeffs[a2] * v3;
float v2 = ic2eq + mCoeffs[a2] * ic1eq + mCoeffs[a3] * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
vy[n] = mCoeffs[m0] * v0 + mCoeffs[m1] * v1 + mCoeffs[m2] * v2;
}
return vy;
}
inline DSPVector operator()(const DSPVector vx, const _vcoeffs vc)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float v3 = v0 - ic2eq;
float v1 = vc.constRow(a1)[n] * ic1eq + vc.constRow(a2)[n] * v3;
float v2 = ic2eq + vc.constRow(a2)[n] * ic1eq + vc.constRow(a3)[n] * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
vy[n] = vc.constRow(m0)[n] * v0 + vc.constRow(m1)[n] * v1 + vc.constRow(m2)[n] * v2;
}
return vy;
}
};
class Bell
{
struct _coeffs
{
float a1, a2, a3, m1;
};
float ic1eq{0};
float ic2eq{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega, float k, float A)
{
float kc = k / A; // correct k
float piOmega = kPi * omega;
float g = tanf(piOmega);
float a1 = 1.f / (1.f + g * (g + kc));
float a2 = g * a1;
float a3 = g * a2;
float m1 = kc * (A * A - 1.f);
return {a1, a2, a3, m1};
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
float v0 = vx[n];
float v3 = v0 - ic2eq;
float v1 = mCoeffs.a1 * ic1eq + mCoeffs.a2 * v3;
float v2 = ic2eq + mCoeffs.a2 * ic1eq + mCoeffs.a3 * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
vy[n] = v0 + mCoeffs.m1 * v1;
}
return vy;
}
};
// A one pole filter. see https://ccrma.stanford.edu/~jos/fp/One_Pole.html
class OnePole
{
struct _coeffs
{
float a0, b1;
};
float y1{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega)
{
float x = expf(-omega * kTwoPi);
return {1.f - x, x};
}
static _coeffs passthru() { return {1.f, 0.f}; }
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
y1 = mCoeffs.a0 * vx[n] + mCoeffs.b1 * y1;
vy[n] = y1;
}
return vy;
}
};
// A one-pole, one-zero filter to attenuate DC.
// Works well, but beware of its effects on bass sounds. An omega of 0.05 is a
// good starting point. see https://ccrma.stanford.edu/~jos/fp/DC_Blocker.html
// for more.
class DCBlocker
{
typedef float _coeffs;
float x1{0};
float y1{0};
public:
_coeffs mCoeffs{};
static _coeffs coeffs(float omega) { return cosf(omega); }
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
const float x0 = vx[n];
const float y0 = x0 - x1 + mCoeffs * y1;
y1 = y0;
x1 = x0;
vy[n] = y0;
}
return vy;
}
};
// Differentiator
class Differentiator
{
float x1{0};
public:
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
vy[0] = x1 - vx[0];
// TODO SIMD
for (int n = 1; n < kFloatsPerDSPVector; ++n)
{
vy[n] = vx[n - 1] - vx[n];
}
x1 = vx[kFloatsPerDSPVector];
return vy;
}
};
// Integrator
class Integrator
{
float y1{0};
public:
// set leak to a value such as 0.001 for stability
float mLeak{0};
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
y1 -= y1 * mLeak;
y1 += vx[n];
vy[n] = y1;
}
return vy;
}
};
// Peak with exponential decay
class Peak
{
struct _coeffs
{
float a0, b1;
};
float y1{0};
int peakHoldCounter{0};
public:
_coeffs mCoeffs{0};
int peakHoldSamples{44100};
static _coeffs coeffs(float omega)
{
float x = expf(-omega * kTwoPi);
return {1.f - x, x};
}
static _coeffs passthru() { return {1.f, 0.f}; }
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
DSPVector vxSquared = vx * vx;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
if (vxSquared[n] > y1)
{
// set peak and reset counter
y1 = vxSquared[n];
peakHoldCounter = peakHoldSamples;
}
else
{
// decay
if (peakHoldCounter <= 0)
{
y1 = mCoeffs.a0 * vxSquared[n] + mCoeffs.b1 * y1;
}
}
vy[n] = y1;
}
if (peakHoldCounter > 0)
{
peakHoldCounter -= kFloatsPerDSPVector;
}
return sqrt(vy);
}
};
// filtered RMS
class RMS
{
struct _coeffs
{
float a0, b1;
};
float y1{0};
public:
_coeffs mCoeffs{0};
static _coeffs coeffs(float omega)
{
float x = expf(-omega * kTwoPi);
return {1.f - x, x};
}
static _coeffs passthru() { return {1.f, 0.f}; }
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
DSPVector vxSquared = vx * vx;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
y1 = mCoeffs.a0 * vxSquared[n] + mCoeffs.b1 * y1;
vy[n] = y1;
}
return sqrt(vy);
}
};
// IntegerDelay delays a signal a whole number of samples.
class IntegerDelay
{
std::vector<float> mBuffer;
int mIntDelayInSamples{0};
uintptr_t mWriteIndex{0};
uintptr_t mLengthMask{0};
public:
IntegerDelay() = default;
IntegerDelay(int d)
{
setMaxDelayInSamples(d);
setDelayInSamples(d);
}
~IntegerDelay() = default;
// for efficiency, no bounds checking is done. Because mLengthMask is used to
// constrain all reads, bad values here may make bad sounds (buffer wraps) but
// will not attempt to read from outside the buffer.
inline void setDelayInSamples(int d) { mIntDelayInSamples = d; }
void setMaxDelayInSamples(float d)
{
int dMax = floorf(d);
int newSize = 1 << bitsToContain(dMax + kFloatsPerDSPVector);
mBuffer.resize(newSize);
mLengthMask = newSize - 1;
mWriteIndex = 0;
clear();
}
inline void clear() { std::fill(mBuffer.begin(), mBuffer.end(), 0.f); }
inline DSPVector operator()(const DSPVector vx)
{
// write
uintptr_t writeEnd = mWriteIndex + kFloatsPerDSPVector;
if (writeEnd <= mLengthMask + 1)
{
const float* srcStart = vx.getConstBuffer();
std::copy(srcStart, srcStart + kFloatsPerDSPVector, mBuffer.data() + mWriteIndex);
}
else
{
uintptr_t excess = writeEnd - mLengthMask - 1;
const float* srcStart = vx.getConstBuffer();
const float* srcSplice = srcStart + kFloatsPerDSPVector - excess;
const float* srcEnd = srcStart + kFloatsPerDSPVector;
std::copy(srcStart, srcSplice, mBuffer.data() + mWriteIndex);
std::copy(srcSplice, srcEnd, mBuffer.data());
}
// read
DSPVector vy;
uintptr_t readStart = (mWriteIndex - mIntDelayInSamples) & mLengthMask;
uintptr_t readEnd = readStart + kFloatsPerDSPVector;
float* srcBuf = mBuffer.data();
if (readEnd <= mLengthMask + 1)
{
std::copy(srcBuf + readStart, srcBuf + readEnd, vy.getBuffer());
}
else
{
uintptr_t excess = readEnd - mLengthMask - 1;
uintptr_t readSplice = readStart + kFloatsPerDSPVector - excess;
float* pDest = vy.getBuffer();
std::copy(srcBuf + readStart, srcBuf + readSplice, pDest);
std::copy(srcBuf, srcBuf + excess, pDest + (kFloatsPerDSPVector - excess));
}
// update index
mWriteIndex += kFloatsPerDSPVector;
mWriteIndex &= mLengthMask;
return vy;
}
inline DSPVector operator()(const DSPVector x, const DSPVector delay)
{
DSPVector y;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
// write
mBuffer[mWriteIndex] = x[n];
// read
mIntDelayInSamples = static_cast<int>(delay[n]);
uintptr_t readIndex = (mWriteIndex - mIntDelayInSamples) & mLengthMask;
y[n] = mBuffer[readIndex];
mWriteIndex++;
mWriteIndex &= mLengthMask;
}
return y;
}
inline float processSample(float x)
{
// write
// note that, for performance, there is no bounds checking. If you crash
// here, you probably didn't allocate enough delay memory.
mBuffer[mWriteIndex] = x;
// read
uintptr_t readIndex = (mWriteIndex - mIntDelayInSamples) & mLengthMask;
float y = mBuffer[readIndex];
// update index
mWriteIndex++;
mWriteIndex &= mLengthMask;
return y;
}
};
// First order allpass section with a single sample of delay.
class Allpass1
{
private:
float x1{0}, y1{0};
public:
float mCoeffs;
Allpass1() : mCoeffs(0.f) {}
Allpass1(float a) : mCoeffs(a) {}
~Allpass1() {}
inline void clear()
{
x1 = 0.f;
y1 = 0.f;
}
// get allpass coefficient from a delay fraction d.
// to minimize modulation noise, d should be in the range [0.618 - 1.618].
static float coeffs(float d)
{
// return 2nd order approx around 1 to (1.f - d) / (1.f + d)
float xm1 = (d - 1.f);
return -0.53f * xm1 + 0.24f * xm1 * xm1;
}
inline float processSample(const float x)
{
// one-multiply form. see
// https://ccrma.stanford.edu/~jos/pasp/One_Multiply_Scattering_Junctions.html
float y = x1 + (x - y1) * mCoeffs;
x1 = x;
y1 = y;
return y;
}
inline DSPVector operator()(const DSPVector vx)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
vy[n] = processSample(vx[n]);
}
return vy;
}
};
// Combining the integer delay and first order allpass section
// gives us an allpass-interpolated fractional delay. In general, modulating the
// delay time will change the allpass coefficient, producing clicks in the
// output.
class FractionalDelay
{
IntegerDelay mIntegerDelay;
Allpass1 mAllpassSection;
float mDelayInSamples{};
public:
FractionalDelay() = default;
FractionalDelay(float d)
{
setMaxDelayInSamples(d);
setDelayInSamples(d);
}
~FractionalDelay() = default;
inline void clear()
{
mIntegerDelay.clear();
mAllpassSection.clear();
}
inline void setDelayInSamples(float d)
{
mDelayInSamples = d;
float fDelayInt = floorf(d);
int delayInt = fDelayInt;
float delayFrac = d - fDelayInt;
// constrain D to [0.618 - 1.618] if possible
if ((delayFrac < 0.618f) && (delayInt > 0))
{
delayFrac += 1.f;
delayInt -= 1;
}
mIntegerDelay.setDelayInSamples(delayInt);
mAllpassSection.mCoeffs = Allpass1::coeffs(delayFrac);
}
inline void setMaxDelayInSamples(float d) { mIntegerDelay.setMaxDelayInSamples(floorf(d)); }
// return the input signal, delayed by the constant delay time
// mDelayInSamples.
inline DSPVector operator()(const DSPVector vx) { return mAllpassSection(mIntegerDelay(vx)); }
// return the input signal, delayed by the varying delay time vDelayInSamples.
inline DSPVector operator()(const DSPVector vx, const DSPVector vDelayInSamples)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
setDelayInSamples(vDelayInSamples[n]);
vy[n] = mAllpassSection.processSample(mIntegerDelay.processSample(vx[n]));
}
return vy;
}
// return the input signal, delayed by the varying delay time vDelayInSamples,
// but only allow changes to the delay time when vChangeTicks is nonzero.
inline DSPVector operator()(const DSPVector vx, const DSPVector vDelayInSamples,
const DSPVectorInt vChangeTicks)
{
DSPVector vy;
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
if (vChangeTicks[n] != 0)
{
setDelayInSamples(vDelayInSamples[n]);
}
vy[n] = mAllpassSection.processSample(mIntegerDelay.processSample(vx[n]));
}
return vy;
}
};
// Crossfading two allpass-interpolated delays allows modulating the delay
// time without clicks. See "A Lossless, Click-free, Pitchbend-able Delay Line
// Loop Interpolation Scheme", Van Duyne, Jaffe, Scandalis, Stilson, ICMC 1997.
namespace PitchbendableDelayConsts
{
// period in samples of allpass fade cycle. must be a power of 2 less than or
// equal to kFloatsPerDSPVector. 32 sounds good.
constexpr int kFadePeriod{32};
constexpr int fadeRamp(int n) { return n % kFadePeriod; }
constexpr int ticks1(int n) { return fadeRamp(n) == kFadePeriod / 2; }
constexpr int ticks2(int n) { return fadeRamp(n) == 0; }
constexpr float fadeFn(int n)
{
// triangle from 0 to 1 to 0
return 2.f * ((fadeRamp(n)) > kFadePeriod / 2 ? 1.0f - (fadeRamp(n)) / (kFadePeriod + 0.f)
: (fadeRamp(n)) / (kFadePeriod + 0.f));
}
// generate vectors of ticks indicating when delays can change
// equality operators on vectors return 0 or 0xFFFFFFFF
// note: mDelay1's delay time will be 0 when the object is created and before
// the first half fade period. so there is a warmup time of one half fade
// period: any input before this will be attenuated.
//
// constexpr fill is used. unfortunately this cannot be made to work with a
// lambda in C++11. TODO Revisit.
constexpr DSPVectorInt test1(fadeRamp);
constexpr DSPVectorInt kvDelay1Changes(ticks1);
constexpr DSPVectorInt kvDelay2Changes(ticks2);
constexpr DSPVector kvFade(fadeFn);
}; // namespace PitchbendableDelayConsts
class PitchbendableDelay
{
FractionalDelay mDelay1, mDelay2;
public:
PitchbendableDelay() = default;
inline void setMaxDelayInSamples(float d)
{
mDelay1.setMaxDelayInSamples(d);
mDelay2.setMaxDelayInSamples(d);
}
inline void clear()
{
mDelay1.clear();
mDelay2.clear();
}
inline DSPVector operator()(const DSPVector vInput, const DSPVector vDelayInSamples)
{
using namespace PitchbendableDelayConsts;
// run the fractional delays and crossfade the results.
return lerp(mDelay1(vInput, vDelayInSamples, kvDelay1Changes),
mDelay2(vInput, vDelayInSamples, kvDelay2Changes), kvFade);
}
};
// General purpose allpass filter with arbitrary delay length.
// For efficiency, the minimum delay time is one DSPVector.
template <typename DELAY_TYPE>
class Allpass
{
DELAY_TYPE mDelay;
DSPVector vy1{};
public:
float mGain{0.f};
// use setDelayInSamples to set a constant delay time with DELAY_TYPE of
// IntegerDelay or FractionalDelay.
inline void setDelayInSamples(float d) { mDelay.setDelayInSamples(d - kFloatsPerDSPVector); }
inline void setMaxDelayInSamples(float d)
{
mDelay.setMaxDelayInSamples(d - kFloatsPerDSPVector);
}
inline void clear()
{
mDelay.clear();
vy1 = DSPVector();
}
// use with constant delay time.
inline DSPVector operator()(const DSPVector vInput)
{
DSPVector vGain(-mGain);
DSPVector vDelayInput = vInput - vy1 * vGain;
DSPVector y = vDelayInput * vGain + vy1;
vy1 = mDelay(vDelayInput);
return y;
}
// use vDelayInSamples parameter to set a varying delay time with DELAY_TYPE =
// PitchbendableDelay.
inline DSPVector operator()(const DSPVector vInput, const DSPVector vDelayInSamples)
{
DSPVector vGain(-mGain);
DSPVector vDelayInput = vInput - vy1 * vGain;
DSPVector y = vDelayInput * vGain + vy1;
vy1 = mDelay(vDelayInput, vDelayInSamples - DSPVector(kFloatsPerDSPVector));
return y;
}
};
// FDN
// A general Feedback Delay Network with N delay lines connected in an NxN
// matrix.
// TODO DELAY_TYPE parameter for modulation?
template <int SIZE>
class FDN
{
std::array<IntegerDelay, SIZE> mDelays;
std::array<OnePole, SIZE> mFilters;
std::array<DSPVector, SIZE> mDelayInputVectors{{{DSPVector(0.f)}}};
public:
// feedback gains array is public—just copy values to set.
std::array<float, SIZE> mFeedbackGains{{0}};
void setDelaysInSamples(std::array<float, SIZE> times)
{
for (int n = 0; n < SIZE; ++n)
{
// we have one DSPVector feedback latency, so compensate delay times for
// that.
int len = times[n] - kFloatsPerDSPVector;
len = max(1, len);
mDelays[n].setDelayInSamples(len);
}
}
void setFilterCutoffs(std::array<float, SIZE> omegas)
{
for (int n = 0; n < SIZE; ++n)
{
mFilters[n].mCoeffs = ml::OnePole::coeffs(omegas[n]);
}
}
// stereo output function
// TODO generalize n-channel output function somehow
DSPVectorArray<2> operator()(const DSPVector x)
{
// run delays, getting DSPVector for each delay
for (int n = 0; n < SIZE; ++n)
{
mDelayInputVectors[n] = mDelays[n](mDelayInputVectors[n]);
}
// get output sum
DSPVector sumR, sumL;
for (int n = 0; n < (SIZE & (~1)); ++n)
{
if (n & 1)
{
sumL += mDelayInputVectors[n];
}
else
{
sumR += mDelayInputVectors[n];
}
}
// inputs = input gains*input sample + filters(M*delay outputs)
// The feedback matrix M is a unit-gain Householder matrix, which is just
// the identity matrix minus a constant k, where k = 2/size. Since
// multiplying this can be simplified so much, you just see a few operations
// here, not a general matrix multiply.
DSPVector sumOfDelays;
for (int n = 0; n < SIZE; ++n)
{
sumOfDelays += mDelayInputVectors[n];
}
sumOfDelays *= DSPVector(2.0f / SIZE);
for (int n = 0; n < SIZE; ++n)
{
mDelayInputVectors[n] -= (sumOfDelays);
mDelayInputVectors[n] = mFilters[n](mDelayInputVectors[n]) * DSPVector(mFeedbackGains[n]);
mDelayInputVectors[n] += x;
}
return concatRows(sumL, sumR);
}
};
// Half Band Filter
// Polyphase allpass filter used to upsample or downsample a signal by 2x.
// Structure due to fred harris, A. G. Constantinides and Valenzuela.
class HalfBandFilter
{
public:
inline DSPVector upsampleFirstHalf(const DSPVector vx)
{
DSPVector vy;
int i2 = 0;
for (int i = 0; i < kFloatsPerDSPVector / 2; ++i)
{
vy[i2++] = apa1.processSample(apa0.processSample(vx[i]));
vy[i2++] = apb1.processSample(apb0.processSample(vx[i]));
}
return vy;
}
inline DSPVector upsampleSecondHalf(const DSPVector vx)
{
DSPVector vy;
int i2 = 0;
for (int i = kFloatsPerDSPVector / 2; i < kFloatsPerDSPVector; ++i)
{
vy[i2++] = apa1.processSample(apa0.processSample(vx[i]));
vy[i2++] = apb1.processSample(apb0.processSample(vx[i]));
}
return vy;
}
inline DSPVector downsample(const DSPVector vx1, const DSPVector vx2)
{
DSPVector vy;
int i2 = 0;
for (int i = 0; i < kFloatsPerDSPVector / 2; ++i)
{
float a0 = apa1.processSample(apa0.processSample(vx1[i2]));
float b0 = apb1.processSample(apb0.processSample(vx1[i2 + 1]));
vy[i] = (a0 + b1) * 0.5f;
b1 = b0;
i2 += 2;
}
i2 = 0;
for (int i = kFloatsPerDSPVector / 2; i < kFloatsPerDSPVector; ++i)
{
float a0 = apa1.processSample(apa0.processSample(vx2[i2]));
float b0 = apb1.processSample(apb0.processSample(vx2[i2 + 1]));
vy[i] = (a0 + b1) * 0.5f;
b1 = b0;
i2 += 2;
}
return vy;
}
private:
// order=4, rejection=70dB, transition band=0.1.
Allpass1 apa0{0.07986642623635751f}, apa1{0.5453536510711322f}, apb0{0.28382934487410993f},
apb1{0.8344118914807379f};
float b1{0};
};
// Downsampler
// a cascade of half band filters, one for each octave.
class Downsampler
{
std::vector<HalfBandFilter> _filters;
std::vector<float> _buffers;
int _octaves;
int _numBuffers;
int _bufferSizeInFloats;
uint32_t _counter{0};
float* bufferPtr(int idx, int channel)
{
return _buffers.data() + idx * _bufferSizeInFloats + kFloatsPerDSPVector * channel;
}
public:
Downsampler(int channels, int octavesDown) : _octaves(octavesDown)
{
if (_octaves)
{
// one pair of buffers for each octave plus one output buffer.
_numBuffers = 2 * _octaves + 1;
_bufferSizeInFloats = kFloatsPerDSPVector * channels;
// each octave uses one filter for each channel.
_filters.resize(_octaves * channels);
// get all buffers as a single contiguous array of floats.
_buffers.resize(_bufferSizeInFloats * _numBuffers);
}
}
~Downsampler() = default;
// write a vector of samples to the filter chain, run filters, and return
// true if there is a new vector of output to read (every 2^octaves writes)
template <size_t CHANNELS>
bool write(DSPVectorArray<CHANNELS> v)
{
if (_octaves)
{
// write input to one of first two buffers
const float* pSrc = v.getConstBuffer();
float* pDest = bufferPtr(_counter & 1, 0);
std::copy(pSrc, pSrc + kFloatsPerDSPVector * CHANNELS, pDest);
// look at the bits of the counter from lowest to highest.
// there is one bit for each octave of downsampling.
// each octave is run if its bit and all lesser bits are 1.
uint32_t mask = 1;
for (int h = 0; h < _octaves; ++h)
{
bool b0 = _counter & mask;
if (!b0) break;
mask <<= 1;
bool b1 = _counter & mask;
// downsample each channel of the buffer
for (int c = 0; c < CHANNELS; ++c)
{
HalfBandFilter* f = &(_filters[h * CHANNELS + c]);
DSPVector vSrc1(bufferPtr(h * 2, c));
DSPVector vSrc2(bufferPtr(h * 2 + 1, c));
DSPVector vDest = f->downsample(vSrc1, vSrc2);
store(vDest, bufferPtr(h * 2 + 2 + b1, c));
}
}
// advance and wrap counter. If it's back to 0, we have output
uint32_t counterMask = (1 << _octaves) - 1;
_counter = (_counter + 1) & counterMask;
return (_counter == 0);
}
else
{
// write input to final buffer
const float* pSrc = v.getConstBuffer();
float* pDest = bufferPtr(_numBuffers - 1, 0);
std::copy(pSrc, pSrc + kFloatsPerDSPVector * CHANNELS, pDest);
return true;
}
}
template <size_t CHANNELS>
DSPVectorArray<CHANNELS> read()
{
return DSPVectorArray<CHANNELS>(bufferPtr(_numBuffers - 1, 0));
}
};
// PLL: Phase Locked Loop for synching an output phasor to an input phasor at some ratio.
class PLL
{
// phasor on [0. - 1.), changes at rate of input phasor * input ratio
float _omega{0};
float _x1{0};
public:
// negative phase signals unknown offset.
void clear() { _omega = -1.f; }
// function call takes 3 inputs:
// x: the input phasor to follow
// dydx: the ratio to the input at which to lock the output phasor
// feedback: amount of feedback to apply in PLL loop.
// 1.0/sampleRate is a good amount of feedback to start with.
DSPVector operator()(DSPVector x, DSPVector dydx, DSPVector feedback)
{
DSPVector y;
// if input phasor is inactive, reset and bail.
// (inactive / active switch is only done every vector)
if (x[0] < 0.f)
{
clear();
y = DSPVector(-1.f);
}
else
{
// startup: if active but phase is unknown, jump to current phase.
if (_omega == -1.f)
{
// estimate previous input sample
_x1 = x[0] - (x[1] - x[0]);
_omega = fmod(x[0] * dydx[0], 1.0f);
}
DSPVector dxdy = divideApprox(DSPVector(1.0f), dydx);
// run the PLL, correcting the output phasor to the input phasor and ratio.
for (int n = 0; n < kFloatsPerDSPVector; ++n)
{
// TODO try using SIMD for differentiator object
float px = x[n];
float dxdt = px - _x1;
if (dxdt < 0.f) dxdt += 1.f;
_x1 = px;
float dydt = dxdt * dydx[n];
// get error term at each sample by comparing output to scaled input
// or scaled input to output depending on ratio.
float error;
if (dydx[n] >= 1.f)
{
error = _omega - fmod(px * dydx[n], 1.0f);
}
else
{
error = fmod(_omega * dxdy[n], 1.0f) - px;
}
// send error towards closest sync
error = roundf(error) - error;
// feedback = negative error * time constant
dydt += feedback[n] * error;
// don't ever run clock backwards.
dydt = ml::max(dydt, 0.f);
// wrap phasor
// TODO try using SIMD for fmod(x, 1.0), test
_omega = fmod(_omega + dydt, 1.0f);
y[n] = _omega;
}
}
return y;
}
};
} // namespace ml