################################################################################
# H90 CrushStation - Overdrive/distortion with octaves and sag
# Eventide H90 algorithm emulation for the MOD Dwarf
# Upload at https://builder.mod.audio/buildroot with a MOD unit connected.
################################################################################

H90_CRUSHSTATION_VERSION     = 61d38eb638449647fb8395a35c5b8dab7e981ba7
H90_CRUSHSTATION_SITE        = https://github.com/DISTRHO/DPF.git
H90_CRUSHSTATION_SITE_METHOD = git
H90_CRUSHSTATION_BUNDLES     = h90-crushstation.lv2

define H90_CRUSHSTATION_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED

#define DISTRHO_PLUGIN_BRAND       "H90"
#define DISTRHO_PLUGIN_NAME        "CrushStation"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:h90-crushstation"
#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 {
    kMix = 0,
    kDrive,
    kCompressor,
    kSag,
    kOctaves,
    kGrit,
    kBass,
    kMids,
    kMidsFreq,
    kTreble,
    kParameterCount
};

#endif
endef

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

START_NAMESPACE_DISTRHO

static const float kPi    = 3.14159265359f;
static const float kTwoPi = 6.28318530718f;

// One-pole LP with own state
struct LP {
    float z1; LP():z1(0.f){}
    float process(float in, float c){ z1=(1.f-c)*in+c*z1; return z1; }
};

// Simple peak EQ biquad (direct form 1)
struct PeakEQ {
    float x1,x2,y1,y2;
    PeakEQ():x1(0.f),x2(0.f),y1(0.f),y2(0.f){}
    float process(float in, float freq, float gain_db, float Q, float sr){
        float A     = powf(10.f, gain_db/40.f);
        float w0    = kTwoPi*freq/sr;
        float alpha = sinf(w0)/(2.f*Q);
        float b0    = 1.f+alpha*A;
        float b1    = -2.f*cosf(w0);
        float b2    = 1.f-alpha*A;
        float a0    = 1.f+alpha/A;
        float a1    = b1;
        float a2    = 1.f-alpha/A;
        float out   = (b0/a0)*in+(b1/a0)*x1+(b2/a0)*x2
                     -(a1/a0)*y1-(a2/a0)*y2;
        x2=x1; x1=in; y2=y1; y1=out;
        return out;
    }
};

// Low shelf biquad
struct LowShelf {
    float x1,x2,y1,y2;
    LowShelf():x1(0.f),x2(0.f),y1(0.f),y2(0.f){}
    float process(float in, float gain_db, float sr){
        float fc    = 400.f;
        float A     = powf(10.f, gain_db/40.f);
        float w0    = kTwoPi*fc/sr;
        float cosw  = cosf(w0);
        float sinw  = sinf(w0);
        float alpha = sinw/2.f*sqrtf((A+1.f/A)*(1.f/0.9f-1.f)+2.f);
        float b0    =   A*((A+1.f)-(A-1.f)*cosw+2.f*sqrtf(A)*alpha);
        float b1    = 2.f*A*((A-1.f)-(A+1.f)*cosw);
        float b2    =   A*((A+1.f)-(A-1.f)*cosw-2.f*sqrtf(A)*alpha);
        float a0    =      (A+1.f)+(A-1.f)*cosw+2.f*sqrtf(A)*alpha;
        float a1    =  -2.f*((A-1.f)+(A+1.f)*cosw);
        float a2    =       (A+1.f)+(A-1.f)*cosw-2.f*sqrtf(A)*alpha;
        float out   = (b0/a0)*in+(b1/a0)*x1+(b2/a0)*x2
                     -(a1/a0)*y1-(a2/a0)*y2;
        x2=x1; x1=in; y2=y1; y1=out;
        return out;
    }
};

// High shelf biquad
struct HighShelf {
    float x1,x2,y1,y2;
    HighShelf():x1(0.f),x2(0.f),y1(0.f),y2(0.f){}
    float process(float in, float gain_db, float sr){
        float fc    = 1800.f;
        float A     = powf(10.f, gain_db/40.f);
        float w0    = kTwoPi*fc/sr;
        float cosw  = cosf(w0);
        float sinw  = sinf(w0);
        float alpha = sinw/2.f*sqrtf((A+1.f/A)*(1.f/0.9f-1.f)+2.f);
        float b0    =   A*((A+1.f)+(A-1.f)*cosw+2.f*sqrtf(A)*alpha);
        float b1    =-2.f*A*((A-1.f)+(A+1.f)*cosw);
        float b2    =   A*((A+1.f)+(A-1.f)*cosw-2.f*sqrtf(A)*alpha);
        float a0    =      (A+1.f)-(A-1.f)*cosw+2.f*sqrtf(A)*alpha;
        float a1    =  2.f*((A-1.f)-(A+1.f)*cosw);
        float a2    =       (A+1.f)-(A-1.f)*cosw-2.f*sqrtf(A)*alpha;
        float out   = (b0/a0)*in+(b1/a0)*x1+(b2/a0)*x2
                     -(a1/a0)*y1-(a2/a0)*y2;
        x2=x1; x1=in; y2=y1; y1=out;
        return out;
    }
};

