# Eventide H90 PitchFuzz - Fuzz, Triple Pitch Shift, and Dual Delay Combo Engine
# Incorporating full H90 manual specifications & enumerated dropdown controls.
# Upload at https://builder.mod.audio/buildroot

H90_PITCHFUZZ_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
H90_PITCHFUZZ_SITE = https://github.com/DISTRHO/DPF.git
H90_PITCHFUZZ_SITE_METHOD = git
H90_PITCHFUZZ_BUNDLES = h90-pitchfuzz.lv2

# ---------------------------------------------------------------------------
# DistrhoPluginInfo.h
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_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 PitchFuzz"
#define DISTRHO_PLUGIN_URI          "urn:mod-cookbook:h90-pitchfuzz"
#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 {
    kFuzz = 0,
    kFuzzTone,
    kPitchAmount,
    kPitchA,
    kPitchB,
    kPitchC,
    kDelayLevel,
    kDelayA,
    kDelayB,
    kFeedback,
    kParameterCount
};

#endif
endef

# ---------------------------------------------------------------------------
# H90PitchFuzzPlugin.cpp
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>
#include <algorithm>

START_NAMESPACE_DISTRHO

static const float kPi = 3.14159265359f;

// Heap delay line with linear interpolation
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]);
    }
};

// True Mathematical Parallel Tilt EQ
// Uses static filters to guarantee 100% DSP stability in both directions
struct TiltFilter {
    float lpState;
    float hpState;
    float alpha;
    float mixBass, mixTreble;

    TiltFilter() { reset(); }

    void reset() {
        lpState = 0.0f;
        hpState = 0.0f;
        alpha = 0.1f;
        mixBass = 1.0f;
        mixTreble = 1.0f;
    }

    void update(float toneKnob, float sr) {
        // Pivot point at ~700 Hz
        float cutoff = 700.0f;
        float dt = 1.0f / sr;
        float rc = 1.0f / (2.0f * kPi * cutoff);
        alpha = dt / (rc + dt);

        // toneKnob ranges from -1.0 to 1.0
        // Clockwise (+): Boost Treble, Cut Bass
        // Anticlockwise (-): Boost Bass, Cut Treble
        if (toneKnob >= 0.0f) {
            mixTreble = 1.0f + (toneKnob * 1.5f); // up to +3.5dB treble boost
            mixBass   = 1.0f - (toneKnob * 0.6f); // down to -4dB bass attenuation
        } else {
            mixTreble = 1.0f + (toneKnob * 0.6f); // down to -4dB treble attenuation
            mixBass   = 1.0f - (toneKnob * 1.5f); // up to +3.5dB bass boost
        }
    }

    inline float process(float in) {
        // One-pole Low Pass
        lpState += alpha * (in - lpState);
        
        // One-pole High Pass
        float hp = in - lpState;

        // Recombine paths using the calculated tilt mix weights
        return (lpState * mixBass) + (hp * mixTreble);
    }
};

// Simple pitch shifter using a windowed dual-tap modulated delay line
struct PitchShifter {
    DelayLine buf;
    float phase1, phase2;
    static const int kWindowSamples = 1024;

    PitchShifter() : phase1(0.f), phase2(0.5f) {}

    void init() {
        buf.init(8192);
    }

    float process(float in, float semis) {
        buf.write(in);

        float ratio = std::pow(2.f, semis / 12.f);
        float speed = 1.f - ratio;

        phase1 += speed / (float)kWindowSamples;
        phase2 += speed / (float)kWindowSamples;
        if (phase1 >= 1.f) phase1 -= 1.f;
        if (phase1 < 0.f)  phase1 += 1.f;
        if (phase2 >= 1.f) phase2 -= 1.f;
        if (phase2 < 0.f)  phase2 += 1.f;

        float d1 = phase1 * (float)kWindowSamples;
        float d2 = phase2 * (float)kWindowSamples;

        float w1 = std::sin(phase1 * kPi);
        float w2 = std::sin(phase2 * kPi);

        float s1 = buf.read(d1);
        float s2 = buf.read(d2);

        float wsum = w1 + w2;
        if (wsum < 0.001f) wsum = 0.001f;
        return (s1 * w1 + s2 * w2) / wsum;
    }
};

static inline float fuzzShape(float x, float amt) {
    float drive = 1.f + amt * 40.f;
    float y = std::tanh(x * drive);
    if (amt > 0.5f) {
        float hardAmt = (amt - 0.5f) * 2.f;
        float hard = (y > 0.f) ? std::min(y * 2.f, 1.f) : std::max(y * 2.f, -1.f);
        y = y * (1.f - hardAmt) + hard * hardAmt;
    }
    return y;
}

