/*
* NANO DRUM ENGINE – BLAST BEAT KICK v9.1
* Final production-ready version – 2026
* Single-pad kick drum trigger (optimized for metal blast beat 240–320 BPM)
*
* Key features & optimizations:
* • Single-pad focus: No multi-pad, emphasis on anti-retrigger for kick pedal rebound
* • Velocity curve: Logaritmik LUT 16-entry with interpolation (boost ghost, compress accent)
* • Anti-retrigger: Retrigger lock, cancel window, post-hit rebound mask
* • Non-blocking loop (millis/micros only)
* • TM1637: Velocity after hit, threshold/lock status
* • TMRpcm: Quality 0, volume 4, low pin idle (minimal pop/crackle)
* • Fast ADC ~77 kHz
* • Squared energy + slope filter + hysteresis
* • LED fade non-blocking
*
* Hardware notes for production:
* - Piezo: 27–35 mm disc on resonant head, offset from beater
* - Damping: Moongel/gel thick on piezo + foam behind
* - Protection: 2x 1N4148 diodes parallel A0 (to GND & 5V) + 1MΩ pull-down
* - Audio: RC filter on D9 (1.5kΩ + 47nF to GND) + 10µF coupling cap + LM386 amp
* - SD: Class 10, FAT32, cluster 32KB; samples <150 ms, 16kHz mono 8-bit
* - Pots: A1 threshold (15–120), A2 lock (35–80 ms)
* - Test: Heel-toe 300 BPM → no double/miss, velocity 20–100 smooth
*
* Samples on SD: kick_soft.wav (<35 vel), kick_medium.wav (35–75), kick_hard.wav (>75)
*
* Pin Mapping Optimized:
* - SD CS to D4 (avoid PWM conflict)
* - 74HC595 DATA to D7 (free pin)
*/
#include <SPI.h>
#include <SD.h>
#include <TMRpcm.h>
#include <TM1637Display.h>
// ── Debug & Logging ──────────────────────────────────────────────────────────
#define DEBUG 1
#define DEBUG_ENVELOPE 0
#if DEBUG
#define LOG_INIT() Serial.begin(115200)
#define LOG_THROTTLE(msg, val, interval) do { \
static unsigned long _last = 0; \
if (millis() - _last >= interval) { \
Serial.print(F(msg)); Serial.println(val); \
_last = millis(); \
} \
} while(0)
#define LOG_HIT(vel, en, lk) do { \
Serial.print(F("HIT | V:")); Serial.print(vel); \
Serial.print(F(" E:")); Serial.print(en); \
Serial.print(F(" L:")); Serial.println(lk); \
} while(0)
#else
#define LOG_INIT()
#define LOG_THROTTLE(msg,val,interval)
#define LOG_HIT(vel,en,lk)
#endif
// ── Pin Definitions ──────────────────────────────────────────────────────────
#define PIEZO_PIN A1
#define POT_THRESHOLD A2
#define POT_LOCK A3
#define SD_CS_PIN 4 // Optimized: Moved to D4 to avoid PWM conflict
#define AUDIO_PWM_PIN 9 // Optimal for TMRpcm PWM
#define SR_CLOCK_PIN 2 // 74HC595 SHCP
#define SR_LATCH_PIN 3 // 74HC595 STCP
#define SR_DATA_PIN 7 // Optimized: Moved to D7 (free pin)
#define TM1637_CLK 5
#define TM1637_DIO 6
TMRpcm tmrpcm;
TM1637Display tm(TM1637_CLK, TM1637_DIO);
// ── Tunable Constants ────────────────────────────────────────────────────────
const int THRESHOLD_MIN = 15;
const int THRESHOLD_MAX = 120;
const int LOCK_MIN_MS = 35;
const int LOCK_MAX_MS = 80;
const int CANCEL_MIN_MS = 25;
const int CANCEL_MAX_MS = 60;
const unsigned long SCAN_US = 700; // Low latency for fast peak scan
const int SLOPE_MIN = 22;
const long ENERGY_THRESHOLD = 2800;
const uint8_t ENVELOPE_DECAY_SHIFT = 3;
const int MIN_VELOCITY = 5; // Gate noise vs ghost
const int REBOUND_MASK_MS = 40; // Post-hit rebound ignore time
const int REBOUND_THRESHOLD = 25; // % of peak (e.g. /4 for 25%)
// ── Velocity Curve LUT (16-entry Logaritmik Agresif) ─────────────────────────
const uint8_t velocityCurve[16] PROGMEM = {
0, // 0%
6,
14,
24,
35, // Ghost note boost
47,
58,
68,
77,
84,
89,
93,
96,
98,
99,
100 // Compress accent
};
// ── Global State ─────────────────────────────────────────────────────────────
int noiseFloor = 0;
int baseThreshold = 40;
int dynamicThreshold = 40;
int lastRaw = 0;
int envelope = 0;
int lastPeakEnv = 0;
bool triggerArmed = true;
unsigned long lastHitMillis = 0;
unsigned long cancelUntil = 0;
unsigned long reboundIgnoreUntil = 0;
int lastKickPeak = 0;
int retriggerLockMs = 50;
int cancelWindowMs = 35;
int currentVelocity = 0;
unsigned long lastPotRead = 0;
unsigned long lastDisplayUpdate = 0;
unsigned long lastEnvLog = 0;
unsigned long lastPotLog = 0;
int prevThreshRaw = -1;
int prevLockRaw = -1;
// ── Functions ────────────────────────────────────────────────────────────────
// LED Bar Update (non-blocking fade)
void updateLedBar(int velocity) {
unsigned long now = millis();
if (now - lastHitMillis < 50) {
int leds = map(velocity, 0, 100, 1, 10);
displayBar((1UL << leds) - 1);
} else if (now - lastHitMillis < 350) {
int elapsed = now - lastHitMillis - 50;
int level = map(elapsed, 0, 300, 10, 0);
int leds = map(level, 0, 10, 0, map(velocity, 0, 100, 1, 10));
displayBar((1UL << leds) - 1);
} else {
displayBar(0);
}
}
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);
}
// TM1637 Display Update
void updateDisplay() {
unsigned long now = millis();
if (now - lastDisplayUpdate < 300) return;
lastDisplayUpdate = now;
if (currentVelocity > 0 && now - lastHitMillis < 1500) {
tm.showNumberDec(currentVelocity, false);
} else {
static bool showThresh = true;
if (showThresh) {
tm.showNumberDec(baseThreshold, true);
tm.setSegments((const uint8_t[]){0x71, 0x68, 0x00, 0x00}, false, 0); // t h
} else {
tm.showNumberDec(retriggerLockMs, true);
tm.setSegments((const uint8_t[]){0x38, 0x39, 0x00, 0x00}, false, 0); // L o
}
showThresh = !showThresh;
}
}
// Velocity Curve with LUT + Interpolation
int applyVelocityCurve(float normalized) {
if (normalized <= 0.0f) return 0;
if (normalized >= 1.0f) return 100;
float indexF = normalized * 15.0f;
uint8_t idx = (uint8_t)indexF;
float frac = indexF - (float)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);
}
// ── Setup ────────────────────────────────────────────────────────────────────
void setup() {
LOG_INIT();
ADCSRA |= (1 << ADPS2); ADCSRA &= ~(1 << ADPS1); ADCSRA &= ~(1 << ADPS0);
pinMode(SR_DATA_PIN, OUTPUT);
pinMode(SR_LATCH_PIN, OUTPUT);
pinMode(SR_CLOCK_PIN, OUTPUT);
pinMode(AUDIO_PWM_PIN, OUTPUT);
digitalWrite(AUDIO_PWM_PIN, LOW);
tm.setBrightness(0x0f);
tm.clear();
if (!SD.begin(SD_CS_PIN)) {
LOG_THROTTLE("SD failed", 0, 0);
displayBar(0b1111111111);
while (true);
}
tmrpcm.speakerPin = AUDIO_PWM_PIN;
tmrpcm.setVolume(4);
tmrpcm.quality(0);
tmrpcm.play("kick_soft.wav");
delay(80);
tmrpcm.stopPlayback();
long sum = 0;
for (uint8_t i = 0; i < 120; i++) {
sum += analogRead(PIEZO_PIN);
delay(3);
}
noiseFloor = sum / 120;
LOG_THROTTLE("Noise floor:", noiseFloor, 0);
tm.showNumberDec(noiseFloor, false);
delay(800);
tm.clear();
}
// ── Main Loop ────────────────────────────────────────────────────────────────
void loop() {
unsigned long now = millis();
// Cancel window + rebound mask
if (now < cancelUntil || now < reboundIgnoreUntil) {
analogRead(PIEZO_PIN); // dummy
if (envelope < (lastKickPeak * REBOUND_THRESHOLD / 100)) {
updateDisplay();
updateLedBar(currentVelocity);
return;
}
}
int raw = analogRead(PIEZO_PIN);
// Envelope follower
int deviation = abs(raw - noiseFloor);
if (deviation > envelope) {
envelope = deviation;
} else {
envelope -= envelope >> ENVELOPE_DECAY_SHIFT;
}
int slope = raw - lastRaw;
lastRaw = raw;
#if DEBUG_ENVELOPE
LOG_THROTTLE("Env:", envelope, 2000);
#endif
// Pot reading
if (now - lastPotRead >= 250) {
int tRaw = analogRead(POT_THRESHOLD);
baseThreshold = map(tRaw, 0, 1023, THRESHOLD_MIN, THRESHOLD_MAX);
int lRaw = analogRead(POT_LOCK);
retriggerLockMs = map(lRaw, 0, 1023, LOCK_MIN_MS, LOCK_MAX_MS);
cancelWindowMs = map(lRaw, 0, 1023, CANCEL_MIN_MS, CANCEL_MAX_MS);
if (triggerArmed) dynamicThreshold = baseThreshold;
if ((now - lastPotLog >= 800) &&
(abs(tRaw - prevThreshRaw) > 10 || abs(lRaw - prevLockRaw) > 10 || prevThreshRaw < 0)) {
Serial.print(F("[TUNE] Th:")); Serial.print(tRaw);
Serial.print(F("→")); Serial.print(baseThreshold);
Serial.print(F(" L:")); Serial.print(lRaw);
Serial.print(F("→")); Serial.print(retriggerLockMs);
Serial.print(F(" ms C:")); Serial.print(cancelWindowMs); Serial.println(F(" ms"));
prevThreshRaw = tRaw;
prevLockRaw = lRaw;
lastPotLog = now;
}
lastPotRead = now;
}
// Trigger logic
if (triggerArmed && slope > SLOPE_MIN && envelope > dynamicThreshold) {
if (now - lastHitMillis > (unsigned long)retriggerLockMs) {
int peak = raw;
long energy = 0UL;
unsigned long start = micros();
while (micros() - start < SCAN_US) {
int r = analogRead(PIEZO_PIN);
if (r > peak) peak = r;
long d = (long)r - noiseFloor;
energy += d * d;
}
energy = (energy * 100) / (lastPeakEnv > 0 ? lastPeakEnv : 1);
if (energy > ENERGY_THRESHOLD) {
float normalized = (float)(peak - (noiseFloor + dynamicThreshold)) /
(float)(1023 - noiseFloor - dynamicThreshold + 10);
normalized = constrain(normalized, 0.0f, 1.0f);
currentVelocity = applyVelocityCurve(normalized);
if (currentVelocity >= MIN_VELOCITY) {
const char* sample;
if (currentVelocity < 35) sample = "kick_soft.wav";
else if (currentVelocity < 75) sample = "kick_medium.wav";
else sample = "kick_hard.wav";
tmrpcm.play(sample);
updateLedBar(currentVelocity);
LOG_HIT(currentVelocity, energy, retriggerLockMs);
lastHitMillis = now;
lastPeakEnv = envelope;
triggerArmed = false;
dynamicThreshold = baseThreshold + (peak >> 2); // Optimized: Smoother hysteresis (~ +25% of peak)
cancelUntil = now + cancelWindowMs;
// Anti-retrigger rebound mask
lastKickPeak = peak;
reboundIgnoreUntil = now + REBOUND_MASK_MS;
if (!tmrpcm.isPlaying()) digitalWrite(AUDIO_PWM_PIN, LOW);
}
}
}
}
// Re-arm
if (!triggerArmed && envelope < (lastPeakEnv >> (ENVELOPE_DECAY_SHIFT + 1))) {
triggerArmed = true;
dynamicThreshold = baseThreshold;
}
updateLedBar(currentVelocity);
updateDisplay();
}