# Copyright (c) 2026
# SPDX-License-Identifier: MIT

RK2000_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
RK2000_SITE = https://github.com/DISTRHO/DPF.git
RK2000_SITE_METHOD = git
RK2000_BUNDLES = rk2000.lv2

define RK2000_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <algorithm>

START_NAMESPACE_DISTRHO

static inline float rk2000Clamp01(float x)
{
    return std::max(0.0f, std::min(1.0f, x));
}

static inline float rk2000Quantize12(float x)
{
    const float clamped = std::max(-1.0f, std::min(1.0f, x));
    const float steps = 2047.0f;
    return std::round(clamped * steps) / steps;
}

class Rk2000Plugin : public Plugin
{
public:
    Rk2000Plugin()
        : Plugin(kParameterCount, 0, 0),
          fInput(0.5f), fBlend(0.5f), fFdbk(0.3f), fFilter(0.0f),
          fDepth(0.0f), fRate(2.0f), fTimeMs(350.0f),
          fModOn(0.0f), fSub(0.0f), fStereoMode(0.0f), fHold(0.0f),
          fBufL(nullptr), fBufR(nullptr), fBufLen(0), fWritePos(0),
          fLpStateL(0.0f), fLpStateR(0.0f),
          fLfoPhase(0.0f)
    {
    }