// Octave sub (-1 oct) via half-wave rectify + LP
struct OctaveSub {
    LP lp;
    float process(float in, float sr){
        float rect = (in < 0.f) ? -in : 0.f; // half-wave
        return lp.process(rect, expf(-kTwoPi*120.f/sr));
    }
};

// Octave up (+1 oct) via full-wave rectify + LP
struct OctaveUp {
    LP lp;
    float process(float in, float sr){
        float rect = fabsf(in);
        return lp.process(rect, expf(-kTwoPi*5000.f/sr));
    }
};

// Compressor: simple RMS gain computer
struct Compressor {
    float envL, envR, gainL, gainR;
    Compressor():envL(0.f),envR(0.f),gainL(1.f),gainR(1.f){}
    void process(float& l, float& r, float amount, float sr){
        // amount: -1=max pre-comp, 0=off, +1=max post-comp
        // We use abs(amount) for compression strength
        float amt = fabsf(amount);
        if(amt < 0.01f) return;
        float atk = expf(-1.f/(0.005f*sr));
        float rel = expf(-1.f/(0.150f*sr));
        float aL  = fabsf(l), aR = fabsf(r);
        float cL  = (aL>envL)?atk:rel; envL=cL*envL+(1.f-cL)*aL;
        float cR  = (aR>envR)?atk:rel; envR=cR*envR+(1.f-cR)*aR;
        float thresh = 0.5f - amt*0.4f; // threshold moves down with comp
        float ratio  = 1.f + amt*7.f;   // ratio 1:1 .. 8:1
        auto computeGain = [&](float env) -> float {
            if(env < thresh) return 1.f;
            float over = env - thresh;
            float reduced = thresh + over/ratio;
            return (env > 0.0001f) ? (reduced/env) : 1.f;
        };
        gainL = gainL*0.999f + computeGain(envL)*0.001f;
        gainR = gainR*0.999f + computeGain(envR)*0.001f;
        l *= gainL; r *= gainR;
    }
};

// Sag: envelope-controlled soft-clip depth (power rail sag)
struct Sag {
    float env;
    Sag():env(0.f){}
    void process(float& l, float& r, float amount, float sr){
        if(amount < 0.001f) return;
        float aIn = 0.5f*(fabsf(l)+fabsf(r));
        float sagAtk = expf(-1.f/(0.020f*sr));
        float sagRel = expf(-1.f/(0.500f*sr));
        float c = (aIn > env) ? sagAtk : sagRel;
        env = c*env + (1.f-c)*aIn;
        // Sag reduces headroom — higher env = more compression+clip
        float sag = 1.f - env*amount*0.8f;
        if(sag < 0.1f) sag = 0.1f;
        l = (l/sag > 1.f) ? 1.f : (l/sag < -1.f) ? -1.f : l/sag;
        r = (r/sag > 1.f) ? 1.f : (r/sag < -1.f) ? -1.f : r/sag;
        l *= sag; r *= sag;
    }
};

// Asymmetric soft-clip distortion
static float distort(float in, float drive, float grit){
    float gain = 1.f + drive * 49.f;      // 1..50x
    float gritBoost = 1.f + grit * 0.5f;   // low-end boost pre-dist
    float x = in * gain * gritBoost;
    // Asymmetric: positive clips softer (diode emulation)
    if(x >= 0.f) return x / (1.f + x);
    else {
        float n = -x * 1.6f;
        return -(n / (1.f + n));
    }
}

class CrushStationPlugin : public Plugin {
public:
    CrushStationPlugin()
        : Plugin(kParameterCount,0,0),
          fMix(100.f),fDrive(50.f),fCompressor(0.f),fSag(0.f),
          fOctaves(0.f),fGrit(0.f),fBass(0.f),fMids(0.f),
          fMidsFreq(800.f),fTreble(0.f),fSr(48000.f)
    { sampleRateChanged(getSampleRate()); }

protected:
    const char* getLabel()       const override { return "CrushStation"; }
    const char* getDescription() const override { return "Overdrive/distortion with octaves and sag. Eventide H90 CrushStation emulation."; }
    const char* getMaker()       const override { return "H90"; }
    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','C','S','T'); }

