# Chase Bliss Blooper Emulation - MOD Dwarf plugin
# Creative Phrase Looper with Realtime Stability Degradation & Modifiers
# Upload at https://builder.mod.audio/buildroot

BLOOPER_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
BLOOPER_SITE = https://github.com/DISTRHO/DPF.git
BLOOPER_SITE_METHOD = git
BLOOPER_BUNDLES = chase-bliss-blooper.lv2

define BLOOPER_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>
#include <algorithm>

START_NAMESPACE_DISTRHO

#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif

#define BLOOPER_MAX_SAMPLES 524288 // ~12 seconds of loop memory at 44.1kHz

// Realtime phrase capture loop module
struct BlooperMemory {
    float buffer[BLOOPER_MAX_SAMPLES];
    int writeHead;
    float readHead;
    int loopLength;
    bool isRecording;
    bool hasLoop;

    BlooperMemory() : writeHead(0), readHead(0.f), loopLength(0), isRecording(false), hasLoop(false) {
        std::memset(buffer, 0, sizeof(buffer));
    }

    void reset() {
        std::memset(buffer, 0, sizeof(buffer));
        writeHead = 0;
        readHead = 0.f;
        loopLength = 0;
        isRecording = false;
        hasLoop = false;
    }

    inline float readLinear(float pos) {
        if (loopLength <= 0) return 0.f;
        int idx1 = (int)pos;
        int idx2 = idx1 + 1;
        float frac = pos - (float)idx1;

        idx1 = (idx1 % loopLength + loopLength) % loopLength;
        idx2 = (idx2 % loopLength + loopLength) % loopLength;

        return (1.f - frac) * buffer[idx1] + frac * buffer[idx2];
    }
};

class BlooperPlugin : public Plugin {
public:
    BlooperPlugin() : Plugin(kParameterCount, 0, 0),
        fSR(44100.f), pRecord(0.f), pPlaySpeed(1.f), pStability(0.f), pVolume(1.f), pMix(0.5f),
        wowPhase(0.f), flutterPhase(0.f), noiseZ(0.f), prevRecordState(0.f)
    {
        loopL.reset();
        loopR.reset();
    }

protected:
    const char* getLabel() const override { return "CBBlooper"; }
    const char* getDescription() const override { return "Chase Bliss Blooper style creative looper with stability analog tape degradation and speed modifiers."; }
    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', 'L', 'O', 'O'); }

    void initParameter(uint32_t i, Parameter& p) override {
        switch (i) {
        case kRecord: p.hints = kParameterIsAutomatable | kParameterIsBoolean; p.name = "Record / Master Capture"; p.symbol = "record";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 1.f; break;
        case kPlaySpeed: p.hints = kParameterIsAutomatable; p.name = "Modifier (Speed/Dir)"; p.symbol = "speed";
            p.ranges.def = 1.f; p.ranges.min = -2.f; p.ranges.max = 2.f; break; // Supports reverse, tape stop, double speed
        case kStability: p.hints = kParameterIsAutomatable; p.name = "Stability (Tape Aging)"; p.symbol = "stability";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 1.f; break;
        case kVolume: p.hints = kParameterIsAutomatable; p.name = "Loop Level"; p.symbol = "volume";
            p.ranges.def = 1.f; p.ranges.min = 0.f; p.ranges.max = 1.5f; break;
        case kMix: p.hints = kParameterIsAutomatable; p.name = "Dry / Loop Mix"; p.symbol = "mix";
            p.ranges.def = 0.5f; p.ranges.min = 0.f; p.ranges.max = 1.f; break;
        default: break;
        }
    }

    float getParameterValue(uint32_t i) const override {
        switch (i) {
        case kRecord: return pRecord; case kPlaySpeed: return pPlaySpeed;
        case kStability: return pStability; case kVolume: return pVolume; case kMix: return pMix;
        default: return 0.f;
        }
    }

    void setParameterValue(uint32_t i, float v) override {
        switch (i) {
        case kRecord: pRecord = v; break; case kPlaySpeed: pPlaySpeed = v; break;
        case kStability: pStability = v; break; case kVolume: pVolume = v; break; case kMix: pMix = v; break;
        default: break;
        }
    }

