# Eventide H90 Chorus - Multi-Type Modulated Chorus Engine (Repaired Fixed Phase Edition)
# Incorporating full H90 manual specifications & enumerated dropdown controls.
# Upload at https://builder.mod.audio/buildroot

H90_CHORUS_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
H90_CHORUS_SITE = https://github.com/DISTRHO/DPF.git
H90_CHORUS_SITE_METHOD = git
H90_CHORUS_BUNDLES = h90-chorus.lv2

# ---------------------------------------------------------------------------
# DistrhoPluginInfo.h
# ---------------------------------------------------------------------------
define H90_CHORUS_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND        "Eventide"
#define DISTRHO_PLUGIN_NAME         "H90 Chorus"
#define DISTRHO_PLUGIN_URI          "urn:mod-cookbook:h90-chorus"
#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 {
    kIntensity = 0,
    kType,
    kDepth,
    kSpeed,
    kShape,
    kFeedback,
    kDepthMod,
    kSpeedMod,
    kModRate,
    kModSource,
    kParameterCount
};

#endif
endef

# ---------------------------------------------------------------------------
# H90ChorusPlugin.cpp
# ---------------------------------------------------------------------------
define H90_CHORUS_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <algorithm>

START_NAMESPACE_DISTRHO

static const float kPi    = 3.14159265359f;
static const float kTwoPi = 6.28318530718f;

struct DelayLine {
    float* buf;
    int mask;
    int wpos;

    DelayLine() : buf(nullptr), mask(0), wpos(0) {}
    ~DelayLine() { delete[] buf; }

    void init(int n) {
        int sz = 1;
        while (sz < n) sz <<= 1;
        delete[] buf;
        buf = new float[sz]();
        mask = sz - 1;
        wpos = 0;
    }

    void write(float v) {
        buf[wpos & mask] = v;
        ++wpos;
    }

    float read(float d) const {
        if (d < 0.5f) d = 0.5f;
        float rd = (float)wpos - d - 1.f;
        int ri = (int)rd;
        float fr = rd - (float)ri;
        return buf[ri & mask] + fr * (buf[(ri + 1) & mask] - buf[ri & mask]);
    }
};

// Fixed Smooth LFO with absolute phase offset injection tracking
struct LFO {
    float phase, sah, sahPrev, env;

    LFO() : phase(0.f), sah(0.f), sahPrev(0.f), env(0.f) {}

    void reset() { phase = 0.f; sah = 0.f; sahPrev = 0.f; env = 0.f; }

    float tick(float rate, float sr, int shape, float offset = 0.0f, float audioIn = 0.f) {
        // Evaluate dynamic phase + global offset cleanly wrapped
        float p = phase + offset;
        while (p >= 1.f) p -= 1.f;
        while (p < 0.f)  p += 1.f;

        float out = 0.f;
        switch (shape) {
        case 0: out = std::sin(p * kTwoPi); break;
        case 1: out = (p < 0.5f) ? (4.f * p - 1.f) : (3.f - 4.f * p); break;
        case 2: out = std::sin(p * kPi) * 2.f - 1.f; break;
        case 3: out = sahPrev + (sah - sahPrev) * p; break;
        case 4: out = (p < 0.5f) ? 0.8f : -0.8f; break; // Slightly scaled down square edge
        case 5: out = 2.f * p - 1.f; break;
        case 6: out = sah; break;
        case 7: {
            float atk = std::exp(-1.f / (0.005f * sr));
            float rel = std::exp(-1.f / (0.150f * sr));
            float a = std::fabs(audioIn);
            float c = (a > env) ? atk : rel;
            env = c * env + (1.f - c) * a;
            out = std::min(1.f, env * 6.f) * 2.f - 1.f;
            break;
        }
        default: out = std::sin(p * kTwoPi); break;
        }
        
        phase += rate / sr;
        if (phase >= 1.f) {
            phase -= 1.f;
            sahPrev = sah;
            sah = ((float)std::rand() / (float)RAND_MAX) * 2.f - 1.f;
        }
        return out;
    }
};

struct OnePoleLP {
    float z1;
    OnePoleLP() : z1(0.f) {}
    float process(float in, float c) {
        z1 = (1.f - c) * in + c * z1;
        return z1;
    }
};