    ~Rk2000Plugin() override
    {
        delete[] fBufL;
        delete[] fBufR;
    }

protected:
    const char* getLabel()       const override { return "RK2000"; }
    const char* getDescription() const override { return "12-bit-style stereo modulation delay inspired by the Ibanez DM2000 / Keeley RK2000."; }
    const char* getMaker()       const override { return "ModCookbook"; }
    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('R', 'k', '2', 'k'); }

    void initParameter(uint32_t index, Parameter& parameter) override
    {
        parameter.hints = kParameterIsAutomatable;

        switch (index) {
        case kInput:
            parameter.name = "Input";
            parameter.symbol = "input";
            parameter.ranges.def = 0.5f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            break;
        case kBlend:
            parameter.name = "Blend";
            parameter.symbol = "blend";
            parameter.ranges.def = 0.5f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            break;
        case kFdbk:
            parameter.name = "FDBK";
            parameter.symbol = "fdbk";
            parameter.ranges.def = 0.3f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.05f;
            break;
        case kFilter:
            parameter.name = "Filter";
            parameter.symbol = "filter";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = -1.0f;
            parameter.ranges.max = 1.0f;
            break;
        case kDepth:
            parameter.name = "Depth";
            parameter.symbol = "depth";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            break;
        case kRate:
            parameter.name = "Rate";
            parameter.symbol = "rate";
            parameter.unit = "Hz";
            parameter.ranges.def = 2.0f;
            parameter.ranges.min = 0.05f;
            parameter.ranges.max = 10.0f;
            break;
        case kTimeMs:
            parameter.name = "Time";
            parameter.symbol = "time_ms";
            parameter.unit = "ms";
            parameter.ranges.def = 350.0f;
            parameter.ranges.min = 1.0f;
            parameter.ranges.max = 1023.0f;
            break;
        case kModOn:
            parameter.name = "Mod";
            parameter.symbol = "mod_on";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            parameter.hints |= kParameterIsInteger;
            break;
        case kSub:
            parameter.name = "Phase / Sub";
            parameter.symbol = "sub";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            parameter.hints |= kParameterIsInteger;
            break;
        case kStereoMode:
            parameter.name = "Stereo Mode";
            parameter.symbol = "stereo_mode";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            parameter.hints |= kParameterIsInteger;
            break;
        case kHold:
            parameter.name = "Hold";
            parameter.symbol = "hold";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 1.0f;
            parameter.hints |= kParameterIsInteger;
            break;
        }
    }

    float getParameterValue(uint32_t index) const override
    {
        switch (index) {
        case kInput: return fInput;
        case kBlend: return fBlend;
        case kFdbk: return fFdbk;
        case kFilter: return fFilter;
        case kDepth: return fDepth;
        case kRate: return fRate;
        case kTimeMs: return fTimeMs;
        case kModOn: return fModOn;
        case kSub: return fSub;
        case kStereoMode: return fStereoMode;
        case kHold: return fHold;
        default: return 0.0f;
        }
    }

    void setParameterValue(uint32_t index, float value) override
    {
        switch (index) {
        case kInput: fInput = rk2000Clamp01(value); break;
        case kBlend: fBlend = rk2000Clamp01(value); break;
        case kFdbk: fFdbk = value; break;
        case kFilter: fFilter = value; break;
        case kDepth: fDepth = rk2000Clamp01(value); break;
        case kRate: fRate = value; break;
        case kTimeMs: fTimeMs = value; break;
        case kModOn: fModOn = value; break;
        case kSub: fSub = value; break;
        case kStereoMode: fStereoMode = value; break;
        case kHold: fHold = value; break;
        }
    }

    void activate() override
    {
        const float sr = static_cast<float>(getSampleRate());
        const float maxDelayMs = 1023.0f;
        const float modHeadroomMs = 60.0f;
        fBufLen = static_cast<uint32_t>((maxDelayMs + modHeadroomMs) * sr / 1000.0f) + 16;

        delete[] fBufL;
        delete[] fBufR;
        fBufL = new float[fBufLen];
        fBufR = new float[fBufLen];
        for (uint32_t i = 0; i < fBufLen; ++i) {
            fBufL[i] = 0.0f;
            fBufR[i] = 0.0f;
        }
        fWritePos = 0;
        fLpStateL = 0.0f;
        fLpStateR = 0.0f;
        fLfoPhase = 0.0f;
    }

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

        const float sr = static_cast<float>(getSampleRate());
        const float inputGain = 1.0f + fInput * 3.0f;
        const float blend = fBlend;
        const bool pingPong = (fStereoMode > 0.5f);
        const bool holdOn = (fHold > 0.5f);
        const bool subInvert = (fSub > 0.5f);
        const bool modOn = (fModOn > 0.5f);
        const float fdbk = holdOn ? 0.999f : std::min(fFdbk, 1.0f);
        const float fdbkSign = subInvert ? -1.0f : 1.0f;

        const float baseDelaySamples = (fTimeMs / 1000.0f) * sr;
        const float modAmpSamples = fDepth * 0.005f * sr;
        const float lfoInc = fRate / sr;

        const float filterAmt = fFilter;
        float lpCoef = 0.0f, hpCoef = 0.0f;
        bool useLp = (filterAmt > 0.0f);
        bool useHp = (filterAmt < 0.0f);
        if (useLp) {
            const float cutoff = 8000.0f - filterAmt * 7000.0f;
            lpCoef = 1.0f - std::exp(-2.0f * 3.14159265f * cutoff / sr);
        }
        if (useHp) {
            const float cutoff = 80.0f + (-filterAmt) * 1500.0f;
            hpCoef = 1.0f - std::exp(-2.0f * 3.14159265f * cutoff / sr);
        }

        for (uint32_t i = 0; i < frames; ++i) {
            float lfoVal = 0.0f;
            if (modOn) {
                lfoVal = std::sin(fLfoPhase * 2.0f * 3.14159265f) * modAmpSamples;
                fLfoPhase += lfoInc;
                if (fLfoPhase >= 1.0f) fLfoPhase -= 1.0f;
            }

            float readPosF = static_cast<float>(fWritePos) - baseDelaySamples + lfoVal;
            while (readPosF < 0.0f) readPosF += static_cast<float>(fBufLen);

            const uint32_t idx0 = static_cast<uint32_t>(readPosF) % fBufLen;
            const uint32_t idx1 = (idx0 + 1) % fBufLen;
            const float frac = readPosF - std::floor(readPosF);

            const float delayedL = fBufL[idx0] + (fBufL[idx1] - fBufL[idx0]) * frac;
            const float delayedR = fBufR[idx0] + (fBufR[idx1] - fBufR[idx0]) * frac;

            float filteredL = delayedL;
            float filteredR = delayedR;
            if (useLp) {
                fLpStateL += (delayedL - fLpStateL) * lpCoef;
                fLpStateR += (delayedR - fLpStateR) * lpCoef;
                filteredL = fLpStateL;
                filteredR = fLpStateR;
            } else if (useHp) {
                fLpStateL += (delayedL - fLpStateL) * hpCoef;
                fLpStateR += (delayedR - fLpStateR) * hpCoef;
                filteredL = delayedL - fLpStateL;
                filteredR = delayedR - fLpStateR;
            }

            const float dryL = inL[i];
            const float dryR = inR[i];
            const float satL = std::tanh(dryL * inputGain) / std::tanh(inputGain + 1e-6f);
            const float satR = std::tanh(dryR * inputGain) / std::tanh(inputGain + 1e-6f);

            float feedL, feedR;
            if (pingPong) {
                feedL = filteredR * fdbk * fdbkSign;
                feedR = filteredL * fdbk * fdbkSign;
            } else {
                feedL = filteredL * fdbk * fdbkSign;
                feedR = filteredR * fdbk * fdbkSign;
            }

            if (holdOn) {
                fBufL[fWritePos] = rk2000Quantize12(filteredL * fdbkSign * fdbk);
                fBufR[fWritePos] = rk2000Quantize12(filteredR * fdbkSign * fdbk);
            } else {
                fBufL[fWritePos] = rk2000Quantize12(satL + feedL);
                fBufR[fWritePos] = rk2000Quantize12(satR + feedR);
            }

            fWritePos = (fWritePos + 1) % fBufLen;

            outL[i] = dryL * (1.0f - blend) + filteredL * blend;
            outR[i] = dryR * (1.0f - blend) + filteredR * blend;
        }
    }