    void initParameter(uint32_t index, Parameter& p) override {
        switch(index){
        case kMix:
            p.hints=kParameterIsAutomatable; p.name="Mix"; p.symbol="mix"; p.unit="%";
            p.ranges.def=100.f; p.ranges.min=0.f; p.ranges.max=100.f; break;
        case kDrive:
            p.hints=kParameterIsAutomatable; p.name="Drive"; p.symbol="drive"; p.unit="%";
            p.ranges.def=50.f; p.ranges.min=0.f; p.ranges.max=100.f; break;
        case kCompressor:
            p.hints=kParameterIsAutomatable; p.name="Compressor"; p.symbol="compressor"; p.unit="";
            p.ranges.def=0.f; p.ranges.min=-100.f; p.ranges.max=100.f; break;
        case kSag:
            p.hints=kParameterIsAutomatable; p.name="Sag"; p.symbol="sag"; p.unit="%";
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=100.f; break;
        case kOctaves:
            p.hints=kParameterIsAutomatable; p.name="Octaves"; p.symbol="octaves"; p.unit="%";
            p.ranges.def=0.f; p.ranges.min=-100.f; p.ranges.max=100.f; break;
        case kGrit:
            p.hints=kParameterIsAutomatable; p.name="Grit"; p.symbol="grit"; p.unit="%";
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=100.f; break;
        case kBass:
            p.hints=kParameterIsAutomatable; p.name="Bass"; p.symbol="bass"; p.unit="dB";
            p.ranges.def=0.f; p.ranges.min=-18.f; p.ranges.max=12.f; break;
        case kMids:
            p.hints=kParameterIsAutomatable; p.name="Mids"; p.symbol="mids"; p.unit="dB";
            p.ranges.def=0.f; p.ranges.min=-18.f; p.ranges.max=12.f; break;
        case kMidsFreq:
            p.hints=kParameterIsAutomatable; p.name="Mids Frequency"; p.symbol="mids_freq"; p.unit="Hz";
            p.ranges.def=800.f; p.ranges.min=100.f; p.ranges.max=8000.f; break;
        case kTreble:
            p.hints=kParameterIsAutomatable; p.name="Treble"; p.symbol="treble"; p.unit="dB";
            p.ranges.def=0.f; p.ranges.min=-18.f; p.ranges.max=12.f; break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch(index){
        case kMix:        return fMix;
        case kDrive:      return fDrive;
        case kCompressor: return fCompressor;
        case kSag:        return fSag;
        case kOctaves:    return fOctaves;
        case kGrit:       return fGrit;
        case kBass:       return fBass;
        case kMids:       return fMids;
        case kMidsFreq:   return fMidsFreq;
        case kTreble:     return fTreble;
        } return 0.f;
    }

    void setParameterValue(uint32_t index, float v) override {
        switch(index){
        case kMix:        fMix=v;        break;
        case kDrive:      fDrive=v;      break;
        case kCompressor: fCompressor=v; break;
        case kSag:        fSag=v;        break;
        case kOctaves:    fOctaves=v;    break;
        case kGrit:       fGrit=v;       break;
        case kBass:       fBass=v;       break;
        case kMids:       fMids=v;       break;
        case kMidsFreq:   fMidsFreq=v;   break;
        case kTreble:     fTreble=v;     break;
        }
    }

    void sampleRateChanged(double newSR) override { fSr=(float)newSR; }

    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 mix       = fMix/100.f;
        const float drive     = fDrive/100.f;
        const float comp      = fCompressor/100.f; // -1..+1
        const float sag       = fSag/100.f;
        const float octaves   = fOctaves/100.f;    // -1..+1: neg=sub, pos=up
        const float grit      = fGrit/100.f;
        const bool  preComp   = (comp < 0.f);
        const float compAmt   = fabsf(comp);

        for(uint32_t i=0;i<frames;++i){
            float l=inL[i], r=inR[i];

            // Octaves mixed in pre-distortion
            float subL=0.f,subR=0.f,upL=0.f,upR=0.f;
            if(fabsf(octaves)>0.01f){
                if(octaves < 0.f){
                    subL=fOctSubL.process(l,fSr)*(-octaves);
                    subR=fOctSubR.process(r,fSr)*(-octaves);
                } else {
                    upL=fOctUpL.process(l,fSr)*octaves;
                    upR=fOctUpR.process(r,fSr)*octaves;
                }
            }
            float preL=l+subL+upL;
            float preR=r+subR+upR;

            // Pre-distortion compression
            if(preComp) fComp.process(preL,preR,compAmt,fSr);

            // Sag (pre-distortion)
            fSag.process(preL,preR,sag,fSr);

            // Distortion
            float distL=distort(preL,drive,grit);
            float distR=distort(preR,drive,grit);

            // Post-distortion compression
            if(!preComp) fComp.process(distL,distR,compAmt,fSr);

            // EQ: bass shelf, mids peak, treble shelf
            float eqL=fBassEQL.process(distL,fBass,fSr);
            float eqR=fBassEQR.process(distR,fBass,fSr);
            eqL=fMidsEQL.process(eqL,fMidsFreq,fMids,1.2f,fSr);
            eqR=fMidsEQR.process(eqR,fMidsFreq,fMids,1.2f,fSr);
            eqL=fTrebEQL.process(eqL,fTreble,fSr);
            eqR=fTrebEQR.process(eqR,fTreble,fSr);

            outL[i]=l*(1.f-mix)+eqL*mix;
            outR[i]=r*(1.f-mix)+eqR*mix;
        }
    }

private:
    float fMix,fDrive,fCompressor,fSag,fOctaves,fGrit;
    float fBass,fMids,fMidsFreq,fTreble,fSr;

