Sensing hit-velocity and quick subsequent hits of a piezo with an Arduino / Teensy

Using a piezo and an Arduino / Teensy to create an electronic drum pad is not an unusual task. I wanted my pad to be velocity-sensitive however, and a quick search of the Web didn’t provide any code for doing this. Some pages suggested using a capacitor in the circuit to “hold” the piezo’s signal value for a longer time. But the method I eventually developed does not require any additional non-standard piezo circuitry – except I had to add an additional resistor to dampen the piezo’s signal, whose voltage got too high for my Teensy when hit hard. If your piezo’s signal is reaching your Arduino / Teensy’s maximum analogRead() level (usually 1023), you’ll need to do this too.

The stock-standard way to get Arduino code to register “hit” information from a piezo is to get it to listen to the analog input from it, and register a hit whenever the piezo’s signal rises above a certain threshold.

Determining velocity from the piezo’s signal, however, is a different story. At the point in time at which the piezo’s signal first rises above a given threshold, there’s no way for the code to predict whether the signal will continue to rise quite high, meaning a high-velocity hit, or whether it’s already almost arrived at its peak, which would mean a low-velocity hit.

I tried a few different methods, and the most accurate by far was: when the signal breaches the threshold, have the code spend the next couple of milliseconds or so looking out for the highest reading of the piezo’s signal. At some point during these first couple of milliseconds, the piezo’s signal will reach its peak, and the value of this peak is an accurate indicator of the hit’s velocity.

A note here – piezos have a polarity. Which way around you wire it will change what the signal looks like – and from my experience, it’s not necessarily clear-cut which way is better. I suggest you try both. On the pad / piezo that I worked with, when wired one way the signal rose to its peak in only about 0.5ms (good) but this peak was a poor indicator of velocity; the other way around the peak usually took around 2ms to reach (not as good but still fine) but was a much better indicator of velocity, but the piezo received much more “cross-talk” from hits to other pads on the same object.

Great, so now we have a velocity-sensitive drum pad. One problem though is that once our code has registered the hit, we have to tell it to stop “listening” to the piezo for a little while – say, 200ms – because the piezo’s signal will remain above the hit-threshold for a portion of this time, and we don’t want this to cause the incorrectly registering of any additional hits. This unfortunately means, however, that the user can’t perform hits too close together, or else the code will miss subsequent ones entirely. How can we get around this?

After a given hit, the piezo’s signal will fall gradually, and we can put this fact to use in our method of listening for a quick subsequent hit. We know that immediately after the initial hit, the piezo’s signal is likely to still be quite high – so we’d only want to register a subsequent hit if we saw the signal go really high. A few milliseconds later, though, we’d expect the piezo’s signal to have tapered off a bit after the initial hit, so our threshold for registering a subsequent hit would be lower.

Here is the code I came up with – functional for one single drum pad, and turning its signal into USB MIDI note-ons thanks to Teensy. The line which references usbMIDI is the only bit that won’t work on a non-Teensy Arduino. Fine-tune the code by altering the constants at the top – I’m sure these are heavily dependent on the the piezo and pad-object you’re using.

#define triggerThreshold 10 // If this is set too low, hits on other pads will trigger a "hit" on this pad
#define initialHitReadDuration 500 // In microseconds. Shorter times will mean less latency, but less accuracy. 500 microseconds is nothing, anyway
#define midiVelocityScaleDownAmount 2 // Number of halvings that will be applied to MIDI velocity
#define inputPin A0

// Getting the ideal balance of these two constants will ensure that fast subsequent hits are perceived accurately, but false hits are not generated
#define subsequentHitThreshold 1.7
#define subsequentHitThresholdDecaySpeed 14

uint16_t highestYet;
uint32_t startReadingTime;
uint32_t highestValueTime;
boolean hitOccurredRecently = false;

void setup() {
}

