# Eventide H90 Sculpt - Multi-band Distortion w/ Envelope Follower Control Filters
# Incorporating full H90 manual specifications & enumerated dropdown controls.
# Upload at https://builder.mod.audio/buildroot

H90_SCULPT_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
H90_SCULPT_SITE = https://github.com/DISTRHO/DPF.git
H90_SCULPT_SITE_METHOD = git
H90_SCULPT_BUNDLES = h90-sculpt.lv2

# ---------------------------------------------------------------------------
# DistrhoPluginInfo.h
# ---------------------------------------------------------------------------
define H90_SCULPT_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 Sculpt"
#define DISTRHO_PLUGIN_URI          "urn:mod-cookbook:h90-sculpt"
#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,
    kBandMix,
    kCrossover,
    kLowDrive,
    kHighDrive,
    kCompressor,
    kLowBoost,
    kFilterPre,
    kFilterPost,
    kEnvFollower,
    kStereoMode,
    kParameterCount
};

#endif
endef

# ---------------------------------------------------------------------------
# H90SculptPlugin.cpp
# ---------------------------------------------------------------------------
define H90_SCULPT_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>
#include <algorithm>

START_NAMESPACE_DISTRHO

static const float kPi = 3.14159265359f;

// State-variable filter (Chamberlin form) for crossover band splitting
struct SVFilter {
    float low, band;
    SVFilter() : low(0.f), band(0.f) {}
    void reset() { low = 0.f; band = 0.f; }

    void process(float in, float freq, float q, float sr, float& outLow, float& outHigh) {
        float f = 2.f * std::sin(kPi * freq / sr);
        f = std::max(0.001f, std::min(f, 1.0f));
        float qInv = 1.f / std::max(0.5f, q);

        float l = low + f * band;
        float h = in - l - qInv * band;
        float b = f * h + band;

        low = l;
        band = b;

        outLow  = l;
        outHigh = h;
    }
};

// One-pole peaking filter, swept between cut and boost
struct PeakFilter {
    float z1;
    PeakFilter() : z1(0.f) {}
    void reset() { z1 = 0.f; }

    // amount: -1 (full cut) .. 0 (flat) .. +1 (full boost)
    float process(float in, float freq, float amount, float sr) {
        float c = std::exp(-2.f * kPi * freq / sr);
        float lp = (1.f - c) * in + c * z1;
        z1 = lp;
        float band = in - lp;
        return in + band * amount;
    }
};

// Simple feed-forward compressor/sustainer
struct Compressor {
    float env;
    Compressor() : env(0.f) {}
    void reset() { env = 0.f; }

    float process(float in, float amount, float sr) {
        float atk = std::exp(-1.f / (0.003f * sr));
        float rel = std::exp(-1.f / (0.150f * sr));
        float a = std::fabs(in);
        float c = (a > env) ? atk : rel;
        env = c * env + (1.f - c) * a;

        float thresh = 0.35f;
        float ratio = 1.f + amount * 7.f;
        float gain = 1.f;
        if (env > thresh && env > 1e-6f) {
            float over = env / thresh;
            float compressed = std::pow(over, (1.f / ratio) - 1.f);
            gain = compressed;
        }
        float makeup = 1.f + amount * 0.5f;
        return in * gain * makeup;
    }
};

// Envelope follower for filter modulation
struct EnvFollow {
    float env;
    EnvFollow() : env(0.f) {}
    void reset() { env = 0.f; }

    float process(float in, float sensitivity, float sr) {
        float atk = std::exp(-1.f / (0.005f * sr));
        float rel = std::exp(-1.f / (0.120f * sr));
        float a = std::fabs(in);
        float c = (a > env) ? atk : rel;
        env = c * env + (1.f - c) * a;
        return std::min(1.f, env * (1.f + sensitivity * 8.f));
    }
};

static inline float softClip(float x, float drive) {
    float d = 1.f + drive * 0.20f;
    return std::tanh(x * d) / std::tanh(d);
}