class H90PitchFuzzPlugin : public Plugin
{
public:
    H90PitchFuzzPlugin()
        : Plugin(kParameterCount, 0, 0),
          fFuzz(40.f), fFuzzTone(0.f), fPitchAmount(1.0f),
          fPitchA(0.f), fPitchB(7.f), fPitchC(-5.f),
          fDelayLevel(40.f), fDelayA(250.f), fDelayB(375.f), fFeedback(45.f),
          fSr(48000.f)
    {
        fPitchShiftAL.init(); fPitchShiftAR.init();
        fPitchShiftBL.init(); fPitchShiftBR.init();
        fPitchShiftCL.init(); fPitchShiftCR.init();
        fDelayLineAL.init(1 << 17);
        fDelayLineAR.init(1 << 17);
        fDelayLineBL.init(1 << 17);
        fDelayLineBR.init(1 << 17);
    }

protected:
    const char* getLabel()       const override { return "H90 PitchFuzz"; }
    const char* getDescription() const override { return "Fuzz, triple pitch shift, and dual delay combo. Eventide H90 PitchFuzz emulation."; }
    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','P'); }

    void initParameter(uint32_t index, Parameter& p) override {
        p.hints = kParameterIsAutomatable;
        switch (index) {
        case kFuzz:
            p.name = "Fuzz"; p.symbol = "fuzz"; p.unit = "%";
            p.ranges.def = 40.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kFuzzTone:
            p.name = "Fuzz Tone"; p.symbol = "fuzz_tone";
            p.ranges.def = 0.f; p.ranges.min = -100.f; p.ranges.max = 100.f; break;
        case kPitchAmount:
            p.name = "Pitch Amount"; p.symbol = "pitch_amount";
            p.ranges.def = 1.0f; p.ranges.min = 0.f; p.ranges.max = 3.f; break;
        case kPitchA:
            p.name = "Pitch A"; p.symbol = "pitch_a"; p.unit = "st";
            p.ranges.def = 0.f; p.ranges.min = -24.f; p.ranges.max = 24.f; break;
        case kPitchB:
            p.name = "Pitch B"; p.symbol = "pitch_b"; p.unit = "st";
            p.ranges.def = 7.f; p.ranges.min = -24.f; p.ranges.max = 24.f; break;
        case kPitchC:
            p.name = "Pitch C"; p.symbol = "pitch_c"; p.unit = "st";
            p.ranges.def = -5.f; p.ranges.min = -24.f; p.ranges.max = 24.f; break;
        case kDelayLevel:
            p.name = "Delay Level"; p.symbol = "delay_level";
            p.ranges.def = 40.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kDelayA:
            p.name = "Delay A"; p.symbol = "delay_a"; p.unit = "ms";
            p.ranges.def = 250.f; p.ranges.min = 0.f; p.ranges.max = 2500.f; break;
        case kDelayB:
            p.name = "Delay B"; p.symbol = "delay_b"; p.unit = "ms";
            p.ranges.def = 375.f; p.ranges.min = 0.f; p.ranges.max = 2500.f; break;
        case kFeedback:
            p.name = "Feedback"; p.symbol = "feedback"; p.unit = "%";
            p.ranges.def = 45.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch (index) {
        case kFuzz:        return fFuzz;
        case kFuzzTone:    return fFuzzTone;
        case kPitchAmount: return fPitchAmount;
        case kPitchA:      return fPitchA;
        case kPitchB:      return fPitchB;
        case kPitchC:      return fPitchC;
        case kDelayLevel:  return fDelayLevel;
        case kDelayA:      return fDelayA;
        case kDelayB:      return fDelayB;
        case kFeedback:    return fFeedback;
        }
        return 0.f;
    }

    void setParameterValue(uint32_t index, float value) override {
        switch (index) {
        case kFuzz:        fFuzz = value; break;
        case kFuzzTone:    fFuzzTone = value; break;
        case kPitchAmount: fPitchAmount = value; break;
        case kPitchA:      fPitchA = value; break;
        case kPitchB:      fPitchB = value; break;
        case kPitchC:      fPitchC = value; break;
        case kDelayLevel:  fDelayLevel = value; break;
        case kDelayA:      fDelayA = value; break;
        case kDelayB:      fDelayB = value; break;
        case kFeedback:    fFeedback = value; break;
        }
    }

    void sampleRateChanged(double newSampleRate) override {
        fSr = (float)newSampleRate;
        fTiltL.reset(); fTiltR.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 fuzzAmt = fFuzz * 0.01f;
        
        // Maps nicely into -1.0 to 1.0 ranges
        float toneNormalized = fFuzzTone * 0.01f;
        fTiltL.update(toneNormalized, fSr);
        fTiltR.update(toneNormalized, fSr);

        // Pitch Amount: one knob crossfades through all three pitch voices
        // in sequence rather than mixing them all at once - this is by
        // design (matches the original H90 PitchFuzz behavior), not a bug:
        //   0.0-1.0 : Voice A fades in (Pitch A knob)
        //   1.0-2.0 : Voice A stays full, Voice B fades in (Pitch B knob)
        //   2.0-3.0 : Voices A+B stay full, Voice C fades in (Pitch C knob)
        // The Pitch C knob will have NO audible effect until Pitch Amount
        // is turned up past 2.0 - that is expected, not broken.
        float pa = fPitchAmount;
        float voiceA = std::min(1.f, std::max(0.f, pa));
        float voiceB = std::min(1.f, std::max(0.f, pa - 1.f));
        float voiceC = std::min(1.f, std::max(0.f, pa - 2.f));

        // Delay Level is a clean 0-100% wet mix into the dual delay taps.
        float delayLevelAmt = fDelayLevel * 0.01f;
        float feedbackAmt = std::min(0.98f, fFeedback * 0.01f); 
        
        float delaySampA = (fDelayA * 0.001f) * fSr;
        float delaySampB = (fDelayB * 0.001f) * fSr;

        for (uint32_t i = 0; i < frames; ++i) {
            float l = inL[i];
            float r = inR[i];

            // Fuzz stage
            float fuzzedL = fuzzShape(l, fuzzAmt);
            float fuzzedR = fuzzShape(r, fuzzAmt);
            
            // Stable 6dB/oct Parallel Tilt EQ
            fuzzedL = fTiltL.process(fuzzedL);
            fuzzedR = fTiltR.process(fuzzedR);

            // Three pitch-shifted voices
            float pAL = fPitchShiftAL.process(fuzzedL, fPitchA) * voiceA;
            float pAR = fPitchShiftAR.process(fuzzedR, fPitchA) * voiceA;
            float pBL = fPitchShiftBL.process(fuzzedL, fPitchB) * voiceB;
            float pBR = fPitchShiftBR.process(fuzzedR, fPitchB) * voiceB;
            float pCL = fPitchShiftCL.process(fuzzedL, fPitchC) * voiceC;
            float pCR = fPitchShiftCR.process(fuzzedR, fPitchC) * voiceC;

            float voicesL = pAL + pBL + pCL;
            float voicesR = pAR + pBR + pCR;

            // Delay section
            float delayInputL = voicesL;
            float delayInputR = voicesR;

            float rdAL = fDelayLineAL.read(delaySampA);
            float rdAR = fDelayLineAR.read(delaySampA);
            float rdBL = fDelayLineBL.read(delaySampB);
            float rdBR = fDelayLineBR.read(delaySampB);

            fDelayLineAL.write(delayInputL + rdAL * feedbackAmt);
            fDelayLineAR.write(delayInputR + rdAR * feedbackAmt);
            fDelayLineBL.write(delayInputL + rdBL * feedbackAmt);
            fDelayLineBR.write(delayInputR + rdBR * feedbackAmt);

            float delayedL = (rdAL + rdBL) * 0.5f;
            float delayedR = (rdAR + rdBR) * 0.5f;

            // Mix outputs
            float wetL = (voicesL * (1.0f - delayLevelAmt * 0.5f)) + (delayedL * delayLevelAmt * 1.5f);
            float wetR = (voicesR * (1.0f - delayLevelAmt * 0.5f)) + (delayedR * delayLevelAmt * 1.5f);

            outL[i] = wetL;
            outR[i] = wetR;
        }
    }

private:
    float fFuzz, fFuzzTone, fPitchAmount, fPitchA, fPitchB, fPitchC;
    float fDelayLevel, fDelayA, fDelayB, fFeedback;
    float fSr;

    TiltFilter fTiltL, fTiltR;
    PitchShifter fPitchShiftAL, fPitchShiftAR;
    PitchShifter fPitchShiftBL, fPitchShiftBR;
    PitchShifter fPitchShiftCL, fPitchShiftCR;
    DelayLine fDelayLineAL, fDelayLineAR;
    DelayLine fDelayLineBL, fDelayLineBR;

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(H90PitchFuzzPlugin)
};

Plugin* createPlugin() { return new H90PitchFuzzPlugin(); }

END_NAMESPACE_DISTRHO
endef

# ---------------------------------------------------------------------------
# Makefile
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME      = h90-pitchfuzz
FILES_DSP = H90PitchFuzzPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

# ---------------------------------------------------------------------------
# LV2 TTL Manifest
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_MANIFEST_TTL
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<urn:mod-cookbook:h90-pitchfuzz>
    a lv2:Plugin ;
    lv2:binary <h90-pitchfuzz_dsp.so> ;
    rdfs:seeAlso <h90-pitchfuzz.ttl> .
endef

# ---------------------------------------------------------------------------
# LV2 TTL Plugin Configuration
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_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-pitchfuzz>
    a lv2:Plugin , lv2:DistortionPlugin ;
    doap:name "H90 PitchFuzz" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "Eventide" ] ;
    rdfs:comment "Fuzz, triple pitch shift, and dual delay combo. Eventide H90 PitchFuzz 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 "fuzz" ; lv2:name "Fuzz" ;
        lv2:default 40.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "fuzz_tone" ; lv2:name "Fuzz Tone" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ; lv2:symbol "pitch_amount" ; lv2:name "Pitch Amount" ;
        lv2:default 1.0 ; lv2:minimum 0.0 ; lv2:maximum 3.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "pitch_a" ; lv2:name "Pitch A" ;
        lv2:default 0.0 ; lv2:minimum -24.0 ; lv2:maximum 24.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ; lv2:symbol "pitch_b" ; lv2:name "Pitch B" ;
        lv2:default 7.0 ; lv2:minimum -24.0 ; lv2:maximum 24.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; lv2:symbol "pitch_c" ; lv2:name "Pitch C" ;
        lv2:default -5.0 ; lv2:minimum -24.0 ; lv2:maximum 24.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ; lv2:symbol "delay_level" ; lv2:name "Delay Level" ;
        lv2:default 40.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "delay_a" ; lv2:name "Delay A" ;
        lv2:default 250.0 ; lv2:minimum 0.0 ; lv2:maximum 2500.0 ;
        units:unit units:ms
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ; lv2:symbol "delay_b" ; lv2:name "Delay B" ;
        lv2:default 375.0 ; lv2:minimum 0.0 ; lv2:maximum 2500.0 ;
        units:unit units:ms
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ; lv2:symbol "feedback" ; lv2:name "Feedback" ;
        lv2:default 45.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] .