    void activate() override {
        fSR = (float)getSampleRate();
        loopL.reset();
        loopR.reset();
        wowPhase = 0.f;
        flutterPhase = 0.f;
        noiseZ = 0.f;
        prevRecordState = 0.f;
    }

    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];

        for (uint32_t i = 0; i < frames; ++i) {
            // Edge detection for loop state manipulation
            if (pRecord >= 0.5f && prevRecordState < 0.5f) {
                // Instantly punch into recording
                loopL.isRecording = true; loopR.isRecording = true;
                loopL.writeHead = 0;      loopR.writeHead = 0;
                loopL.hasLoop = false;    loopR.hasLoop = false;
            } 
            else if (pRecord < 0.5f && prevRecordState >= 0.5f) {
                // Dropping out of record registers loop dimensions
                loopL.isRecording = false;                  loopR.isRecording = false;
                loopL.loopLength = loopL.writeHead;         loopR.loopLength = loopR.writeHead;
                loopL.readHead = 0.f;                       loopR.readHead = 0.f;
                if (loopL.loopLength > 100) loopL.hasLoop = true;
                if (loopR.loopLength > 100) loopR.hasLoop = true;
            }
            prevRecordState = pRecord;

            // Handle active frame writing logic
            if (loopL.isRecording) {
                if (loopL.writeHead < BLOOPER_MAX_SAMPLES) {
                    loopL.buffer[loopL.writeHead] = inL[i];
                    loopL.writeHead++;
                }
            }
            if (loopR.isRecording) {
                if (loopR.writeHead < BLOOPER_MAX_SAMPLES) {
                    loopR.buffer[loopR.writeHead] = inR[i];
                    loopR.writeHead++;
                }
            }

            // Realtime Stability Tape Emulation math
            wowPhase += 1.2f / fSR;     if (wowPhase >= 1.f) wowPhase -= 1.f;
            flutterPhase += 18.5f / fSR; if (flutterPhase >= 1.f) flutterPhase -= 1.f;

            // Generate structural degradation parameters using high density coefficients
            float wow = std::sin(wowPhase * 2.f * M_PI) * 0.003f * pStability;
            float flutter = std::sin(flutterPhase * 2.f * M_PI) * 0.0015f * std::pow(pStability, 2.f);

            // Compute the composite speed modifier factor
            float totalSpeedModifier = pPlaySpeed + wow + flutter;

            float wetL = 0.f;
            float wetR = 0.f;

            // Calculate loop memory readout if valid frames exist
            if (loopL.hasLoop && loopL.loopLength > 0) {
                wetL = loopL.readLinear(loopL.readHead);
                loopL.readHead += totalSpeedModifier;
                if (loopL.readHead >= (float)loopL.loopLength) loopL.readHead -= (float)loopL.loopLength;
                if (loopL.readHead < 0.f) loopL.readHead += (float)loopL.loopLength;
            }

            if (loopR.hasLoop && loopR.loopLength > 0) {
                wetR = loopR.readLinear(loopR.readHead);
                loopR.readHead += totalSpeedModifier;
                if (loopR.readHead >= (float)loopR.loopLength) loopR.readHead -= (float)loopR.loopLength;
                if (loopR.readHead < 0.f) loopR.readHead += (float)loopR.loopLength;
            }

            // Generate high pass background tape noise filtering matching stability depth
            float rawNoise = ((float)std::rand() / (float)RAND_MAX) * 2.f - 1.f;
            noiseZ = 0.95f * noiseZ + 0.05f * rawNoise; // Smooth noise node
            float filteredNoise = rawNoise - noiseZ;

            // Add hardware character degradation directly to the loop path
            if (loopL.hasLoop || loopR.hasLoop) {
                wetL += filteredNoise * pStability * 0.015f;
                wetR += filteredNoise * pStability * 0.015f;
            }

            // Master loop gain processing
            wetL *= pVolume;
            wetR *= pVolume;

            // Output stage mix formulation
            outL[i] = (inL[i] * (1.f - pMix)) + (wetL * pMix);
            outR[i] = (inR[i] * (1.f - pMix)) + (wetR * pMix);
        }
    }

