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

SHIMMER_PITCH_DELAY_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
SHIMMER_PITCH_DELAY_SITE = https://github.com/DISTRHO/DPF.git
SHIMMER_PITCH_DELAY_SITE_METHOD = git
SHIMMER_PITCH_DELAY_BUNDLES = shimmer-pitch-delay.lv2

define SHIMMER_PITCH_DELAY_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>

START_NAMESPACE_DISTRHO

class ShimmerPitchDelayPlugin : public Plugin
{
public:
    ShimmerPitchDelayPlugin()
        : Plugin(kParameterCount, 0, 0),
          fDelayMs(400.0f),
          fPitch(12.0f),
          fFeedback(40.0f),
          fAttack(30.0f),
          fDecay(40.0f),
          fMix(40.0f),
          fWriteIdx(0),
          fGrainPhase(0.0f),
          fDriftSamp(0.0f),
          fDampStateL(0.0f),
          fDampStateR(0.0f)
    {
        for (uint32_t i = 0; i < kBufferSize; ++i) {
            fBufferL[i] = 0.0f;
            fBufferR[i] = 0.0f;
        }
    }

protected:
    const char* getLabel()       const override { return "Shimmer Pitch Delay"; }
    const char* getDescription() const override { return "Stereo pitch-shifting delay where each repeat is shifted by a set interval and cascades through feedback, with adjustable attack softening and decay damping, inspired by shimmer/pitch-delay effects."; }
    const char* getMaker()       const override { return "MOD"; }
    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('S', 'h', 'P', 'd'); }

    void initParameter(uint32_t index, Parameter& parameter) override
    {
        switch (index) {
        case kDelayMs:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Delay";
            parameter.symbol     = "delay_ms";
            parameter.unit       = "ms";
            parameter.ranges.def = 400.0f;
            parameter.ranges.min = 10.0f;
            parameter.ranges.max = 2000.0f;
            break;
        case kPitch:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Pitch";
            parameter.symbol     = "pitch";
            parameter.unit       = "st";
            parameter.ranges.def = 12.0f;
            parameter.ranges.min = -12.0f;
            parameter.ranges.max = 12.0f;
            break;
        case kFeedback:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Feedback";
            parameter.symbol     = "feedback";
            parameter.unit       = "%";
            parameter.ranges.def = 40.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 95.0f;
            break;
        case kAttack:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Attack";
            parameter.symbol     = "attack";
            parameter.unit       = "%";
            parameter.ranges.def = 30.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 100.0f;
            break;
        case kDecay:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Decay";
            parameter.symbol     = "decay";
            parameter.unit       = "%";
            parameter.ranges.def = 40.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 100.0f;
            break;
        case kMix:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Mix";
            parameter.symbol     = "mix";
            parameter.unit       = "%";
            parameter.ranges.def = 40.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 100.0f;
            break;
        }
    }

    float getParameterValue(uint32_t index) const override
    {
        switch (index) {
        case kDelayMs:  return fDelayMs;
        case kPitch:    return fPitch;
        case kFeedback: return fFeedback;
        case kAttack:   return fAttack;
        case kDecay:    return fDecay;
        case kMix:      return fMix;
        default:        return 0.0f;
        }
    }

    void setParameterValue(uint32_t index, float value) override
    {
        switch (index) {
        case kDelayMs:  fDelayMs = value;  break;
        case kPitch:    fPitch = value;    break;
        case kFeedback: fFeedback = value; break;
        case kAttack:   fAttack = value;   break;
        case kDecay:    fDecay = value;    break;
        case kMix:      fMix = value;      break;
        }
    }

    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 double sr = getSampleRate();

        const float fb        = fFeedback * 0.01f;
        const float attackNrm = fAttack * 0.01f;
        const float decayNrm  = fDecay * 0.01f;
        const float mixNrm    = fMix * 0.01f;

        const float baseDelaySamp = (float)(fDelayMs * 0.001 * sr);

        // Pitch ratio for the shifted read pointer (1.0 = no shift).
        const float pitchRatio = std::pow(2.0f, fPitch / 12.0f);

        // Grain size for the overlapping-pointer pitch shifter. A fixed
        // ~60ms grain works well across the musical pitch range without
        // excessive granular artifacts.
        const float grainSamp = (float)(0.06 * sr);
        const float grainInc  = 1.0f / grainSamp;

        // Attack: softens the onset of each new grain/repeat. Higher
        // attackNrm = slower rise from the start of each grain window.
        const float attackRise = 0.5f - attackNrm * 0.495f; // smaller = slower attack, retriggered per grain

        // Decay damping: one-pole low-pass coefficient applied inside the
        // feedback loop, simulating progressive "warmth loss" per repeat.
        // Higher decayNrm = darker/faster damping = quicker perceived decay.
        const float dampCoeff = 0.05f + decayNrm * 0.5f;

        for (uint32_t i = 0; i < frames; ++i) {
            float delaySamp = baseDelaySamp;
            if (delaySamp < (grainSamp + 4.0f)) delaySamp = grainSamp + 4.0f;
            if (delaySamp > (float)(kBufferSize - (int)grainSamp - 4)) delaySamp = (float)(kBufferSize - (int)grainSamp - 4);

            // fDriftSamp accumulates continuously at (1 - pitchRatio) samples
            // per sample, so the read pointer steadily pulls away from (or
            // toward) the write pointer. This is what actually produces the
            // pitch shift: a faster-draining or slower-draining read tap.
            // Each grain just reads a windowed snapshot of that drifting
            // position, and the two staggered grains crossfade across the
            // periodic phase wraps so the drift can keep accumulating
            // indefinitely without ever exceeding one grain length of jump.
            const float grainPos = fGrainPhase * grainSamp;
            const float grainPosB = std::fmod(fGrainPhase + 0.5f, 1.0f) * grainSamp;

            const float windowA = std::sin((float)M_PI * fGrainPhase);
            const float windowB = std::sin((float)M_PI * std::fmod(fGrainPhase + 0.5f, 1.0f));
            const float winA2 = windowA * windowA;
            const float winB2 = windowB * windowB;
            const float winSum = winA2 + winB2 > 0.0001f ? winA2 + winB2 : 1.0f;

            // Each grain reads from (write - delay - drift), with a small
            // per-grain offset so the two staggered grains sample slightly
            // different points, smoothing the wrap discontinuity.
            float readPosAL = (float)fWriteIdx - delaySamp - fDriftSamp - grainPos;
            float readPosBL = (float)fWriteIdx - delaySamp - fDriftSamp - grainPosB;
            float readPosAR = readPosAL;
            float readPosBR = readPosBL;

            while (readPosAL < 0.0f) readPosAL += (float)kBufferSize;
            while (readPosBL < 0.0f) readPosBL += (float)kBufferSize;
            while (readPosAR < 0.0f) readPosAR += (float)kBufferSize;
            while (readPosBR < 0.0f) readPosBR += (float)kBufferSize;

            const float sampleAL = readInterp(fBufferL, readPosAL);
            const float sampleBL = readInterp(fBufferL, readPosBL);
            const float sampleAR = readInterp(fBufferR, readPosAR);
            const float sampleBR = readInterp(fBufferR, readPosBR);

            // Attack: build an envelope from each grain's own phase so the
            // onset of every repeat is softened by the same amount,
            // retriggering naturally as grains cycle. Using each grain's
            // individual phase (not the shared fGrainPhase) keeps both
            // overlapping grains independently shaped.
            const float phaseA = fGrainPhase;
            const float phaseB = std::fmod(fGrainPhase + 0.5f, 1.0f);
            const float envA = std::min(1.0f, phaseA / attackRise);
            const float envB = std::min(1.0f, phaseB / attackRise);

            const float softenedAL = sampleAL * envA;
            const float softenedBL = sampleBL * envB;
            const float softenedAR = sampleAR * envA;
            const float softenedBR = sampleBR * envB;

            const float shiftedL = (softenedAL * winA2 + softenedBL * winB2) / winSum;
            const float shiftedR = (softenedAR * winA2 + softenedBR * winB2) / winSum;

            // Decay damping: one-pole low-pass inside the feedback path.
            fDampStateL += (shiftedL - fDampStateL) * dampCoeff;
            fDampStateR += (shiftedR - fDampStateR) * dampCoeff;

            // Write input plus damped, pitch-shifted feedback into the buffer.
            const float wL = inL[i] + fDampStateL * fb;
            const float wR = inR[i] + fDampStateR * fb;
            fBufferL[fWriteIdx] = wL;
            fBufferR[fWriteIdx] = wR;

            const float wetL = shiftedL;
            const float wetR = shiftedR;

            outL[i] = inL[i] * (1.0f - mixNrm) + wetL * mixNrm;
            outR[i] = inR[i] * (1.0f - mixNrm) + wetR * mixNrm;

            fWriteIdx = (fWriteIdx + 1) % kBufferSize;

            fGrainPhase += grainInc;
            if (fGrainPhase >= 1.0f)
                fGrainPhase -= 1.0f;

            // Accumulate the drift that produces the pitch shift. Wrapping
            // modulo grainSamp keeps it bounded; since the window crossfade
            // already provides a seamless handoff every half-grain, wrapping
            // the drift by a whole grain length introduces no audible
            // discontinuity (it just changes which absolute buffer position
            // the next grain starts reading from by a full cycle).
            fDriftSamp += (1.0f - pitchRatio);
            if (fDriftSamp >= grainSamp) fDriftSamp -= grainSamp;
            if (fDriftSamp < 0.0f) fDriftSamp += grainSamp;
        }
    }

