Teensy 4.1 - two piezo snare code

Lestra

Active member
Hey everybody, I am working on this code and need some honest review from you guys. Thanks in advance!
Code:
/*
 * TeensyDrum v0.8 - Snare: Head + Rim
 *
 * v0.8: Retrigger detection in aftershock.
 *   If signal jumps by retriggerThresh in one read = new hit,
 *   exit aftershock immediately so IDLE catches it.
 */

// ======================== ACTIVE PADS =================
//#define PAD_KICK         // A0
#define PAD_SNARE          // A1=head
#define PAD_SNARE_RIM      // A2=rim


// ======================== MIDI ========================
const int channel  = 10;

// ======================== SNARE =======================
#ifdef PAD_SNARE
const int snareNoteHead = 38;
const int snareNoteRim  = 37;
const int snarePinHead  = A1;
const int snarePinRim   = A2;

int   headThreshold   = 40;
int   headSensitivity = 920;
float headExponent    = 1.8;
int   headPeakDrops   = 1;
unsigned int headScanMs  = 3;
unsigned int headAfterMs = 2;
int   headRetrigger   = 100;      // sudden rise to detect new hit in aftershock

int   rimThreshold    = 80;
int   rimSensitivity  = 500;
float rimExponent     = 1.5;
int   rimPeakDrops    = 1;
unsigned int rimScanMs  = 6;
unsigned int rimAfterMs = 6;
int   rimRetrigger    = 80;

// Zone detection
float rimRatio = 0.3;
float rimRatioTimingBoost = 0.15;

uint8_t headLUT[128];
uint8_t rimLUT[128];
#endif

// ======================== KICK ========================
#ifdef PAD_KICK
const int kickNote = 36;
const int kickPin  = A0;

int   kickThreshold   = 40;
int   kickSensitivity = 1100;
float kickExponent    = 1.0;
int   kickPeakDrops   = 1;
unsigned int kickScanMs  = 3;
unsigned int kickAfterMs = 0;
int   kickRetrigger   = 80;

uint8_t kickLUT[128];
#endif

// ===================== VELOCITY LUT ===================
void buildLUT(uint8_t* lut, float exp) {
  lut[0] = 0;
  for (int i = 1; i < 128; i++) {
    float x = (float)i / 127.0;
    lut[i] = (uint8_t)constrain(lroundf(pow(x, exp) * 126.0) + 1, 1, 127);
  }
}

void buildAllLUTs() {
#ifdef PAD_SNARE
  buildLUT(headLUT, headExponent);
  buildLUT(rimLUT, rimExponent);
#endif
#ifdef PAD_KICK
  buildLUT(kickLUT, kickExponent);
#endif
}

int calcVelocity(int peak, int threshold, int sens, uint8_t* lut) {
  if (peak <= threshold) return 1;
  if (peak >= sens) return lut[127];
  int idx = (int)((float)(peak - threshold) / (float)(sens - threshold) * 127.0);
  return lut[constrain(idx, 1, 127)];
}

// ======================== SETUP ========================
void setup() {
  Serial.begin(115200);
  analogReadResolution(10);
#ifdef PAD_SNARE
  pinMode(snarePinHead, INPUT);
  pinMode(snarePinRim, INPUT);
#endif
#ifdef PAD_KICK
  pinMode(kickPin, INPUT);
#endif
  while (!Serial && millis() < 2500) ;
  buildAllLUTs();
  Serial.println("TeensyDrum v0.8");
}

// ======================== LOOP =========================
void loop() {
#ifdef PAD_SNARE
  {
    int head = analogRead(snarePinHead);
#ifdef PAD_SNARE_RIM
    int rim  = analogRead(snarePinRim);
#else
    int rim  = 0;
#endif
    snareDetect(head, rim);
  }
#endif
#ifdef PAD_KICK
  {
    int kick = analogRead(kickPin);
    kickDetect(kick);
  }
#endif
  checkSerial();
  while (usbMIDI.read()) {}
}