private:
    float fSR;
    float pRecord, pPlaySpeed, pStability, pVolume, pMix;
    
    float wowPhase;
    float flutterPhase;
    float noiseZ;
    float prevRecordState;

    BlooperMemory loopL, loopR;

    enum Parameters { kRecord = 0, kPlaySpeed, kStability, kVolume, kMix, kParameterCount };
    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BlooperPlugin)
};

Plugin* createPlugin() { return new BlooperPlugin(); }
END_NAMESPACE_DISTRHO
endef

define BLOOPER_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  "Blooper Looper"
#define DISTRHO_PLUGIN_URI   "urn:mod-cookbook:chase-bliss-blooper"
#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 { kRecord = 0, kPlaySpeed, kStability, kVolume, kMix, kParameterCount };
#endif
endef

define BLOOPER_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = chase-bliss-blooper
FILES_DSP = BlooperPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

define BLOOPER_MANIFEST_TTL
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<urn:mod-cookbook:chase-bliss-blooper>
    a lv2:Plugin, lv2:LooperPlugin;
    lv2:binary <chase-bliss-blooper_dsp.so>;
    rdfs:seeAlso <chase-bliss-blooper.ttl>.
endef

define BLOOPER_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#> .
<urn:mod-cookbook:chase-bliss-blooper>
    a lv2:Plugin, lv2:LooperPlugin;
    doap:name "Blooper Looper";
    doap:license <http://opensource.org/licenses/MIT>;
    doap:maintainer [foaf:name "MOD Cookbook"; foaf:homepage <https://mod.audio>];
    rdfs:comment "A multi-mode modifier loop phrase recorder replicating bottomless structural tape tape aging and speed variations.";
    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 "record"; lv2:name "Record Loop";
        lv2:default 0.0; lv2:minimum 0.0; lv2:maximum 1.0;
        lv2:portProperty lv2:toggled;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 5; lv2:symbol "speed"; lv2:name "Speed / Direction";
        lv2:default 1.0; lv2:minimum -2.0; lv2:maximum 2.0;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 6; lv2:symbol "stability"; lv2:name "Stability (Tape Aging)";
        lv2:default 0.0; lv2:minimum 0.0; lv2:maximum 1.0;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 7; lv2:symbol "volume"; lv2:name "Loop Volume";
        lv2:default 1.0; lv2:minimum 0.0; lv2:maximum 1.5;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 8; lv2:symbol "mix"; lv2:name "Mix Balance";
        lv2:default 0.5; lv2:minimum 0.0; lv2:maximum 1.0;
    ].
endef

export BLOOPER_PLUGIN_CPP BLOOPER_PLUGIN_INFO_H BLOOPER_PLUGIN_MAKEFILE
export BLOOPER_MANIFEST_TTL BLOOPER_PLUGIN_TTL

define BLOOPER_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/chase-bliss-blooper
	printf '%s' "$$BLOOPER_PLUGIN_CPP"      > $(@D)/examples/chase-bliss-blooper/BlooperPlugin.cpp
	printf '%s' "$$BLOOPER_PLUGIN_INFO_H"   > $(@D)/examples/chase-bliss-blooper/DistrhoPluginInfo.h
	printf '%s' "$$BLOOPER_PLUGIN_MAKEFILE" > $(@D)/examples/chase-bliss-blooper/Makefile
endef

define BLOOPER_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/chase-bliss-blooper lv2_dsp
endef

define BLOOPER_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/chase-bliss-blooper.lv2
	cp $(@D)/bin/chase-bliss-blooper.lv2/chase-bliss-blooper_dsp.so $($(PKG)_PKGDIR)/chase-bliss-blooper.lv2/
	printf '%s' "$$BLOOPER_MANIFEST_TTL" > $($(PKG)_PKGDIR)/chase-bliss-blooper.lv2/manifest.ttl
	printf '%s' "$$BLOOPER_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/chase-bliss-blooper.lv2/chase-bliss-blooper.ttl
endef

$(eval $(generic-package))
