# Copyright (c) 2026 MOD Audio Limited
# SPDX-License-Identifier: MIT
#
# Quad Phaser: mono input split to 4 independent phaser voices, each with
# its own LFO rate, depth, centre frequency, and 4-stage all-pass chain.
# Voices are panned and mixed to stereo output.
#
# Voice panning defaults: A=hard left, B=centre-left, C=centre-right, D=hard right
#
# Upload at https://builder.mod.audio/buildroot with MOD unit over USB.

QUAD_PHASER_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
QUAD_PHASER_SITE = https://github.com/DISTRHO/DPF.git
QUAD_PHASER_SITE_METHOD = git
QUAD_PHASER_BUNDLES = quad-phaser.lv2

define QUAD_PHASER_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>

START_NAMESPACE_DISTRHO

// One first-order all-pass stage
// y[n] = a*(x[n] - y[n-1]) + x[n-1]
// where a = (1 - tan(pi*f/fs)) / (1 + tan(pi*f/fs))
struct APF
{
    float a;   // coefficient
    float xz;  // x[n-1]
    float yz;  // y[n-1]

    APF() : a(0.0f), xz(0.0f), yz(0.0f) {}

    void setFreq(float freq, float sampleRate)
    {
        float t = tanf(3.14159265f * freq / sampleRate);
        a = (1.0f - t) / (1.0f + t);
    }

    float process(float x)
    {
        float y = a * (x - yz) + xz;
        xz = x;
        yz = y;
        return y;
    }
};

// 4-stage all-pass phaser voice
struct PhaserVoice
{
    APF    stages[4];
    float  phase;     // LFO phase 0..1
    float  lastFb;    // feedback sample

    PhaserVoice() : phase(0.0f), lastFb(0.0f) {}

    float process(float in, float rate, float depth, float centreFreq,
                  float feedback, float sampleRate, float phaseOffset)
    {
        // LFO: sine, mapped to frequency sweep range
        float lfo = sinf((phase + phaseOffset) * 6.283185f);
        phase += rate / sampleRate;
        if (phase >= 1.0f) phase -= 1.0f;

        // Sweep: centre +/- depth octaves
        float freq = centreFreq * powf(2.0f, lfo * depth);
        if (freq < 20.0f)   freq = 20.0f;
        if (freq > 18000.0f) freq = 18000.0f;

        // Update all 4 stages to same swept frequency
        for (int s = 0; s < 4; ++s)
            stages[s].setFreq(freq, sampleRate);

        // Feed input + feedback into chain
        float sig = in + lastFb * feedback;

        for (int s = 0; s < 4; ++s)
            sig = stages[s].process(sig);

        lastFb = sig;
        return sig;
    }

    void reset()
    {
        for (int s = 0; s < 4; ++s)
        {
            stages[s].xz = 0.0f;
            stages[s].yz = 0.0f;
        }
        phase   = 0.0f;
        lastFb  = 0.0f;
    }
};

class QuadPhaserPlugin : public Plugin
{
public:
    QuadPhaserPlugin()
        : Plugin(kParameterCount, 0, 0),
          fSampleRate(44100.0f),
          fMix(0.5f),
          fFeedback(0.5f)
    {
        // Stagger LFO phases 0, 90, 180, 270 degrees
        static const float defRate[4]   = { 0.5f,  0.7f,   0.4f,  0.6f  };
        static const float defDepth[4]  = { 1.0f,  1.0f,   1.0f,  1.0f  };
        static const float defCentre[4] = { 800.0f, 1000.0f, 600.0f, 1200.0f };
        static const float defPan[4]    = { 0.0f,  0.33f,  0.67f, 1.0f  };

        for (int v = 0; v < 4; ++v)
        {
            fRate[v]   = defRate[v];
            fDepth[v]  = defDepth[v];
            fCentre[v] = defCentre[v];
            fPan[v]    = defPan[v];
            fVoice[v].phase = (float)v * 0.25f; // stagger phases
        }
    }

protected:
    const char* getLabel()       const override { return "QuadPhaser"; }
    const char* getDescription() const override { return "Mono input, 4-voice phaser with independent LFOs, panned to stereo."; }
    const char* getMaker()       const override { return "MOD Cookbook"; }
    const char* getHomePage()    const override { return "https://mod.audio"; }
    const char* getLicense()     const override { return "MIT"; }
    uint32_t    getVersion()     const override { return d_version(1, 0, 0); }
    int64_t     getUniqueId()    const override { return d_cconst('Q', 'P', 'H', '4'); }