// ======================== SNARE ========================
#ifdef PAD_SNARE
void snareDetect(int head, int rim) {
  static int  state;
  static int  peakHead, peakRim;
  static int  prevHead, prevRim;
  static int  drops;
  static int  lastNote;
  static bool isRim;
  static int  firstTrigger;
  static elapsedMillis msec;

  switch (state) {

    // ---- IDLE ----
    case 0: {
      bool headRise = (head > headThreshold && prevHead < headThreshold);
      bool rimRise  = (rim  > rimThreshold  && prevRim  < rimThreshold);

      if (headRise || rimRise) {
        peakHead = head;
        peakRim  = rim;
        prevHead = head;
        prevRim  = rim;
        drops    = 0;
        msec     = 0;

        if (headRise && rimRise) firstTrigger = 3;
        else if (rimRise)        firstTrigger = 2;
        else                     firstTrigger = 1;

        state = 1;
      }
      prevHead = head;
      prevRim  = rim;
      return;
    }

    // ---- PEAK TRACKING ----
    case 1: {
      if (head > peakHead) { peakHead = head; drops = 0; }
      if (rim  > peakRim)  { peakRim  = rim;  drops = 0; }

      int curMax  = max(head, rim);
      int prevMax = max(prevHead, prevRim);
      if (curMax < prevMax) drops++;
      else drops = 0;

      prevHead = head;
      prevRim  = rim;

      int reqDrops = max(headPeakDrops, rimPeakDrops);
      unsigned int timeout = max(headScanMs, rimScanMs);

      if (drops >= reqDrops || msec >= timeout) {
        float ratio = (peakHead > 0) ? (float)peakRim / (float)peakHead : 100.0;
        float ratioNeeded = rimRatio;

        if (firstTrigger == 2) ratioNeeded = rimRatioTimingBoost;
        if (firstTrigger == 1) ratioNeeded = rimRatio + 0.2;

        isRim = (peakRim > rimThreshold && ratio > ratioNeeded);

        int note, velocity;

        if (isRim) {
          note     = snareNoteRim;
          velocity = calcVelocity(peakRim, rimThreshold, rimSensitivity, rimLUT);
        } else {
          note     = snareNoteHead;
          velocity = calcVelocity(peakHead, headThreshold, headSensitivity, headLUT);
        }

        velocity = constrain(velocity, 1, 127);
        usbMIDI.sendNoteOn(note, velocity, channel);
        lastNote = note;
        msec     = 0;
        state    = 2;
      }
      return;
    }

    // ---- AFTERSHOCK with retrigger detection ----
    default: {
      unsigned int afterMs = isRim ? rimAfterMs : headAfterMs;
      int retrig = isRim ? rimRetrigger : headRetrigger;

      // New hit during aftershock: sudden rise
      int headRise = head - prevHead;
      int rimRise  = rim  - prevRim;
      if (headRise > retrig || rimRise > retrig) {
        usbMIDI.sendNoteOff(lastNote, 0, channel);
        state = 0;
        // Don't update prev - let IDLE catch the rising edge
        return;
      }

      prevHead = head;
      prevRim  = rim;

      if (head > headThreshold || rim > rimThreshold) {
        msec = 0;
      } else if (msec > afterMs) {
        usbMIDI.sendNoteOff(lastNote, 0, channel);
        state = 0;
      }
    }
  }
}
#endif

// ======================== KICK =========================
#ifdef PAD_KICK
void kickDetect(int voltage) {
  static int  state;
  static int  peak;
  static int  prevVoltage;
  static int  drops;
  static elapsedMillis msec;

  switch (state) {
    case 0:
      if (voltage > kickThreshold && prevVoltage < kickThreshold) {
        peak = voltage;
        prevVoltage = voltage;
        drops = 0;
        msec  = 0;
        state = 1;
      }
      prevVoltage = voltage;
      return;

    case 1:
      if (voltage > peak) { peak = voltage; drops = 0; }
      if (voltage < prevVoltage) drops++;
      else drops = 0;
      prevVoltage = voltage;

      if (drops >= kickPeakDrops || msec >= kickScanMs) {
        int velocity = calcVelocity(peak, kickThreshold, kickSensitivity, kickLUT);
        velocity = constrain(velocity, 1, 127);
        usbMIDI.sendNoteOn(kickNote, velocity, channel);
        msec  = 0;
        state = 2;
      }
      return;

    default: {
      int rise = voltage - prevVoltage;
      if (rise > kickRetrigger) {
        usbMIDI.sendNoteOff(kickNote, 0, channel);
        state = 0;
        return;
      }
      prevVoltage = voltage;

      if (voltage > kickThreshold) {
        msec = 0;
      } else if (msec > kickAfterMs) {
        usbMIDI.sendNoteOff(kickNote, 0, channel);
        state = 0;
      }
    }
  }
}
#endif

// ======================== SERIAL TUNING ========================
void checkSerial() {
  if (!Serial.available()) return;
  char buf[32];
  uint8_t len = 0;
  while (Serial.available() && len < 31) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') break;
    buf[len++] = c;
  }
  buf[len] = 0;
  while (Serial.available()) Serial.read();
  if (len == 0) return;

  char* p = buf;
  char cmd[8] = {0};
  int ci = 0;
  while (*p && *p != ' ' && ci < 7) cmd[ci++] = *p++;
  while (*p == ' ') p++;

  bool rebuildLUT = false;