class H90ChorusPlugin : public Plugin
{
public:
    H90ChorusPlugin()
        : Plugin(kParameterCount, 0, 0),
          fIntensity(75.f), fType(0.f), fDepth(50.f), fSpeed(1.0f), fShape(0.f),
          fFeedback(0.f), fDepthMod(0.f), fSpeedMod(0.f), fModRate(0.3f), fModSource(0.f),
          fSr(48000.f)
    {
        for (int v = 0; v < 3; ++v) {
            fDelayL[v].init(16384);
            fDelayR[v].init(16384);
            fSmoothingFiltersL[v] = 0.0f;
            fSmoothingFiltersR[v] = 0.0f;
        }
    }

protected:
    const char* getLabel()       const override { return "H90 Chorus"; }
    const char* getDescription() const override { return "Multi-type chorus with distinct phase staggered voices."; }
    const char* getMaker()       const override { return "Eventide"; }
    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('H','9','0','C'); }

    void initParameter(uint32_t index, Parameter& p) override {
        p.hints = kParameterIsAutomatable;
        switch (index) {
        case kIntensity:
            p.name = "Intensity"; p.symbol = "intensity"; p.unit = "%";
            p.ranges.def = 75.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kType:
            p.name = "Type"; p.symbol = "type";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 3.f;
            p.hints |= kParameterIsInteger; break;
        case kDepth:
            p.name = "Depth"; p.symbol = "depth"; p.unit = "%";
            p.ranges.def = 50.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kSpeed:
            p.name = "Speed"; p.symbol = "speed"; p.unit = "Hz";
            p.ranges.def = 1.0f; p.ranges.min = 0.01f; p.ranges.max = 10.f; break;
        case kShape:
            p.name = "Shape"; p.symbol = "shape";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 7.f;
            p.hints |= kParameterIsInteger; break;
        case kFeedback:
            p.name = "Feedback"; p.symbol = "feedback"; p.unit = "%";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 99.f; break;
        case kDepthMod:
            p.name = "Depth Mod"; p.symbol = "depth_mod"; p.unit = "%";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kSpeedMod:
            p.name = "Speed Mod"; p.symbol = "speed_mod"; p.unit = "%";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kModRate:
            p.name = "Mod Rate"; p.symbol = "mod_rate"; p.unit = "Hz";
            p.ranges.def = 0.3f; p.ranges.min = 0.01f; p.ranges.max = 10.f; break;
        case kModSource:
            p.name = "Mod Source"; p.symbol = "mod_source";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 7.f;
            p.hints |= kParameterIsInteger; break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch (index) {
        case kIntensity: return fIntensity;
        case kType:      return fType;
        case kDepth:     return fDepth;
        case kSpeed:     return fSpeed;
        case kShape:     return fShape;
        case kFeedback:  return fFeedback;
        case kDepthMod:  return fDepthMod;
        case kSpeedMod:  return fSpeedMod;
        case kModRate:   return fModRate;
        case kModSource: return fModSource;
        }
        return 0.f;
    }

    void setParameterValue(uint32_t index, float value) override {
        switch (index) {
        case kIntensity: fIntensity = value; break;
        case kType:      fType = value; break;
        case kDepth:     fDepth = value; break;
        case kSpeed:     fSpeed = value; break;
        case kShape:     fShape = value; break;
        case kFeedback:  fFeedback = value; break;
        case kDepthMod:  fDepthMod = value; break;
        case kSpeedMod:  fSpeedMod = value; break;
        case kModRate:   fModRate = value; break;
        case kModSource: fModSource = value; break;
        }
    }

    void sampleRateChanged(double newSampleRate) override {
        fSr = (float)newSampleRate;
        for (int v = 0; v < 3; ++v) {
            fLFO[v].reset();
            fSmoothingFiltersL[v] = 0.0f;
            fSmoothingFiltersR[v] = 0.0f;
        }
        fModLFO.reset();
    }

    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];

        float intensity  = fIntensity * 0.01f;
        
        // Repaired True Chorus Base Delays: centered smoothly at 15 milliseconds
        float baseDelayMs = 15.0f; 
        float baseDepthSamp = (baseDelayMs * 0.001f) * fSr;
        
        // Repaired Wave Swing: maps up to an expansive 8ms swing depth window
        float maxSwingSamp = (8.0f * 0.001f) * fSr * (fDepth * 0.01f);

        float feedback   = fFeedback * 0.01f * 0.92f;
        int   type       = (int)fType;
        int   shape      = (int)fShape;
        float modSpeed   = fSpeed;
        float fbLPc      = 0.25f;

        float depthModAmt = fDepthMod * 0.01f;
        float speedModAmt = fSpeedMod * 0.01f;
        float modRate     = fModRate;
        int   modShape    = (int)fModSource;

        // Anti-click slew smoothing coefficient (approx 20Hz lowpass filter coefficient)
        float SlewCoeff = 1.0f - std::exp(-2.0f * kPi * 20.0f / fSr);

        for (uint32_t i = 0; i < frames; ++i) {
            float l = inL[i];
            float r = inR[i];

            float modLfoOut = fModLFO.tick(modRate, fSr, modShape, 0.0f, (l + r) * 0.5f);
            float curSpeed = modSpeed * (1.f + speedModAmt * modLfoOut * 0.75f);
            if (curSpeed < 0.01f) curSpeed = 0.01f;

            float curSwing = maxSwingSamp * (1.f + depthModAmt * modLfoOut * 0.5f);
            if (curSwing < 0.f) curSwing = 0.f;

            float sumL = 0.f, sumR = 0.f;

            // 3-Voice Processing Loop
            for (int v = 0; v < 3; ++v) {
                // REPAIRED: Spread target phase points uniformly (0, 120, 240 degrees)
                float phaseOffset = (float)v * 0.33333f;
                
                float lfoOut = fLFO[v].tick(curSpeed, fSr, shape, phaseOffset, (l + r) * 0.5f);
                
                float targetDelL = baseDepthSamp + (curSwing * lfoOut);
                
                // Type 0 (Liquid) or Type 3 (Classic) phase manipulation styles
                float targetDelR = (type == 0 || type == 3) ? (baseDepthSamp + (curSwing * (-lfoOut))) : targetDelL;

                // REPAIRED: Slew-rate limiter smoothing filters completely remove stepping clicks
                fSmoothingFiltersL[v] += SlewCoeff * (targetDelL - fSmoothingFiltersL[v]);
                fSmoothingFiltersR[v] += SlewCoeff * (targetDelR - fSmoothingFiltersR[v]);

                float finalDelL = std::max(2.0f, fSmoothingFiltersL[v]);
                float finalDelR = std::max(2.0f, fSmoothingFiltersR[v]);

                float rdL = fDelayL[v].read(finalDelL);
                float rdR = fDelayR[v].read(finalDelR);

                float fbL = 0.f, fbR = 0.f;
                if (type == 0 || type == 2) {
                    fbL = fFbLP[v * 2    ].process(rdL * feedback, fbLPc);
                    fbR = fFbLP[v * 2 + 1].process(rdR * feedback, fbLPc);
                }

                fDelayL[v].write(l + fbL);
                fDelayR[v].write(r + fbR);

                sumL += rdL;
                sumR += rdR;
            }
            
            sumL *= 0.33333f;
            sumR *= 0.33333f;

            if (type == 1) { // Organic Mode micro-noise layer
                float noise = (((float)std::rand() / (float)RAND_MAX) * 2.f - 1.f) * 0.002f;
                sumL += noise; sumR += noise;
            }

            outL[i] = l * (1.f - intensity) + sumL * intensity;
            outR[i] = r * (1.f - intensity) + sumR * intensity;
        }
    }