void loop() {

  // Assume the normal hit-threshold
  uint16_t thresholdNow = triggerThreshold;

  // But, if a hit occurred very recently, we need to set a higher threshold for triggering another hit, otherwise the dissipating vibrations
  // of the previous hit would trigger another one now
  if (hitOccurredRecently) {

      // Work out how high a reading we'd need to see right now in order to conclude that another hit has occurred
      uint16_t currentDynamicThreshold = (highestYet >> ((micros() - highestValueTime) >> subsequentHitThresholdDecaySpeed)) * subsequentHitThreshold;

      // If that calculated threshold is now as low as the regular threshold, we can go back to just waiting for a regular, isolated hit
      if (currentDynamicThreshold <= triggerThreshold) hitOccurredRecently = false;

      // Otherwise, do use this higher threshold
      else thresholdNow = currentDynamicThreshold;
  }

  // Read the piezo
  uint16_t value = analogRead(inputPin);

  // If we've breached the threshold, it means we've got a hit!
  if (value >= thresholdNow) {
    startReadingTime = micros();
    highestYet = 0;

    // For the next few milliseconds, look out for the highest "spike" in the reading from the piezo. Its height is representative of the hit's velocity
    do {
      if (value > highestYet) {
        highestYet = value;
        highestValueTime = micros();
      }
      value = analogRead(inputPin);
    } while (timeGreaterOrEqual(startReadingTime + initialHitReadDuration, micros()));

    // Send the MIDI note
    usbMIDI.sendNoteOn(0, (highestYet >> midiVelocityScaleDownAmount) + 1, 1); // We add 1 onto the velocity so that the result is never 0, which would mean the same as a note-off
    Serial.println(highestYet); // Send the unscaled velocity value to the serial monitor too, for debugging / fine-tuning
    hitOccurredRecently = true;
  }
}

// Compares times without being prone to problems when the micros() counter overflows, every ~70 mins
boolean timeGreaterOrEqual(uint32_t lhs, uint32_t rhs) {
  return (((lhs - rhs) & 2147483648) == 0);
}

Having tuned the above code for use in my own construction, I can say that it works very nicely, registering quick subsequent hits surprisingly accurately and giving consistent velocity values. It’s even fairly good at correctly registering the quick successive hits achieved by momentarily “dribbling” a drum stick with one hand. But it’s not perfect, and after a while spent fine-tuning the code’s approximation of the expected decay-shape of the piezo’s signal after an initial hit (used to determine what signal-change should count as another successive hit, as discussed above), I decided to bite the bullet and write an additional bit of code to actually record the real decay-shape of a given piezo attached to a given drum pad – so the code can really, really know if a change in input signal at a given number of milliseconds after an initial hit can count as another one.

I haven’t spent time commenting or fully documenting the resulting code, but in case it’s any use to you, here it is. The code listens to as many test-hits as you care to play it, and keeps a running high-score of the signal-level (relative to the overall velocity of the hit) at each number of milliseconds after the hit began. This is stored in your Teensy or Arduino’s EEPROM. Set “learning” to 1 to put it in this learning mode, and then back to 0 to have the code function normally. While in learning mode, make sure your hits are at least ~300ms apart – even one hit too soon would completely corrupt the learned data and you’d have to start again. Try to hit your pad in a variety of positions, at a variety of velocities.

#include <EEPROM.h>

#define triggerThreshold 3 // If this is set too low, hits on other pads will trigger a "hit" on this pad
#define initialHitReadDuration 500 // In microseconds. Shorter times will mean less latency, but less velocity-accuracy
#define midiVelocityScaleDownAmount 2 // Number of halvings that will be applied to MIDI velocity
#define inputPin A0

#define learning 0

uint16_t highestYet;
uint32_t startReadingTime;
uint32_t highestValueTime;
boolean hitOccurredRecently = false;
boolean newRecordSet;