private:
    static const uint32_t kBufferSize = 98304; // headroom for 2000ms delay at high sample rates plus grain size

    static float readInterp(const float* buf, float pos)
    {
        const uint32_t idx0 = (uint32_t)pos % kBufferSize;
        const uint32_t idx1 = (idx0 + 1) % kBufferSize;
        const float frac = pos - std::floor(pos);
        return buf[idx0] + (buf[idx1] - buf[idx0]) * frac;
    }

    float fDelayMs;
    float fPitch;
    float fFeedback;
    float fAttack;
    float fDecay;
    float fMix;

    uint32_t fWriteIdx;
    float fGrainPhase;
    float fDriftSamp;
    float fDampStateL;
    float fDampStateR;

    float fBufferL[kBufferSize];
    float fBufferR[kBufferSize];

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ShimmerPitchDelayPlugin)
};

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

END_NAMESPACE_DISTRHO
endef

define SHIMMER_PITCH_DELAY_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND       "MOD"
#define DISTRHO_PLUGIN_NAME        "Shimmer Pitch Delay"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:shimmer-pitch-delay"

#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 {
    kDelayMs = 0,
    kPitch,
    kFeedback,
    kAttack,
    kDecay,
    kMix,
    kParameterCount
};

#endif
endef

define SHIMMER_PITCH_DELAY_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = shimmer-pitch-delay
FILES_DSP = ShimmerPitchDelayPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

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