endef

export H90_PITCHFUZZ_PLUGIN_CPP H90_PITCHFUZZ_PLUGIN_INFO_H H90_PITCHFUZZ_PLUGIN_MAKEFILE
export H90_PITCHFUZZ_MANIFEST_TTL H90_PITCHFUZZ_PLUGIN_TTL

# ---------------------------------------------------------------------------
# Buildroot Platform Compilation Commands
# ---------------------------------------------------------------------------
define H90_PITCHFUZZ_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/h90-pitchfuzz
	printf '%s' "$$H90_PITCHFUZZ_PLUGIN_CPP"      > $(@D)/examples/h90-pitchfuzz/H90PitchFuzzPlugin.cpp
	printf '%s' "$$H90_PITCHFUZZ_PLUGIN_INFO_H"   > $(@D)/examples/h90-pitchfuzz/DistrhoPluginInfo.h
	printf '%s' "$$H90_PITCHFUZZ_PLUGIN_MAKEFILE" > $(@D)/examples/h90-pitchfuzz/Makefile
endef

define H90_PITCHFUZZ_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/h90-pitchfuzz lv2_dsp
endef

define H90_PITCHFUZZ_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/h90-pitchfuzz.lv2
	cp $(@D)/bin/h90-pitchfuzz.lv2/h90-pitchfuzz_dsp.so $($(PKG)_PKGDIR)/h90-pitchfuzz.lv2/
	printf '%s' "$$H90_PITCHFUZZ_MANIFEST_TTL" > $($(PKG)_PKGDIR)/h90-pitchfuzz.lv2/manifest.ttl
	printf '%s' "$$H90_PITCHFUZZ_PLUGIN_TTL" > $($(PKG)_PKGDIR)/h90-pitchfuzz.lv2/h90-pitchfuzz.ttl
endef

$(eval $(generic-package))
