# Copyright (c) 2026 MOD Audio Limited
# SPDX-License-Identifier: MIT
#
# Gristleizer: stereo LFO-driven VCA/VCF effect inspired by the legendary
# Roy Gwinn design used by Throbbing Gristle.
#
# Signal chain:
#   Stereo in → Gain stage (JFET-style drive)
#   → Mode: VCA (LFO modulates amplitude) or VCF (LFO modulates filter cutoff)
#   → Filter Mix (dry + filtered blend in VCF mode)
#   → Level (output volume)
#
# Controls (matching the original + Endangered Audio panel):
#   Gain       - input drive (also adds JFET-style distortion at high levels)
#   Bias       - sets LFO modulation depth/shape; at extreme values adds
#                asymmetric distortion and DC offset shift (the Gristle magic)
#   Freq       - LFO rate (wide range: slow throb to audio-rate pseudo ring-mod)
#   Depth      - LFO modulation depth applied to VCA or VCF
#   Wave       - LFO waveform: 0=sine, 1=triangle, 2=sawtooth, 3=square
#   Mode       - 0=VCA (tremolo/AM), 1=VCF (filter sweep)
#   Filter Mix - (VCF mode) blend of dry and filtered signal
#   Level      - output level
#
# The Bias control is the key to the Gristleizer's character:
#   At centre (0.5): clean LFO modulation
#   Low Bias: LFO biased toward negative — asymmetric gating, quiet/whisper
#   High Bias: LFO biased toward positive — asymmetric clipping, loud/distorted
#   At extremes: pseudo ring-mod, hard gating, industrial noise
#
# Upload at https://builder.mod.audio/buildroot with MOD unit over USB.

GRISTLEIZER_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
GRISTLEIZER_SITE = https://github.com/DISTRHO/DPF.git
GRISTLEIZER_SITE_METHOD = git
GRISTLEIZER_BUNDLES = gristleizer.lv2

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

START_NAMESPACE_DISTRHO

// One-pole filter
struct P1 {
    float z;
    P1() : z(0.f) {}
    void reset() { z = 0.f; }
    float lp(float x, float a) { return z = x*(1.f-a) + z*a; }
    float hp(float x, float a) { float y=x*(1.f-a)+z*a; z=y; return x-y; }
};

// State-variable filter (2-pole LP)
struct SVF {
    float lp, bp;
    float sr;
    SVF() : lp(0.f), bp(0.f), sr(44100.f) {}
    void reset() { lp = bp = 0.f; }
    float tick(float in, float cutHz, float Q) {
        cutHz = fmaxf(20.f, fminf(cutHz, 18000.f));
        float f = 2.f * sinf(3.14159265f * cutHz / sr);
        float q = 1.f / fmaxf(Q, 0.1f);
        float hp = in - lp - q*bp;
        bp = f*hp + bp;
        lp = f*bp + lp;
        if (lp >  3.f) lp =  3.f;
        if (lp < -3.f) lp = -3.f;
        return lp;
    }
};

// JFET-style asymmetric soft distortion
// Models the second-harmonic character of JFET transistors
static inline float jfetDist(float x, float gain) {
    x *= gain;
    // Asymmetric: positive half clips softer (JFET pinch-off)
    if (x > 0.f)
        return 1.f - expf(-x);
    else
        return -tanhf(-x * 0.8f);
}