    void initParameter(uint32_t index, Parameter& parameter) override
    {
        switch (index)
        {
        case kMix:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Dry/Wet";
            parameter.symbol     = "mix";
            parameter.unit       = "";
            parameter.ranges.def = 0.5f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            return;

        case kFeedback:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Feedback";
            parameter.symbol     = "feedback";
            parameter.unit       = "";
            parameter.ranges.def = 0.5f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 0.95f;
            return;

        default: break;
        }

        static const char* vLabel[4] = {"A","B","C","D"};
        static const char* rateSyms[4]   = {"v_a_rate",   "v_b_rate",   "v_c_rate",   "v_d_rate"};
        static const char* depthSyms[4]  = {"v_a_depth",  "v_b_depth",  "v_c_depth",  "v_d_depth"};
        static const char* centreSyms[4] = {"v_a_centre", "v_b_centre", "v_c_centre", "v_d_centre"};
        static const char* panSyms[4]    = {"v_a_pan",    "v_b_pan",    "v_c_pan",    "v_d_pan"};

        static const float defRate[4]   = { 0.5f,   0.7f,    0.4f,   0.6f   };
        static const float defDepth[4]  = { 1.0f,   1.0f,    1.0f,   1.0f   };
        static const float defCentre[4] = { 800.0f, 1000.0f, 600.0f, 1200.0f };
        static const float defPan[4]    = { 0.0f,   0.33f,   0.67f,  1.0f   };

        uint32_t slot = (index - kVoiceARate) / 4;
        uint32_t type = (index - kVoiceARate) % 4; // 0=Rate 1=Depth 2=Centre 3=Pan

        if (slot >= 4) return;

        if (type == 0)
        {
            parameter.hints      = kParameterIsAutomatable;
            char buf[24];
            buf[0]='V';buf[1]='o';buf[2]='i';buf[3]='c';buf[4]='e';buf[5]=' ';
            buf[6]=vLabel[slot][0];buf[7]=':';buf[8]=' ';
            buf[9]='R';buf[10]='a';buf[11]='t';buf[12]='e';buf[13]='\0';
            parameter.name       = buf;
            parameter.symbol     = rateSyms[slot];
            parameter.unit       = "Hz";
            parameter.ranges.def = defRate[slot];
            parameter.ranges.min = 0.05f;
            parameter.ranges.max = 5.0f;
        }
        else if (type == 1)
        {
            parameter.hints      = kParameterIsAutomatable;
            char buf[24];
            buf[0]='V';buf[1]='o';buf[2]='i';buf[3]='c';buf[4]='e';buf[5]=' ';
            buf[6]=vLabel[slot][0];buf[7]=':';buf[8]=' ';
            buf[9]='D';buf[10]='e';buf[11]='p';buf[12]='t';buf[13]='h';buf[14]='\0';
            parameter.name       = buf;
            parameter.symbol     = depthSyms[slot];
            parameter.unit       = "oct";
            parameter.ranges.def = defDepth[slot];
            parameter.ranges.min = 0.1f;
            parameter.ranges.max = 4.0f;
        }
        else if (type == 2)
        {
            parameter.hints      = kParameterIsAutomatable;
            char buf[24];
            buf[0]='V';buf[1]='o';buf[2]='i';buf[3]='c';buf[4]='e';buf[5]=' ';
            buf[6]=vLabel[slot][0];buf[7]=':';buf[8]=' ';
            buf[9]='C';buf[10]='e';buf[11]='n';buf[12]='t';buf[13]='r';buf[14]='e';buf[15]='\0';
            parameter.name       = buf;
            parameter.symbol     = centreSyms[slot];
            parameter.unit       = "Hz";
            parameter.ranges.def = defCentre[slot];
            parameter.ranges.min = 100.0f;
            parameter.ranges.max = 8000.0f;
        }
        else
        {
            parameter.hints      = kParameterIsAutomatable;
            char buf[24];
            buf[0]='V';buf[1]='o';buf[2]='i';buf[3]='c';buf[4]='e';buf[5]=' ';
            buf[6]=vLabel[slot][0];buf[7]=':';buf[8]=' ';
            buf[9]='P';buf[10]='a';buf[11]='n';buf[12]='\0';
            parameter.name       = buf;
            parameter.symbol     = panSyms[slot];
            parameter.unit       = "";
            parameter.ranges.def = defPan[slot];
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
        }
    }