void setup() {
  if (learning) {
    for (int i = 0; i < 256; i++) {
      EEPROM.write(i, 0);
    }
  }
}

void loop() {

  // Assume the normal hit-threshold
  uint16_t thresholdNow = triggerThreshold;

  // Read the piezo
  uint16_t value = analogRead(inputPin);

  uint32_t msPassed;

  // But, if a hit occurred very recently, we need to set a higher threshold for triggering another hit, otherwise the dissipating vibrations
  // of the previous hit would trigger another one now
  if (hitOccurredRecently) {

      uint32_t usPassed = micros() - highestValueTime;
      msPassed = usPassed >> 10;

      if (learning) {
        if (msPassed >= 256) {
          hitOccurredRecently = false;
          if (newRecordSet) {
            Serial.println("----------------------------------------");
            for (int i = 0; i < 256; i++) {
              Serial.print(String((int)EEPROM.read(i)) + ", ");
              if (i % 16 == 0) Serial.println();
            }
          }
        }
        else {
          uint16_t nowComparedToHighest = ceil((float)value / (float)highestYet * 256);
          uint8_t previousRecord = EEPROM.read(msPassed);
          if (nowComparedToHighest > previousRecord) {
            newRecordSet = true;
            EEPROM.write(msPassed, min(255, nowComparedToHighest));
          }
        }
        thresholdNow = 1024;
      }

      else {
        if (msPassed >= 256) hitOccurredRecently = false;
        else {
          // Work out how high a reading we'd need to see right now in order to conclude that another hit has occurred
          uint32_t currentDynamicThreshold;
          if (usPassed < initialHitReadDuration) currentDynamicThreshold = highestYet;
          else currentDynamicThreshold = ((uint32_t)EEPROM.read(msPassed) * highestYet) >> 8;

          thresholdNow += currentDynamicThreshold;
        }
      }
  }

  // If we've breached the threshold, it means we've got a hit!
  if (value >= thresholdNow) {

    startReadingTime = micros();
    highestYet = 0;

    // For the next few milliseconds, look out for the highest "spike" in the reading from the piezo. Its height is representative of the hit's velocity
    do {
      if (value > highestYet) {
        highestYet = value;
        highestValueTime = micros();
      }
      value = analogRead(inputPin);
    } while (timeGreaterOrEqual(startReadingTime + initialHitReadDuration, micros()));

    // Send the MIDI note
    usbMIDI.sendNoteOn(0, (highestYet >> midiVelocityScaleDownAmount) + 1, 1); // We add 1 onto the velocity so that the result is never 0, which would mean the same as a note-off
    Serial.println(highestYet); // Send the unscaled velocity value to the serial monitor too, for debugging / fine-tuning
    hitOccurredRecently = true;
    newRecordSet = false;
  }
}

// Compares times without being prone to problems when the micros() counter overflows, every ~70 mins
boolean timeGreaterOrEqual(uint32_t lhs, uint32_t rhs) {
  return (((lhs - rhs) & 2147483648) == 0);
}

Update 2015-02-15: I have since developed a version of this sketch to work with multiple drum pads simultaneously, plus some other improvements. Apologies – this is even more un-commented than the above code, but if you do want to take a look, download it here.

Advertisements

