// Drum sequencer — 14-port board, four Light Grids (kick · snare · hi-hat · clap), three Buttons, Knob, Speaker.
// Each grid is one drum: the eight outer LEDs are the eight steps (clockwise from top-left), the white dot is the playhead,
// the centre lights when that drum is selected. Button 5 = next drum · button 6 = tap a hit on the beat (again = remove,
// hold 1 s = clear the drum) · button 11 = play / stop (hold 1 s = clear everything) · knob = tempo 60–200 BPM.
#include <Arduino.h>
#include <math.h>
#include "modules/led/neopixel.h"
#include "modules/input/button.h"
#include "modules/input/rotary_encoder.h"
#include "modules/audio/speaker.h"

NeoPixelGrid  grids[4] = { NeoPixelGrid(G0A, G0B), NeoPixelGrid(G1A, G1B), NeoPixelGrid(G2A, G2B), NeoPixelGrid(G3A, G3B) };
ButtonModule  drumBtn(BTN_DRUM), tapBtn(BTN_TAP), playBtn(BTN_PLAY);
RotaryEncoder knob(KNOB_CLK, KNOB_DT, KNOB_SW, KNOB_RING);
Speaker       speaker(SPK_BCLK, SPK_LRCLK, SPK_DIN);

static const int STEPS = 8, DRUMS = 4, SAMPLE_RATE = 32000, HOLD_MS = 1000, BPM_MIN = 60, BPM_MAX = 200;
// The speaker driver's I2S DMA buffer (8 x 256 frames) is kept full, so a sound comes out this much after we render it.
// The playhead LED and the tap quantisation are shifted by the same amount so what you see and tap matches what you hear.
static const int LATENCY_MS = 8 * 256 * 1000 / SAMPLE_RATE;   // 64 ms at 32 kHz

const uint8_t COLOR[DRUMS][3] = { { 255, 30, 20 }, { 40, 90, 255 }, { 255, 200, 20 }, { 30, 220, 60 } };   // kick red · snare blue · hi-hat yellow · clap green
const char*   NAME[DRUMS]     = { "kick", "snare", "hi-hat", "clap" };
const uint8_t RING[STEPS]     = { 0, 1, 2, 5, 8, 7, 6, 3 };   // outer LEDs clockwise from top-left (index = row * 3 + col); 4 = centre

bool     hit[DRUMS][STEPS];
int      sel = 0, bpm = 110, step = 0, shownStep = -1;
bool     playing = true, dirty = true;
uint32_t stepAt = 0;                       // when the current step was triggered
uint32_t tapDown = 0, playDown = 0; bool tapHeld = false, playHeld = false;
int32_t  knobLast = 0;

// ---------- synthesized drums, mixed in our own audio task and pushed to the speaker as raw PCM ----------
volatile bool trig[DRUMS];                 // main loop -> audio task: start this drum now
int   vpos[DRUMS] = { -1, -1, -1, -1 };    // samples since the drum was triggered, -1 = silent
float kickPhase = 0, lastNoise = 0;
uint32_t rng = 0x9E3779B9;
inline float noise() { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return (int32_t)rng / 2147483648.0f; }

// returns the sample, sets done when the sound is over
float renderDrum(int d, int n, bool& done) {
    float t, env;
    switch (d) {
        case 0: {   // kick: sine sweep 150 -> 50 Hz over 120 ms, fading out
            const int len = SAMPLE_RATE * 120 / 1000; done = n >= len - 1; t = (float)n / len;
            float f = 150.0f * expf(-1.0986f * t);                 // 150 * (1/3)^t
            kickPhase += 2.0f * (float)M_PI * f / SAMPLE_RATE; if (kickPhase > 2.0f * (float)M_PI) kickPhase -= 2.0f * (float)M_PI;
            env = (1.0f - t) * (1.0f - t);
            return sinf(kickPhase) * env * 0.9f;
        }
        case 1: {   // snare: 100 ms of noise with a 180 Hz tone under it
            const int len = SAMPLE_RATE * 100 / 1000; done = n >= len - 1; t = (float)n / len; env = 1.0f - t;
            return noise() * 0.45f * env * env + sinf(2.0f * (float)M_PI * 180.0f * n / SAMPLE_RATE) * 0.4f * env * env * env;
        }
        case 2: {   // hi-hat: 30 ms of (high-passed) noise
            const int len = SAMPLE_RATE * 30 / 1000; done = n >= len - 1; t = (float)n / len;
            float w = noise(), hp = (w - lastNoise) * 0.5f; lastNoise = w;
            return hp * 0.5f * (1.0f - t);
        }
        default: {  // clap: three 15 ms noise bursts 20 ms apart
            int ms = n * 1000 / SAMPLE_RATE, burst = ms / 35, in = ms % 35; done = ms >= 85;
            if (in >= 15) return 0.0f;
            return noise() * 0.5f * (1.0f - in / 15.0f) * (1.0f - burst * 0.2f);
        }
    }
}

void audioTask(void*) {
    static int16_t buf[64];
    for (;;) {
        for (int d = 0; d < DRUMS; d++) if (trig[d]) { trig[d] = false; vpos[d] = 0; if (d == 0) kickPhase = 0; }
        for (int i = 0; i < 64; i++) {
            float mix = 0;
            for (int d = 0; d < DRUMS; d++) if (vpos[d] >= 0) { bool done = false; mix += renderDrum(d, vpos[d]++, done); if (done) vpos[d] = -1; }
            mix = constrain(mix, -1.0f, 1.0f);
            buf[i] = (int16_t)(mix * 32000.0f);
        }
        speaker.writeSamples(buf, 64);       // blocks when the DMA buffer is full: that is what paces this loop
    }
}

