# Copyright (c) 2026 MOD Audio Limited
# SPDX-License-Identifier: MIT

BIT_CRUSHER_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
BIT_CRUSHER_SITE = https://github.com/DISTRHO/DPF.git
BIT_CRUSHER_SITE_METHOD = git
BIT_CRUSHER_BUNDLES = bit-crusher.lv2

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

START_NAMESPACE_DISTRHO

class BitCrusherPlugin : public Plugin
{
public:
    BitCrusherPlugin()
        : Plugin(kParameterCount, 0, 0),
          fBitDepth(16.0f),
          fSrReduce(1.0f),
          fDrive(1.0f),
          fMix(1.0f),
          fLowPass(20000.0f),
          fHighPass(20.0f),
          fGateThresh(0.0f),
          fSampleRate(48000.0f),
          fSrCounter(0.0f),
          fHeldL(0.0f),
          fHeldR(0.0f),
          fLpZ1L(0.0f), fLpZ1R(0.0f),
          fHpZ1L(0.0f), fHpZ1R(0.0f),
          fHpOutZ1L(0.0f), fHpOutZ1R(0.0f),
          fGateGainL(0.0f), fGateGainR(0.0f)
    {
        updateFilters();
    }

protected:
    const char* getLabel()       const override { return "BitCrusher"; }
    const char* getDescription() const override { return "Stereo bit crusher with sample rate reduction, drive, EQ and noise gate."; }
    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('B', 'C', 'r', '1'); }

    void initParameter(uint32_t index, Parameter& parameter) override
    {
        switch (index)
        {
        case kBitDepth:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Bit Depth";
            parameter.symbol     = "bitdepth";
            parameter.unit       = "bits";
            parameter.ranges.def = 16.0f;
            parameter.ranges.min = 1.0f;
            parameter.ranges.max = 16.0f;
            break;
        case kSrReduce:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Sample Rate Reduce";
            parameter.symbol     = "srreduce";
            parameter.unit       = "x";
            parameter.ranges.def = 1.0f;
            parameter.ranges.min = 1.0f;
            parameter.ranges.max = 32.0f;
            break;
        case kDrive:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Drive";
            parameter.symbol     = "drive";
            parameter.unit       = "dB";
            parameter.ranges.def = 0.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 24.0f;
            break;
        case kMix:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Mix";
            parameter.symbol     = "mix";
            parameter.unit       = "%";
            parameter.ranges.def = 100.0f;
            parameter.ranges.min = 0.0f;
            parameter.ranges.max = 100.0f;
            break;
        case kLowPass:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Low Pass";
            parameter.symbol     = "lowpass";
            parameter.unit       = "Hz";
            parameter.ranges.def = 20000.0f;
            parameter.ranges.min = 200.0f;
            parameter.ranges.max = 20000.0f;
            break;
        case kHighPass:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "High Pass";
            parameter.symbol     = "highpass";
            parameter.unit       = "Hz";
            parameter.ranges.def = 20.0f;
            parameter.ranges.min = 20.0f;
            parameter.ranges.max = 5000.0f;
            break;
        case kGateThresh:
            parameter.hints      = kParameterIsAutomatable;
            parameter.name       = "Gate Threshold";
            parameter.symbol     = "gatethresh";
            parameter.unit       = "dB";
            parameter.ranges.def = -80.0f;
            parameter.ranges.min = -80.0f;
            parameter.ranges.max = 0.0f;
            break;
        }
    }

    float getParameterValue(uint32_t index) const override
    {
        switch (index)
        {
        case kBitDepth:   return fBitDepth;
        case kSrReduce:   return fSrReduce;
        case kDrive:      return 20.0f * log10f(fDrive > 0.00001f ? fDrive : 0.00001f);
        case kMix:        return fMix * 100.0f;
        case kLowPass:    return fLowPass;
        case kHighPass:   return fHighPass;
        case kGateThresh: return (fGateThresh > 0.00001f)
                                 ? 20.0f * log10f(fGateThresh) : -80.0f;
        default:          return 0.0f;
        }
    }

    void setParameterValue(uint32_t index, float value) override
    {
        switch (index)
        {
        case kBitDepth:
            fBitDepth = value;
            break;
        case kSrReduce:
            fSrReduce = value;
            break;
        case kDrive:
            fDrive = powf(10.0f, value / 20.0f);
            break;
        case kMix:
            fMix = value / 100.0f;
            break;
        case kLowPass:
            fLowPass = value;
            updateFilters();
            break;
        case kHighPass:
            fHighPass = value;
            updateFilters();
            break;
        case kGateThresh:
            fGateThresh = powf(10.0f, value / 20.0f);
            break;
        }
    }

    void activate() override
    {
        fSampleRate = (float)getSampleRate();
        fSrCounter  = 0.0f;
        fHeldL = fHeldR = 0.0f;
        fLpZ1L = fLpZ1R = 0.0f;
        fHpZ1L = fHpZ1R = 0.0f;
        fHpOutZ1L = fHpOutZ1R = 0.0f;
        fGateGainL = fGateGainR = 0.0f;
        updateFilters();
    }

    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 wet        = fMix;
        const float dry        = 1.0f - fMix;
        const float drive      = fDrive;
        const float srStep     = fSrReduce;
        const float gateThresh = fGateThresh;
        const float lpCoeff    = fLpCoeff;
        const float hpCoeff    = fHpCoeff;

        // quantisation levels from bit depth
        const float bits    = fBitDepth < 1.0f ? 1.0f : fBitDepth;
        const float levels  = powf(2.0f, bits) - 1.0f;
        const float invLev  = (levels > 0.0f) ? (1.0f / levels) : 1.0f;

        // gate attack/release coefficients (~1ms attack, ~50ms release)
        const float gateAttack  = 1.0f - expf(-1.0f / (0.001f * fSampleRate));
        const float gateRelease = 1.0f - expf(-1.0f / (0.05f  * fSampleRate));

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

            // --- drive ---
            float sL = dryL * drive;
            float sR = dryR * drive;

            // --- sample rate reduction (sample & hold) ---
            fSrCounter += 1.0f;
            if (fSrCounter >= srStep)
            {
                fSrCounter -= srStep;
                fHeldL = sL;
                fHeldR = sR;
            }
            sL = fHeldL;
            sR = fHeldR;

            // --- bit crush (midtread quantisation) ---
            sL = floorf(sL * levels + 0.5f) * invLev;
            sR = floorf(sR * levels + 0.5f) * invLev;

            // clamp to avoid runaway from drive
            if (sL >  2.0f) sL =  2.0f;
            if (sL < -2.0f) sL = -2.0f;
            if (sR >  2.0f) sR =  2.0f;
            if (sR < -2.0f) sR = -2.0f;

            // --- high pass (1-pole) ---
            float hpInL  = sL;
            float hpInR  = sR;
            fHpZ1L       = hpCoeff * fHpZ1L + (1.0f - hpCoeff) * hpInL;
            fHpZ1R       = hpCoeff * fHpZ1R + (1.0f - hpCoeff) * hpInR;
            sL = hpInL - fHpZ1L;
            sR = hpInR - fHpZ1R;

            // --- low pass (1-pole) ---
            fLpZ1L = fLpZ1L + lpCoeff * (sL - fLpZ1L);
            fLpZ1R = fLpZ1R + lpCoeff * (sR - fLpZ1R);
            sL = fLpZ1L;
            sR = fLpZ1R;

            // --- noise gate (applied to wet signal) ---
            float absL = sL < 0.0f ? -sL : sL;
            float absR = sR < 0.0f ? -sR : sR;
            float envL = absL > fGateGainL ? absL : fGateGainL;
            float envR = absR > fGateGainR ? absR : fGateGainR;

            // smooth envelope
            if (absL > fGateGainL)
                fGateGainL += gateAttack   * (absL - fGateGainL);
            else
                fGateGainL += gateRelease  * (absL - fGateGainL);

            if (absR > fGateGainR)
                fGateGainR += gateAttack   * (absR - fGateGainR);
            else
                fGateGainR += gateRelease  * (absR - fGateGainR);

            float gL = (fGateGainL > gateThresh) ? 1.0f : 0.0f;
            float gR = (fGateGainR > gateThresh) ? 1.0f : 0.0f;

            sL *= gL;
            sR *= gR;

            // --- mix ---
            outL[i] = dry * dryL + wet * sL;
            outR[i] = dry * dryR + wet * sR;
        }
    }

