# Copyright (c) 2026 MOD Audio Limited
# SPDX-License-Identifier: MIT
#
# MuRF: Multiple Resonance Filter Array inspired by the Moogfooger MF-105 MuRF.
#
# Signal chain:
#   Mono in → Drive → 8 resonant bandpass filters (fixed frequencies)
#   → Animation sequencer (24 patterns modulate each filter's level envelope)
#   → Stereo out (odd filters → L, even filters → R)
#   → Mix (dry/wet) → Output level
#
# Controls:
#   Drive      - input gain
#   Output     - output level
#   Mix        - dry/wet blend
#   Pattern    - selects 1 of 12 animation patterns (0..11)
#   Bank       - 0=MIDS (200Hz-3.4kHz), 1=BASS (110Hz-1.8kHz)
#   Rate       - animation speed
#   Envelope   - shape of each filter's volume envelope (fast/percussive → slow/smooth)
#   Filter 1-8 - individual level sliders for each bandpass filter
#
# Animation: each pattern is a 16-step sequence per filter. On each step a
# filter's envelope is either triggered (attack→decay) or held low.
# The Envelope control morphs between short-decay (percussive) and
# long-decay (smooth/shimmering).
#
# Stereo: odd filters (1,3,5,7) → left, even (2,4,6,8) → right,
# matching the real hardware behaviour.
#
# Upload at https://builder.mod.audio/buildroot with MOD unit over USB.

MURF_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
MURF_SITE = https://github.com/DISTRHO/DPF.git
MURF_SITE_METHOD = git
MURF_BUNDLES = moog-murf.lv2

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

START_NAMESPACE_DISTRHO

// ── Bandpass filter (state-variable, 2-pole) ──────────────────────────────────
struct BPF {
    float lp, bp;
    float sr;
    BPF() : lp(0.f), bp(0.f), sr(44100.f) {}
    void reset() { lp=bp=0.f; }
    // Returns bandpass output
    float tick(float in, float centreHz, float Q) {
        centreHz = fmaxf(20.f, fminf(centreHz, 18000.f));
        float f = 2.f * sinf(3.14159265f * centreHz / sr);
        float q = 1.f / fmaxf(Q, 0.1f);
        float hp = in - lp - q*bp;
        bp = f*hp + bp;
        lp = f*bp + lp;
        if (bp >  3.f) bp =  3.f;
        if (bp < -3.f) bp = -3.f;
        return bp;
    }
};

// ── One-pole LP (for filter level smoothing) ──────────────────────────────────
struct P1 {
    float z;
    P1() : z(0.f) {}
    void reset() { z=0.f; }
    float lp(float x, float a) { return z=x*(1.f-a)+z*a; }
};

// ── Per-filter envelope (attack/decay triggered by Animation) ─────────────────
struct FilterEnv {
    enum { IDLE, ATTACK, DECAY } stage;
    float val, ac, dc;
    FilterEnv() : stage(IDLE), val(0.f), ac(0.f), dc(0.f) {}
    void set(float attackMs, float decayMs, float sr) {
        ac = expf(-1.f / (fmaxf(attackMs, 0.5f)*0.001f*sr));
        dc = expf(-1.f / (fmaxf(decayMs,  1.f )*0.001f*sr));
    }
    void trigger() { stage=ATTACK; }
    float tick() {
        switch(stage) {
        case ATTACK:
            val += (1.f-val)*(1.f-ac);
            if (val>=0.99f) { val=1.f; stage=DECAY; }
            break;
        case DECAY:
            val *= dc;
            if (val<0.001f) { val=0.f; stage=IDLE; }
            break;
        default: val=0.f; break;
        }
        return val;
    }
};

