From 5a80534a2eebc68b84bba62dae668fc906308d92 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 8 Sep 2026 14:55:22 +0200 Subject: [PATCH] feat(fx): add effect rack and effect units --- Source/EffectUnits/Biquad.h | 107 ++++++++++++++++++++++++ Source/EffectUnits/Chorus.cpp | 63 ++++++++++++++ Source/EffectUnits/Chorus.h | 26 ++++++ Source/EffectUnits/Compressor.cpp | 52 ++++++++++++ Source/EffectUnits/Compressor.h | 25 ++++++ Source/EffectUnits/Delay.cpp | 62 ++++++++++++++ Source/EffectUnits/Delay.h | 26 ++++++ Source/EffectUnits/Distortion.cpp | 44 ++++++++++ Source/EffectUnits/Distortion.h | 27 ++++++ Source/EffectUnits/EQ.cpp | 39 +++++++++ Source/EffectUnits/EQ.h | 25 ++++++ Source/EffectUnits/Flanger.cpp | 59 +++++++++++++ Source/EffectUnits/Flanger.h | 26 ++++++ Source/EffectUnits/Hyper.cpp | 80 ++++++++++++++++++ Source/EffectUnits/Hyper.h | 34 ++++++++ Source/EffectUnits/Phaser.cpp | 54 ++++++++++++ Source/EffectUnits/Phaser.h | 25 ++++++ Source/EffectUnits/Reverb.cpp | 132 ++++++++++++++++++++++++++++++ Source/EffectUnits/Reverb.h | 33 ++++++++ Source/FXProcessor.cpp | 111 +++++++++++++++++++++++++ Source/FXProcessor.h | 54 ++++++++++++ 21 files changed, 1104 insertions(+) create mode 100644 Source/EffectUnits/Biquad.h create mode 100644 Source/EffectUnits/Chorus.cpp create mode 100644 Source/EffectUnits/Chorus.h create mode 100644 Source/EffectUnits/Compressor.cpp create mode 100644 Source/EffectUnits/Compressor.h create mode 100644 Source/EffectUnits/Delay.cpp create mode 100644 Source/EffectUnits/Delay.h create mode 100644 Source/EffectUnits/Distortion.cpp create mode 100644 Source/EffectUnits/Distortion.h create mode 100644 Source/EffectUnits/EQ.cpp create mode 100644 Source/EffectUnits/EQ.h create mode 100644 Source/EffectUnits/Flanger.cpp create mode 100644 Source/EffectUnits/Flanger.h create mode 100644 Source/EffectUnits/Hyper.cpp create mode 100644 Source/EffectUnits/Hyper.h create mode 100644 Source/EffectUnits/Phaser.cpp create mode 100644 Source/EffectUnits/Phaser.h create mode 100644 Source/EffectUnits/Reverb.cpp create mode 100644 Source/EffectUnits/Reverb.h create mode 100644 Source/FXProcessor.cpp create mode 100644 Source/FXProcessor.h diff --git a/Source/EffectUnits/Biquad.h b/Source/EffectUnits/Biquad.h new file mode 100644 index 0000000..ddeec83 --- /dev/null +++ b/Source/EffectUnits/Biquad.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +namespace serum +{ + +// =========================================================================== +// Small RBJ-cookbook biquad (direct form 1) used by EQ and the Hyper crossover. +// =========================================================================== +class Biquad +{ +public: + void setSampleRate (double sr) noexcept { sampleRate = sr; } + + void clear() noexcept + { + x1 = x2 = y1 = y2 = 0.0; + } + + float process (float in) noexcept + { + const double out = b0 * in + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + x2 = x1; x1 = in; + y2 = y1; y1 = out; + return (float) out; + } + + void setLowpass (double freq, double q) + { + const double w0 = 2.0 * juce::MathConstants::pi * freq / sampleRate; + const double cw = std::cos (w0); + const double sw = std::sin (w0); + const double alpha = sw / (2.0 * q); + const double a0 = 1.0 + alpha; + b0 = ((1.0 - cw) / 2.0) / a0; + b1 = (1.0 - cw) / a0; + b2 = b0; + a1 = (-2.0 * cw) / a0; + a2 = (1.0 - alpha) / a0; + } + + void setHighpass (double freq, double q) + { + const double w0 = 2.0 * juce::MathConstants::pi * freq / sampleRate; + const double cw = std::cos (w0); + const double sw = std::sin (w0); + const double alpha = sw / (2.0 * q); + const double a0 = 1.0 + alpha; + b0 = ((1.0 + cw) / 2.0) / a0; + b1 = -(1.0 + cw) / a0; + b2 = b0; + a1 = (-2.0 * cw) / a0; + a2 = (1.0 - alpha) / a0; + } + + void setLowShelf (double freq, double gainDb) + { + const double a = std::pow (10.0, gainDb / 40.0); + const double w0 = 2.0 * juce::MathConstants::pi * freq / sampleRate; + const double cw = std::cos (w0); + const double sw = std::sin (w0); + const double alpha = sw / 2.0 * std::sqrt (2.0); + const double a0 = (a + 1.0) + (a - 1.0) * cw + 2.0 * std::sqrt (a) * alpha; + b0 = (a * ((a + 1.0) - (a - 1.0) * cw + 2.0 * std::sqrt (a) * alpha)) / a0; + b1 = (2.0 * a * ((a - 1.0) - (a + 1.0) * cw)) / a0; + b2 = (a * ((a + 1.0) - (a - 1.0) * cw - 2.0 * std::sqrt (a) * alpha)) / a0; + a1 = (-2.0 * ((a - 1.0) + (a + 1.0) * cw)) / a0; + a2 = ((a + 1.0) + (a - 1.0) * cw - 2.0 * std::sqrt (a) * alpha) / a0; + } + + void setHighShelf (double freq, double gainDb) + { + const double a = std::pow (10.0, gainDb / 40.0); + const double w0 = 2.0 * juce::MathConstants::pi * freq / sampleRate; + const double cw = std::cos (w0); + const double sw = std::sin (w0); + const double alpha = sw / 2.0 * std::sqrt (2.0); + const double a0 = (a + 1.0) - (a - 1.0) * cw + 2.0 * std::sqrt (a) * alpha; + b0 = (a * ((a + 1.0) + (a - 1.0) * cw + 2.0 * std::sqrt (a) * alpha)) / a0; + b1 = (-2.0 * a * ((a - 1.0) + (a + 1.0) * cw)) / a0; + b2 = (a * ((a + 1.0) + (a - 1.0) * cw - 2.0 * std::sqrt (a) * alpha)) / a0; + a1 = (2.0 * ((a - 1.0) - (a + 1.0) * cw)) / a0; + a2 = ((a + 1.0) - (a - 1.0) * cw - 2.0 * std::sqrt (a) * alpha) / a0; + } + + void setPeak (double freq, double gainDb, double q) + { + const double a = std::pow (10.0, gainDb / 40.0); + const double w0 = 2.0 * juce::MathConstants::pi * freq / sampleRate; + const double cw = std::cos (w0); + const double sw = std::sin (w0); + const double alpha = sw / (2.0 * q); + const double a0 = 1.0 + alpha / a; + b0 = (1.0 + alpha * a) / a0; + b1 = (-2.0 * cw) / a0; + b2 = (1.0 - alpha * a) / a0; + a1 = (-2.0 * cw) / a0; + a2 = (1.0 - alpha / a) / a0; + } + + double sampleRate = 44100.0; + double b0 = 1.0, b1 = 0.0, b2 = 0.0, a1 = 0.0, a2 = 0.0; + double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Chorus.cpp b/Source/EffectUnits/Chorus.cpp new file mode 100644 index 0000000..3f6553f --- /dev/null +++ b/Source/EffectUnits/Chorus.cpp @@ -0,0 +1,63 @@ +#include "Chorus.h" + +namespace serum +{ + +void ChorusUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + const int maxDelay = (int) (sr * 0.05); // 50ms + delayL.assign ((size_t) maxDelay, 0.0f); + delayR.assign ((size_t) maxDelay, 0.0f); + reset(); +} + +void ChorusUnit::reset() +{ + std::fill (delayL.begin(), delayL.end(), 0.0f); + std::fill (delayR.begin(), delayR.end(), 0.0f); + writePos = 0; + lfoPhase = 0.0; +} + +void ChorusUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const int len = (int) delayL.size(); + if (len < 4) + return; + + const double rate = 0.1 + p[0] * 4.9; // 0.1..5 Hz + const double depth = (0.5 + p[1] * 5.5) * sr / 1000.0; // 0.5..6 ms + const double base = 8.0 * sr / 1000.0; // 8 ms centre + const float width = p[2]; + + for (int i = 0; i < numSamples; ++i) + { + const float inL = l[i], inR = r[i]; + delayL[(size_t) writePos] = inL; + delayR[(size_t) writePos] = inR; + + lfoPhase += 6.28318530717958647692 * rate / sr; + if (lfoPhase > 6.28318530717958647692) lfoPhase -= 6.28318530717958647692; + + float sumL = 0.0f, sumR = 0.0f; + for (int v = 0; v < 2; ++v) + { + const double mod = std::sin (lfoPhase + v * 3.14159265358979323846) * depth; + const double d = base + mod + (v == 1 ? width * depth * 0.5 : 0.0); + double readPos = writePos - d; + if (readPos < 0.0) readPos += len; + const int i0 = (int) readPos; + const int i1 = (i0 + 1) % len; + const float frac = (float) (readPos - std::floor (readPos)); + sumL += delayL[(size_t) i0] * (1.0f - frac) + delayL[(size_t) i1] * frac; + sumR += delayR[(size_t) i0] * (1.0f - frac) + delayR[(size_t) i1] * frac; + } + + l[i] = sumL * 0.5f; + r[i] = sumR * 0.5f; + writePos = (writePos + 1) % len; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Chorus.h b/Source/EffectUnits/Chorus.h new file mode 100644 index 0000000..415c6da --- /dev/null +++ b/Source/EffectUnits/Chorus.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Chorus — two LFO-modulated delay taps per channel. p[0]=rate, p[1]=depth, +// p[2]=width, p[3]=unused. +// =========================================================================== +class ChorusUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + std::vector delayL, delayR; + int writePos = 0; + double lfoPhase = 0.0; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Compressor.cpp b/Source/EffectUnits/Compressor.cpp new file mode 100644 index 0000000..14cf0c1 --- /dev/null +++ b/Source/EffectUnits/Compressor.cpp @@ -0,0 +1,52 @@ +#include "Compressor.h" + +namespace serum +{ + +void CompressorUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + reset(); +} + +void CompressorUnit::reset() +{ + envL = envR = 0.0f; + gainL = gainR = 1.0f; +} + +void CompressorUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const double thresholdDb = -40.0 + p[0] * 40.0; // -40..0 dB + const double ratio = 2.0 + p[1] * 18.0; // 2..20 + const double attackMs = 2.0 + p[2] * 98.0; + const double releaseMs = 30.0 + p[3] * 470.0; + const double attackCoef = std::exp (-1.0 / (attackMs / 1000.0 * sr)); + const double releaseCoef = std::exp (-1.0 / (releaseMs / 1000.0 * sr)); + const double threshold = std::pow (10.0, thresholdDb / 20.0); + const double makeup = std::pow (10.0, -thresholdDb * 0.5 / 20.0); + + for (int i = 0; i < numSamples; ++i) + { + const float aL = std::abs (l[i]); + const float aR = std::abs (r[i]); + + envL = aL > envL ? (float) (attackCoef * envL + (1.0 - attackCoef) * aL) + : (float) (releaseCoef * envL + (1.0 - releaseCoef) * aL); + envR = aR > envR ? (float) (attackCoef * envR + (1.0 - attackCoef) * aR) + : (float) (releaseCoef * envR + (1.0 - releaseCoef) * aR); + + const double targetL = (envL > threshold) ? threshold * std::pow (envL / threshold, 1.0 / ratio) : envL; + const double targetR = (envR > threshold) ? threshold * std::pow (envR / threshold, 1.0 / ratio) : envR; + const float desiredL = (float) juce::jlimit (0.05, 1.0, (envL > 1e-6) ? targetL / envL : 1.0); + const float desiredR = (float) juce::jlimit (0.05, 1.0, (envR > 1e-6) ? targetR / envR : 1.0); + + gainL += 0.01f * (desiredL - gainL); + gainR += 0.01f * (desiredR - gainR); + + l[i] = (float) (l[i] * gainL * makeup); + r[i] = (float) (r[i] * gainR * makeup); + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Compressor.h b/Source/EffectUnits/Compressor.h new file mode 100644 index 0000000..4353525 --- /dev/null +++ b/Source/EffectUnits/Compressor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Compressor — soft-knee RMS compressor with makeup gain. p[0]=threshold, +// p[1]=ratio, p[2]=attack, p[3]=release. +// =========================================================================== +class CompressorUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + float envL = 0.0f, envR = 0.0f; + float gainL = 1.0f, gainR = 1.0f; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Delay.cpp b/Source/EffectUnits/Delay.cpp new file mode 100644 index 0000000..7153720 --- /dev/null +++ b/Source/EffectUnits/Delay.cpp @@ -0,0 +1,62 @@ +#include "Delay.h" + +namespace serum +{ + +void DelayUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + const int maxDelay = (int) (sr * 2.1); // 2.1 seconds + delayL.assign ((size_t) maxDelay, 0.0f); + delayR.assign ((size_t) maxDelay, 0.0f); + reset(); +} + +void DelayUnit::reset() +{ + std::fill (delayL.begin(), delayL.end(), 0.0f); + std::fill (delayR.begin(), delayR.end(), 0.0f); + writePos = 0; + dampL = dampR = 0.0f; +} + +void DelayUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const int len = (int) delayL.size(); + if (len < 4) + return; + + const double delaySamples = juce::jlimit (2.0, (double) len - 2.0, maps::delayToMs (p[0]) / 1000.0 * sr); + const float feedback = p[1] * 0.92f; + const float dampCoef = 1.0f - p[2] * 0.95f; // 1 = no damping + const float pingpong = p[3]; + + for (int i = 0; i < numSamples; ++i) + { + double readPos = writePos - delaySamples; + if (readPos < 0.0) readPos += len; + const int i0 = (int) readPos; + const int i1 = (i0 + 1) % len; + const float frac = (float) (readPos - std::floor (readPos)); + + float dl = delayL[(size_t) i0] * (1.0f - frac) + delayL[(size_t) i1] * frac; + float dr = delayR[(size_t) i0] * (1.0f - frac) + delayR[(size_t) i1] * frac; + + // Damping lowpass in the feedback path. + dampL = dl * (1.0f - dampCoef) + dampL * dampCoef; + dampR = dr * (1.0f - dampCoef) + dampR * dampCoef; + + // Ping-pong: cross-feed between channels. + const float feedL = dampR * pingpong + dampL * (1.0f - pingpong); + const float feedR = dampL * pingpong + dampR * (1.0f - pingpong); + + delayL[(size_t) writePos] = l[i] + feedL * feedback; + delayR[(size_t) writePos] = r[i] + feedR * feedback; + + l[i] = dl; + r[i] = dr; + writePos = (writePos + 1) % len; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Delay.h b/Source/EffectUnits/Delay.h new file mode 100644 index 0000000..e0fc23b --- /dev/null +++ b/Source/EffectUnits/Delay.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Delay — stereo delay with feedback, damping and ping-pong. p[0]=time, +// p[1]=feedback, p[2]=damping, p[3]=ping-pong/width. +// =========================================================================== +class DelayUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + std::vector delayL, delayR; + int writePos = 0; + float dampL = 0.0f, dampR = 0.0f; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Distortion.cpp b/Source/EffectUnits/Distortion.cpp new file mode 100644 index 0000000..7b2aff3 --- /dev/null +++ b/Source/EffectUnits/Distortion.cpp @@ -0,0 +1,44 @@ +#include "Distortion.h" + +namespace serum +{ + +void DistortionUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + lpL.setSampleRate (sr); lpR.setSampleRate (sr); + lpL.setLowpass (8000.0, 0.7071); lpR.setLowpass (8000.0, 0.7071); + reset(); +} + +void DistortionUnit::reset() +{ + lpL.clear(); lpR.clear(); +} + +float DistortionUnit::shape (float x, float shapeParam) noexcept +{ + if (shapeParam < 0.33f) return std::tanh (x); + if (shapeParam < 0.66f) return juce::jlimit (-1.0f, 1.0f, x); + return std::sin (x * 1.5707963267948966f); // sine fold +} + +void DistortionUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const float drive = 1.0f + p[0] * 24.0f; + const float shapeP = p[1]; + const float output = 0.4f + p[3] * 1.2f; + + // Tone: re-tune the lowpass from 800..16000 Hz. + const double toneHz = 800.0 + p[2] * 15200.0; + lpL.setLowpass (toneHz, 0.7071); + lpR.setLowpass (toneHz, 0.7071); + + for (int i = 0; i < numSamples; ++i) + { + l[i] = lpL.process (shape (l[i] * drive, shapeP) * output); + r[i] = lpR.process (shape (r[i] * drive, shapeP) * output); + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Distortion.h b/Source/EffectUnits/Distortion.h new file mode 100644 index 0000000..8275c0b --- /dev/null +++ b/Source/EffectUnits/Distortion.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../FXProcessor.h" +#include "Biquad.h" + +namespace serum +{ + +// =========================================================================== +// Distortion — waveshaper with drive and a post lowpass tone. p[0]=drive, +// p[1]=shape (soft/hard/fold), p[2]=tone, p[3]=output. +// =========================================================================== +class DistortionUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + Biquad lpL, lpR; + + static float shape (float x, float shapeParam) noexcept; +}; + +} // namespace serum diff --git a/Source/EffectUnits/EQ.cpp b/Source/EffectUnits/EQ.cpp new file mode 100644 index 0000000..cd95656 --- /dev/null +++ b/Source/EffectUnits/EQ.cpp @@ -0,0 +1,39 @@ +#include "EQ.h" + +namespace serum +{ + +void EQUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + lowL.setSampleRate (sr); lowR.setSampleRate (sr); + midL.setSampleRate (sr); midR.setSampleRate (sr); + highL.setSampleRate (sr); highR.setSampleRate (sr); + reset(); +} + +void EQUnit::reset() +{ + lowL.clear(); lowR.clear(); midL.clear(); midR.clear(); highL.clear(); highR.clear(); +} + +void EQUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const double lowGain = (p[0] - 0.5f) * 2.0f * 15.0; + const double midGain = (p[1] - 0.5f) * 2.0f * 12.0; + const double highGain = (p[2] - 0.5f) * 2.0f * 15.0; + const double midFreq = 200.0 + p[3] * 4800.0; + + lowL.setLowShelf (200.0, lowGain); lowR.setLowShelf (200.0, lowGain); + midL.setPeak (midFreq, midGain, 0.8); midR.setPeak (midFreq, midGain, 0.8); + highL.setHighShelf (4000.0, highGain); highR.setHighShelf (4000.0, highGain); + + for (int i = 0; i < numSamples; ++i) + { + const float xL = l[i], xR = r[i]; + l[i] = highL.process (midL.process (lowL.process (xL))); + r[i] = highR.process (midR.process (lowR.process (xR))); + } +} + +} // namespace serum diff --git a/Source/EffectUnits/EQ.h b/Source/EffectUnits/EQ.h new file mode 100644 index 0000000..d5ca95e --- /dev/null +++ b/Source/EffectUnits/EQ.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../FXProcessor.h" +#include "Biquad.h" + +namespace serum +{ + +// =========================================================================== +// EQ — three-band (low shelf / peak / high shelf). p[0]=low gain, p[1]=mid +// gain, p[2]=high gain, p[3]=mid frequency. +// =========================================================================== +class EQUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + Biquad lowL, lowR, midL, midR, highL, highR; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Flanger.cpp b/Source/EffectUnits/Flanger.cpp new file mode 100644 index 0000000..9ac922f --- /dev/null +++ b/Source/EffectUnits/Flanger.cpp @@ -0,0 +1,59 @@ +#include "Flanger.h" + +namespace serum +{ + +void FlangerUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + const int maxDelay = (int) (sr * 0.03); // 30ms + delayL.assign ((size_t) maxDelay, 0.0f); + delayR.assign ((size_t) maxDelay, 0.0f); + reset(); +} + +void FlangerUnit::reset() +{ + std::fill (delayL.begin(), delayL.end(), 0.0f); + std::fill (delayR.begin(), delayR.end(), 0.0f); + writePos = 0; + lfoPhase = 0.0; +} + +void FlangerUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const int len = (int) delayL.size(); + if (len < 4) + return; + + const double rate = 0.05 + p[0] * 2.0; + const double depth = (0.3 + p[1] * 4.7) * sr / 1000.0; // 0.3..5 ms + const double base = 1.5 * sr / 1000.0; + const float feedback = p[2] * 0.9f; + + for (int i = 0; i < numSamples; ++i) + { + const float inL = l[i], inR = r[i]; + + lfoPhase += 6.28318530717958647692 * rate / sr; + if (lfoPhase > 6.28318530717958647692) lfoPhase -= 6.28318530717958647692; + const double mod = std::sin (lfoPhase) * depth; + + double readPos = writePos - (base + mod); + if (readPos < 0.0) readPos += len; + const int i0 = (int) readPos; + const int i1 = (i0 + 1) % len; + const float frac = (float) (readPos - std::floor (readPos)); + const float outL = delayL[(size_t) i0] * (1.0f - frac) + delayL[(size_t) i1] * frac; + const float outR = delayR[(size_t) i0] * (1.0f - frac) + delayR[(size_t) i1] * frac; + + delayL[(size_t) writePos] = inL + outL * feedback; + delayR[(size_t) writePos] = inR + outR * feedback; + + l[i] = outL; + r[i] = outR; + writePos = (writePos + 1) % len; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Flanger.h b/Source/EffectUnits/Flanger.h new file mode 100644 index 0000000..a16b325 --- /dev/null +++ b/Source/EffectUnits/Flanger.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Flanger — LFO-modulated delay with feedback. p[0]=rate, p[1]=depth, +// p[2]=feedback, p[3]=unused. +// =========================================================================== +class FlangerUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + std::vector delayL, delayR; + int writePos = 0; + double lfoPhase = 0.0; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Hyper.cpp b/Source/EffectUnits/Hyper.cpp new file mode 100644 index 0000000..87557f7 --- /dev/null +++ b/Source/EffectUnits/Hyper.cpp @@ -0,0 +1,80 @@ +#include "Hyper.h" + +namespace serum +{ + +void HyperUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + lpL.setSampleRate (sr); lpR.setSampleRate (sr); + hpL.setSampleRate (sr); hpR.setSampleRate (sr); + lpL.setLowpass (150.0, 0.7071); lpR.setLowpass (150.0, 0.7071); + hpL.setHighpass (2500.0, 0.7071); hpR.setHighpass (2500.0, 0.7071); + reset(); +} + +void HyperUnit::reset() +{ + lpL.clear(); lpR.clear(); hpL.clear(); hpR.clear(); + for (auto& b : bL) { b.env = 0.0f; b.gain = 1.0f; } + for (auto& b : bR) { b.env = 0.0f; b.gain = 1.0f; } +} + +float HyperUnit::computeGain (Band& band, float absIn, float intensity) noexcept +{ + // Peak envelope follower with fast attack, slower release. + const float attack = (absIn > band.env) ? 0.3f : 0.003f; + band.env += attack * (absIn - band.env); + + const float env = band.env + 1e-6f; + const float ratio = 4.0f + 8.0f * intensity; + const float upAmount = intensity * 0.7f; + + // Downward compression above the reference level, upward expansion below. + float target; + if (env > 1.0f) + target = 1.0f + (env - 1.0f) / ratio; + else + target = 1.0f - (1.0f - env) * (1.0f - upAmount); + + float desired = target / env; + desired = juce::jlimit (0.2f, 4.0f, desired); + band.gain += 0.01f * (desired - band.gain); + return band.gain; +} + +void HyperUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const float intensity = p[0]; + const float lowAmt = 0.4f + 0.6f * p[1]; + const float highAmt = 0.4f + 0.6f * p[2]; + const float output = 0.5f + p[3] * 1.0f; + + for (int i = 0; i < numSamples; ++i) + { + const float inL = l[i], inR = r[i]; + + const float loL = lpL.process (inL); + const float restL = inL - loL; + const float hiL = hpL.process (restL); + const float midL = restL - hiL; + + const float loR = lpR.process (inR); + const float restR = inR - loR; + const float hiR = hpR.process (restR); + const float midR = restR - hiR; + + const float gLL = computeGain (bL[0], std::abs (loL), intensity); + const float gML = computeGain (bL[1], std::abs (midL), intensity); + const float gHL = computeGain (bL[2], std::abs (hiL), intensity); + + const float gLR = computeGain (bR[0], std::abs (loR), intensity); + const float gMR = computeGain (bR[1], std::abs (midR), intensity); + const float gHR = computeGain (bR[2], std::abs (hiR), intensity); + + l[i] = (loL * gLL * lowAmt + midL * gML + hiL * gHL * highAmt) * output; + r[i] = (loR * gLR * lowAmt + midR * gMR + hiR * gHR * highAmt) * output; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Hyper.h b/Source/EffectUnits/Hyper.h new file mode 100644 index 0000000..ac2d1e6 --- /dev/null +++ b/Source/EffectUnits/Hyper.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../FXProcessor.h" +#include "Biquad.h" + +namespace serum +{ + +// =========================================================================== +// Hyper — an OTT-style three-band upward/downward compressor. p[0]=intensity, +// p[1]=low amount, p[2]=high amount, p[3]=output. +// =========================================================================== +class HyperUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + struct Band + { + float env = 0.0f; + float gain = 1.0f; + }; + + double sr = 44100.0; + Biquad lpL, lpR, hpL, hpR; + Band bL[3], bR[3]; + + float computeGain (Band& band, float absIn, float intensity) noexcept; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Phaser.cpp b/Source/EffectUnits/Phaser.cpp new file mode 100644 index 0000000..66971a2 --- /dev/null +++ b/Source/EffectUnits/Phaser.cpp @@ -0,0 +1,54 @@ +#include "Phaser.h" + +namespace serum +{ + +void PhaserUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + reset(); +} + +void PhaserUnit::reset() +{ + lfoPhase = 0.0; + for (int i = 0; i < 6; ++i) + { + xL[i] = xR[i] = yL[i] = yR[i] = 0.0f; + } +} + +void PhaserUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const double rate = 0.05 + p[0] * 2.0; + const float depth = 0.3f + p[1] * 0.6f; + const float feedback = p[2] * 0.7f; + const int stages = juce::jlimit (2, 6, 2 + (int) (p[3] * 4.0f + 0.5f)); + + for (int i = 0; i < numSamples; ++i) + { + lfoPhase += 6.28318530717958647692 * rate / sr; + if (lfoPhase > 6.28318530717958647692) lfoPhase -= 6.28318530717958647692; + + const float sweep = (float) (0.5 + 0.5 * std::sin (lfoPhase)); + const float a = 0.3f + depth * sweep; // allpass coefficient + + float outL = l[i] + yL[5] * feedback; + float outR = r[i] + yR[5] * feedback; + + for (int s = 0; s < stages; ++s) + { + // y = a*x + x_prev - a*y_prev + const float inL = outL, inR = outR; + outL = a * inL + xL[s] - a * yL[s]; + outR = a * inR + xR[s] - a * yR[s]; + xL[s] = inL; xR[s] = inR; + yL[s] = outL; yR[s] = outR; + } + + l[i] = outL; + r[i] = outR; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Phaser.h b/Source/EffectUnits/Phaser.h new file mode 100644 index 0000000..16737db --- /dev/null +++ b/Source/EffectUnits/Phaser.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Phaser — cascade of four first-order allpass stages with an LFO-modulated +// coefficient and feedback. p[0]=rate, p[1]=depth, p[2]=feedback, p[3]=stages. +// =========================================================================== +class PhaserUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + double lfoPhase = 0.0; + float xL[6] {}, xR[6] {}, yL[6] {}, yR[6] {}; +}; + +} // namespace serum diff --git a/Source/EffectUnits/Reverb.cpp b/Source/EffectUnits/Reverb.cpp new file mode 100644 index 0000000..4b02aa5 --- /dev/null +++ b/Source/EffectUnits/Reverb.cpp @@ -0,0 +1,132 @@ +#include "Reverb.h" + +namespace serum +{ + +namespace +{ + constexpr int combTuning[8] = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 }; + constexpr int allpassTuning[4] = { 556, 441, 341, 225 }; +} + +void ReverbUnit::prepare (double sampleRate, int) +{ + sr = sampleRate; + const double scale = sr / 44100.0; + + for (int i = 0; i < 8; ++i) + { + const int len = (int) (combTuning[i] * scale) + 1; + combL[(size_t) i].assign ((size_t) len, 0.0f); + combR[(size_t) i].assign ((size_t) len, 0.0f); + } + for (int i = 0; i < 4; ++i) + { + const int len = (int) (allpassTuning[i] * scale) + 1; + apL[(size_t) i].assign ((size_t) len, 0.0f); + apR[(size_t) i].assign ((size_t) len, 0.0f); + } + + const int preLen = (int) (sr * 0.2); + preL.assign ((size_t) preLen, 0.0f); + preR.assign ((size_t) preLen, 0.0f); + reset(); +} + +void ReverbUnit::reset() +{ + for (int i = 0; i < 8; ++i) + { + std::fill (combL[(size_t) i].begin(), combL[(size_t) i].end(), 0.0f); + std::fill (combR[(size_t) i].begin(), combR[(size_t) i].end(), 0.0f); + combPosL[(size_t) i] = combPosR[(size_t) i] = 0; + combFiltL[(size_t) i] = combFiltR[(size_t) i] = 0.0f; + } + for (int i = 0; i < 4; ++i) + { + std::fill (apL[(size_t) i].begin(), apL[(size_t) i].end(), 0.0f); + std::fill (apR[(size_t) i].begin(), apR[(size_t) i].end(), 0.0f); + apPosL[(size_t) i] = apPosR[(size_t) i] = 0; + } + std::fill (preL.begin(), preL.end(), 0.0f); + std::fill (preR.begin(), preR.end(), 0.0f); + prePos = 0; +} + +void ReverbUnit::process (float* l, float* r, int numSamples, const float p[4]) +{ + const float feedback = maps::sizeToValue (p[0]); + const float damping = p[1]; + const float width = p[2]; + const int preLen = (int) preL.size(); + const float preDelay = (preLen > 0) ? p[3] * 0.15f * sr : 0.0f; + + for (int i = 0; i < numSamples; ++i) + { + // Predelay. + float inL = l[i], inR = r[i]; + if (preLen > 0) + { + const int read = (prePos - (int) preDelay + preLen) % preLen; + preL[(size_t) prePos] = inL; + preR[(size_t) prePos] = inR; + inL = preL[(size_t) read]; + inR = preR[(size_t) read]; + prePos = (prePos + 1) % preLen; + } + + // Combs. + float sumL = 0.0f, sumR = 0.0f; + for (int c = 0; c < 8; ++c) + { + auto& buf = combL[(size_t) c]; + const int pos = combPosL[(size_t) c]; + float out = buf[(size_t) pos]; + combFiltL[(size_t) c] = out * (1.0f - damping) + combFiltL[(size_t) c] * damping; + buf[(size_t) pos] = inL + combFiltL[(size_t) c] * feedback; + combPosL[(size_t) c] = (pos + 1) % (int) buf.size(); + sumL += out; + } + for (int c = 0; c < 8; ++c) + { + auto& buf = combR[(size_t) c]; + const int pos = combPosR[(size_t) c]; + float out = buf[(size_t) pos]; + combFiltR[(size_t) c] = out * (1.0f - damping) + combFiltR[(size_t) c] * damping; + buf[(size_t) pos] = inR + combFiltR[(size_t) c] * feedback; + combPosR[(size_t) c] = (pos + 1) % (int) buf.size(); + sumR += out; + } + + // Allpasses. + float aL = sumL, aR = sumR; + for (int a = 0; a < 4; ++a) + { + auto& buf = apL[(size_t) a]; + const int pos = apPosL[(size_t) a]; + const float stored = buf[(size_t) pos]; + buf[(size_t) pos] = aL + stored * 0.5f; + aL = stored - aL; + apPosL[(size_t) a] = (pos + 1) % (int) buf.size(); + } + for (int a = 0; a < 4; ++a) + { + auto& buf = apR[(size_t) a]; + const int pos = apPosR[(size_t) a]; + const float stored = buf[(size_t) pos]; + buf[(size_t) pos] = aR + stored * 0.5f; + aR = stored - aR; + apPosR[(size_t) a] = (pos + 1) % (int) buf.size(); + } + + // Width (stereo cross-mix). + const float w = 1.0f - width * 0.5f; + const float outL = aL * w + aR * (1.0f - w); + const float outR = aR * w + aL * (1.0f - w); + + l[i] = outL; + r[i] = outR; + } +} + +} // namespace serum diff --git a/Source/EffectUnits/Reverb.h b/Source/EffectUnits/Reverb.h new file mode 100644 index 0000000..f0579f0 --- /dev/null +++ b/Source/EffectUnits/Reverb.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../FXProcessor.h" + +namespace serum +{ + +// =========================================================================== +// Reverb — Freeverb-style Schroeder reverb (8 combs + 4 allpasses per channel) +// with predelay. p[0]=size, p[1]=damping, p[2]=width, p[3]=predelay. +// =========================================================================== +class ReverbUnit : public FXUnit +{ +public: + void prepare (double sampleRate, int maxBlockSize) override; + void reset() override; + void process (float* l, float* r, int numSamples, const float p[4]) override; + +private: + double sr = 44100.0; + + std::array, 8> combL, combR; + std::array combPosL {}, combPosR {}; + std::array combFiltL {}, combFiltR {}; + + std::array, 4> apL, apR; + std::array apPosL {}, apPosR {}; + + std::vector preL, preR; + int prePos = 0; +}; + +} // namespace serum diff --git a/Source/FXProcessor.cpp b/Source/FXProcessor.cpp new file mode 100644 index 0000000..72a296b --- /dev/null +++ b/Source/FXProcessor.cpp @@ -0,0 +1,111 @@ +#include "FXProcessor.h" +#include "EffectUnits/Hyper.h" +#include "EffectUnits/Chorus.h" +#include "EffectUnits/Flanger.h" +#include "EffectUnits/Phaser.h" +#include "EffectUnits/Distortion.h" +#include "EffectUnits/EQ.h" +#include "EffectUnits/Compressor.h" +#include "EffectUnits/Delay.h" +#include "EffectUnits/Reverb.h" + +namespace serum +{ + +FXProcessor::FXProcessor() +{ + units[0] = std::make_unique(); + units[1] = std::make_unique(); + units[2] = std::make_unique(); + units[3] = std::make_unique(); + units[4] = std::make_unique(); + units[5] = std::make_unique(); + units[6] = std::make_unique(); + units[7] = std::make_unique(); + units[8] = std::make_unique(); +} + +FXProcessor::~FXProcessor() = default; + +void FXProcessor::prepare (double sampleRate, int maxBlockSize) +{ + for (auto& u : units) + u->prepare (sampleRate, maxBlockSize); + dry.setSize (2, maxBlockSize, false, false, true); + wet.setSize (2, maxBlockSize, false, false, true); + reset(); +} + +void FXProcessor::reset() +{ + for (auto& u : units) + u->reset(); + dry.clear(); + wet.clear(); +} + +juce::String FXProcessor::fxTypeName (int type) +{ + switch ((FxType) type) + { + case FxType::Off: return "Off"; + case FxType::Hyper: return "Hyper"; + case FxType::Chorus: return "Chorus"; + case FxType::Flanger: return "Flanger"; + case FxType::Phaser: return "Phaser"; + case FxType::Distortion: return "Distortion"; + case FxType::EQ: return "EQ"; + case FxType::Compressor: return "Compressor"; + case FxType::Delay: return "Delay"; + case FxType::Reverb: return "Reverb"; + default: return {}; + } +} + +void FXProcessor::process (juce::AudioBuffer& buffer, const FxSlotParams* slots, int numSlots) +{ + const int n = buffer.getNumSamples(); + if (n <= 0) + return; + + for (int s = 0; s < numSlots; ++s) + { + const FxSlotParams& slot = slots[s]; + if (slot.type == (int) FxType::Off) + continue; + + const int unitIdx = slot.type - 1; + if (unitIdx < 0 || unitIdx >= (int) units.size()) + continue; + + dry.copyFrom (0, 0, buffer, 0, 0, n); + dry.copyFrom (1, 1, buffer, 1, 0, n); + wet.copyFrom (0, 0, buffer, 0, 0, n); + wet.copyFrom (1, 1, buffer, 1, 0, n); + + units[(size_t) unitIdx]->process (wet.getWritePointer (0), wet.getWritePointer (1), n, slot.p); + + const float mix = juce::jlimit (0.0f, 1.0f, slot.mix); + if (mix >= 1.0f) + { + buffer.copyFrom (0, 0, wet, 0, 0, n); + buffer.copyFrom (1, 0, wet, 1, 0, n); + } + else if (mix > 0.0f) + { + const float* dL = dry.getReadPointer (0); + const float* dR = dry.getReadPointer (1); + const float* wL = wet.getReadPointer (0); + const float* wR = wet.getReadPointer (1); + float* oL = buffer.getWritePointer (0); + float* oR = buffer.getWritePointer (1); + for (int i = 0; i < n; ++i) + { + oL[i] = dL[i] * (1.0f - mix) + wL[i] * mix; + oR[i] = dR[i] * (1.0f - mix) + wR[i] * mix; + } + } + } +} + +} // namespace serum diff --git a/Source/FXProcessor.h b/Source/FXProcessor.h new file mode 100644 index 0000000..9eb6876 --- /dev/null +++ b/Source/FXProcessor.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include "Params.h" + +namespace serum +{ + +// =========================================================================== +// Base class for a stereo effect unit. Each unit processes a full-wet buffer. +// =========================================================================== +class FXUnit +{ +public: + virtual ~FXUnit() = default; + virtual void prepare (double sampleRate, int maxBlockSize) = 0; + virtual void reset() = 0; + virtual void process (float* l, float* r, int numSamples, const float p[4]) = 0; +}; + +// =========================================================================== +// Snapshot of one FX slot. +// =========================================================================== +struct FxSlotParams +{ + int type = (int) FxType::Off; + float mix = 0.0f; + float p[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; +}; + +// =========================================================================== +// Reorderable effects rack. Slots are processed in list order; reordering is +// simply swapping FxSlotParams entries. Nine effect unit implementations are +// owned here and shared across slots. +// =========================================================================== +class FXProcessor +{ +public: + FXProcessor(); + ~FXProcessor(); + + void prepare (double sampleRate, int maxBlockSize); + void reset(); + + void process (juce::AudioBuffer& buffer, const FxSlotParams* slots, int numSlots); + + static juce::String fxTypeName (int type); + +private: + std::array, 9> units; + juce::AudioBuffer dry, wet; +}; + +} // namespace serum