private:
    // parameters
    float fBitDepth;
    float fSrReduce;
    float fDrive;
    float fMix;
    float fLowPass;
    float fHighPass;
    float fGateThresh;

    // runtime
    float fSampleRate;
    float fSrCounter;
    float fHeldL, fHeldR;

    // filter state
    float fLpCoeff;
    float fHpCoeff;
    float fLpZ1L, fLpZ1R;
    float fHpZ1L, fHpZ1R;
    float fHpOutZ1L, fHpOutZ1R;

    // gate state
    float fGateGainL, fGateGainR;

    void updateFilters()
    {
        float sr = fSampleRate > 0.0f ? fSampleRate : 48000.0f;
        // 1-pole LP coefficient
        float lpFreq = fLowPass  < 20.0f ? 20.0f : (fLowPass  > sr * 0.49f ? sr * 0.49f : fLowPass);
        float hpFreq = fHighPass < 1.0f  ? 1.0f  : (fHighPass > sr * 0.49f ? sr * 0.49f : fHighPass);
        fLpCoeff = 1.0f - expf(-2.0f * 3.14159265f * lpFreq / sr);
        fHpCoeff = expf(-2.0f * 3.14159265f * hpFreq / sr);
    }

    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusherPlugin)
};

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

END_NAMESPACE_DISTRHO
endef