// ---------- sequencer ----------
uint32_t stepMs() { return 30000 / bpm; }   // eight steps = one bar of eighth notes
void fire(int s) { for (int d = 0; d < DRUMS; d++) if (hit[d][s]) trig[d] = true; }

void demoPattern() {
    memset(hit, 0, sizeof(hit));
    for (int s = 0; s < STEPS; s++) { hit[0][s] = (s % 2 == 0); hit[2][s] = true; }   // four-on-the-floor kick, hi-hat every step
    hit[1][2] = hit[1][6] = true;                                                      // snare on 3 and 7
}

// the step a tap at `now` belongs to: nearest step onset, corrected for the audio delay
int tappedStep(uint32_t now) {
    if (!playing) return step;
    int32_t t = (int32_t)(now - stepAt) - LATENCY_MS;
    int off = (int)lroundf((float)t / (float)stepMs());
    return (step + off + 4 * STEPS) % STEPS;
}

void draw() {
    for (int d = 0; d < DRUMS; d++) {
        for (int s = 0; s < STEPS; s++) {
            if (s == shownStep)      grids[d].setPixel(RING[s], 255, 255, 255);
            else if (hit[d][s])      grids[d].setPixel(RING[s], COLOR[d][0], COLOR[d][1], COLOR[d][2]);
            else                     grids[d].setPixel(RING[s], 0, 0, 0);
        }
        if (d == sel) grids[d].setPixel(4, COLOR[d][0], COLOR[d][1], COLOR[d][2]); else grids[d].setPixel(4, 0, 0, 0);
        grids[d].show();
    }
}

void setPlaying(bool on) {
    playing = on;
    if (on) { step = 0; stepAt = millis(); fire(0); Serial.printf("[drums] play %d bpm\n", bpm); }
    else Serial.println("[drums] stop");
    dirty = true;
}

void setup() {
    Serial.begin(115200);
    for (int i = 0; i < DRUMS; i++) grids[i].begin();
    drumBtn.begin(); tapBtn.begin(); playBtn.begin();
    knob.begin(); knob.setAcceleration(true, 4); knob.enableRing(true); knob.setRingColor(255, 255, 255); knob.setRingBrightness(25);
    knob.setRingPosition((bpm - BPM_MIN) * 11.0f / (BPM_MAX - BPM_MIN));
    knobLast = knob.getPosition();
    speaker.begin(SAMPLE_RATE); speaker.setVolume(0.5f);
    xTaskCreatePinnedToCore(audioTask, "drums", 4096, nullptr, 3, nullptr, 0);
    demoPattern();
    Serial.printf("[drums] boot: %d drums x %d steps, demo pattern, %d bpm\n", DRUMS, STEPS, bpm);
    setPlaying(true);
}

void loop() {
    uint32_t now = millis();
    drumBtn.update(); tapBtn.update(); playBtn.update(); knob.update();

    // knob = tempo
    int32_t p = knob.getPosition();
    if (p != knobLast) {
        bpm = constrain(bpm + (int)(p - knobLast), BPM_MIN, BPM_MAX); knobLast = p;
        knob.setRingPosition((bpm - BPM_MIN) * 11.0f / (BPM_MAX - BPM_MIN));
        Serial.printf("[drums] %d bpm\n", bpm);
    }

    // clock
    if (playing && now - stepAt >= stepMs()) {
        stepAt += stepMs(); if (now - stepAt > stepMs()) stepAt = now;   // never try to catch up after a stall
        step = (step + 1) % STEPS; fire(step);
    }
    int vis = step;                                                      // the playhead shows the step you are hearing
    if (playing && now - stepAt < (uint32_t)LATENCY_MS) vis = (step + STEPS - 1) % STEPS;
    if (vis != shownStep) { shownStep = vis; dirty = true; }

    // button 5: next drum
    if (drumBtn.wasPressed()) { sel = (sel + 1) % DRUMS; dirty = true; Serial.printf("[drums] %s selected\n", NAME[sel]); }

    // button 6: tap = toggle a hit on the step you are on (and hear it); hold = clear the drum
    if (tapBtn.wasPressed()) {
        tapDown = now; tapHeld = false;
        int s = tappedStep(now); hit[sel][s] = !hit[sel][s]; if (hit[sel][s]) trig[sel] = true; dirty = true;
    }
    if (tapBtn.isPressed() && !tapHeld && now - tapDown >= (uint32_t)HOLD_MS) {
        tapHeld = true; memset(hit[sel], 0, STEPS); dirty = true; Serial.printf("[drums] %s cleared\n", NAME[sel]);
    }

    // button 11: tap = play / stop (acts on release so a hold does not toggle); hold = clear everything
    if (playBtn.wasPressed()) { playDown = now; playHeld = false; }
    if (playBtn.isPressed() && !playHeld && now - playDown >= (uint32_t)HOLD_MS) {
        playHeld = true; memset(hit, 0, sizeof(hit)); dirty = true; Serial.println("[drums] all cleared");
    }
    if (playBtn.wasReleased() && !playHeld) setPlaying(!playing);

    if (dirty) { dirty = false; draw(); }
    delay(2);
}