<urn:mod-cookbook:shimmer-pitch-delay>
    a lv2:Plugin , lv2:DelayPlugin ;
    lv2:binary <shimmer-pitch-delay_dsp.so> ;
    rdfs:seeAlso <shimmer-pitch-delay.ttl> .
endef

define SHIMMER_PITCH_DELAY_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:shimmer-pitch-delay>
    a lv2:Plugin , lv2:DelayPlugin ;
    doap:name "Shimmer Pitch Delay" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [
        foaf:name "MOD" ;
        foaf:homepage <https://mod.audio>
    ] ;
    rdfs:comment "Stereo pitch-shifting delay where each repeat is shifted by a set interval and cascades through feedback, with adjustable attack softening and decay damping, inspired by shimmer/pitch-delay effects." ;
    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 "delay_ms" ;
        lv2:name "Delay" ;
        lv2:default 400.0 ;
        lv2:minimum 10.0 ;
        lv2:maximum 2000.0 ;
        units:unit units:ms
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ;
        lv2:symbol "pitch" ;
        lv2:name "Pitch" ;
        lv2:default 12.0 ;
        lv2:minimum -12.0 ;
        lv2:maximum 12.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ;
        lv2:symbol "feedback" ;
        lv2:name "Feedback" ;
        lv2:default 40.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 95.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ;
        lv2:symbol "attack" ;
        lv2:name "Attack" ;
        lv2:default 30.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ;
        lv2:symbol "decay" ;
        lv2:name "Decay" ;
        lv2:default 40.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ;
        lv2:symbol "mix" ;
        lv2:name "Mix" ;
        lv2:default 40.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 100.0 ;
        units:unit units:pc
    ] .
endef

export SHIMMER_PITCH_DELAY_PLUGIN_CPP
export SHIMMER_PITCH_DELAY_PLUGIN_INFO_H
export SHIMMER_PITCH_DELAY_PLUGIN_MAKEFILE
export SHIMMER_PITCH_DELAY_MANIFEST_TTL
export SHIMMER_PITCH_DELAY_PLUGIN_TTL

define SHIMMER_PITCH_DELAY_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/shimmer-pitch-delay
	printf '%s' "$$SHIMMER_PITCH_DELAY_PLUGIN_CPP"      > $(@D)/examples/shimmer-pitch-delay/ShimmerPitchDelayPlugin.cpp
	printf '%s' "$$SHIMMER_PITCH_DELAY_PLUGIN_INFO_H"   > $(@D)/examples/shimmer-pitch-delay/DistrhoPluginInfo.h
	printf '%s' "$$SHIMMER_PITCH_DELAY_PLUGIN_MAKEFILE" > $(@D)/examples/shimmer-pitch-delay/Makefile
endef

define SHIMMER_PITCH_DELAY_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/shimmer-pitch-delay lv2_dsp
endef

define SHIMMER_PITCH_DELAY_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/shimmer-pitch-delay.lv2
	cp $(@D)/bin/shimmer-pitch-delay.lv2/shimmer-pitch-delay_dsp.so $($(PKG)_PKGDIR)/shimmer-pitch-delay.lv2/
	printf '%s' "$$SHIMMER_PITCH_DELAY_MANIFEST_TTL" > $($(PKG)_PKGDIR)/shimmer-pitch-delay.lv2/manifest.ttl
	printf '%s' "$$SHIMMER_PITCH_DELAY_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/shimmer-pitch-delay.lv2/shimmer-pitch-delay.ttl
endef

$(eval $(generic-package))