    float getParameterValue(uint32_t index) const override
    {
        if (index == kMix)      return fMix;
        if (index == kFeedback) return fFeedback;
        uint32_t slot = (index - kVoiceARate) / 4;
        uint32_t type = (index - kVoiceARate) % 4;
        if (slot >= 4) return 0.0f;
        if (type == 0) return fRate[slot];
        if (type == 1) return fDepth[slot];
        if (type == 2) return fCentre[slot];
        return fPan[slot];
    }

    void setParameterValue(uint32_t index, float value) override
    {
        if (index == kMix)      { fMix      = value; return; }
        if (index == kFeedback) { fFeedback = value; return; }
        uint32_t slot = (index - kVoiceARate) / 4;
        uint32_t type = (index - kVoiceARate) % 4;
        if (slot >= 4) return;
        if      (type == 0) fRate[slot]   = value;
        else if (type == 1) fDepth[slot]  = value;
        else if (type == 2) fCentre[slot] = value;
        else                fPan[slot]    = value;
    }

    void activate() override
    {
        fSampleRate = (float)getSampleRate();
        for (int v = 0; v < 4; ++v)
            fVoice[v].reset();
        // Re-stagger phases after reset
        for (int v = 0; v < 4; ++v)
            fVoice[v].phase = (float)v * 0.25f;
    }

    void run(const float** inputs, float** outputs, uint32_t frames) override
    {
        const float* in   = inputs[0];
        float*       outL = outputs[0];
        float*       outR = outputs[1];

        const float wet = fMix;
        const float dry = 1.0f - fMix;
        const float fb  = fFeedback;

        for (uint32_t i = 0; i < frames; ++i)
        {
            const float inSample = in[i];

            float sumL = 0.0f;
            float sumR = 0.0f;

            for (int v = 0; v < 4; ++v)
            {
                float voiceSample = fVoice[v].process(
                    inSample,
                    fRate[v],
                    fDepth[v],
                    fCentre[v],
                    fb,
                    fSampleRate,
                    0.0f  // phase offset already baked into fVoice[v].phase at init
                );

                // Equal-power pan
                float panAngle = fPan[v] * 1.5707963f;
                sumL += voiceSample * cosf(panAngle);
                sumR += voiceSample * sinf(panAngle);
            }

            // Scale by voice count
            sumL *= 0.25f;
            sumR *= 0.25f;

            outL[i] = dry * inSample + wet * sumL;
            outR[i] = dry * inSample + wet * sumR;
        }
    }

private:
    float       fSampleRate;
    float       fMix;
    float       fFeedback;
    float       fRate[4];
    float       fDepth[4];
    float       fCentre[4];
    float       fPan[4];
    PhaserVoice fVoice[4];

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(QuadPhaserPlugin)
};

Plugin* createPlugin() { return new QuadPhaserPlugin(); }

END_NAMESPACE_DISTRHO
endef

define QUAD_PHASER_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND       "MOD Cookbook"
#define DISTRHO_PLUGIN_NAME        "Quad Phaser"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:quad-phaser"

#define DISTRHO_PLUGIN_HAS_UI       0
#define DISTRHO_PLUGIN_IS_RT_SAFE   1
#define DISTRHO_PLUGIN_NUM_INPUTS   1
#define DISTRHO_PLUGIN_NUM_OUTPUTS  2

enum Parameters {
    kMix = 0,
    kFeedback,
    kVoiceARate,  kVoiceADepth,  kVoiceACentre,  kVoiceAPan,
    kVoiceBRate,  kVoiceBDepth,  kVoiceBCentre,  kVoiceBPan,
    kVoiceCRate,  kVoiceCDepth,  kVoiceCCentre,  kVoiceCPan,
    kVoiceDRate,  kVoiceDDepth,  kVoiceDCentre,  kVoiceDPan,
    kParameterCount
};

#endif
endef

define QUAD_PHASER_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = quad-phaser
FILES_DSP = QuadPhaserPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $$(TARGETS)
endef

define QUAD_PHASER_MANIFEST_TTL
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<urn:mod-cookbook:quad-phaser>
    a lv2:Plugin , lv2:PhaserPlugin ;
    lv2:binary <quad-phaser_dsp.so> ;
    rdfs:seeAlso <quad-phaser.ttl> .
endef

define QUAD_PHASER_PLUGIN_TTL
@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix foaf:  <http://xmlns.com/foaf/0.1/> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .

