Browse Source

Switch to biquad for DC

* only up/downsample if needed
tags/v1.1.0^2
hemmer 4 years ago
parent
commit
b4241e035d
2 changed files with 150 additions and 382 deletions
  1. +30
    -23
      src/ChoppingKinky.cpp
  2. +120
    -359
      src/VariableOversampling.hpp

+ 30
- 23
src/ChoppingKinky.cpp View File

@@ -67,19 +67,14 @@ struct ChoppingKinky : Module {
dsp::SchmittTrigger trigger; dsp::SchmittTrigger trigger;
bool output_a_to_chopp; bool output_a_to_chopp;
float previous_a = 0.0; float previous_a = 0.0;
VariableOversampling<> oversampler[NUM_CHANNELS]; VariableOversampling<> oversampler[NUM_CHANNELS];
int oversamplingIndex = 2; int oversamplingIndex = 2;
dsp::RCFilter choppDC;
dsp::BiquadFilter blockDCFilter;
bool blockDC = false; bool blockDC = false;
unsigned int currentFrame = 0;
ChoppingKinky() { ChoppingKinky() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configParam(FOLD_A_PARAM, 0.f, 2.f, 0.f, ""); configParam(FOLD_A_PARAM, 0.f, 2.f, 0.f, "");
@@ -89,14 +84,13 @@ struct ChoppingKinky : Module {
// calculate up/downsampling rates // calculate up/downsampling rates
onSampleRateChange(); onSampleRateChange();
} }
void onSampleRateChange() override { void onSampleRateChange() override {
// SampleRateConverter needs integer value // SampleRateConverter needs integer value
int sampleRate = APP->engine->getSampleRate(); int sampleRate = APP->engine->getSampleRate();
choppDC.setCutoff(10.3f / sampleRate);
blockDCFilter.setParameters(dsp::BiquadFilter::HIGHPASS, 10.3f / sampleRate, M_SQRT1_2, 1.0f);
for (int channel_idx = 0; channel_idx < NUM_CHANNELS; channel_idx++) { for (int channel_idx = 0; channel_idx < NUM_CHANNELS; channel_idx++) {
oversampler[channel_idx].setOversamplingIndex(oversamplingIndex); oversampler[channel_idx].setOversamplingIndex(oversamplingIndex);
@@ -150,33 +144,46 @@ struct ChoppingKinky : Module {
} }
} }
bool chopp_is_required = outputs[OUT_CHOPP_OUTPUT].isConnected();
bool a_is_required = outputs[OUT_A_OUTPUT].isConnected() || chopp_is_required;
bool b_is_required = outputs[OUT_B_OUTPUT].isConnected() || chopp_is_required;
in_a = in_a * gain_a; in_a = in_a * gain_a;
in_b = in_b * gain_b; in_b = in_b * gain_b;
float in_chopp = output_a_to_chopp ? 1.f : 0.f; float in_chopp = output_a_to_chopp ? 1.f : 0.f;
oversampler[CHANNEL_A].upsample(in_a);
oversampler[CHANNEL_B].upsample(in_b);
oversampler[CHANNEL_CHOPP].upsample(in_chopp);
if (a_is_required) {
oversampler[CHANNEL_A].upsample(in_a);
}
if (b_is_required) {
oversampler[CHANNEL_B].upsample(in_b);
}
if (chopp_is_required) {
oversampler[CHANNEL_CHOPP].upsample(in_chopp);
}
float* osBufferA = oversampler[CHANNEL_A].getOSBuffer(); float* osBufferA = oversampler[CHANNEL_A].getOSBuffer();
float* osBufferB = oversampler[CHANNEL_B].getOSBuffer(); float* osBufferB = oversampler[CHANNEL_B].getOSBuffer();
float* osBufferChopp = oversampler[CHANNEL_CHOPP].getOSBuffer(); float* osBufferChopp = oversampler[CHANNEL_CHOPP].getOSBuffer();
for (int i = 0; i < oversampler[0].getOversamplingRatio(); i++) { for (int i = 0; i < oversampler[0].getOversamplingRatio(); i++) {
// TOO SLOW
//break;
osBufferA[i] = foldResponse(4.0f * osBufferA[i], -0.2);
osBufferB[i] = foldResponseSin(4.0f * osBufferB[i]);
osBufferChopp[i] = osBufferChopp[i] * osBufferA[i] + (1.f - osBufferChopp[i]) * osBufferB[i];
if (a_is_required) {
osBufferA[i] = foldResponse(4.0f * osBufferA[i], -0.2);
}
if (b_is_required) {
osBufferB[i] = foldResponseSin(4.0f * osBufferB[i]);
}
if (chopp_is_required) {
osBufferChopp[i] = osBufferChopp[i] * osBufferA[i] + (1.f - osBufferChopp[i]) * osBufferB[i];
}
} }
float out_a = oversampler[CHANNEL_A].downsample();
float out_b = oversampler[CHANNEL_B].downsample();
float out_chopp = oversampler[CHANNEL_CHOPP].downsample();
float out_a = a_is_required ? oversampler[CHANNEL_A].downsample() : 0.f;
float out_b = b_is_required ? oversampler[CHANNEL_B].downsample() : 0.f;
float out_chopp = chopp_is_required ? oversampler[CHANNEL_CHOPP].downsample() : 0.f;
if (blockDC) { if (blockDC) {
choppDC.process(out_chopp);
out_chopp -= choppDC.lowpass();
out_chopp = blockDCFilter.process(out_chopp);
} }
previous_a = in_a; previous_a = in_a;


+ 120
- 359
src/VariableOversampling.hpp View File

@@ -2,336 +2,86 @@


#include <rack.hpp> #include <rack.hpp>
#include <dsp/common.hpp> #include <dsp/common.hpp>
#include <type_traits>


/** Digital IIR filter processor. Using TDF-II structure:
* https://ccrma.stanford.edu/~jos/filters/Transposed_Direct_Forms.html
*/
template <int ORDER, typename T = float>
struct IIRFilter {
/** transfer function numerator coefficients: b_0, b_1, etc.*/
T b[ORDER] = {};

/** transfer function denominator coefficients: a_0, a_1, etc.*/
T a[ORDER] = {};


/** filter state */
T z[ORDER];
/**
High-order filter to be used for anti-aliasing or anti-imaging.
The template parameter N should be 1/2 the desired filter order.


IIRFilter() {
reset();
}
Currently uses an 2*N-th order Butterworth filter.
@TODO: implement Chebyshev, Elliptic filter options.
*/
template<int N>
class AAFilter {
public:
AAFilter() = default;


void reset() {
std::fill(z, &z[ORDER], 0.0f);
}
/** Calculate Q values for a Butterworth filter of a given order */
static std::vector<float> calculateButterQs(int order) {
const int lim = int (order / 2);
std::vector<float> Qs;


void setCoefficients(const T* b, const T* a) {
for (int i = 0; i < ORDER; i++) {
this->b[i] = b[i];
for (int k = 1; k <= lim; ++k) {
auto b = -2.0f * std::cos((2.0f * k + order - 1) * 3.14159 / (2.0f * order));
Qs.push_back(1.0f / b);
} }
for (int i = 1; i < ORDER; i++) {
this->a[i] = a[i];
}
}


template <int N = ORDER>
inline typename std::enable_if <N == 2, T>::type
process(T x) noexcept {
T y = z[1] + x * b[0];
z[1] = x * b[1] - y * a[1];
return y;
}

template <int N = ORDER>
inline typename std::enable_if <N == 3, T>::type
process(T x) noexcept {
T y = z[1] + x * b[0];
z[1] = z[2] + x * b[1] - y * a[1];
z[2] = x * b[2] - y * a[2];
return y;
}

template <int N = ORDER>
inline typename std::enable_if <(N > 3), T>::type
process(T x) noexcept {
T y = z[1] + x * b[0];

for (int i = 1; i < ORDER-1; ++i)
z[i] = z[i+1] + x * b[i] - y * a[i];

z[ORDER-1] = x * b[ORDER-1] - y * a[ORDER-1];

return y;
}

/** Computes the complex transfer function $H(s)$ at a particular frequency
s: normalized angular frequency equal to $2 \pi f / f_{sr}$ ($\pi$ is the Nyquist frequency)
*/
std::complex<T> getTransferFunction(T s) {
// Compute sum(a_k z^-k) / sum(b_k z^-k) where z = e^(i s)
std::complex<T> bSum(b[0], 0);
std::complex<T> aSum(1, 0);
for (int i = 1; i < ORDER; i++) {
T p = -i * s;
std::complex<T> z(simd::cos(p), simd::sin(p));
bSum += b[i] * z;
aSum += a[i - 1] * z;
}
return bSum / aSum;
std::reverse(Qs.begin(), Qs.end());
return Qs;
} }


T getFrequencyResponse(T f) {
return simd::abs(getTransferFunction(2 * M_PI * f));
/**
* Resets the filter to process at a new sample rate.
*
* @param sampleRate: The base (i.e. pre-oversampling) sample rate of the audio being processed
* @param osRatio: The oversampling ratio at which the filter is being used
*/
void reset(float sampleRate, int osRatio) {
float fc = 0.98f * (sampleRate / 2.0f);
auto Qs = calculateButterQs(2 * N);

for (int i = 0; i < N; ++i)
filters[i].setParameters(dsp::BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
} }


T getFrequencyPhase(T f) {
return simd::arg(getTransferFunction(2 * M_PI * f));
}
};

template <typename T = float>
struct TOnePole : IIRFilter<2, T> {
enum Type {
LOWPASS,
HIGHPASS,
NUM_TYPES
};

TOnePole() {
setParameters(LOWPASS, 0.0f);
}

/** Calculates and sets the one-pole transfer function coefficients.
f: normalized frequency (cutoff frequency / sample rate), must be less than 0.5
*/
void setParameters(Type type, float f) {
switch (type) {
case LOWPASS: {
this->a[1] = -std::exp(-2.f * M_PI * f);
this->b[0] = 1.f + this->a[0];
} break;

case HIGHPASS: {
this->a[1] = std::exp(-2.f * M_PI * (0.5f - f));
this->b[0] = 1.f - this->a[0];
} break;

default: break;
}
}
};
inline float process(float x) noexcept {
for (int i = 0; i < N; ++i)
x = filters[i].process(x);


typedef TOnePole<> OnePole;

template <typename T = float>
struct TBiquadFilter : IIRFilter<3, T> {
enum Type {
LOWPASS,
HIGHPASS,
LOWSHELF,
HIGHSHELF,
BANDPASS,
PEAK,
NOTCH,
NUM_TYPES
};

TBiquadFilter() {
setParameters(LOWPASS, 0.f, 0.f, 1.f);
return x;
} }


/** Calculates and sets the biquad transfer function coefficients.
f: normalized frequency (cutoff frequency / sample rate), must be less than 0.5
Q: quality factor
V: gain
*/
void setParameters(Type type, float f, float Q, float V) {
float K = std::tan(M_PI * f);
switch (type) {
case LOWPASS: {
float norm = 1.f / (1.f + K / Q + K * K);
this->b[0] = K * K * norm;
this->b[1] = 2.f * this->b[0];
this->b[2] = this->b[0];
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - K / Q + K * K) * norm;
} break;

case HIGHPASS: {
float norm = 1.f / (1.f + K / Q + K * K);
this->b[0] = norm;
this->b[1] = -2.f * this->b[0];
this->b[2] = this->b[0];
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - K / Q + K * K) * norm;

} break;

case LOWSHELF: {
float sqrtV = std::sqrt(V);
if (V >= 1.f) {
float norm = 1.f / (1.f + M_SQRT2 * K + K * K);
this->b[0] = (1.f + M_SQRT2 * sqrtV * K + V * K * K) * norm;
this->b[1] = 2.f * (V * K * K - 1.f) * norm;
this->b[2] = (1.f - M_SQRT2 * sqrtV * K + V * K * K) * norm;
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - M_SQRT2 * K + K * K) * norm;
}
else {
float norm = 1.f / (1.f + M_SQRT2 / sqrtV * K + K * K / V);
this->b[0] = (1.f + M_SQRT2 * K + K * K) * norm;
this->b[1] = 2.f * (K * K - 1) * norm;
this->b[2] = (1.f - M_SQRT2 * K + K * K) * norm;
this->a[1] = 2.f * (K * K / V - 1.f) * norm;
this->a[2] = (1.f - M_SQRT2 / sqrtV * K + K * K / V) * norm;
}
} break;

case HIGHSHELF: {
float sqrtV = std::sqrt(V);
if (V >= 1.f) {
float norm = 1.f / (1.f + M_SQRT2 * K + K * K);
this->b[0] = (V + M_SQRT2 * sqrtV * K + K * K) * norm;
this->b[1] = 2.f * (K * K - V) * norm;
this->b[2] = (V - M_SQRT2 * sqrtV * K + K * K) * norm;
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - M_SQRT2 * K + K * K) * norm;
}
else {
float norm = 1.f / (1.f / V + M_SQRT2 / sqrtV * K + K * K);
this->b[0] = (1.f + M_SQRT2 * K + K * K) * norm;
this->b[1] = 2.f * (K * K - 1.f) * norm;
this->b[2] = (1.f - M_SQRT2 * K + K * K) * norm;
this->a[1] = 2.f * (K * K - 1.f / V) * norm;
this->a[2] = (1.f / V - M_SQRT2 / sqrtV * K + K * K) * norm;
}
} break;

case BANDPASS: {
float norm = 1.f / (1.f + K / Q + K * K);
this->b[0] = K / Q * norm;
this->b[1] = 0.f;
this->b[2] = -this->b[0];
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - K / Q + K * K) * norm;
} break;

case PEAK: {
float c = 1.0f / K;
float phi = c * c;
float Knum = c / Q;
float Kdenom = Knum;

if(V > 1.0f)
Knum *= V;
else
Kdenom /= V;

float norm = phi + Kdenom + 1.0;
this->b[0] = (phi + Knum + 1.0f) / norm;
this->b[1] = 2.0f * (1.0f - phi) / norm;
this->b[2] = (phi - Knum + 1.0f) / norm;
this->a[1] = 2.0f * (1.0f - phi) / norm;
this->a[2] = (phi - Kdenom + 1.0f) / norm;
} break;

case NOTCH: {
float norm = 1.f / (1.f + K / Q + K * K);
this->b[0] = (1.f + K * K) * norm;
this->b[1] = 2.f * (K * K - 1.f) * norm;
this->b[2] = this->b[0];
this->a[1] = this->b[1];
this->a[2] = (1.f - K / Q + K * K) * norm;
} break;

default: break;
}
}
};

