# Chase Bliss MOOD Spatial Micro-Looper Emulation - MOD Dwarf plugin
# Dual-Channel Micro-Sampler, Ambient Space Delay, and Internal Variable Clock
# Upload at https://builder.mod.audio/buildroot

MOOD_VERSION = 61d38eb638449647fb8395a35c5b8dab7e981ba7
MOOD_SITE = https://github.com/DISTRHO/DPF.git
MOOD_SITE_METHOD = git
MOOD_BUNDLES = chase-bliss-mood.lv2

define MOOD_PLUGIN_CPP
#include "DistrhoPlugin.hpp"
#include <cmath>
#include <cstring>
#include <algorithm>

START_NAMESPACE_DISTRHO

#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif

#define MOOD_MICRO_BUF_SIZE 96000 // ~2 seconds of micro-loop memory at 48kHz
#define MOOD_DELAY_BUF_SIZE 48000 // Space engine buffer memory size

// Micro-looper module capturing short fragments of sound continuously
struct MicroLooperEngine {
    float buffer[MOOD_MICRO_BUF_SIZE];
    int writePointer;
    float readPointer;
    int loopLength;
    bool isFrozen;

    MicroLooperEngine() : writePointer(0), readPointer(0.f), loopLength(MOOD_MICRO_BUF_SIZE), isFrozen(false) {
        std::memset(buffer, 0, sizeof(buffer));
    }

    void reset() {
        std::memset(buffer, 0, sizeof(buffer));
        writePointer = 0;
        readPointer = 0.f;
        loopLength = MOOD_MICRO_BUF_SIZE;
        isFrozen = false;
    }

    inline void write(float in) {
        if (!isFrozen) {
            buffer[writePointer] = in;
            writePointer = (writePointer + 1) % MOOD_MICRO_BUF_SIZE;
        }
    }

    inline float readLinear(float pos) {
        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];
    }
};

// Ambient space reverb/delay tracking matrix
struct SpaceDelayEngine {
    float buffer[MOOD_DELAY_BUF_SIZE];
    int writePointer;
    int delayLength;

    SpaceDelayEngine() : writePointer(0), delayLength(24000) {
        std::memset(buffer, 0, sizeof(buffer));
    }

    void reset() {
        std::memset(buffer, 0, sizeof(buffer));
        writePointer = 0;
    }

    inline float process(float in, float feedback, int length) {
        delayLength = std::max(100, std::min(length, MOOD_DELAY_BUF_SIZE - 1));
        int readPointer = writePointer - delayLength;
        if (readPointer < 0) readPointer += MOOD_DELAY_BUF_SIZE;

        float out = buffer[readPointer];
        buffer[writePointer] = in + out * feedback;
        
        writePointer = (writePointer + 1) % MOOD_DELAY_BUF_SIZE;
        return out;
    }
};