define BIT_CRUSHER_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        "Bit Crusher"
#define DISTRHO_PLUGIN_URI         "urn:mod-cookbook:bit-crusher"

#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 {
    kBitDepth = 0,
    kSrReduce,
    kDrive,
    kMix,
    kLowPass,
    kHighPass,
    kGateThresh,
    kParameterCount
};

#endif
endef

define BIT_CRUSHER_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = bit-crusher
FILES_DSP = BitCrusherPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

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

<urn:mod-cookbook:bit-crusher>
    a lv2:Plugin , lv2:DistortionPlugin ;
    lv2:binary <bit-crusher_dsp.so> ;
    rdfs:seeAlso <bit-crusher.ttl> .
endef

define BIT_CRUSHER_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:bit-crusher>
    a lv2:Plugin , lv2:DistortionPlugin ;
    doap:name "Bit Crusher" ;
    doap:license <http://opensource.org/licenses/MIT> ;
    doap:maintainer [
        foaf:name "MOD Cookbook" ;
        foaf:homepage <https://mod.audio>
    ] ;
    rdfs:comment "Stereo bit crusher with sample rate reduction, drive, LP/HP EQ and noise gate." ;
    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 "bitdepth" ;
        lv2:name "Bit Depth" ;
        lv2:default 16.0 ;
        lv2:minimum 1.0 ;
        lv2:maximum 16.0 ;
        units:unit units:bits
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 5 ;
        lv2:symbol "srreduce" ;
        lv2:name "Sample Rate Reduce" ;
        lv2:default 1.0 ;
        lv2:minimum 1.0 ;
        lv2:maximum 32.0
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 6 ;
        lv2:symbol "drive" ;
        lv2:name "Drive" ;
        lv2:default 0.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 24.0 ;
        units:unit units:db
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 7 ;
        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 8 ;
        lv2:symbol "lowpass" ;
        lv2:name "Low Pass" ;
        lv2:default 20000.0 ;
        lv2:minimum 200.0 ;
        lv2:maximum 20000.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 9 ;
        lv2:symbol "highpass" ;
        lv2:name "High Pass" ;
        lv2:default 20.0 ;
        lv2:minimum 20.0 ;
        lv2:maximum 5000.0 ;
        units:unit units:hz
    ] , [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 10 ;
        lv2:symbol "gatethresh" ;
        lv2:name "Gate Threshold" ;
        lv2:default -80.0 ;
        lv2:minimum -80.0 ;
        lv2:maximum 0.0 ;
        units:unit units:db
    ] .
endef

export BIT_CRUSHER_PLUGIN_CPP
export BIT_CRUSHER_PLUGIN_INFO_H
export BIT_CRUSHER_PLUGIN_MAKEFILE
export BIT_CRUSHER_MANIFEST_TTL
export BIT_CRUSHER_PLUGIN_TTL

define BIT_CRUSHER_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/bit-crusher
	printf '%s' "$$BIT_CRUSHER_PLUGIN_CPP"      > $(@D)/examples/bit-crusher/BitCrusherPlugin.cpp
	printf '%s' "$$BIT_CRUSHER_PLUGIN_INFO_H"   > $(@D)/examples/bit-crusher/DistrhoPluginInfo.h
	printf '%s' "$$BIT_CRUSHER_PLUGIN_MAKEFILE" > $(@D)/examples/bit-crusher/Makefile
endef

define BIT_CRUSHER_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/bit-crusher lv2_dsp
endef

define BIT_CRUSHER_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/bit-crusher.lv2
	cp $(@D)/bin/bit-crusher.lv2/bit-crusher_dsp.so $($(PKG)_PKGDIR)/bit-crusher.lv2/
	printf '%s' "$$BIT_CRUSHER_MANIFEST_TTL" > $($(PKG)_PKGDIR)/bit-crusher.lv2/manifest.ttl
	printf '%s' "$$BIT_CRUSHER_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/bit-crusher.lv2/bit-crusher.ttl
endef

$(eval $(generic-package))