class H90SculptPlugin : public Plugin
{
public:
    H90SculptPlugin()
        : Plugin(kParameterCount, 0, 0),
          fMix(75.f), fBandMix(50.f), fCrossover(800.f), fLowDrive(30.f), fHighDrive(30.f),
          fCompressor(0.f), fLowBoost(0.f), fFilterPre(0.f), fFilterPost(0.f),
          fEnvFollower(0.f), fStereoMode(0.f),
          fSr(48000.f)
    {}

protected:
    const char* getLabel()       const override { return "H90 Sculpt"; }
    const char* getDescription() const override { return "Multi-band distortion with envelope-follower controlled filters. Eventide H90 Sculpt 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','S'); }

    void initParameter(uint32_t index, Parameter& p) override {
        p.hints = kParameterIsAutomatable;
        switch (index) {
        case kMix:
            p.name = "Mix"; p.symbol = "mix"; p.unit = "%";
            p.ranges.def = 75.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kBandMix:
            p.name = "Band Mix"; p.symbol = "band_mix"; p.unit = "%";
            p.ranges.def = 50.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kCrossover:
            p.name = "Crossover Frequency"; p.symbol = "crossover"; p.unit = "Hz";
            p.ranges.def = 800.f; p.ranges.min = 100.f; p.ranges.max = 4000.f; break;
        case kLowDrive:
            p.name = "Low Drive"; p.symbol = "low_drive"; p.unit = "%";
            p.ranges.def = 30.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kHighDrive:
            p.name = "High Drive"; p.symbol = "high_drive"; p.unit = "%";
            p.ranges.def = 30.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kCompressor:
            p.name = "Compressor"; p.symbol = "compressor";
            p.ranges.def = 0.f; p.ranges.min = -100.f; p.ranges.max = 100.f; break;
        case kLowBoost:
            p.name = "Low Boost"; p.symbol = "low_boost";
            p.ranges.def = 0.f; p.ranges.min = -100.f; p.ranges.max = 100.f; break;
        case kFilterPre:
            p.name = "Filter-Pre"; p.symbol = "filter_pre";
            p.ranges.def = 0.f; p.ranges.min = -100.f; p.ranges.max = 100.f; break;
        case kFilterPost:
            p.name = "Filter-Post"; p.symbol = "filter_post";
            p.ranges.def = 0.f; p.ranges.min = -100.f; p.ranges.max = 100.f; break;
        case kEnvFollower:
            p.name = "Envelope Follower"; p.symbol = "env_follower"; p.unit = "%";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 100.f; break;
        case kStereoMode:
            p.name = "Stereo Mode"; p.symbol = "stereo_mode";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 1.f;
            p.hints |= kParameterIsInteger; break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch (index) {
        case kMix:         return fMix;
        case kBandMix:     return fBandMix;
        case kCrossover:   return fCrossover;
        case kLowDrive:    return fLowDrive;
        case kHighDrive:   return fHighDrive;
        case kCompressor:  return fCompressor;
        case kLowBoost:    return fLowBoost;
        case kFilterPre:   return fFilterPre;
        case kFilterPost:  return fFilterPost;
        case kEnvFollower: return fEnvFollower;
        case kStereoMode:  return fStereoMode;
        }
        return 0.f;
    }

    void setParameterValue(uint32_t index, float value) override {
        switch (index) {
        case kMix:         fMix = value; break;
        case kBandMix:     fBandMix = value; break;
        case kCrossover:   fCrossover = value; break;
        case kLowDrive:    fLowDrive = value; break;
        case kHighDrive:   fHighDrive = value; break;
        case kCompressor:  fCompressor = value; break;
        case kLowBoost:    fLowBoost = value; break;
        case kFilterPre:   fFilterPre = value; break;
        case kFilterPost:  fFilterPost = value; break;
        case kEnvFollower: fEnvFollower = value; break;
        case kStereoMode:  fStereoMode = value; break;
        }
    }

    void sampleRateChanged(double newSampleRate) override {
        fSr = (float)newSampleRate;
        fXoverL.reset(); fXoverR.reset();
        fPreFiltL.reset(); fPreFiltR.reset();
        fPostFiltL.reset(); fPostFiltR.reset();
        fCompL.reset(); fCompR.reset();
        fEnvL.reset(); fEnvR.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 mix = fMix * 0.01f;
        float bandMix = fBandMix * 0.01f;
        float lowDriveAmt = fLowDrive * 0.01f * 10.f;
        float highDriveAmt = fHighDrive * 0.01f * 10.f;
        float compAmt = fCompressor * 0.01f; // -1..1, sign = pre/post
        float lowBoostAmt = fLowBoost * 0.01f;
        float filterPreAmt = fFilterPre * 0.01f;
        float filterPostAmt = fFilterPost * 0.01f;
        float envSens = fEnvFollower * 0.01f;
        bool splitMode = fStereoMode > 0.5f;

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

            // Envelope follower drives filter modulation depth
            float envValL = fEnvL.process(l, envSens, fSr);
            float envValR = fEnvR.process(r, envSens, fSr);
            float envAvg = 0.5f * (envValL + envValR);

            // Low Boost: applied pre or post distortion based on sign
            float boostedL = l, boostedR = r;
            if (lowBoostAmt < 0.f) {
                boostedL = fPreFiltL.process(l, 200.f, -lowBoostAmt, fSr);
                boostedR = fPreFiltR.process(r, 200.f, -lowBoostAmt, fSr);
            }

            // Filter-Pre, swept by envelope follower
            float preAmt = filterPreAmt * (1.f + envAvg * envSens);
            float preL = fPreFiltL.process(boostedL, 1200.f, preAmt, fSr);
            float preR = fPreFiltR.process(boostedR, 1200.f, preAmt, fSr);

            // Pre-distortion compression: negative Compressor values squeeze
            // the signal BEFORE it hits the crossover/drive stage, so it
            // actually changes how hard the distortion gets driven.
            if (compAmt < 0.f) {
                preL = fCompL.process(preL, -compAmt, fSr);
                preR = fCompR.process(preR, -compAmt, fSr);
            }

            // Crossover split
            float lowL, highL, lowR, highR;
            fXoverL.process(preL, fCrossover, 0.707f, fSr, lowL, highL);
            fXoverR.process(preR, fCrossover, 0.707f, fSr, lowR, highR);

            // Per-band distortion
            float distLowL  = softClip(lowL,  lowDriveAmt);
            float distLowR  = softClip(lowR,  lowDriveAmt);
            float distHighL = softClip(highL, highDriveAmt);
            float distHighR = softClip(highR, highDriveAmt);

            float bandedL = distLowL * (1.f - bandMix) + distHighL * bandMix;
            float bandedR = distLowR * (1.f - bandMix) + distHighR * bandMix;

            // Post-distortion compression: positive Compressor values
            // level/sustain the signal AFTER drive, for a tighter clamp
            // rather than a harder-driven distortion character.
            float compL = bandedL, compR = bandedR;
            if (compAmt > 0.f) {
                compL = fCompL.process(bandedL, compAmt, fSr);
                compR = fCompR.process(bandedR, compAmt, fSr);
            }

            // Low Boost post (positive values)
            if (lowBoostAmt > 0.f) {
                compL = fPostFiltL.process(compL, 200.f, lowBoostAmt, fSr);
                compR = fPostFiltR.process(compR, 200.f, lowBoostAmt, fSr);
            }

            // Filter-Post, swept by envelope follower
            float postAmt = filterPostAmt * (1.f + envAvg * envSens);
            float postL = fPostFiltL.process(compL, 1200.f, postAmt, fSr);
            float postR = fPostFiltR.process(compR, 1200.f, postAmt, fSr);

            float wetL = postL;
            float wetR = postR;

            // Split stereo mode: spread bands across channels for spectral panning
            if (splitMode) {
                float spreadL = distLowL * 0.7f + distHighR * 0.3f;
                float spreadR = distHighR * 0.7f + distLowL * 0.3f;
                wetL = wetL * 0.6f + spreadL * 0.4f;
                wetR = wetR * 0.6f + spreadR * 0.4f;
            }

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

private:
    float fMix, fBandMix, fCrossover, fLowDrive, fHighDrive;
    float fCompressor, fLowBoost, fFilterPre, fFilterPost, fEnvFollower, fStereoMode;
    float fSr;

    SVFilter fXoverL, fXoverR;
    PeakFilter fPreFiltL, fPreFiltR;
    PeakFilter fPostFiltL, fPostFiltR;
    Compressor fCompL, fCompR;
    EnvFollow fEnvL, fEnvR;

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(H90SculptPlugin)
};

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

END_NAMESPACE_DISTRHO
endef

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

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

<urn:mod-cookbook:h90-sculpt>
    a lv2:Plugin ;
    lv2:binary <h90-sculpt_dsp.so> ;
    rdfs:seeAlso <h90-sculpt.ttl> .
endef

# ---------------------------------------------------------------------------
# LV2 TTL Plugin Configuration with Custom Enumerations Dropdowns
# ---------------------------------------------------------------------------
define H90_SCULPT_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-sculpt>
    a lv2:Plugin , lv2:DistortionPlugin ;
    doap:name "H90 Sculpt" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "Eventide" ] ;
    rdfs:comment "Multi-band distortion with envelope-follower controlled filters. Eventide H90 Sculpt 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 75.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "band_mix" ; lv2:name "Band Mix" ;
        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 "crossover" ; lv2:name "Crossover Frequency" ;
        lv2:default 800.0 ; lv2:minimum 100.0 ; lv2:maximum 4000.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "low_drive" ; lv2:name "Low Drive" ;
        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 "high_drive" ; lv2:name "High Drive" ;
        lv2:default 30.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; 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 10 ; lv2:symbol "low_boost" ; lv2:name "Low Boost" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "filter_pre" ; lv2:name "Filter-Pre" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 12 ; lv2:symbol "filter_post" ; lv2:name "Filter-Post" ;
        lv2:default 0.0 ; lv2:minimum -100.0 ; lv2:maximum 100.0 ;
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 13 ; lv2:symbol "env_follower" ; lv2:name "Envelope Follower" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 100.0 ;
        units:unit units:pc
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 14 ; lv2:symbol "stereo_mode" ; lv2:name "Stereo Mode" ;
        lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 1 ;
        lv2:portProperty lv2:integer , lv2:enumeration ;
        lv2:scalePoint [ rdfs:label "Dual Mono" ; rdf:value 0 ] ;
        lv2:scalePoint [ rdfs:label "Split" ;     rdf:value 1 ] ;
    ] .
endef

export H90_SCULPT_PLUGIN_CPP H90_SCULPT_PLUGIN_INFO_H H90_SCULPT_PLUGIN_MAKEFILE
export H90_SCULPT_MANIFEST_TTL H90_SCULPT_PLUGIN_TTL

# ---------------------------------------------------------------------------
# Buildroot Platform Compilation Commands
# ---------------------------------------------------------------------------
define H90_SCULPT_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/h90-sculpt
	printf '%s' "$$H90_SCULPT_PLUGIN_CPP"      > $(@D)/examples/h90-sculpt/H90SculptPlugin.cpp
	printf '%s' "$$H90_SCULPT_PLUGIN_INFO_H"   > $(@D)/examples/h90-sculpt/DistrhoPluginInfo.h
	printf '%s' "$$H90_SCULPT_PLUGIN_MAKEFILE" > $(@D)/examples/h90-sculpt/Makefile
endef

define H90_SCULPT_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/h90-sculpt lv2_dsp
endef

define H90_SCULPT_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/h90-sculpt.lv2
	cp $(@D)/bin/h90-sculpt.lv2/h90-sculpt_dsp.so $($(PKG)_PKGDIR)/h90-sculpt.lv2/
	printf '%s' "$$H90_SCULPT_MANIFEST_TTL" > $($(PKG)_PKGDIR)/h90-sculpt.lv2/manifest.ttl
	printf '%s' "$$H90_SCULPT_PLUGIN_TTL" > $($(PKG)_PKGDIR)/h90-sculpt.lv2/h90-sculpt.ttl
endef

$(eval $(generic-package))
