/*
* NANO DRUM ENGINE – PRODUCTION READY V2.2 FINAL
* Non-blocking sampling, smart calibration, per-pad retrigger lock
* Proteksi audio startup & SD card check
*/
#include <SPI.h>
#include <SD.h>
#include <TMRpcm.h>
// ── PIN DEFINITIONS ──────────────────────────────────────────
#define KICK_PIN A1
#define SNARE_PIN A0
#define POT_THRESHOLD A2
#define POT_LOCK A3
#define AUDIO_PWM_PIN 9
#define SD_CS_PIN 4
#define SR_CLOCK_PIN 2
#define SR_LATCH_PIN 3
#define SR_DATA_PIN 7
TMRpcm tmrpcm;
// ── CONSTANTS ────────────────────────────────────────────────
const int TH_MIN = 15, TH_MAX = 140;
const int LOCK_MIN = 25, LOCK_MAX = 80;
const int SLOPE_MIN = 18;
const uint8_t ENVELOPE_DECAY_SHIFT = 3;
const long ENERGY_THRESHOLD = 1800;
// Velocity Curve disimpan di PROGMEM
const uint8_t velocityCurve[16] PROGMEM = {
0, 6, 14, 24, 35, 47, 58, 68, 77, 84, 89, 93, 96, 98, 99, 100
};
struct Pad {
uint8_t pin;
int noiseFloor;
int envelope;
int lastRaw;
int threshold;
int dynamicThreshold;
int lastPeakEnv;
bool armed;
unsigned long lastHit;
int velocity;
int retriggerLock; // per-pad lock
};
Pad kick = {KICK_PIN, 512, 0, 512, 40, 40, 0, true, 0, 0, 50};
Pad snare = {SNARE_PIN, 512, 0, 512, 55, 55, 0, true, 0, 0, 60};
// ── LED DRIVER ───────────────────────────────────────────────
inline void displayBar(uint16_t mask) {
digitalWrite(SR_LATCH_PIN, LOW);
shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, (mask >> 8) & 0xFF);
shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, mask & 0xFF);
digitalWrite(SR_LATCH_PIN, HIGH);
}
void updateLedBar() {
unsigned long now = millis();
Pad* active = (snare.lastHit >= kick.lastHit) ? &snare : &kick;
unsigned long latest = active->lastHit;
if (now - latest > 350) {
displayBar(0);
return;
}
unsigned long elapsed = now - latest;
int maxLeds = map(active->velocity, 0, 100, 1, 10);
int currentLeds = (elapsed < 40) ? maxLeds : map(elapsed, 40, 350, maxLeds, 0);
if (currentLeds > 0) displayBar((1UL << currentLeds) - 1);
else displayBar(0);
}
// ── CORE ENGINE ──────────────────────────────────────────────
int applyVelocityCurve(float n) {
if (n <= 0) return 0; if (n >= 1) return 100;
float idxf = n * 15.0f;
uint8_t idx = (uint8_t)idxf;
float frac = idxf - idx;
uint8_t low = pgm_read_byte(&velocityCurve[idx]);
uint8_t high = pgm_read_byte(&velocityCurve[idx + 1]);
return low + (int)((high - low) * frac + 0.5f);
}
// Non-blocking energy sampling dengan buffer kecil
long sampleEnergy(Pad &pad, int &peak) {
long energy = 0;
peak = pad.lastRaw;
for (int i = 0; i < 8; i++) {
int r = analogRead(pad.pin);
if (r > peak) peak = r;
long d = (long)r - pad.noiseFloor;
energy += d * d;
}
return energy;
}
void processPad(Pad &pad, const char* sft, const char* med, const char* hrd) {
unsigned long now = millis();
if (now - pad.lastHit < (unsigned long)pad.retriggerLock) return;
int raw = analogRead(pad.pin);
int deviation = (raw > pad.noiseFloor) ? (raw - pad.noiseFloor) : 0;
if (deviation > pad.envelope) pad.envelope = deviation;
else pad.envelope -= (pad.envelope >> ENVELOPE_DECAY_SHIFT);
int slope = raw - pad.lastRaw;
pad.lastRaw = raw;
if (pad.armed && slope > SLOPE_MIN && pad.envelope > pad.dynamicThreshold) {
int peak;
long energy = sampleEnergy(pad, peak);
if (energy > ENERGY_THRESHOLD) {
float norm = constrain((float)(peak - pad.noiseFloor - pad.dynamicThreshold) / 750.0f, 0.0, 1.0);
pad.velocity = applyVelocityCurve(norm);
if (pad.velocity > 5) {
const char* sample = (pad.velocity < 35) ? sft : (pad.velocity < 75) ? med : hrd;
tmrpcm.play((char*)sample, 0);
pad.lastHit = now;
pad.lastPeakEnv = pad.envelope;
pad.armed = false;
pad.dynamicThreshold = pad.threshold + (pad.envelope >> 2);
}
}
}
if (!pad.armed && (pad.envelope < (pad.lastPeakEnv >> 3) || now - pad.lastHit > 600)) {
pad.armed = true;
pad.dynamicThreshold = pad.threshold;
}
}
// ── SETUP & LOOP ─────────────────────────────────────────────
void setup() {
// Overclock ADC
ADCSRA = (ADCSRA & 0xf8) | 0x04;
pinMode(SR_CLOCK_PIN, OUTPUT);
pinMode(SR_LATCH_PIN, OUTPUT);
pinMode(SR_DATA_PIN, OUTPUT);
pinMode(AUDIO_PWM_PIN, OUTPUT);
digitalWrite(AUDIO_PWM_PIN, LOW); // cegah pop saat startup
if (!SD.begin(SD_CS_PIN)) {
displayBar(0b1010101010);
while(1);
}
tmrpcm.speakerPin = AUDIO_PWM_PIN;
tmrpcm.setVolume(5);
tmrpcm.quality(1);
// Kalibrasi noise floor dengan moving average
for (int i = 0; i < 200; i++) {
kick.noiseFloor = (kick.noiseFloor * 7 + analogRead(KICK_PIN)) / 8;
snare.noiseFloor = (snare.noiseFloor * 7 + analogRead(SNARE_PIN)) / 8;
delayMicroseconds(500);
}
displayBar(0x3FF); delay(500); displayBar(0);
}
void loop() {
static int lastThRaw = 0;
static int lastLockRaw = 0;
int currentThRaw = analogRead(POT_THRESHOLD);
int currentLockRaw = analogRead(POT_LOCK);
if (abs(currentThRaw - lastThRaw) > 4 || abs(currentLockRaw - lastLockRaw) > 4) {
kick.threshold = map(currentThRaw, 0, 1023, TH_MIN, TH_MAX);
snare.threshold = kick.threshold;
kick.retriggerLock = map(currentLockRaw, 0, 1023, LOCK_MIN, LOCK_MAX);
snare.retriggerLock = kick.retriggerLock + 10; // snare lebih longgar
lastThRaw = currentThRaw;
lastLockRaw = currentLockRaw;
}
processPad(kick, "k_s.wav", "k_m.wav", "k_h.wav");
processPad(snare, "s_s.wav", "s_m.wav", "s_h.wav");
updateLedBar();
}