class GristleizerPlugin : public Plugin
{
public:
    GristleizerPlugin()
    : Plugin(kParameterCount, 0, 0),
      fSR(44100.f),
      pGain(0.5f), pBias(0.5f), pFreq(2.f), pDepth(0.7f),
      pWave(0.f), pMode(0.f), pFilterMix(0.5f), pLevel(0.8f),
      lfoPhL(0.f), lfoPhR(0.f)
    {}

protected:
    const char* getLabel()       const override { return "Gristleizer"; }
    const char* getDescription() const override { return "LFO-driven VCA/VCF effect inspired by the Throbbing Gristle Gristleizer. Tremolo, filter sweep, pseudo ring-mod and industrial noise."; }
    const char* getMaker()       const override { return "MOD Cookbook"; }
    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('G','R','S','T'); }

    void initParameter(uint32_t index, Parameter& p) override {
        switch(index) {
        case kGain:
            p.hints=kParameterIsAutomatable; p.name="Gain"; p.symbol="gain";
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kBias:
            p.hints=kParameterIsAutomatable; p.name="Bias"; p.symbol="bias";
            // 0=negative bias (gating), 0.5=centre (clean), 1=positive bias (distortion)
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kFreq:
            p.hints=kParameterIsAutomatable; p.name="Freq"; p.symbol="freq"; p.unit="Hz";
            // Wide range: slow throb to audio-rate pseudo ring-mod
            p.ranges.def=2.f; p.ranges.min=0.05f; p.ranges.max=500.f; break;
        case kDepth:
            p.hints=kParameterIsAutomatable; p.name="Depth"; p.symbol="depth";
            p.ranges.def=0.7f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kWave:
            p.hints=kParameterIsAutomatable|kParameterIsInteger;
            p.name="Wave"; p.symbol="wave";
            // 0=sine, 1=triangle, 2=sawtooth, 3=square
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=3.f; break;
        case kMode:
            p.hints=kParameterIsAutomatable|kParameterIsBoolean;
            p.name="Mode"; p.symbol="mode";
            // 0=VCA (tremolo), 1=VCF (filter sweep)
            p.ranges.def=0.f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kFilterMix:
            p.hints=kParameterIsAutomatable; p.name="Filter Mix"; p.symbol="filter_mix";
            p.ranges.def=0.5f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        case kLevel:
            p.hints=kParameterIsAutomatable; p.name="Level"; p.symbol="level";
            p.ranges.def=0.8f; p.ranges.min=0.f; p.ranges.max=1.f; break;
        default: break;
        }
    }

    float getParameterValue(uint32_t index) const override {
        switch(index) {
        case kGain:      return pGain;
        case kBias:      return pBias;
        case kFreq:      return pFreq;
        case kDepth:     return pDepth;
        case kWave:      return pWave;
        case kMode:      return pMode;
        case kFilterMix: return pFilterMix;
        case kLevel:     return pLevel;
        default:         return 0.f;
        }
    }

    void setParameterValue(uint32_t index, float v) override {
        switch(index) {
        case kGain:      pGain=v;      break;
        case kBias:      pBias=v;      break;
        case kFreq:      pFreq=v;      break;
        case kDepth:     pDepth=v;     break;
        case kWave:      pWave=v;      break;
        case kMode:      pMode=v;      break;
        case kFilterMix: pFilterMix=v; break;
        case kLevel:     pLevel=v;     break;
        default: break;
        }
    }

    void activate() override {
        fSR = (float)getSampleRate();
        fFiltL.sr = fFiltR.sr = fSR;
        fFiltL.reset(); fFiltR.reset();
        fDCL.reset(); fDCR.reset();
        lfoPhL = 0.f;
        lfoPhR = 0.f; // both channels share same LFO phase for mono-source compatibility
    }

    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];

        // Gain: 0..1 → 1..12x (JFET drive range)
        float driveGain = 1.f + pGain * pGain * 11.f;

        // Bias: shift the LFO operating point
        // At 0.5 = centred, 0 = biased negative (gate), 1 = biased positive (clip)
        float biasOffset = (pBias - 0.5f) * 2.f;  // -1..+1

        // VCF base cutoff: bias also shifts where the filter sits
        float vcfBaseCutoff = 200.f + pBias * 3800.f;

        bool  isVCF   = (pMode >= 0.5f);
        int   waveInt = (int)(pWave + 0.5f);
        float dcCut   = expf(-6.2832f * 10.f / fSR);

        for (uint32_t i=0; i<frames; ++i) {
            // ── LFO ───────────────────────────────────────────────────
            float lfo;
            switch(waveInt) {
            case 1: // triangle
                lfo = (lfoPhL < 0.5f) ? (4.f*lfoPhL - 1.f) : (3.f - 4.f*lfoPhL);
                break;
            case 2: // sawtooth
                lfo = 2.f*lfoPhL - 1.f;
                break;
            case 3: // square
                lfo = (lfoPhL < 0.5f) ? 1.f : -1.f;
                break;
            default: // sine
                lfo = sinf(lfoPhL * 6.2832f);
                break;
            }
            lfoPhL += pFreq / fSR;
            if (lfoPhL >= 1.f) lfoPhL -= 1.f;

            // Apply Bias offset to LFO output
            // This is the key Gristleizer behaviour: bias shifts the LFO
            // so modulation becomes asymmetric — the defining Gristle character
            float biasedLFO = lfo + biasOffset;

            // Clamp biased LFO to reasonable range
            if (biasedLFO >  1.5f) biasedLFO =  1.5f;
            if (biasedLFO < -1.5f) biasedLFO = -1.5f;

            // ── Gain stage ────────────────────────────────────────────
            float sL = jfetDist(inL[i], driveGain);
            float sR = jfetDist(inR[i], driveGain);

            // Remove DC after drive
            sL = fDCL.hp(sL, dcCut);
            sR = fDCR.hp(sR, dcCut);

            float wL, wR;

            if (!isVCF) {
                // ── VCA mode: LFO modulates amplitude ─────────────────
                // gain = 0.5 + biasedLFO * depth * 0.5
                // → at depth=1, bias=centre: gain sweeps 0..1
                // → at high bias: gain stays high (loud/distorted)
                // → at low bias: gain dips to near-zero (gating)
                float vcaGain = 0.5f + biasedLFO * pDepth * 0.5f;
                if (vcaGain < 0.f) vcaGain = 0.f;
                if (vcaGain > 2.f) vcaGain = 2.f; // allow some overdrive at high bias
                wL = sL * vcaGain;
                wR = sR * vcaGain;
            } else {
                // ── VCF mode: LFO modulates filter cutoff ──────────────
                // Cutoff sweeps from base up/down based on LFO + bias
                float cutoffMod = biasedLFO * pDepth * 4000.f;
                float cutoffL   = vcfBaseCutoff + cutoffMod;
                float cutoffR   = vcfBaseCutoff + cutoffMod;
                if (cutoffL < 20.f)    cutoffL = 20.f;
                if (cutoffL > 18000.f) cutoffL = 18000.f;
                if (cutoffR < 20.f)    cutoffR = 20.f;
                if (cutoffR > 18000.f) cutoffR = 18000.f;

                float filtL = fFiltL.tick(sL, cutoffL, 2.5f);
                float filtR = fFiltR.tick(sR, cutoffR, 2.5f);

                // Filter Mix: 0=dry only, 0.5=50/50, 1=filter only
                wL = sL*(1.f - pFilterMix) + filtL*pFilterMix;
                wR = sR*(1.f - pFilterMix) + filtR*pFilterMix;
            }

            outL[i] = wL * pLevel;
            outR[i] = wR * pLevel;
        }
    }