typedef TBiquadFilter<> BiquadFilter;


/**
High-order filter to be used for anti-aliasing or anti-imaging.
The template parameter N should be 1/2 the desired filter order.

Currently uses an 2*N-th order Butterworth filter.
@TODO: implement Chebyshev, Elliptic filter options.
*/
template<int N>
class AAFilter
{
public:
AAFilter() = default;

/** Calculate Q values for a Butterworth filter of a given order */
static std::vector<float> calculateButterQs(int order) {
const int lim = int (order / 2);
std::vector<float> Qs;

for(int k = 1; k <= lim; ++k) {
auto b = -2.0f * std::cos((2.0f * k + order - 1) * 3.14159 / (2.0f * order));
Qs.push_back(1.0f / b);
}

std::reverse(Qs.begin(), Qs.end());
return Qs;
}

/**
* Resets the filter to process at a new sample rate.
*
* @param sampleRate: The base (i.e. pre-oversampling) sample rate of the audio being processed
* @param osRatio: The oversampling ratio at which the filter is being used
*/
void reset(float sampleRate, int osRatio) {
float fc = 0.98f * (sampleRate / 2.0f);
auto Qs = calculateButterQs(2*N);

for(int i = 0; i < N; ++i)
filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f);
}
inline float process(float x) noexcept {
for(int i = 0; i < N; ++i)
x = filters[i].process(x);
return x;
}