// ── 24 animation patterns (16 steps × 8 filters) ─────────────────────────────
// Each row = one pattern, each column = 16-step bitmask for one filter (8 filters)
// 1 = trigger filter envelope on this step, 0 = hold low
// Patterns 0-11: MIDS bank   Patterns 12-23: BASS bank (same patterns, diff freqs)
static const uint16_t PATTERNS[12][8] = {
    // Pat 0: sequential up (classic MuRF sweep)
    {0x8080, 0x4040, 0x2020, 0x1010, 0x0808, 0x0404, 0x0202, 0x0101},
    // Pat 1: sequential down
    {0x0101, 0x0202, 0x0404, 0x0808, 0x1010, 0x2020, 0x4040, 0x8080},
    // Pat 2: all together (rhythmic chop)
    {0x8888, 0x8888, 0x8888, 0x8888, 0x8888, 0x8888, 0x8888, 0x8888},
    // Pat 3: odd/even alternating
    {0xAAAA, 0x5555, 0xAAAA, 0x5555, 0xAAAA, 0x5555, 0xAAAA, 0x5555},
    // Pat 4: bounce (up then down)
    {0x8040, 0x2010, 0x0804, 0x0201, 0x0102, 0x0408, 0x1020, 0x4080},
    // Pat 5: low pair then high pair
    {0xCC00, 0xCC00, 0x00CC, 0x00CC, 0xCC00, 0xCC00, 0x00CC, 0x00CC},
    // Pat 6: every other step on all
    {0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA},
    // Pat 7: sparse random-feel
    {0x8200, 0x0080, 0x4010, 0x0802, 0x1040, 0x0004, 0x2008, 0x0100},
    // Pat 8: low filters only cycling
    {0xF0F0, 0xF0F0, 0x0F0F, 0x0F0F, 0x0000, 0x0000, 0x0000, 0x0000},
    // Pat 9: high filters only cycling
    {0x0000, 0x0000, 0x0000, 0x0000, 0xF0F0, 0xF0F0, 0x0F0F, 0x0F0F},
    // Pat 10: triplet feel
    {0x9249, 0x2492, 0x4924, 0x9249, 0x2492, 0x4924, 0x9249, 0x2492},
    // Pat 11: shimmer (fast all)
    {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF},
};

// MIDS filter centre frequencies: 200,300,450,675,1000,1500,2200,3400 Hz
static const float FREQS_MIDS[8] = { 200.f, 300.f, 450.f, 675.f, 1000.f, 1500.f, 2200.f, 3400.f };
// BASS filter centre frequencies: 110(LP),160,240,350,525,775,1200,1800 Hz
static const float FREQS_BASS[8] = { 110.f, 160.f, 240.f, 350.f,  525.f,  775.f, 1200.f, 1800.f };