15 thoughts on “Sensing hit-velocity and quick subsequent hits of a piezo with an Arduino / Teensy

  1. Hi man! Excellent article … I’m making MIDI drums on Arduino Nano. But, I want to control about 15 piezo sensors. So, can I use your code with CD4051 multiplexer and analog input pins from Nano (1x CD4051 and 7 analog inputs from Nano)? What can I expect from mixing multiplexer and Nano analog inputs?

    Thanks!

      • Wow!
        Should I use Zener (3.3 V) in parallel with the piezo or just 1 MOhm resistor when using Teensy?
        Thanks in advance.

      • I just use a resistor – well, actually I use two resistors in series, and then connect the Teensy’s analog input to the mid-point between these. This attenuates the voltage, which was otherwise coming in too high. You may or may not need to do this, and if you do, you’ll need to experiment with the resistor values to get an appropriate voltage range (the resistors should together total about 1M). Good luck!

  2. I have a similar setup with a Teensy 2.0 and 4 piezos/drum pads. I’ve read that besides running a parallel 1m ohm resistor for each piezo, people also recommend a 10k resistor at the teensy pin to protect the board? I’m also currently getting no MIDI signal once I hooked up the piezos, and the board starts to get very warm…any help would be great!

  3. Hi I just tried your code for a single pad, and while i can’t say I can follow what’s going on with the code (yet) it seems to work great!

    But now i just tried the code for multiple pads and get this error on line 88 when I compile:
    In member function ‘void Pad::tick(uint8_t, boolean)’:
    sketch_mar17b:88: error: ‘timeGreaterOrEqual’ was not declared in this scope
    if (timeGreaterOrEqual(micros(), startReadingTime + initialHitReadDuration)) {
    ^
    exit status 1
    ‘timeGreaterOrEqual’ was not declared in this scope

    I’m going to try to understand the code and see if I can figure it out, but if you have a fix would love to see it!

      • Hello,
        It seems the function ‘timeGreaterOrEqual’ function was not declared, but in fact it is declared in the end of the code.
        I have already faced similar problems, and the solution was simply move the function to somewhere before the place where it is called on the code.

  4. Hi BEAMMYSELFINTOTHEFUTU,
    Im really keen to try out your code with my Teensy 3.2 but get the follow error and cant compile. I understand the code enough to see the problem – any ideas?

    Thanks.

    C:\Users\Nick\Documents\Arduino\Learning_Drum_Pad_Mod\Learning_Drum_Pad_Mod.ino: In member function ‘void Pad::tick(uint8_t, boolean)’:

    Learning_Drum_Pad_Mod:91: error: ‘timeGreaterOrEqual’ was not declared in this scope
    if (timeGreaterOrEqual(micros(), startReadingTime + initialHitReadDuration)) {

    ^

    Learning_Drum_Pad_Mod:95: error: ‘usbMIDI’ was not declared in this scope
    To make a USB MIDI device, use the Tools > USB Type menu
    usbMIDI.sendNoteOn(4 – padNo, midiVelocity, 1); // We add 1 onto the velocity so that the result is never 0, which would mean the same as a note-off

    ^

    C:\Users\Nick\Documents\Arduino\Learning_Drum_Pad_Mod\Learning_Drum_Pad_Mod.ino: At global scope:

    Learning_Drum_Pad_Mod:243: error: expected constructor, destructor, or type conversion before ‘;’ token
    timeGreaterOrEqual(uint32_t lhs, uint32_t rhs) {

    ^

    Learning_Drum_Pad_Mod: In function ‘void loop()’:
    Learning_Drum_Pad_Mod:233: error: ‘usbMIDI’ was not declared in this scope
    To make a USB MIDI device, use the Tools > USB Type menu
    usbMIDI.sendNoteOn(0, 64, 1);

    ^

    C:\Users\Nick\Documents\Arduino\Learning_Drum_Pad_Mod\Learning_Drum_Pad_Mod.ino: At global scope:

    Learning_Drum_Pad_Mod:243: error: ISO C++ forbids declaration of ‘timeGreaterOrEqual’ with no type [-fpermissive]
    timeGreaterOrEqual(uint32_t lhs, uint32_t rhs) {

    ^

    ‘timeGreaterOrEqual’ was not declared in this scope

  5. This code looks great, I have made a 12 piezo midi controller in the form of a glove for my dissertation project.

    The only problem was cross-talk while I was using digital inputs so now I have abandoned the standard firmata and am attempting to use your code.

    It would be really helpful if you could upload the full code you were using (the link has gone dead), then I might have a chance of getting this working!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s