/*
* NANO DRUM ENGINE v6.1 - REVISED FINAL PATCH
* Revisions based on suggestions:
* - Added playback protection with !audio.isPlaying()
* - Improved energy calculation using squared deviation from noise floor
* - Increased retrigger lock to 80ms for better debounce
* - Added simple multiple sample layering based on velocity (soft/medium/hard)
* - Added logarithmic velocity curve for more natural feel
* - Implemented basic LED fade-out effect
* - Added hysteresis to threshold (temp increase after hit)
* - Fixed 'kick.txt' to 'kick.wav' in setup
* - Minor tweaks for stability
*
* Assumptions: You have "kick_soft.wav", "kick_medium.wav", "kick_hard.wav" on SD card.
* If not, adjust playback logic accordingly.
*/
#include <SPI.h>
#include <SD.h>
#include <TMRpcm.h>
// --- DEBUG & LOGGING MACROS ---
#define DEBUG 1
#if DEBUG
#define LOG_INIT() Serial.begin(115200)
#define LOG_THROTTLE(msg, val, interval) { \
static unsigned long lastLog = 0; \
if (millis() - lastLog > interval) { \
Serial.print(F(msg)); Serial.println(val); \
lastLog = millis(); \
} \
}
#define LOG_HIT(vel, energy) { \
Serial.print(F(">>> HIT! Velocity: ")); Serial.print(vel); \
Serial.print(F("% | Energy: ")); Serial.println(energy); \
}
#else
#define LOG_INIT()
#define LOG_THROTTLE(msg, val, interval)
#define LOG_HIT(vel, energy)
#endif
// --- PIN CONFIGURATION ---
#define PIEZO_PIN A0
#define POT_PIN A1
#define SD_CS_PIN 10
#define AUDIO_OUT_PIN 9
// Pins for 74HC595 Shift Register
#define SR_DATA_PIN 4 // DS (Pin 14 on 74HC595)
#define SR_LATCH_PIN 3 // STCP (Pin 12 on 74HC595)
#define SR_CLOCK_PIN 2 // SHCP (Pin 11 on 74HC595)
TMRpcm audio;
// --- GLOBAL VARIABLES ---
int baseThreshold = 30;
int threshold = 30; // Dynamic with hysteresis
int lastRaw = 0;
int envelope = 0;
int lastPeakEnvelope = 0;
long noiseFloor = 0;
bool armed = true;
unsigned long lastHitTime = 0;
#define RETRIGGER_LOCK_MS 80 // Increased for better debounce
// --- FUNCTIONS ---
// Update 10-segment Bar Graph via Shift Register
void displayGraph(uint16_t mask) {
digitalWrite(SR_LATCH_PIN, LOW);
// Send 16-bit data (for 2 daisy-chained 74HC595, but only using 10 bits)
shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, (mask >> 8) & 0xFF); // Chip 2 (LED 9-10)
shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, mask & 0xFF); // Chip 1 (LED 1-8)
digitalWrite(SR_LATCH_PIN, HIGH);
}
void setup() {
LOG_INIT();
// 1. FAST ADC (Set prescaler to 16 for ~77kHz sampling)
ADCSRA |= (1 << ADPS2); ADCSRA &= ~(1 << ADPS1); ADCSRA &= ~(1 << ADPS0);
// 2. PIN MODES
pinMode(SR_DATA_PIN, OUTPUT);
pinMode(SR_LATCH_PIN, OUTPUT);
pinMode(SR_CLOCK_PIN, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
// 3. SD CARD INIT
if (!SD.begin(SD_CS_PIN)) {
LOG_THROTTLE("SD Card Error!", 0, 0);
displayGraph(0b1111111111); // All LEDs on for error
while(1);
}
// 4. AUDIO WARMUP
audio.speakerPin = AUDIO_OUT_PIN;
audio.setVolume(0);
audio.play("kick_soft.wav"); // Fixed typo, using soft for warmup
delay(100);
audio.stopPlayback();
// 5. CALIBRATE NOISE FLOOR
long sum = 0;
for(int i=0; i<100; i++) { sum += analogRead(PIEZO_PIN); delay(2); }
noiseFloor = sum / 100;
LOG_THROTTLE("System Ready. Noise Floor: ", noiseFloor, 0);
}
void loop() {
int rawValue = analogRead(PIEZO_PIN);
unsigned long now = millis();
// --- STEP 1: ENVELOPE TRACKING ---
int signal = abs(rawValue - (int)noiseFloor);
if (signal > envelope) {
envelope = signal; // Fast attack
} else {
envelope -= envelope >> 4; // Slow decay (bitshift 1/16)
}
// --- STEP 2: SLOPE CALCULATION ---
int slope = rawValue - lastRaw;
lastRaw = rawValue;
// --- STEP 3: NON-BLOCKING LOGGING ---
LOG_THROTTLE("Live Envelope: ", envelope, 1000); // Peek envelope every 1s
// --- STEP 4: TRIGGER LOGIC ---
if (armed && slope > 15 && envelope > threshold) {
if (now - lastHitTime > RETRIGGER_LOCK_MS) { // Increased lock time
// Peak Scan Window (1.2ms)
int peak = 0;
long energy = 0;
int scanLast = rawValue;
unsigned long startScan = micros();
while (micros() - startScan < 1200) {
int r = analogRead(PIEZO_PIN);
if (r > peak) peak = r;
int deviation = r - (int)noiseFloor;
energy += (long)deviation * deviation; // Squared for true energy
scanLast = r;
}
// Energy Gate Validation (adjusted for squared energy)
if (energy > 2500) { // Tune this based on testing
// Velocity Calculation with Log Curve
float vel_float = (peak - (noiseFloor + threshold)) / (float)(1023 - noiseFloor - threshold + 1);
vel_float = pow(vel_float, 1.6f); // Compression for natural feel
int velocity = constrain((int)(vel_float * 100), 0, 100);
// Playback with Layering and Protection
if (!audio.isPlaying()) {
if (velocity < 40) {
audio.play("kick_soft.wav");
} else if (velocity < 80) {
audio.play("kick_medium.wav");
} else {
audio.play("kick_hard.wav");
}
}
// LED Graph Visual (Mapping 100 velocity to 10 LEDs)
int ledsToLight = map(velocity, 0, 100, 1, 10);
uint16_t mask = (1 << ledsToLight) - 1;
displayGraph(mask);
LOG_HIT(velocity, energy);
lastHitTime = now;
lastPeakEnvelope = envelope;
armed = false;
threshold = baseThreshold * 2; // Hysteresis: Temp increase threshold
}
}
}
// --- STEP 5: ADAPTIVE RE-ARMING ---
if (!armed && envelope < (lastPeakEnvelope >> 2)) {
armed = true;
threshold = baseThreshold; // Reset threshold after re-arm
}
// --- STEP 6: UI & CLEANUP ---
// LED Fade-Out Effect
if (now - lastHitTime > 60) {
if (now - lastHitTime < 400) {
// Simple linear fade: Reduce LEDs over time
int fadeTime = now - lastHitTime - 60;
int maxLeds = map(100, 0, 100, 1, 10); // Assume max for simplicity
int ledsToLight = map(fadeTime, 0, 340, maxLeds, 0);
uint16_t mask = (1 << ledsToLight) - 1;
displayGraph(mask);
} else {
displayGraph(0);
}
}
// Monitor Potentiometer for Base Threshold
static unsigned long lastPotRead = 0;
if (now - lastPotRead > 200) {
baseThreshold = map(analogRead(POT_PIN), 0, 1023, 10, 400);
if (armed) threshold = baseThreshold; // Update only if armed
lastPotRead = now;
}
}