private:
    float fIntensity, fType, fDepth, fSpeed, fShape;
    float fFeedback, fDepthMod, fSpeedMod, fModRate, fModSource;
    float fSr;

    DelayLine fDelayL[3], fDelayR[3];
    LFO fLFO[3];
    LFO fModLFO;
    OnePoleLP fFbLP[6];
    
    // Smooth target trackers to eliminate clicks on rough wave selections
    float fSmoothingFiltersL[3];
    float fSmoothingFiltersR[3];

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(H90ChorusPlugin)
};

Plugin* createPlugin() { return new H90ChorusPlugin(); }

END_NAMESPACE_DISTRHO
endef

# ---------------------------------------------------------------------------
# Makefile
# ---------------------------------------------------------------------------
define H90_CHORUS_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME      = h90-chorus
FILES_DSP = H90ChorusPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

# ---------------------------------------------------------------------------
# LV2 TTL Manifest
# ---------------------------------------------------------------------------
define H90_CHORUS_MANIFEST_TTL
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<urn:mod-cookbook:h90-chorus>
    a lv2:Plugin ;
    lv2:binary <h90-chorus_dsp.so> ;
    rdfs:seeAlso <h90-chorus.ttl> .
endef

# ---------------------------------------------------------------------------
# LV2 TTL Plugin Configuration
# ---------------------------------------------------------------------------
define H90_CHORUS_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:h90-chorus>
    a lv2:Plugin , lv2:ChorusPlugin ;
    doap:name "H90 Chorus" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "Eventide" ] ;
    rdfs:comment "Multi-type chorus with distinct phase staggered voices. Eventide H90 Chorus emulation." ;

    lv2:port [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 0 ; lv2:symbol "in_l" ; lv2:name "Input L"
    ] , [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 1 ; lv2:symbol "in_r" ; lv2:name "Input R"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 2 ; lv2:symbol "out_l" ; lv2:name "Output L"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 3 ; lv2:symbol "out_r" ; rdf:name "Output R"
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 4 ; lv2:symbol "intensity" ; lv2:name "Intensity" ;
        lv2:default 75.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "type" ; lv2:name "Type" ;
        lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 3 ;
        lv2:portProperty lv2:integer , lv2:enumeration ;
        lv2:scalePoint [ rdfs:label "Liquid" ;  rdf:value 0 ] ;
        lv2:scalePoint [ rdfs:label "Organic" ; rdf:value 1 ] ;
        lv2:scalePoint [ rdfs:label "Shimmer" ; rdf:value 2 ] ;
        lv2:scalePoint [ rdfs:label "Classic" ; rdf:value 3 ] ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ; lv2:symbol "depth" ; lv2:name "Depth" ;
        lv2:default 50.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "speed" ; lv2:name "Speed" ;
        lv2:default 1.0 ; lv2:minimum 0.01 ; lv2:maximum 10.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ; lv2:symbol "shape" ; lv2:name "Shape" ;
        lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 7 ;
        lv2:portProperty lv2:integer , lv2:enumeration ;
        lv2:scalePoint [ rdfs:label "Sine" ;     rdf:value 0 ] ;
        lv2:scalePoint [ rdfs:label "Triangle" ; rdf:value 1 ] ;
        lv2:scalePoint [ rdfs:label "Peak" ;     rdf:value 2 ] ;
        lv2:scalePoint [ rdfs:label "Random" ;   rdf:value 3 ] ;
        lv2:scalePoint [ rdfs:label "Square" ;   rdf:value 4 ] ;
        lv2:scalePoint [ rdfs:label "Ramp" ;     rdf:value 5 ] ;
        lv2:scalePoint [ rdfs:label "SampHold" ; rdf:value 6 ] ;
        lv2:scalePoint [ rdfs:label "Envelope" ; rdf:value 7 ] ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; lv2:symbol "feedback" ; lv2:name "Feedback" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 99.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ; lv2:symbol "depth_mod" ; lv2:name "Depth Mod" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "speed_mod" ; lv2:name "Speed Mod" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ; lv2:symbol "mod_rate" ; lv2:name "Mod Rate" ;
        lv2:default 0.3 ; lv2:minimum 0.01 ; lv2:maximum 10.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ; lv2:symbol "mod_source" ; lv2:name "Mod Source" ;
        lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 7 ;
        lv2:portProperty lv2:integer , lv2:enumeration ;
        lv2:scalePoint [ rdfs:label "Sine" ;     rdf:value 0 ] ;
        lv2:scalePoint [ rdfs:label "Triangle" ; rdf:value 1 ] ;
        lv2:scalePoint [ rdfs:label "Peak" ;     rdf:value 2 ] ;
        lv2:scalePoint [ rdfs:label "Random" ;   rdf:value 3 ] ;
        lv2:scalePoint [ rdfs:label "Square" ;   rdf:value 4 ] ;
        lv2:scalePoint [ rdfs:label "Ramp" ;     rdf:value 5 ] ;
        lv2:scalePoint [ rdfs:label "SampHold" ; rdf:value 6 ] ;
        lv2:scalePoint [ rdfs:label "Envelope" ; rdf:value 7 ] ;
    ] .
