- only downsample a source wave generated at higher sample rates - switch to Pade approximation of sin - new panel SVG from Befaco - fix LED logic Moved all chowdsp library code into specific header. Updated .jsontags/v1.1.0^2
| @@ -1,6 +1,6 @@ | |||||
| { | { | ||||
| "slug": "Befaco", | "slug": "Befaco", | ||||
| "version": "1.0.2", | |||||
| "version": "1.1.0", | |||||
| "license": "GPL-3.0-or-later", | "license": "GPL-3.0-or-later", | ||||
| "name": "Befaco", | "name": "Befaco", | ||||
| "author": "VCV", | "author": "VCV", | ||||
| @@ -97,6 +97,7 @@ | |||||
| "tags": [ | "tags": [ | ||||
| "Envelope generator", | "Envelope generator", | ||||
| "Mixer", | "Mixer", | ||||
| "Poly", | |||||
| "Hardware clone" | "Hardware clone" | ||||
| ] | ] | ||||
| }, | }, | ||||
| @@ -108,20 +109,30 @@ | |||||
| "tags": [ | "tags": [ | ||||
| "Mixer", | "Mixer", | ||||
| "Hardware clone", | "Hardware clone", | ||||
| "Poly", | |||||
| "VCA" | "VCA" | ||||
| ] | ] | ||||
| }, | }, | ||||
| { | { | ||||
| "slug": "ChoppingKinky", | "slug": "ChoppingKinky", | ||||
| "name": "ChoppingKinky", | "name": "ChoppingKinky", | ||||
| "description": "", | |||||
| "tags": [] | |||||
| "description": "Voltage controllable, dual channel wavefolder", | |||||
| "tags": [ | |||||
| "Dual", | |||||
| "Hardware clone", | |||||
| "Voltage-controlled amplifier", | |||||
| "Waveshaper" | |||||
| ] | |||||
| }, | }, | ||||
| { | { | ||||
| "slug": "Kickall", | "slug": "Kickall", | ||||
| "name": "Kickall", | "name": "Kickall", | ||||
| "description": "", | "description": "", | ||||
| "tags": [] | |||||
| "tags": [ | |||||
| "Drum", | |||||
| "Hardware clone", | |||||
| "Synth voice" | |||||
| ] | |||||
| } | } | ||||
| ] | ] | ||||
| } | } | ||||
| @@ -1,21 +1,10 @@ | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "Common.hpp" | #include "Common.hpp" | ||||
| #include "ChowDSP.hpp" | |||||
| static const size_t BUF_LEN = 32; | static const size_t BUF_LEN = 32; | ||||
| template <typename T> | |||||
| T sin2pi_pade_05_5_4(T x) { | |||||
| x -= 0.5f; | |||||
| return (T(-6.283185307) * x + T(33.19863968) * simd::pow(x, 3) - T(32.44191367) * simd::pow(x, 5)) | |||||
| / (1 + T(1.296008659) * simd::pow(x, 2) + T(0.7028072946) * simd::pow(x, 4)); | |||||
| } | |||||
| template <typename T> | |||||
| T tanh_pade(T x) { | |||||
| T x2 = x * x; | |||||
| T q = 12.f + x2; | |||||
| return 12.f * x * q / (36.f * x2 + q * q); | |||||
| } | |||||
| static float foldResponse(float inputGain, float G) { | static float foldResponse(float inputGain, float G) { | ||||
| return std::tanh(inputGain) + G * std::sin(M_PI * inputGain); | return std::tanh(inputGain) + G * std::sin(M_PI * inputGain); | ||||
| @@ -69,7 +58,7 @@ struct ChoppingKinky : Module { | |||||
| bool outputAToChopp; | bool outputAToChopp; | ||||
| float previousA = 0.0; | float previousA = 0.0; | ||||
| VariableOversampling<> oversampler[NUM_CHANNELS]; | |||||
| chowdsp::VariableOversampling<> oversampler[NUM_CHANNELS]; | |||||
| int oversamplingIndex = 2; | int oversamplingIndex = 2; | ||||
| dsp::BiquadFilter blockDCFilter; | dsp::BiquadFilter blockDCFilter; | ||||
| @@ -0,0 +1,420 @@ | |||||
| #pragma once | |||||
| namespace chowdsp { | |||||
| // code taken from https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/, commit 21701fb | |||||
| // * AAFilter.hpp | |||||
| // * VariableOversampling.hpp | |||||
| // * oversampling.hpp | |||||
| // * iir.hpp | |||||
| 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]; | |||||
| IIRFilter() { | |||||
| reset(); | |||||
| } | |||||
| void reset() { | |||||
| std::fill(z, &z[ORDER], 0.0f); | |||||
| } | |||||
| void setCoefficients(const T* b, const T* a) { | |||||
| for (int i = 0; i < ORDER; i++) { | |||||
| this->b[i] = b[i]; | |||||
| } | |||||
| 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; | |||||
| } | |||||
| T getFrequencyResponse(T f) { | |||||
| return simd::abs(getTransferFunction(2 * M_PI * f)); | |||||
| } | |||||
| T getFrequencyPhase(T f) { | |||||
| return simd::arg(getTransferFunction(2 * M_PI * f)); | |||||
| } | |||||
| }; | |||||
| 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); | |||||
| } | |||||
| /** 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. | |||||
| source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp | |||||
| */ | |||||
| 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: | |||||
| BiquadFilter filters[N]; | |||||
| }; | |||||
| /** | |||||
| * Base class for oversampling of any order | |||||
| * source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp | |||||
| */ | |||||
| class BaseOversampling { | |||||
| public: | |||||
| BaseOversampling() = default; | |||||
| virtual ~BaseOversampling() {} | |||||
| /** 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; | |||||
| /** 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; | |||||
| }; | |||||
| /** | |||||
| Class to implement an oversampled process. | |||||
| To use, create an object and prepare using `reset()`. | |||||
| Then use the following code to process samples: | |||||
| @code | |||||
| oversample.upsample(x); | |||||
| for(int k = 0; k < ratio; k++) | |||||
| oversample.osBuffer[k] = processSample(oversample.osBuffer[k]); | |||||
| float y = oversample.downsample(); | |||||
| @endcode | |||||
| */ | |||||
| template<int ratio, int filtN = 4> | |||||
| class Oversampling : public BaseOversampling { | |||||
| public: | |||||
| Oversampling() = default; | |||||
| virtual ~Oversampling() {} | |||||
| 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); | |||||
| 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]); | |||||
| return y; | |||||
| } | |||||
| inline float* getOSBuffer() noexcept override { | |||||
| return osBuffer; | |||||
| } | |||||
| float osBuffer[ratio]; | |||||
| private: | |||||
| AAFilter<filtN> aaFilter; // anti-aliasing filter | |||||
| AAFilter<filtN> aiFilter; // anti-imaging filter | |||||
| }; | |||||
| /** | |||||
| Class to implement an oversampled process, with variable | |||||
| oversampling factor. To use, create an object, set the oversampling | |||||
| factor using `setOversamplingindex()` and prepare using `reset()`. | |||||
| Then use the following code to process samples: | |||||
| @code | |||||
| oversample.upsample(x); | |||||
| float* osBuffer = oversample.getOSBuffer(); | |||||
| for(int k = 0; k < ratio; k++) | |||||
| osBuffer[k] = processSample(osBuffer[k]); | |||||
| float y = oversample.downsample(); | |||||
| @endcode | |||||
| source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp | |||||
| */ | |||||
| template<int filtN = 4> | |||||
| class VariableOversampling { | |||||
| public: | |||||
| VariableOversampling() = default; | |||||
| /** 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; | |||||
| } | |||||
| /** 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); | |||||
| } | |||||
| /** 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 the current oversampling factor */ | |||||
| int getOversamplingRatio() const noexcept { | |||||
| return 1 << osIdx; | |||||
| } | |||||
| 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 }; | |||||
| }; | |||||
| } | |||||
| @@ -45,203 +45,16 @@ struct BefacoInputPort : app::SvgPort { | |||||
| } | } | ||||
| }; | }; | ||||
| /** | |||||
| 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. | |||||
| source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp | |||||
| */ | |||||
| 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(dsp::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: | |||||
| dsp::BiquadFilter filters[N]; | |||||
| }; | |||||
| /** | |||||
| * Base class for oversampling of any order | |||||
| * source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp | |||||
| */ | |||||
| class BaseOversampling { | |||||
| public: | |||||
| BaseOversampling() = default; | |||||
| virtual ~BaseOversampling() {} | |||||
| /** 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; | |||||
| /** 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; | |||||
| }; | |||||
| /** | |||||
| Class to implement an oversampled process. | |||||
| To use, create an object and prepare using `reset()`. | |||||
| Then use the following code to process samples: | |||||
| @code | |||||
| oversample.upsample(x); | |||||
| for(int k = 0; k < ratio; k++) | |||||
| oversample.osBuffer[k] = processSample(oversample.osBuffer[k]); | |||||
| float y = oversample.downsample(); | |||||
| @endcode | |||||
| */ | |||||
| template<int ratio, int filtN = 4> | |||||
| class Oversampling : public BaseOversampling { | |||||
| public: | |||||
| Oversampling() = default; | |||||
| virtual ~Oversampling() {} | |||||
| 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); | |||||
| 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]); | |||||
| return y; | |||||
| } | |||||
| inline float* getOSBuffer() noexcept override { | |||||
| return osBuffer; | |||||
| } | |||||
| float osBuffer[ratio]; | |||||
| private: | |||||
| AAFilter<filtN> aaFilter; // anti-aliasing filter | |||||
| AAFilter<filtN> aiFilter; // anti-imaging filter | |||||
| }; | |||||
| /** | |||||
| Class to implement an oversampled process, with variable | |||||
| oversampling factor. To use, create an object, set the oversampling | |||||
| factor using `setOversamplingindex()` and prepare using `reset()`. | |||||
| Then use the following code to process samples: | |||||
| @code | |||||
| oversample.upsample(x); | |||||
| float* osBuffer = oversample.getOSBuffer(); | |||||
| for(int k = 0; k < ratio; k++) | |||||
| osBuffer[k] = processSample(osBuffer[k]); | |||||
| float y = oversample.downsample(); | |||||
| @endcode | |||||
| source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp | |||||
| */ | |||||
| template<int filtN = 4> | |||||
| class VariableOversampling { | |||||
| public: | |||||
| VariableOversampling() = default; | |||||
| /** 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; | |||||
| } | |||||
| /** 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); | |||||
| } | |||||
| /** 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 the current oversampling factor */ | |||||
| int getOversamplingRatio() const noexcept { | |||||
| return 1 << osIdx; | |||||
| } | |||||
| 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 }; | |||||
| }; | |||||
| template <typename T> | |||||
| T sin2pi_pade_05_5_4(T x) { | |||||
| x -= 0.5f; | |||||
| return (T(-6.283185307) * x + T(33.19863968) * simd::pow(x, 3) - T(32.44191367) * simd::pow(x, 5)) | |||||
| / (1 + T(1.296008659) * simd::pow(x, 2) + T(0.7028072946) * simd::pow(x, 4)); | |||||
| } | |||||
| template <typename T> | |||||
| T tanh_pade(T x) { | |||||
| T x2 = x * x; | |||||
| T q = 12.f + x2; | |||||
| return 12.f * x * q / (36.f * x2 + q * q); | |||||
| } | |||||
| @@ -1,5 +1,6 @@ | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "Common.hpp" | #include "Common.hpp" | ||||
| #include "ChowDSP.hpp" | |||||
| struct ADEnvelope { | struct ADEnvelope { | ||||
| @@ -13,6 +14,7 @@ struct ADEnvelope { | |||||
| float env = 0.f; | float env = 0.f; | ||||
| float attackTime = 0.1, decayTime = 0.1; | float attackTime = 0.1, decayTime = 0.1; | ||||
| // TODO: add shape, and generlise to use with Percall | |||||
| ADEnvelope() { }; | ADEnvelope() { }; | ||||
| void process(const float& sampleTime) { | void process(const float& sampleTime) { | ||||
| @@ -98,8 +100,8 @@ struct Kickall : Module { | |||||
| static constexpr float FREQ_A0 = 27.5f; | static constexpr float FREQ_A0 = 27.5f; | ||||
| static constexpr float FREQ_B2 = 123.471f; | static constexpr float FREQ_B2 = 123.471f; | ||||
| static const int UPSAMPLE = 4; | |||||
| Oversampling<UPSAMPLE> oversampler; | |||||
| static const int UPSAMPLE = 8; | |||||
| chowdsp::Oversampling<UPSAMPLE> oversampler; | |||||
| float shaperBuf[UPSAMPLE]; | float shaperBuf[UPSAMPLE]; | ||||
| void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
| @@ -124,19 +126,8 @@ struct Kickall : Module { | |||||
| float freq = params[TUNE_PARAM].getValue(); | float freq = params[TUNE_PARAM].getValue(); | ||||
| freq *= std::pow(2, inputs[TUNE_INPUT].getVoltage()); | freq *= std::pow(2, inputs[TUNE_INPUT].getVoltage()); | ||||
| //float kickFrequency = std::max(50.0f, params[TUNE_PARAM].getValue() + bendRange * bend * pitch.env); | |||||
| float kickFrequency = std::max(50.0f, freq + bendRange * bend * pitch.env); | |||||
| phase += args.sampleTime * kickFrequency; | |||||
| if (phase > 1.0f) { | |||||
| phase -= 1.0f; | |||||
| } | |||||
| // TODO: faster approximation | |||||
| float wave = std::sin(2.0 * M_PI * phase); | |||||
| oversampler.upsample(wave); | |||||
| float* inputBuf = oversampler.getOSBuffer(); | |||||
| const float kickFrequency = std::max(10.0f, freq + bendRange * bend * pitch.env); | |||||
| const float phaseInc = args.sampleTime * kickFrequency / UPSAMPLE; | |||||
| float shape = clamp(inputs[SHAPE_INPUT].getVoltage() / 2.f + params[SHAPE_PARAM].getValue() * 5.0f, 0.0f, 10.0f); | float shape = clamp(inputs[SHAPE_INPUT].getVoltage() / 2.f + params[SHAPE_PARAM].getValue() * 5.0f, 0.0f, 10.0f); | ||||
| shape = clamp(shape, 0.f, 5.0f) * 0.2f; | shape = clamp(shape, 0.f, 5.0f) * 0.2f; | ||||
| @@ -144,14 +135,19 @@ struct Kickall : Module { | |||||
| const float shapeB = (1.0f - shape) / (1.0f + shape); | const float shapeB = (1.0f - shape) / (1.0f + shape); | ||||
| const float shapeA = (4.0f * shape) / ((1.0f - shape) * (1.0f + shape)); | const float shapeA = (4.0f * shape) / ((1.0f - shape) * (1.0f + shape)); | ||||
| float* inputBuf = oversampler.getOSBuffer(); | |||||
| for (int i = 0; i < UPSAMPLE; ++i) { | for (int i = 0; i < UPSAMPLE; ++i) { | ||||
| phase += phaseInc; | |||||
| phase -= std::floor(phase); | |||||
| inputBuf[i] = sin2pi_pade_05_5_4(phase); | |||||
| inputBuf[i] = inputBuf[i] * (shapeA + shapeB) / ((std::abs(inputBuf[i]) * shapeA) + shapeB); | inputBuf[i] = inputBuf[i] * (shapeA + shapeB) / ((std::abs(inputBuf[i]) * shapeA) + shapeB); | ||||
| } | } | ||||
| float out = volume.env * oversampler.downsample() * 5.0f * vcaGain; | float out = volume.env * oversampler.downsample() * 5.0f * vcaGain; | ||||
| outputs[OUT_OUTPUT].setVoltage(out); | outputs[OUT_OUTPUT].setVoltage(out); | ||||
| lights[ENV_LIGHT].setBrightness(volume.stage); | |||||
| lights[ENV_LIGHT].setBrightness(volume.env); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -164,22 +160,22 @@ struct KickallWidget : ModuleWidget { | |||||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | ||||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | ||||
| addParam(createParamCentered<BefacoTinyKnobGrey>(mm2px(Vec(8.76, 29.068)), module, Kickall::TUNE_PARAM)); | |||||
| addParam(createParamCentered<BefacoPush>(mm2px(Vec(22.651, 29.068)), module, Kickall::TRIGG_BUTTON_PARAM)); | |||||
| addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(15.781, 49.278)), module, Kickall::SHAPE_PARAM)); | |||||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(19.913, 64.004)), module, Kickall::DECAY_PARAM)); | |||||
| addParam(createParamCentered<BefacoTinyKnobWhite>(mm2px(Vec(8.977, 71.626)), module, Kickall::TIME_PARAM)); | |||||
| addParam(createParamCentered<BefacoTinyKnobWhite>(mm2px(Vec(8.977, 93.549)), module, Kickall::BEND_PARAM)); | |||||
| addParam(createParamCentered<BefacoTinyKnobGrey>(mm2px(Vec(8.472, 28.97)), module, Kickall::TUNE_PARAM)); | |||||
| addParam(createParamCentered<BefacoPush>(mm2px(Vec(22.409, 29.159)), module, Kickall::TRIGG_BUTTON_PARAM)); | |||||
| addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(15.526, 49.292)), module, Kickall::SHAPE_PARAM)); | |||||
| addParam(createParam<BefacoSlidePot>(mm2px(Vec(19.667, 63.897)), module, Kickall::DECAY_PARAM)); | |||||
| addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(8.521, 71.803)), module, Kickall::TIME_PARAM)); | |||||
| addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(8.521, 93.517)), module, Kickall::BEND_PARAM)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.715, 14.651)), module, Kickall::TRIGG_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.748, 14.651)), module, Kickall::VOLUME_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.697, 113.286)), module, Kickall::TUNE_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.794, 113.286)), module, Kickall::SHAPE_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.891, 113.286)), module, Kickall::DECAY_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.501, 14.508)), module, Kickall::VOLUME_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.499, 14.536)), module, Kickall::TRIGG_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.525, 113.191)), module, Kickall::DECAY_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.499, 113.208)), module, Kickall::TUNE_INPUT)); | |||||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.485, 113.208)), module, Kickall::SHAPE_INPUT)); | |||||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.781, 14.651)), module, Kickall::OUT_OUTPUT)); | |||||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.525, 14.52)), module, Kickall::OUT_OUTPUT)); | |||||
| addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(15.723, 34.971)), module, Kickall::ENV_LIGHT)); | |||||
| addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(15.535, 34.943)), module, Kickall::ENV_LIGHT)); | |||||
| } | } | ||||
| }; | }; | ||||