private:
    float fInput, fBlend, fFdbk, fFilter;
    float fDepth, fRate, fTimeMs;
    float fModOn, fSub, fStereoMode, fHold;

    float* fBufL;
    float* fBufR;
    uint32_t fBufLen;
    uint32_t fWritePos;
    float fLpStateL, fLpStateR;
    float fLfoPhase;

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Rk2000Plugin)
};

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

END_NAMESPACE_DISTRHO
endef

define RK2000_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND       "ModCookbook"
#define DISTRHO_PLUGIN_NAME        "RK2000"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:rk2000"

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

enum Parameters {
    kInput = 0,
    kBlend,
    kFdbk,
    kFilter,
    kDepth,
    kRate,
    kTimeMs,
    kModOn,
    kSub,
    kStereoMode,
    kHold,
    kParameterCount
};

#endif
endef

define RK2000_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = rk2000
FILES_DSP = Rk2000Plugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

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

<urn:mod-cookbook:rk2000>
    a lv2:Plugin , lv2:DelayPlugin ;
    lv2:binary <rk2000_dsp.so> ;
    rdfs:seeAlso <rk2000.ttl> .
endef

define RK2000_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 rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .

<urn:mod-cookbook:rk2000>
    a lv2:Plugin , lv2:DelayPlugin ;
    doap:name "RK2000" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [
        foaf:name "ModCookbook" ;
        foaf:homepage <https://mod.audio>
    ] ;
    rdfs:comment "12-bit-style stereo modulation delay inspired by the Ibanez DM2000 / Keeley RK2000." ;
    lv2:port [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 0 ;
        lv2:symbol "in_l" ;
        lv2:name "Audio In Left"
    ] , [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 1 ;
        lv2:symbol "in_r" ;
        lv2:name "Audio In Right"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 2 ;
        lv2:symbol "out_l" ;
        lv2:name "Audio Out Left"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 3 ;
        lv2:symbol "out_r" ;
        lv2:name "Audio Out Right"
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 4 ;
        lv2:symbol "input" ;
        lv2:name "Input" ;
        lv2:default 0.5 ;
        lv2:minimum 0.0 ;
        lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ;
        lv2:symbol "blend" ;
        lv2:name "Blend" ;
        lv2:default 0.5 ;
        lv2:minimum 0.0 ;
        lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ;
        lv2:symbol "fdbk" ;
        lv2:name "FDBK" ;
        lv2:default 0.3 ;
        lv2:minimum 0.0 ;
        lv2:maximum 1.05
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ;
        lv2:symbol "filter" ;
        lv2:name "Filter" ;
        lv2:default 0.0 ;
        lv2:minimum -1.0 ;
        lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ;
        lv2:symbol "depth" ;
        lv2:name "Depth" ;
        lv2:default 0.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ;
        lv2:symbol "rate" ;
        lv2:name "Rate" ;
        lv2:default 2.0 ;
        lv2:minimum 0.05 ;
        lv2:maximum 10.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ;
        lv2:symbol "time_ms" ;
        lv2:name "Time" ;
        lv2:default 350.0 ;
        lv2:minimum 1.0 ;
        lv2:maximum 1023.0 ;
        units:unit units:ms
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ;
        lv2:symbol "mod_on" ;
        lv2:name "Mod" ;
        lv2:default 0 ;
        lv2:minimum 0 ;
        lv2:maximum 1 ;
        lv2:portProperty lv2:integer ;
        lv2:portProperty lv2:toggled
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ;
        lv2:symbol "sub" ;
        lv2:name "Phase / Sub" ;
        lv2:default 0 ;
        lv2:minimum 0 ;
        lv2:maximum 1 ;
        lv2:portProperty lv2:integer ;
        lv2:portProperty lv2:toggled
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ;
        lv2:symbol "stereo_mode" ;
        lv2:name "Stereo Mode" ;
        lv2:default 0 ;
        lv2:minimum 0 ;
        lv2:maximum 1 ;
        lv2:portProperty lv2:integer ;
        lv2:portProperty lv2:enumeration ;
        lv2:scalePoint [ rdfs:label "Parallel Stereo"  ; rdf:value 0 ] ;
        lv2:scalePoint [ rdfs:label "Panning Ping-Pong" ; rdf:value 1 ]
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 14 ;
        lv2:symbol "hold" ;
        lv2:name "Hold" ;
        lv2:default 0 ;
        lv2:minimum 0 ;
        lv2:maximum 1 ;
        lv2:portProperty lv2:integer ;
        lv2:portProperty lv2:toggled
    ] .
endef

export RK2000_PLUGIN_CPP
export RK2000_PLUGIN_INFO_H
export RK2000_PLUGIN_MAKEFILE
export RK2000_MANIFEST_TTL
export RK2000_PLUGIN_TTL

define RK2000_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/rk2000
	printf '%s' "$$RK2000_PLUGIN_CPP"      > $(@D)/examples/rk2000/Rk2000Plugin.cpp
	printf '%s' "$$RK2000_PLUGIN_INFO_H"   > $(@D)/examples/rk2000/DistrhoPluginInfo.h
	printf '%s' "$$RK2000_PLUGIN_MAKEFILE" > $(@D)/examples/rk2000/Makefile
endef

define RK2000_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/rk2000 lv2_dsp
endef

define RK2000_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/rk2000.lv2
	cp $(@D)/bin/rk2000.lv2/rk2000_dsp.so $($(PKG)_PKGDIR)/rk2000.lv2/
	printf '%s' "$$RK2000_MANIFEST_TTL" > $($(PKG)_PKGDIR)/rk2000.lv2/manifest.ttl
	printf '%s' "$$RK2000_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/rk2000.lv2/rk2000.ttl
endef

$(eval $(generic-package))