#ifdef PAD_SNARE
  if      (strcmp(cmd,"ht")==0)  { headThreshold   = atoi(p); Serial.printf("headThreshold = %d\n", headThreshold); }
  else if (strcmp(cmd,"hs")==0)  { headSensitivity = atoi(p); Serial.printf("headSensitivity = %d\n", headSensitivity); }
  else if (strcmp(cmd,"he")==0)  { headExponent    = atof(p); Serial.printf("headExponent = %.2f\n", headExponent); rebuildLUT = true; }
  else if (strcmp(cmd,"hd")==0)  { headPeakDrops   = atoi(p); Serial.printf("headPeakDrops = %d\n", headPeakDrops); }
  else if (strcmp(cmd,"hsc")==0) { headScanMs      = atoi(p); Serial.printf("headScanMs = %d\n", headScanMs); }
  else if (strcmp(cmd,"ha")==0)  { headAfterMs     = atoi(p); Serial.printf("headAfterMs = %d\n", headAfterMs); }
  else if (strcmp(cmd,"hrt")==0) { headRetrigger   = atoi(p); Serial.printf("headRetrigger = %d\n", headRetrigger); }
  else if (strcmp(cmd,"rt")==0)  { rimThreshold    = atoi(p); Serial.printf("rimThreshold = %d\n", rimThreshold); }
  else if (strcmp(cmd,"rs")==0)  { rimSensitivity  = atoi(p); Serial.printf("rimSensitivity = %d\n", rimSensitivity); }
  else if (strcmp(cmd,"re")==0)  { rimExponent     = atof(p); Serial.printf("rimExponent = %.2f\n", rimExponent); rebuildLUT = true; }
  else if (strcmp(cmd,"rd")==0)  { rimPeakDrops    = atoi(p); Serial.printf("rimPeakDrops = %d\n", rimPeakDrops); }
  else if (strcmp(cmd,"rsc")==0) { rimScanMs       = atoi(p); Serial.printf("rimScanMs = %d\n", rimScanMs); }
  else if (strcmp(cmd,"ra")==0)  { rimAfterMs      = atoi(p); Serial.printf("rimAfterMs = %d\n", rimAfterMs); }
  else if (strcmp(cmd,"rrt")==0) { rimRetrigger    = atoi(p); Serial.printf("rimRetrigger = %d\n", rimRetrigger); }
  else if (strcmp(cmd,"ratio")==0) { rimRatio = atoi(p) / 100.0f; Serial.printf("rimRatio = %.2f\n", rimRatio); }
  else if (strcmp(cmd,"boost")==0) { rimRatioTimingBoost = atoi(p) / 100.0f; Serial.printf("rimRatioTimingBoost = %.2f\n", rimRatioTimingBoost); }
  else
#endif
#ifdef PAD_KICK
  if      (strcmp(cmd,"kt")==0)  { kickThreshold   = atoi(p); Serial.printf("kickThreshold = %d\n", kickThreshold); }
  else if (strcmp(cmd,"ks")==0)  { kickSensitivity = atoi(p); Serial.printf("kickSensitivity = %d\n", kickSensitivity); }
  else if (strcmp(cmd,"ke")==0)  { kickExponent    = atof(p); Serial.printf("kickExponent = %.2f\n", kickExponent); rebuildLUT = true; }
  else if (strcmp(cmd,"kd")==0)  { kickPeakDrops   = atoi(p); Serial.printf("kickPeakDrops = %d\n", kickPeakDrops); }
  else if (strcmp(cmd,"ksc")==0) { kickScanMs      = atoi(p); Serial.printf("kickScanMs = %d\n", kickScanMs); }
  else if (strcmp(cmd,"ka")==0)  { kickAfterMs     = atoi(p); Serial.printf("kickAfterMs = %d\n", kickAfterMs); }
  else if (strcmp(cmd,"krt")==0) { kickRetrigger   = atoi(p); Serial.printf("kickRetrigger = %d\n", kickRetrigger); }
  else
