/*
* NANO DRUM ENGINE – BLAST BEAT KICK v9.2
* Piezo ACTIVE-LOW version (trigger when signal goes LOW)
* Single-pad kick drum trigger – optimized for metal blast beat
*/
#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
#define AUDIO_PWM_PIN 9
#define SR_CLOCK_PIN 2
#define SR_LATCH_PIN 3
#define SR_DATA_PIN 7
#define TM1637_CLK 5
#define TM1637_DIO 6
TMRpcm tmrpcm;
TM1637Display tm(TM1637_CLK, TM1637_DIO);
// ── Tunable Constants ────────────────────────────────────────────────────────
const int THRESHOLD_MIN = 15; // now means: lower value = more sensitive
const int THRESHOLD_MAX = 140; // typical working range 35–110
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;
const int SLOPE_MIN = 20; // negative slope threshold (falling signal)
const long ENERGY_THRESHOLD = 2600; // squared sum – adjust after testing
const uint8_t ENVELOPE_DECAY_SHIFT = 3;
const int MIN_VELOCITY = 5;
const int REBOUND_MASK_MS = 40;
const int REBOUND_THRESHOLD = 25; // % of deepest dip
// ── Velocity Curve LUT (same aggressive log curve) ───────────────────────────
const uint8_t velocityCurve[16] PROGMEM = {
0, 6, 14, 24, 35, 47, 58, 68,
77, 84, 89, 93, 96, 98, 99, 100
};
// ── Global State ─────────────────────────────────────────────────────────────
int noiseFloor = 512; // will be ~512 if centered, or higher/lower
int baseThreshold = 460; // example: trigger when < 460 (stronger = lower)
int dynamicThreshold = 460;
int lastRaw = 512;
int envelope = 0; // now = how much BELOW noiseFloor
int lastPeakEnv = 0; // deepest dip (lowest value)
bool triggerArmed = true;
unsigned long lastHitMillis = 0;
unsigned long cancelUntil = 0;
unsigned long reboundIgnoreUntil = 0;
int lastKickDip = 1023; // deepest value (smallest number)
int retriggerLockMs = 50;
int cancelWindowMs = 35;
int currentVelocity = 0;
unsigned long lastPotRead = 0;
unsigned long lastDisplayUpdate = 0;
// ── Functions ────────────────────────────────────────────────────────────────
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);
}
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;
}
}
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();
// Faster ADC (~77 kHz)
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();
// Measure resting level (active-low → usually high when idle)
long sum = 0;
for (uint8_t i = 0; i < 120; i++) {
sum += analogRead(PIEZO_PIN);
delay(3);
}
noiseFloor = sum / 120;
LOG_THROTTLE("Idle level: ", noiseFloor, 0);
tm.showNumberDec(noiseFloor, false);
delay(800);
tm.clear();
}
// ── Main Loop ────────────────────────────────────────────────────────────────
void loop() {
unsigned long now = millis();
// Still in cancel / rebound window → skip heavy processing
if (now < cancelUntil || now < reboundIgnoreUntil) {
analogRead(PIEZO_PIN); // dummy read
updateDisplay();
updateLedBar(currentVelocity);
return;
}
int raw = analogRead(PIEZO_PIN);
// ── Envelope follower (how far BELOW idle level) ────────────────────────
int deviation = noiseFloor - raw; // positive when signal goes LOW
if (deviation > envelope) {
envelope = deviation;
} else {
envelope -= envelope >> ENVELOPE_DECAY_SHIFT;
}
// ── Slope detection (negative slope = signal falling) ───────────────────
int slope = lastRaw - raw; // positive when going down
lastRaw = raw;
#if DEBUG_ENVELOPE
LOG_THROTTLE("Env:", envelope, 2000);
#endif
// ── Potentiometers ──────────────────────────────────────────────────────
if (now - lastPotRead >= 250) {
int tRaw = analogRead(POT_THRESHOLD);
baseThreshold = map(tRaw, 0, 1023, THRESHOLD_MIN, THRESHOLD_MAX);
// lower threshold value = more sensitive
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;
lastPotRead = now;
}
// ── Trigger logic ───────────────────────────────────────────────────────
if (triggerArmed && slope > SLOPE_MIN && envelope > dynamicThreshold) {
if (now - lastHitMillis > (unsigned long)retriggerLockMs) {
int deepest = raw;
long energy = 0UL;
unsigned long start = micros();
// Quick peak (dip) scan
while (micros() - start < SCAN_US) {
int r = analogRead(PIEZO_PIN);
if (r < deepest) deepest = r;
long d = (long)noiseFloor - r; // positive = stronger hit
energy += d * d;
}
energy = (energy * 100) / (lastPeakEnv > 0 ? lastPeakEnv : 1);
if (energy > ENERGY_THRESHOLD) {
// Normalize: deeper dip → higher velocity
float howMuchBelow = (float)(noiseFloor + dynamicThreshold - deepest);
float range = (float)(noiseFloor + dynamicThreshold - 10);
float normalized = howMuchBelow / range;
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;
lastKickDip = deepest;
triggerArmed = false;
// Dynamic threshold (hysteresis) — make it harder to retrigger
dynamicThreshold = baseThreshold + (envelope >> 2);
cancelUntil = now + cancelWindowMs;
reboundIgnoreUntil = now + REBOUND_MASK_MS;
if (!tmrpcm.isPlaying()) digitalWrite(AUDIO_PWM_PIN, LOW);
}
}
}
}
// ── Re-arm logic ────────────────────────────────────────────────────────
if (!triggerArmed && envelope < (lastPeakEnv >> (ENVELOPE_DECAY_SHIFT + 1))) {
triggerArmed = true;
dynamicThreshold = baseThreshold;
}
updateLedBar(currentVelocity);
updateDisplay();
}