/* ============================================================================== Phaser.h Created: 24 Jul 2026 Author: Armin ============================================================================== */ #pragma once #include #include "Effect.h" /** A phaser effect with configurable stages (pairs of all-pass filters) and LFO modulation. */ class PhaserEffect final : public Effect { public: void initialize(int numChannels, int fxSampleRate) override { sampleRate = fxSampleRate; numChannelsUsed = numChannels; for (auto& stage : stages) { stage[0].reset(); stage[1].reset(); } feedbackSampleL = 0.f; feedbackSampleR = 0.f; currentCenterFreq = -1.f; currentStages = -1; } void setCenterFrequency(float freq) { freq = juce::jlimit(50.f, float(sampleRate) / 4.f, freq); if (std::abs(freq - currentCenterFreq) > 0.5f) { currentCenterFreq = freq; updateAllPassCoefficients(); } } void setNumStages(int numStages) { numStages = juce::jlimit(1, 6, numStages); if (numStages != currentStages) { currentStages = numStages; updateAllPassCoefficients(); } } void setFeedback(float fb) { feedback = juce::jlimit(-0.95f, 0.95f, fb); } void setDepth(float d) { depth = juce::jlimit(0.f, 1.f, d); } /** Process the buffer with a given LFO modulation value [-1, 1]. This is called per block; the center frequency is modulated by the LFO depth. */ void processWithLFO(juce::AudioBuffer& buffer, int numSamples, float lfoValue) { float modulatedFreq = currentCenterFreq * std::pow(2.f, lfoValue * depth * 3.f); setCenterFrequency(modulatedFreq); for (int ch = 0; ch < buffer.getNumChannels(); ch++) { auto* data = buffer.getWritePointer(ch); float& fbSample = (ch == 0) ? feedbackSampleL : feedbackSampleR; for (int i = 0; i < numSamples; i++) { float input = data[i] + fbSample * feedback; float apOutput = input; for (int s = 0; s < currentStages; s++) { auto& stage = stages[static_cast(s)]; auto& filter = (ch == 0) ? stage[0] : stage[1]; apOutput = filter.process(apOutput); } fbSample = apOutput; data[i] = input + apOutput * (-depth); } } } void updateParams(const SamplerParameters& samplerSound, bool /*modulating*/) override { setFeedback(samplerSound.modPhaserFeedback->get()); setNumStages(samplerSound.modPhaserStages->get()); setDepth(samplerSound.modPhaserDepth->get()); } void process(juce::AudioBuffer& buffer, int numSamples, int startSample = 0) override { // When called without LFO, process with no modulation (center freq stays) for (int ch = 0; ch < buffer.getNumChannels(); ch++) { auto* data = buffer.getWritePointer(ch, startSample); float& fbSample = (ch == 0) ? feedbackSampleL : feedbackSampleR; for (int i = 0; i < numSamples; i++) { float input = data[i] + fbSample * feedback; float apOutput = input; for (int s = 0; s < currentStages; s++) { auto& stage = stages[static_cast(s)]; auto& filter = (ch == 0) ? stage[0] : stage[1]; apOutput = filter.process(apOutput); } fbSample = apOutput; data[i] = input + apOutput * (-depth); } } } private: /** Simple 1st-order all-pass filter: y[n] = b0*x[n] + b1*x[n-1] - a1*y[n-1] */ struct AllPassFilter { void reset() { x1 = 0.f; y1 = 0.f; } void setCoefficients(float a, float b) { a1 = a; b1 = b; } float process(float input) { float output = b1 * input + b0 * x1 - a1 * y1; y1 = output; x1 = input; return output; } float b0{ 1.f }; float b1{ 0.f }; float a1{ 0.f }; float x1{ 0.f }; float y1{ 0.f }; }; void updateAllPassCoefficients() { if (sampleRate <= 0) return; for (int s = 0; s < currentStages; s++) { float freq = currentCenterFreq * (1.f + float(s) * 0.3f); freq = juce::jlimit(50.f, float(sampleRate) / 4.f - 10.f, freq); float angle = juce::MathConstants::pi * freq / float(sampleRate); float t = std::tan(angle); float a = (t - 1.f) / (t + 1.f); allPassA[static_cast(s)] = a; allPassB[static_cast(s)] = 1.f; stages[static_cast(s)][0].setCoefficients(a, 1.f); stages[static_cast(s)][1].setCoefficients(a, 1.f); } } static constexpr int MAX_STAGES{ 6 }; std::array, MAX_STAGES> stages; std::array allPassA{}; std::array allPassB{}; int sampleRate{ 0 }; int numChannelsUsed{ 2 }; int currentStages{ 3 }; float currentCenterFreq{ 1000.f }; float feedback{ 0.f }; float depth{ 0.5f }; float feedbackSampleL{ 0.f }; float feedbackSampleR{ 0.f }; };