private: private:
BiquadFilter filters[N];
dsp::BiquadFilter filters[N];
}; };






/** /**
* Base class for oversampling of any order * Base class for oversampling of any order
*/
*/
class BaseOversampling { class BaseOversampling {
public: public:
BaseOversampling() = default;
virtual ~BaseOversampling() {}
BaseOversampling() = default;
virtual ~BaseOversampling() {}


/** Resets the oversampler for processing at some base sample rate */
virtual void reset(float /*baseSampleRate*/) = 0;
/** Resets the oversampler for processing at some base sample rate */
virtual void reset(float /*baseSampleRate*/) = 0;


/** Upsample a single input sample and update the oversampled buffer */
virtual void upsample(float) noexcept = 0;
/** Upsample a single input sample and update the oversampled buffer */
virtual void upsample(float) noexcept = 0;


/** Output a downsampled output sample from the current oversampled buffer */
virtual float downsample() noexcept = 0;
/** Output a downsampled output sample from the current oversampled buffer */
virtual float downsample() noexcept = 0;


/** Returns a pointer to the oversampled buffer */
virtual float* getOSBuffer() noexcept = 0;
/** Returns a pointer to the oversampled buffer */
virtual float* getOSBuffer() noexcept = 0;
}; };


/**
/**
Class to implement an oversampled process. Class to implement an oversampled process.
To use, create an object and prepare using `reset()`. To use, create an object and prepare using `reset()`.

Then use the following code to process samples: Then use the following code to process samples:
@code @code
oversample.upsample(x); oversample.upsample(x);
@@ -341,51 +91,50 @@ public:
@endcode @endcode
*/ */
template<int ratio, int filtN = 4> template<int ratio, int filtN = 4>
class Oversampling : public BaseOversampling
{
class Oversampling : public BaseOversampling {
public: public:
Oversampling() = default;
virtual ~Oversampling() {}
Oversampling() = default;
virtual ~Oversampling() {}


void reset(float baseSampleRate) override {
aaFilter.reset(baseSampleRate, ratio);
aiFilter.reset(baseSampleRate, ratio);
std::fill(osBuffer, &osBuffer[ratio], 0.0f);
}
void reset(float baseSampleRate) override {
aaFilter.reset(baseSampleRate, ratio);
aiFilter.reset(baseSampleRate, ratio);
std::fill(osBuffer, &osBuffer[ratio], 0.0f);
}


inline void upsample(float x) noexcept override {
osBuffer[0] = ratio * x;
std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f);
inline void upsample(float x) noexcept override {
osBuffer[0] = ratio * x;
std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f);


for(int k = 0; k < ratio; k++)
osBuffer[k] = aiFilter.process(osBuffer[k]);
}
for (int k = 0; k < ratio; k++)
osBuffer[k] = aiFilter.process(osBuffer[k]);
}