endef

export H90_CHORUS_PLUGIN_CPP H90_CHORUS_PLUGIN_INFO_H H90_CHORUS_PLUGIN_MAKEFILE
export H90_CHORUS_MANIFEST_TTL H90_CHORUS_PLUGIN_TTL

# ---------------------------------------------------------------------------
# Buildroot Platform Compilation Commands
# ---------------------------------------------------------------------------
define H90_CHORUS_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/h90-chorus
	printf '%s' "$$H90_CHORUS_PLUGIN_CPP"      > $(@D)/examples/h90-chorus/H90ChorusPlugin.cpp
	printf '%s' "$$H90_CHORUS_PLUGIN_INFO_H"   > $(@D)/examples/h90-chorus/DistrhoPluginInfo.h
	printf '%s' "$$H90_CHORUS_PLUGIN_MAKEFILE" > $(@D)/examples/h90-chorus/Makefile
endef

define H90_CHORUS_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/h90-chorus lv2_dsp
endef

define H90_CHORUS_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/h90-chorus.lv2
	cp $(@D)/bin/h90-chorus.lv2/h90-chorus_dsp.so $($(PKG)_PKGDIR)/h90-chorus.lv2/
	printf '%s' "$$H90_CHORUS_MANIFEST_TTL" > $($(PKG)_PKGDIR)/h90-chorus.lv2/manifest.ttl
	printf '%s' "$$H90_CHORUS_PLUGIN_TTL" > $($(PKG)_PKGDIR)/h90-chorus.lv2/h90-chorus.ttl
endef

$(eval $(generic-package))