class MuRFPlugin : public Plugin
{
public:
    MuRFPlugin()
    : Plugin(kParameterCount, 0, 0),
      fSR(44100.f),
      pDrive(0.5f), pOutput(0.8f), pMix(1.0f),
      pPattern(0.f), pBank(0.f), pRate(0.5f), pEnvelope(0.5f),
      pStep(0), fStepPhase(0.f),
      fLastPattern(-1)
    {
        for(int i=0;i<8;++i) pFilt[i]=0.8f;
        memset(fEnvs, 0, sizeof(fEnvs));
        updateEnvParams();
    }

protected:
    const char* getLabel()       const override { return "MoogMuRF"; }
    const char* getDescription() const override { return "Multiple Resonance Filter Array inspired by Moogfooger MF-105 MuRF."; }
    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('M','U','R','F'); }

    void initParameter(uint32_t index, Parameter& p) override {
        switch(index) {
        case kDrive:
            p.hints=kParameterIsAutomatable; p.name="Drive"; p.symbol="drive";
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kOutput:
            p.hints=kParameterIsAutomatable; p.name="Output"; p.symbol="output";
            p.ranges.def=0.8f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kMix:
            p.hints=kParameterIsAutomatable; p.name="Mix"; p.symbol="mix";
            p.ranges.def=1.0f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kPattern:
            p.hints=kParameterIsAutomatable|kParameterIsInteger;
            p.name="Pattern"; p.symbol="pattern";
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=11.f; break;
        case kBank:
            p.hints=kParameterIsAutomatable|kParameterIsBoolean;
            p.name="Bank"; p.symbol="bank";
            // 0=MIDS, 1=BASS
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kRate:
            p.hints=kParameterIsAutomatable; p.name="Rate"; p.symbol="rate";
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kEnvelope:
            p.hints=kParameterIsAutomatable; p.name="Envelope"; p.symbol="envelope";
            // 0=percussive/fast decay, 1=smooth/slow decay
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kFilt1: case kFilt2: case kFilt3: case kFilt4:
        case kFilt5: case kFilt6: case kFilt7: case kFilt8: {
            int n = index - kFilt1 + 1;
            char sym[8], name[12];
            sym[0]='f';sym[1]='i';sym[2]='l';sym[3]='t';sym[4]='0'+n;sym[5]=0;
            name[0]='F';name[1]='i';name[2]='l';name[3]='t';name[4]='e';name[5]='r';
            name[6]=' ';name[7]='0'+n;name[8]=0;
            p.hints=kParameterIsAutomatable;
            p.name=name; p.symbol=sym;
            p.ranges.def=0.8f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        }
        default: break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch(index) {
        case kDrive:    return pDrive;
        case kOutput:   return pOutput;
        case kMix:      return pMix;
        case kPattern:  return pPattern;
        case kBank:     return pBank;
        case kRate:     return pRate;
        case kEnvelope: return pEnvelope;
        default:
            if(index>=kFilt1 && index<=kFilt8) return pFilt[index-kFilt1];
            return 0.f;
        }
    }

    void setParameterValue(uint32_t index, float v) override {
        switch(index) {
        case kDrive:    pDrive=v;    break;
        case kOutput:   pOutput=v;   break;
        case kMix:      pMix=v;      break;
        case kPattern:  pPattern=v;  break;
        case kBank:     pBank=v;     break;
        case kRate:     pRate=v;     break;
        case kEnvelope: pEnvelope=v; updateEnvParams(); break;
        default:
            if(index>=kFilt1 && index<=kFilt8) pFilt[index-kFilt1]=v;
            break;
        }
    }

    void activate() override {
        fSR = (float)getSampleRate();
        for(int i=0;i<8;++i) { fFilts[i].sr=fSR; fFilts[i].reset(); fSmoothL[i].reset(); fSmoothR[i].reset(); }
        pStep=0; fStepPhase=0.f; fLastPattern=-1;
        updateEnvParams();
    }

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

        updateEnvParams();

        int pat  = (int)(pPattern+0.5f);
        int bank = (pBank>=0.5f) ? 1 : 0;
        const float* freqs = (bank==1) ? FREQS_BASS : FREQS_MIDS;

        // Rate: 0..1 → steps/sec 0.5..16
        float stepsPerSec = 0.5f + pRate*15.5f;

        // Fixed Q for Moog-like resonance
        float Q = 3.5f;

        // Drive: 0..1 → 1..8x
        float driveGain = 1.f + pDrive*7.f;

        for(uint32_t i=0; i<frames; ++i) {
            float sig = in[i] * driveGain;

            // Advance animation step
            fStepPhase += stepsPerSec / fSR;
            if(fStepPhase >= 1.f) {
                fStepPhase -= 1.f;
                pStep = (pStep + 1) % 16;
                // Trigger envelopes for this step
                for(int f=0;f<8;++f) {
                    uint16_t mask = PATTERNS[pat][f];
                    if((mask >> (15 - pStep)) & 1)
                        fEnvs[f].trigger();
                }
            }

            float sumL = 0.f, sumR = 0.f;

            for(int f=0; f<8; ++f) {
                float bpOut  = fFilts[f].tick(sig, freqs[f], Q);
                float envVal = fEnvs[f].tick();
                float level  = envVal * pFilt[f];

                // Odd filters (0,2,4,6 = filter 1,3,5,7) → left
                // Even filters (1,3,5,7 = filter 2,4,6,8) → right
                if(f % 2 == 0)
                    sumL += bpOut * level;
                else
                    sumR += bpOut * level;
            }

            // Smooth level outputs
            float aSmooth = expf(-6.2832f * 20.f / fSR);
            sumL = fSmoothL[0].lp(sumL, aSmooth);
            sumR = fSmoothR[0].lp(sumR, aSmooth);

            // Scale
            sumL *= 0.35f * pOutput;
            sumR *= 0.35f * pOutput;

            float wet = pMix;
            float dry = 1.f - pMix;

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

private:
    void updateEnvParams() {
        // Envelope 0=percussive: attack 2ms decay 60ms
        // Envelope 1=smooth:    attack 20ms decay 800ms
        float attackMs = 2.f   + pEnvelope * 18.f;
        float decayMs  = 60.f  + pEnvelope * 740.f;
        for(int i=0;i<8;++i)
            fEnvs[i].set(attackMs, decayMs, fSR);
    }

    float fSR;
    float pDrive, pOutput, pMix;
    float pPattern, pBank, pRate, pEnvelope;
    float pFilt[8];

    int   pStep;
    float fStepPhase;
    int   fLastPattern;

    BPF        fFilts[8];
    FilterEnv  fEnvs[8];
    P1         fSmoothL[8], fSmoothR[8];

    enum Parameters {
        kDrive=0, kOutput, kMix, kPattern, kBank, kRate, kEnvelope,
        kFilt1, kFilt2, kFilt3, kFilt4, kFilt5, kFilt6, kFilt7, kFilt8,
        kParameterCount
    };

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MuRFPlugin)
};