inline float downsample() noexcept override {
float y = 0.0f;
for(int k = 0; k < ratio; k++)
y = aaFilter.process(osBuffer[k]);
inline float downsample() noexcept override {
float y = 0.0f;
for (int k = 0; k < ratio; k++)
y = aaFilter.process(osBuffer[k]);


return y;
}
return y;
}


inline float* getOSBuffer() noexcept override {
return osBuffer;
}
inline float* getOSBuffer() noexcept override {
return osBuffer;
}


float osBuffer[ratio];
float osBuffer[ratio];


private: private:
AAFilter<filtN> aaFilter; // anti-aliasing filter
AAFilter<filtN> aiFilter; // anti-imaging filter
AAFilter<filtN> aaFilter; // anti-aliasing filter
AAFilter<filtN> aiFilter; // anti-imaging filter
}; };




/**
/**
Class to implement an oversampled process, with variable Class to implement an oversampled process, with variable
oversampling factor. To use, create an object, set the oversampling oversampling factor. To use, create an object, set the oversampling
factor using `setOversamplingindex()` and prepare using `reset()`. factor using `setOversamplingindex()` and prepare using `reset()`.
Then use the following code to process samples: Then use the following code to process samples:
@code @code
oversample.upsample(x); oversample.upsample(x);
@@ -398,45 +147,57 @@ private:
template<int filtN = 4> template<int filtN = 4>
class VariableOversampling { class VariableOversampling {
public: public:
VariableOversampling() = default;
VariableOversampling() = default;


/** Prepare the oversampler to process audio at a given sample rate */
void reset(float sampleRate) {
for(auto* os : oss)
os->reset(sampleRate);
}
/** Prepare the oversampler to process audio at a given sample rate */
void reset(float sampleRate) {
for (auto* os : oss)
os->reset(sampleRate);
}