class MoodPlugin : public Plugin {
public:
    MoodPlugin() : Plugin(kParameterCount, 0, 0),
        fSR(44100.f), pFreeze(0.f), pLength(0.5f), pSpaceFeedback(0.4f), pClock(1.f), pMix(0.5f),
        prevFreezeState(0.f)
    {
        looperL.reset(); looperR.reset();
        spaceL.reset();  spaceR.reset();
    }

protected:
    const char* getLabel() const override { return "CBMood"; }
    const char* getDescription() const override { return "Chase Bliss MOOD style spatial micro-looper, granular phrase freezer, and variable clock space engine."; }
    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('M', 'O', 'O', 'D'); }

    void initParameter(uint32_t i, Parameter& p) override {
        switch (i) {
        case kFreeze: p.hints = kParameterIsAutomatable | kParameterIsBoolean; p.name = "Micro-Loop Freeze"; p.symbol = "freeze";
            p.ranges.def = 0.f; p.ranges.min = 0.f; p.ranges.max = 1.f; break;
        case kLength: p.hints = kParameterIsAutomatable; p.name = "Loop Length / Time"; p.symbol = "length";
            p.ranges.def = 0.5f; p.ranges.min = 0.05f; p.ranges.max = 1.f; break;
        case kSpaceFeedback: p.hints = kParameterIsAutomatable; p.name = "Space Decay / Feedback"; p.symbol = "space_decay";
            p.ranges.def = 0.4f; p.ranges.min = 0.f; p.ranges.max = 0.95f; break;
        case kClock: p.hints = kParameterIsAutomatable; p.name = "Internal Clock Rate"; p.symbol = "clock";
            p.ranges.def = 1.f; p.ranges.min = 0.25f; p.ranges.max = 2.f; break; // Slurs sample rate, creating pitch shifts
        case kMix: p.hints = kParameterIsAutomatable; p.name = "Wet Balance 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 kFreeze: return pFreeze; case kLength: return pLength;
        case kSpaceFeedback: return pSpaceFeedback; case kClock: return pClock; case kMix: return pMix;
        default: return 0.f;
        }
    }

    void setParameterValue(uint32_t i, float v) override {
        switch (i) {
        case kFreeze: pFreeze = v; break; case kLength: pLength = v; break;
        case kSpaceFeedback: pSpaceFeedback = v; break; case kClock: pClock = v; break; case kMix: pMix = v; break;
        default: break;
        }
    }

    void activate() override {
        fSR = (float)getSampleRate();
        looperL.reset(); looperR.reset();
        spaceL.reset();  spaceR.reset();
        prevFreezeState = 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) {
            float monoIn = (inL[i] + inR[i]) * 0.5f;

            // Handle momentary or latched micro-looper freeze capture window boundaries
            if (pFreeze >= 0.5f && prevFreezeState < 0.5f) {
                looperL.isFrozen = true;
                looperR.isFrozen = true;
                // Calculate size based on Length knob parameter setting
                int calculatedSize = (int)(pLength * (float)MOOD_MICRO_BUF_SIZE);
                looperL.loopLength = std::max(1024, calculatedSize);
                looperR.loopLength = std::max(1024, calculatedSize);
                looperL.readPointer = 0.f;
                looperR.readPointer = 0.f;
            } 
            else if (pFreeze < 0.5f && prevFreezeState >= 0.5f) {
                looperL.isFrozen = false;
                looperR.isFrozen = false;
            }
            prevFreezeState = pFreeze;

            // Stream continuous ambient tracking background writes to micro loopers
            looperL.write(inL[i]);
            looperR.write(inR[i]);

            // Track internal relative sample clock stepping increments
            float loopIncrement = pClock;

            float wetLooperL = 0.f;
            float wetLooperR = 0.f;

            if (looperL.isFrozen) {
                wetLooperL = looperL.readLinear(looperL.readPointer);
                looperL.readPointer += loopIncrement;
                if (looperL.readPointer >= (float)looperL.loopLength) looperL.readPointer -= (float)looperL.loopLength;

                wetLooperR = looperR.readLinear(looperR.readPointer);
                looperR.readPointer += loopIncrement;
                if (looperR.readPointer >= (float)looperR.loopLength) looperR.readPointer -= (float)looperR.loopLength;
            } else {
                // If not frozen, loop layers pass ambient clean content
                wetLooperL = inL[i];
                wetLooperR = inR[i];
            }

            // Route audio context to the Space Reverberant Delay engine (OBNE side)
            int targetDelayLength = (int)((1.1f - pLength) * 18000.f * (1.f / pClock));
            float wetSpaceL = spaceL.process(monoIn + wetLooperL * 0.3f, pSpaceFeedback, targetDelayLength);
            float wetSpaceR = spaceR.process(monoIn + wetLooperR * 0.3f, pSpaceFeedback, targetDelayLength + 430); // Offset right channel to widen spatial stereo field

            // Combine both channels to mimic interconnected MOOD interplay paths
            float compositeWetL = wetLooperL * 0.6f + wetSpaceL * 0.5f;
            float compositeWetR = wetLooperR * 0.6f + wetSpaceR * 0.5f;

            // Output level structural formatting matrices
            outL[i] = (inL[i] * (1.f - pMix)) + (compositeWetL * pMix);
            outR[i] = (inR[i] * (1.f - pMix)) + (compositeWetR * pMix);
        }
    }