Plugin* createPlugin() { return new MuRFPlugin(); }
END_NAMESPACE_DISTRHO
endef

define MURF_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        "Moog MuRF"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:moog-murf"
#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 {
    kDrive=0, kOutput, kMix, kPattern, kBank, kRate, kEnvelope,
    kFilt1, kFilt2, kFilt3, kFilt4, kFilt5, kFilt6, kFilt7, kFilt8,
    kParameterCount
};
#endif
endef

define MURF_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = moog-murf
FILES_DSP = MuRFPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

define MURF_MANIFEST_TTL
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<urn:mod-cookbook:moog-murf>
    a lv2:Plugin , lv2:FilterPlugin ;
    lv2:binary <moog-murf_dsp.so> ;
    rdfs:seeAlso <moog-murf.ttl> .
endef

define MURF_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#> .
<urn:mod-cookbook:moog-murf>
    a lv2:Plugin , lv2:FilterPlugin ;
    doap:name "Moog MuRF" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "MOD Cookbook" ; foaf:homepage <https://mod.audio> ] ;
    rdfs:comment "Multiple Resonance Filter Array: 8 resonant bandpass filters with animated sequencer, mono in stereo out." ;
    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 "drive" ; lv2:name "Drive" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 4 ; lv2:symbol "output" ; lv2:name "Output" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "mix" ; lv2:name "Mix" ;
        lv2:default 1.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ; lv2:symbol "pattern" ; lv2:name "Pattern" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 11.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "bank" ; lv2:name "Bank" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ; lv2:symbol "rate" ; lv2:name "Rate" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; lv2:symbol "envelope" ; lv2:name "Envelope" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ; lv2:symbol "filt1" ; lv2:name "Filter 1" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "filt2" ; lv2:name "Filter 2" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ; lv2:symbol "filt3" ; lv2:name "Filter 3" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ; lv2:symbol "filt4" ; lv2:name "Filter 4" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 14 ; lv2:symbol "filt5" ; lv2:name "Filter 5" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 15 ; lv2:symbol "filt6" ; lv2:name "Filter 6" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 16 ; lv2:symbol "filt7" ; lv2:name "Filter 7" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 17 ; lv2:symbol "filt8" ; lv2:name "Filter 8" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] .
endef

export MURF_PLUGIN_CPP
export MURF_PLUGIN_INFO_H
export MURF_PLUGIN_MAKEFILE
export MURF_MANIFEST_TTL
export MURF_PLUGIN_TTL

define MURF_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/moog-murf
	printf '%s' "$$MURF_PLUGIN_CPP"      > $(@D)/examples/moog-murf/MuRFPlugin.cpp
	printf '%s' "$$MURF_PLUGIN_INFO_H"   > $(@D)/examples/moog-murf/DistrhoPluginInfo.h
	printf '%s' "$$MURF_PLUGIN_MAKEFILE" > $(@D)/examples/moog-murf/Makefile
endef

define MURF_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/moog-murf lv2_dsp
endef

define MURF_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/moog-murf.lv2
	cp $(@D)/bin/moog-murf.lv2/moog-murf_dsp.so $($(PKG)_PKGDIR)/moog-murf.lv2/
	printf '%s' "$$MURF_MANIFEST_TTL" > $($(PKG)_PKGDIR)/moog-murf.lv2/manifest.ttl
	printf '%s' "$$MURF_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/moog-murf.lv2/moog-murf.ttl
endef

$(eval $(generic-package))