    OctaveSub  fOctSubL,fOctSubR;
    OctaveUp   fOctUpL,fOctUpR;
    Compressor fComp;
    Sag        fSag;
    LowShelf   fBassEQL,fBassEQR;
    PeakEQ     fMidsEQL,fMidsEQR;
    HighShelf  fTrebEQL,fTrebEQR;

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CrushStationPlugin)
};

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

END_NAMESPACE_DISTRHO
endef

define H90_CRUSHSTATION_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME      = h90-crushstation
FILES_DSP = CrushStationPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

define H90_CRUSHSTATION_MANIFEST_TTL
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<urn:mod-cookbook:h90-crushstation>
    a lv2:Plugin , lv2:DistortionPlugin ;
    lv2:binary <h90-crushstation_dsp.so> ;
    rdfs:seeAlso <h90-crushstation.ttl> .
endef

define H90_CRUSHSTATION_PLUGIN_TTL
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@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:h90-crushstation>
    a lv2:Plugin , lv2:DistortionPlugin ;
    doap:name "H90 CrushStation" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "H90" ] ;
    rdfs:comment "Overdrive/distortion with octaves and sag. Eventide H90 CrushStation 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" ; lv2:name "Output R"
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 4 ; lv2:symbol "mix" ; lv2:name "Mix" ;
        lv2:default 100.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 5 ; lv2:symbol "drive" ; lv2:name "Drive" ;
        lv2:default 50.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 6 ; lv2:symbol "compressor" ; lv2:name "Compressor" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 7 ; lv2:symbol "sag" ; lv2:name "Sag" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 8 ; lv2:symbol "octaves" ; lv2:name "Octaves" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 9 ; lv2:symbol "grit" ; lv2:name "Grit" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ; units:unit units:pc
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 10 ; lv2:symbol "bass" ; lv2:name "Bass" ;
        lv2:default 0.0 ; lv2:minimum -18.0 ; lv2:maximum 12.0 ; units:unit units:db
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 11 ; lv2:symbol "mids" ; lv2:name "Mids" ;
        lv2:default 0.0 ; lv2:minimum -18.0 ; lv2:maximum 12.0 ; units:unit units:db
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 12 ; lv2:symbol "mids_freq" ; lv2:name "Mids Frequency" ;
        lv2:default 800.0 ; lv2:minimum 100.0 ; lv2:maximum 8000.0 ; units:unit units:hz
    ] , [ a lv2:InputPort , lv2:ControlPort ; lv2:index 13 ; lv2:symbol "treble" ; lv2:name "Treble" ;
        lv2:default 0.0 ; lv2:minimum -18.0 ; lv2:maximum 12.0 ; units:unit units:db
    ] .
endef

export H90_CRUSHSTATION_PLUGIN_INFO_H
export H90_CRUSHSTATION_PLUGIN_CPP
export H90_CRUSHSTATION_PLUGIN_MAKEFILE
export H90_CRUSHSTATION_MANIFEST_TTL
export H90_CRUSHSTATION_PLUGIN_TTL

define H90_CRUSHSTATION_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/h90-crushstation
	@echo "$$H90_CRUSHSTATION_PLUGIN_INFO_H"  > $(@D)/examples/h90-crushstation/DistrhoPluginInfo.h
	@echo "$$H90_CRUSHSTATION_PLUGIN_CPP"      > $(@D)/examples/h90-crushstation/CrushStationPlugin.cpp
	@echo "$$H90_CRUSHSTATION_PLUGIN_MAKEFILE" > $(@D)/examples/h90-crushstation/Makefile
endef

define H90_CRUSHSTATION_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true \
		-C $(@D)/examples/h90-crushstation lv2_dsp
endef

define H90_CRUSHSTATION_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/h90-crushstation.lv2
	cp $(@D)/bin/h90-crushstation.lv2/h90-crushstation_dsp.so \
		$($(PKG)_PKGDIR)/h90-crushstation.lv2/
	@echo "$$H90_CRUSHSTATION_MANIFEST_TTL" > $($(PKG)_PKGDIR)/h90-crushstation.lv2/manifest.ttl
	@echo "$$H90_CRUSHSTATION_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/h90-crushstation.lv2/h90-crushstation.ttl
endef

$(eval $(generic-package))