/** Sets the oversampling factor as 2^idx */
void setOversamplingIndex (int newIdx) { osIdx = newIdx; }
/** Sets the oversampling factor as 2^idx */
void setOversamplingIndex(int newIdx) {
osIdx = newIdx;
}

/** Returns the oversampling index */
int getOversamplingIndex() const noexcept {
return osIdx;
}


/** Returns the oversampling index */
int getOversamplingIndex() const noexcept { return osIdx; }
/** Upsample a single input sample and update the oversampled buffer */
inline void upsample(float x) noexcept {
oss[osIdx]->upsample(x);
}


/** Upsample a single input sample and update the oversampled buffer */
inline void upsample(float x) noexcept { oss[osIdx]->upsample(x); }
/** Output a downsampled output sample from the current oversampled buffer */
inline float downsample() noexcept {
return oss[osIdx]->downsample();
}


/** Output a downsampled output sample from the current oversampled buffer */
inline float downsample() noexcept { return oss[osIdx]->downsample(); }
/** Returns a pointer to the oversampled buffer */
inline float* getOSBuffer() noexcept {
return oss[osIdx]->getOSBuffer();
}


/** Returns a pointer to the oversampled buffer */
inline float* getOSBuffer() noexcept { return oss[osIdx]->getOSBuffer(); }
/** Returns the current oversampling factor */
int getOversamplingRatio() const noexcept {
return 1 << osIdx;
}


/** Returns the current oversampling factor */
int getOversamplingRatio() const noexcept { return 1 << osIdx; }


private: private:
enum {
NumOS = 5, // number of oversampling options
};
int osIdx = 0;
Oversampling<1 << 0, filtN> os0; // 1x
Oversampling<1 << 1, filtN> os1; // 2x
Oversampling<1 << 2, filtN> os2; // 4x
Oversampling<1 << 3, filtN> os3; // 8x
Oversampling<1 << 4, filtN> os4; // 16x
BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
enum {
NumOS = 5, // number of oversampling options
};
int osIdx = 0;
Oversampling < 1 << 0, filtN > os0; // 1x
Oversampling < 1 << 1, filtN > os1; // 2x
Oversampling < 1 << 2, filtN > os2; // 4x
Oversampling < 1 << 3, filtN > os3; // 8x
Oversampling < 1 << 4, filtN > os4; // 16x
BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };


}; };

Loading…
Cancel
Save