#endif
  if (strcmp(cmd,"list")==0) {
#ifdef PAD_SNARE
    Serial.println("--- SNARE HEAD ---");
    Serial.printf("  threshold=%d  sensitivity=%d  exponent=%.2f\n", headThreshold, headSensitivity, headExponent);
    Serial.printf("  peakDrops=%d  scanMs=%u  afterMs=%u  retrigger=%d\n", headPeakDrops, headScanMs, headAfterMs, headRetrigger);
    Serial.println("--- SNARE RIM ---");
    Serial.printf("  threshold=%d  sensitivity=%d  exponent=%.2f\n", rimThreshold, rimSensitivity, rimExponent);
    Serial.printf("  peakDrops=%d  scanMs=%u  afterMs=%u  retrigger=%d\n", rimPeakDrops, rimScanMs, rimAfterMs, rimRetrigger);
    Serial.printf("--- ZONE ---\n  rimRatio=%.2f  timingBoost=%.2f\n", rimRatio, rimRatioTimingBoost);
    Serial.printf("--- NOTES ---\n  head=%d  rim=%d\n", snareNoteHead, snareNoteRim);
#endif
#ifdef PAD_KICK
    Serial.println("--- KICK ---");
    Serial.printf("  threshold=%d  sensitivity=%d  exponent=%.2f\n", kickThreshold, kickSensitivity, kickExponent);
    Serial.printf("  peakDrops=%d  scanMs=%u  afterMs=%u  retrigger=%d\n", kickPeakDrops, kickScanMs, kickAfterMs, kickRetrigger);
    Serial.printf("  note=%d\n", kickNote);
#endif
  }
  else Serial.printf("Unknown: %s\n", cmd);

  if (rebuildLUT) buildAllLUTs();
}
 
Last edited:
You need to post code using the code tags, i.e. click the </> button. The use of i as an array index is interpreted as italics, which makes nonsense of your code, plus the formatting is preserved…
 
C++:
// ======================== ACTIVE PADS =================
#define PAD_SNARE
#define PAD_SNARE_RIM
//#define PAD_KICK

// ======================== MIDI ========================
const int channel  = 10;

// ======================== SNARE =======================
const int snareNoteHead    = 38;
const int snareNoteRim     = 37;
const int snareNoteRimshot = 40;
const int snarePinHead     = A1;
const int snarePinRim      = A2;

const int thresholdMin     = 40;
const int peakTrackMillis  = 4;
const int aftershockMillis = 4;

// Velocity
int   headSensitivity = 1000;
int   rimSensitivity  = 150;

// Rimshot detection
int   rimshotHeadMin = 560;     // head must be above this
int   rimshotRimMin  = 300;     // rim must be above this

// Velocity curves
float headExponent = 1.0;
float rimExponent  = 1.0;

// ADC offset
const int adcOffset = 25;

// ======================== KICK ========================
#ifdef PAD_KICK
const int kickPin  = A0;
const int kickNote = 36;
const int kickThreshold = 30;
const int kickPeakMs    = 12;
const int kickAfterMs   = 14;
int   kickSensitivity   = 1000;
float kickExponent      = 1.0;
#endif

// ===================== VELOCITY LUT ===================
uint8_t headLUT[128];
uint8_t rimLUT[128];
#ifdef PAD_KICK
uint8_t kickLUT[128];
#endif

void buildLUT(uint8_t* lut, float exp) {
  lut[0] = 0;
  for (int i = 1; i < 128; i++) {
    float x = (float)i / 127.0;
    lut[i] = (uint8_t)constrain(lroundf(pow(x, exp) * 126.0) + 1, 1, 127);
  }
}

void buildAllLUTs() {
  buildLUT(headLUT, headExponent);
  buildLUT(rimLUT, rimExponent);
#ifdef PAD_KICK
  buildLUT(kickLUT, kickExponent);
#endif
}

int calcVelocity(int peak, int threshold, int sens, uint8_t* lut) {
  if (peak <= threshold) return 1;
  if (peak >= sens) return lut[127];
  int idx = (int)((float)(peak - threshold) / (float)(sens - threshold) * 127.0);
  return lut[constrain(idx, 1, 127)];
}

// ======================== SETUP ========================
void setup() {
  Serial.begin(115200);
  analogReadResolution(10);
  while (!Serial && millis() < 2500) ;
  buildAllLUTs();
  Serial.println("TeensyDrum v1.1");
}

// ======================== LOOP =========================
void loop() {
#ifdef PAD_SNARE
  snareDetect();
#endif
#ifdef PAD_KICK
  kickDetect();
#endif
  while (usbMIDI.read()) {}
}