<urn:mod-cookbook:quad-phaser>
    a lv2:Plugin , lv2:PhaserPlugin ;
    doap:name "Quad Phaser" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [
        foaf:name "MOD Cookbook" ;
        foaf:homepage <https://mod.audio>
    ] ;
    rdfs:comment "Mono input, 4-voice phaser with independent LFOs and feedback, panned to stereo." ;
    lv2:port [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 0 ; lv2:symbol "in" ; lv2:name "Audio In"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 1 ; lv2:symbol "out_l" ; lv2:name "Audio Out L"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 2 ; lv2:symbol "out_r" ; lv2:name "Audio Out R"
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 3 ; lv2:symbol "mix" ; lv2:name "Dry/Wet" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 4 ; lv2:symbol "feedback" ; lv2:name "Feedback" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 0.95
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "v_a_rate" ; lv2:name "Voice A: Rate" ;
        lv2:default 0.5 ; lv2:minimum 0.05 ; lv2:maximum 5.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ; lv2:symbol "v_a_depth" ; lv2:name "Voice A: Depth" ;
        lv2:default 1.0 ; lv2:minimum 0.1 ; lv2:maximum 4.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "v_a_centre" ; lv2:name "Voice A: Centre" ;
        lv2:default 800.0 ; lv2:minimum 100.0 ; lv2:maximum 8000.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ; lv2:symbol "v_a_pan" ; lv2:name "Voice A: Pan" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; lv2:symbol "v_b_rate" ; lv2:name "Voice B: Rate" ;
        lv2:default 0.7 ; lv2:minimum 0.05 ; lv2:maximum 5.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ; lv2:symbol "v_b_depth" ; lv2:name "Voice B: Depth" ;
        lv2:default 1.0 ; lv2:minimum 0.1 ; lv2:maximum 4.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "v_b_centre" ; lv2:name "Voice B: Centre" ;
        lv2:default 1000.0 ; lv2:minimum 100.0 ; lv2:maximum 8000.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ; lv2:symbol "v_b_pan" ; lv2:name "Voice B: Pan" ;
        lv2:default 0.33 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ; lv2:symbol "v_c_rate" ; lv2:name "Voice C: Rate" ;
        lv2:default 0.4 ; lv2:minimum 0.05 ; lv2:maximum 5.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 14 ; lv2:symbol "v_c_depth" ; lv2:name "Voice C: Depth" ;
        lv2:default 1.0 ; lv2:minimum 0.1 ; lv2:maximum 4.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 15 ; lv2:symbol "v_c_centre" ; lv2:name "Voice C: Centre" ;
        lv2:default 600.0 ; lv2:minimum 100.0 ; lv2:maximum 8000.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 16 ; lv2:symbol "v_c_pan" ; lv2:name "Voice C: Pan" ;
        lv2:default 0.67 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 17 ; lv2:symbol "v_d_rate" ; lv2:name "Voice D: Rate" ;
        lv2:default 0.6 ; lv2:minimum 0.05 ; lv2:maximum 5.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 18 ; lv2:symbol "v_d_depth" ; lv2:name "Voice D: Depth" ;
        lv2:default 1.0 ; lv2:minimum 0.1 ; lv2:maximum 4.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 19 ; lv2:symbol "v_d_centre" ; lv2:name "Voice D: Centre" ;
        lv2:default 1200.0 ; lv2:minimum 100.0 ; lv2:maximum 8000.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 20 ; lv2:symbol "v_d_pan" ; lv2:name "Voice D: Pan" ;
        lv2:default 1.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] .
endef

export QUAD_PHASER_PLUGIN_CPP
export QUAD_PHASER_PLUGIN_INFO_H
export QUAD_PHASER_PLUGIN_MAKEFILE
export QUAD_PHASER_MANIFEST_TTL
export QUAD_PHASER_PLUGIN_TTL

define QUAD_PHASER_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/quad-phaser
	printf '%s' "$$QUAD_PHASER_PLUGIN_CPP"      > $(@D)/examples/quad-phaser/QuadPhaserPlugin.cpp
	printf '%s' "$$QUAD_PHASER_PLUGIN_INFO_H"   > $(@D)/examples/quad-phaser/DistrhoPluginInfo.h
	printf '%s' "$$QUAD_PHASER_PLUGIN_MAKEFILE" > $(@D)/examples/quad-phaser/Makefile
endef

define QUAD_PHASER_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/quad-phaser lv2_dsp
endef

define QUAD_PHASER_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/quad-phaser.lv2
	cp $(@D)/bin/quad-phaser.lv2/quad-phaser_dsp.so $($(PKG)_PKGDIR)/quad-phaser.lv2/
	printf '%s' "$$QUAD_PHASER_MANIFEST_TTL" > $($(PKG)_PKGDIR)/quad-phaser.lv2/manifest.ttl
	printf '%s' "$$QUAD_PHASER_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/quad-phaser.lv2/quad-phaser.ttl
endef

$(eval $(generic-package))