private:
    float fSR;
    float pFreeze, pLength, pSpaceFeedback, pClock, pMix;
    float prevFreezeState;

    MicroLooperEngine looperL, looperR;
    SpaceDelayEngine  spaceL, spaceR;

    enum Parameters { kFreeze = 0, kLength, kSpaceFeedback, kClock, kMix, kParameterCount };
    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MoodPlugin)
};

Plugin* createPlugin() { return new MoodPlugin(); }
END_NAMESPACE_DISTRHO
endef

define MOOD_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  "MOOD Spatial Micro-Looper"
#define DISTRHO_PLUGIN_URI   "urn:mod-cookbook:chase-bliss-mood"
#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 { kFreeze = 0, kLength, kSpaceFeedback, kClock, kMix, kParameterCount };
#endif
endef

define MOOD_PLUGIN_MAKEFILE
#!/usr/bin/make -f
NAME = chase-bliss-mood
FILES_DSP = MoodPlugin.cpp
include ../../Makefile.plugins.mk
TARGETS = lv2_dsp
all: $(TARGETS)
endef

define MOOD_MANIFEST_TTL
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<urn:mod-cookbook:chase-bliss-mood>
    a lv2:Plugin, lv2:SpatialPlugin;
    lv2:binary <chase-bliss-mood_dsp.so>;
    rdfs:seeAlso <chase-bliss-mood.ttl>.
endef

define MOOD_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-mood>
    a lv2:Plugin, lv2:SpatialPlugin;
    doap:name "MOOD Spatial Micro-Looper";
    doap:license <http://opensource.org/licenses/MIT>;
    doap:maintainer [foaf:name "MOD Cookbook"; foaf:homepage <https://mod.audio>];
    rdfs:comment "A dual channel textures workspace containing an always listening granular phrase sampler paired with a fluid space architecture and dynamic clock shifting logic.";
    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 "freeze"; lv2:name "Micro-Loop Freeze";
        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 "length"; lv2:name "Loop Length / Time";
        lv2:default 0.5; lv2:minimum 0.05; lv2:maximum 1.0;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 6; lv2:symbol "space_decay"; lv2:name "Space Feedback";
        lv2:default 0.4; lv2:minimum 0.0; lv2:maximum 0.95;
    ],[
        a lv2:InputPort, lv2:ControlPort; lv2:index 7; lv2:symbol "clock"; lv2:name "Internal Clock";
        lv2:default 1.0; lv2:minimum 0.25; lv2:maximum 2.0;
    ],[
        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 MOOD_PLUGIN_CPP MOOD_PLUGIN_INFO_H MOOD_PLUGIN_MAKEFILE
export MOOD_MANIFEST_TTL MOOD_PLUGIN_TTL

define MOOD_CONFIGURE_CMDS
	mkdir -p $(@D)/examples/chase-bliss-mood
	printf '%s' "$$MOOD_PLUGIN_CPP"      > $(@D)/examples/chase-bliss-mood/MoodPlugin.cpp
	printf '%s' "$$MOOD_PLUGIN_INFO_H"   > $(@D)/examples/chase-bliss-mood/DistrhoPluginInfo.h
	printf '%s' "$$MOOD_PLUGIN_MAKEFILE" > $(@D)/examples/chase-bliss-mood/Makefile
endef

define MOOD_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) NOOPT=true -C $(@D)/examples/chase-bliss-mood lv2_dsp
endef

define MOOD_INSTALL_TARGET_CMDS
	mkdir -p $($(PKG)_PKGDIR)/chase-bliss-mood.lv2
	cp $(@D)/bin/chase-bliss-mood.lv2/chase-bliss-mood_dsp.so $($(PKG)_PKGDIR)/chase-bliss-mood.lv2/
	printf '%s' "$$MOOD_MANIFEST_TTL" > $($(PKG)_PKGDIR)/chase-bliss-mood.lv2/manifest.ttl
	printf '%s' "$$MOOD_PLUGIN_TTL"   > $($(PKG)_PKGDIR)/chase-bliss-mood.lv2/chase-bliss-mood.ttl
endef

$(eval $(generic-package))