// ======================== SNARE ========================
#ifdef PAD_SNARE
void snareDetect() {
  static int state;
  static int peakA, peakB;
  static elapsedMillis msec;

  int a = analogRead(snarePinHead);
#ifdef PAD_SNARE_RIM
  int b = analogRead(snarePinRim);
#else
  int b = 0;
#endif

  a = max(0, a - adcOffset);
  //b = max(0, b - adcOffset);

  switch (state) {

    // IDLE
    case 0:
      if (a > thresholdMin || b > thresholdMin) {
        peakA = a;
        peakB = b;
        msec  = 0;
        state = 1;
      }
      return;

    // PEAK TRACKING
    case 1:
      if (a > peakA) peakA = a;
      if (b > peakB) peakB = b;
      if (msec >= peakTrackMillis) {
        int note, velocity;

        if (peakA > rimshotHeadMin && peakB > rimshotRimMin) {
          // Rimshot: both strong
          note     = snareNoteRimshot;
          velocity = calcVelocity(peakA, thresholdMin, headSensitivity, headLUT);
        } else if (peakB > peakA) {
          // Rim: rim bigger
          note     = snareNoteRim;
          velocity = calcVelocity(peakB, thresholdMin, rimSensitivity, rimLUT);
        } else {
          // Head
          note     = snareNoteHead;
          velocity = calcVelocity(peakA, thresholdMin, headSensitivity, headLUT);
        }

        usbMIDI.sendNoteOn(note, velocity, channel);
        msec  = 0;
        state = 2;
      }
      return;

    // AFTERSHOCK
    default:
      if (a > thresholdMin || b > thresholdMin) {
        msec = 0;
      } else if (msec > aftershockMillis) {
        usbMIDI.sendNoteOff(snareNoteHead, 0, channel);
        usbMIDI.sendNoteOff(snareNoteRim, 0, channel);
        usbMIDI.sendNoteOff(snareNoteRimshot, 0, channel);
        state = 0;
      }
  }
}
#endif

// ======================== KICK =========================
#ifdef PAD_KICK
void kickDetect() {
  static int state;
  static int peak;
  static elapsedMillis msec;

  int v = analogRead(kickPin);

  switch (state) {
    case 0:
      if (v > kickThreshold) {
        peak = v;
        msec = 0;
        state = 1;
      }
      return;

    case 1:
      if (v > peak) peak = v;
      if (msec >= kickPeakMs) {
        int velocity = calcVelocity(peak, kickThreshold, kickSensitivity, kickLUT);
        usbMIDI.sendNoteOn(kickNote, velocity, channel);
        msec  = 0;
        state = 2;
      }
      return;

    default:
      if (v > kickThreshold) {
        msec = 0;
      } else if (msec > kickAfterMs) {
        usbMIDI.sendNoteOff(kickNote, 0, channel);
        state = 0;
      }
  }
}
#endif
 
That looks a lot easier to understand :)

I‘m not entirely sure what you’re expecting from a “review” - sometimes the best review is your own experience of “does it work as I wanted?”…

Assuming it does pretty much work, all I can do is say what I might have done a bit differently; most of that is down to personal style and experience, rather than being genuine improvements.

I’d encourage you to terminate every case with break, rather than return and leaving the last one just to fall out of the switch. I heartily dislike early returns, and they often make maintenance a nightmare when you forget there’s one in there.

I’m also not a huge fan of conditional compilation, though maybe in this example it’s only during development? It makes code hard to read. Not a problem during initial work, but when you come back a year later it will be.

I think there may be issues with your treatment of snare head and rim readings. There’s some individual thresholds, but then the raw analogue values just get directly compared later on. That may cause issues if you have a setup where the sensitivities are less well-matched. I’d suggest you scale and limit them at the point of reading, before you use them for anything else. I note that the rim doesn’t get an offset applied, as that line is commented out…
 
Thank you for taking the time to review this, I really appreciate the feedback.

This is very much an experimental phase - I'm building the code step by step, testing each addition against real playing before moving on. Once all pads are working and parameters are settled, I plan to refactor everything into a proper library where I'll address the points you raised - replacing conditional compilation with a cleaner structure, using break instead of return, and normalizing the head/rim values before comparison.

For now I can say that from a playing perspective this code works really well. I'm using it on a hybrid snare (mesh head + rim piezo on shell) and getting excellent sensitivity, dynamics and head/rim/rimshot separation with virtually no crosstalk between zones. Ghost notes, buzz rolls, accents - all responding as expected which was a real struggle to get right earlier in the process.

Your point about scaling the raw values before comparison is noted and will definitely be part of the library version. You're right that comparing raw ADC values with different offsets and sensitivities is not ideal, it just happens to work on my current hardware setup.

Thanks again for the input, very helpful.
 
Back
Top