private:
    float fSR;
    float pGain, pBias, pFreq, pDepth, pWave, pMode, pFilterMix, pLevel;
    float lfoPhL, lfoPhR;

    SVF fFiltL, fFiltR;
    P1  fDCL,   fDCR;

    enum Parameters {
        kGain=0, kBias, kFreq, kDepth, kWave, kMode, kFilterMix, kLevel,
        kParameterCount
    };

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GristleizerPlugin)
};

Plugin* createPlugin() { return new GristleizerPlugin(); }
END_NAMESPACE_DISTRHO
endef

define GRISTLEIZER_PLUGIN_INFO_H
#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_INFO_H_INCLUDED
#define DISTRHO_PLUGIN_BRAND       "MOD Cookbook"
#define DISTRHO_PLUGIN_NAME        "Gristleizer"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:gristleizer"
#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 {
    kGain=0, kBias, kFreq, kDepth, kWave, kMode, kFilterMix, kLevel,
    kParameterCount
};
#endif
endef

define GRISTLEIZER_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = gristleizer
FILES_DSP = GristleizerPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

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

define GRISTLEIZER_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:gristleizer>
    a lv2:Plugin , lv2:ModulatorPlugin ;
    doap:name "Gristleizer" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [ foaf:name "MOD Cookbook" ; foaf:homepage <https://mod.audio> ] ;
    rdfs:comment "LFO-driven VCA/VCF inspired by the Throbbing Gristle Gristleizer. Tremolo, filter, pseudo ring-mod, industrial noise." ;
    lv2:port [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 0 ; lv2:symbol "in_l" ; lv2:name "Audio In L"
    ] , [
        a lv2:InputPort , lv2:AudioPort ;
        lv2:index 1 ; lv2:symbol "in_r" ; lv2:name "Audio In R"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 2 ; lv2:symbol "out_l" ; lv2:name "Audio Out L"
    ] , [
        a lv2:OutputPort , lv2:AudioPort ;
        lv2:index 3 ; lv2:symbol "out_r" ; lv2:name "Audio Out R"
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 4 ; lv2:symbol "gain" ; lv2:name "Gain" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ; lv2:symbol "bias" ; lv2:name "Bias" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ; lv2:symbol "freq" ; lv2:name "Freq" ;
        lv2:default 2.0 ; lv2:minimum 0.05 ; lv2:maximum 500.0 ; units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ; lv2:symbol "depth" ; lv2:name "Depth" ;
        lv2:default 0.7 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 8 ; lv2:symbol "wave" ; lv2:name "Wave" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 3.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ; lv2:symbol "mode" ; lv2:name "Mode" ;
        lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ; lv2:symbol "filter_mix" ; lv2:name "Filter Mix" ;
        lv2:default 0.5 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 11 ; lv2:symbol "level" ; lv2:name "Level" ;
        lv2:default 0.8 ; lv2:minimum 0.0 ; lv2:maximum 1.0
    ] .
endef

export GRISTLEIZER_PLUGIN_CPP
export GRISTLEIZER_PLUGIN_INFO_H
export GRISTLEIZER_PLUGIN_MAKEFILE
export GRISTLEIZER_MANIFEST_TTL
export GRISTLEIZER_PLUGIN_TTL

define GRISTLEIZER_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/gristleizer
	printf '%s' "$$GRISTLEIZER_PLUGIN_CPP"      > $(@D)/examples/gristleizer/GristleizerPlugin.cpp
	printf '%s' "$$GRISTLEIZER_PLUGIN_INFO_H"   > $(@D)/examples/gristleizer/DistrhoPluginInfo.h
	printf '%s' "$$GRISTLEIZER_PLUGIN_MAKEFILE" > $(@D)/examples/gristleizer/Makefile
endef

define GRISTLEIZER_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/gristleizer lv2_dsp
endef

define GRISTLEIZER_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/gristleizer.lv2
	cp $(@D)/bin/gristleizer.lv2/gristleizer_dsp.so $($(PKG)_PKGDIR)/gristleizer.lv2/
	printf '%s' "$$GRISTLEIZER_MANIFEST_TTL" > $($(PKG)_PKGDIR)/gristleizer.lv2/manifest.ttl
	printf '%s' "$$GRISTLEIZER_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/gristleizer.lv2/gristleizer.ttl
endef

$(eval $(generic-package))
