Tap Tempo Button Code

Hi all

Not asking for something in particular (but of course comments and remarks are welcome), but I thought I would share my Tap Tempo Code here if someone would need to use it :slight_smile:
It’s tested thoroughly and works fine for me :wink:

It has 2 classes:

  • Button: simple class to check if a push button has been pressed, with deboucing. The class uses the internal pullup resistor (INUPUT_PULLUP mode) so the push button simply needs to be connected to the pin and to ground
  • TapTempoButton: class extending Button and calculating tempo based on x (=TAPTEMPO_AMOUNT_OF_TAPS) successive taps. Feel free to change the value of x - a bigger value will mean a more precise average after many taps, but then if one tap is wrong (let’s say your finger missed the button - happens to me a lot), that wrong value will stay for another x taps.

I added a simple main code to show how to use the class.

Button.h - Button class header

#ifndef __Button_h__
#define __Button_h__

#include <Arduino.h>

#define BUTTON_DEBOUNCE_TIME   30   // x ms after button has changed to avoid double clicks


class Button {
  private:
    byte pin;
    byte lastReadState;
    byte currentState;
    unsigned long timeButtonChange;

  public:
    Button();
    void begin(byte pin); 
    bool hasBeenPressed(); // has to be call regularly to check the button status
};

#endif

Button.cpp - Button class implementation

#include "Button.h"

Button::Button() :
  lastReadState(LOW),
  currentState(LOW),
  pin(NOT_A_PIN),
  timeButtonChange(0) {
  
}

void Button::begin(byte pin) {  
  pinMode(pin, INPUT_PULLUP);
  byte st = digitalRead(pin);
  this->lastReadState = st;
  this->currentState = st;
  this->pin = pin;
}

bool Button::hasBeenPressed() {
  byte newState = digitalRead(this->pin);
  bool returnVal = false;
  unsigned long currentTime = millis();
  if (newState != this->lastReadState) { // has changed - because of noise or because the button is being pressed
    this->timeButtonChange = currentTime;
    this->lastReadState = newState;
  }
  else if ((currentTime - this->timeButtonChange) > long(BUTTON_DEBOUNCE_TIME)) { // state has been stable for x ms
    if (this->currentState == HIGH && newState == LOW) // HIGH to LOW = pressed (with pullup resistor)
      returnVal = true;
    this->currentState = newState;
  }
  return returnVal;
}

TapTempoButton.h = TapTempoButton class header

#ifndef __TapTempoButton_h__
#define __TapTempoButton_h__

#include "Button.h"

#define   TAPTEMPO_AMOUNT_OF_TAPS       6     // tempo will be averaged over x taps
#define   TAPTEMPO_TIMEBETWEEN          2000  // in ms  (2s => 0.5bps = 30bpm is the min tempo)
#define   TAPTEMPO_MAXTEMPO             400   // bpm

class TapTempoButton : public Button {
  private:
    word tempo;
    byte lastTapTime;
    unsigned long tapTimes[TAPTEMPO_AMOUNT_OF_TAPS];

  public:
    TapTempoButton();
    bool hasTempoChanged(); // has to be call regularly to check the button status
    word getTempo();
};

#endif

TapTempoButton.cpp = TapTempoButton class implementation

#include "TapTempoButton.h"


TapTempoButton::TapTempoButton() :
    tempo(0),
    lastTapTime(0),
    tapTimes{0} {
     
}

word TapTempoButton::getTempo() {
  return this->tempo;
}

bool TapTempoButton::hasTempoChanged() { // to call once per loop()
  if (!this->hasBeenPressed())
    return false;

  unsigned long now = millis();
  if (now - this->tapTimes[this->lastTapTime] > TAPTEMPO_TIMEBETWEEN) { // start over
    this->tapTimes[0] = now;
    this->lastTapTime = 0;
    return false;
  }
 
  // has been tapped at least once
  if (this->lastTapTime < TAPTEMPO_AMOUNT_OF_TAPS-1) {
    this->lastTapTime++;
    this->tapTimes[this->lastTapTime] = now;
  }
  else { // we have TAPTEMPO_AMOUNT_OF_TAPS taps, we need to shift all the values
    for (byte i = 1 ; i <= this->lastTapTime ; i++)
      this->tapTimes[i-1] = this->tapTimes[i];
    this->tapTimes[this->lastTapTime] = now;
  }

  word timeDifferences = 0;
  for (byte j = 1 ; j <= this->lastTapTime ; j++)
    timeDifferences += this->tapTimes[j] - this->tapTimes[j-1];

  this->tempo = (60000*this->lastTapTime) / timeDifferences; // timeDiff / lastTapTime = average of the time diff between taps
  if (this->tempo > TAPTEMPO_MAXTEMPO)
    this->tempo = TAPTEMPO_MAXTEMPO;
  return true;
}

Test_Tap_tempo.ino = example main class, which simply writes the tempo on the Serial Monitor.

#include "TapTempoButton.h"

#define   TAP_TEMPO_BUTTON_PIN    2

TapTempoButton tapTempo;

void setup() {
  tapTempo.begin(TAP_TEMPO_BUTTON_PIN);
  Serial.begin(9600);
}

void loop() {
  if (tapTempo.hasTempoChanged()) {
    Serial.print("New tempo: ");
    Serial.println(tapTempo.getTempo());
  }
}

thanks for sharing

it’s not a best practice to set the pins mode in the constructor, you can’t guarantee when this will be called

Button::Button(byte pin) {
  pinMode(pin,INPUT_PULLUP);

if users don’t call that in setup with new.

That’s one reason why many classes have a begin() function that runs in the setup() once everything has been organized for you

why does this matter?

// always call this function once and only once per loop()

why can’t I call this as much as I want? the loop() is nothing special…

In the else part of this code

if (this->lastTapTime < TAPTEMPO_AMOUNT_OF_TAPS-1) {
    this->lastTapTime++;
    this->tapTimes[this->lastTapTime] = now;
  }
  else { // we have TAPTEMPO_AMOUNT_OF_TAPS taps, we need to shift all the values
    for (byte i = 1 ; i <= this->lastTapTime ; i++)
      this->tapTimes[i-1] = this->tapTimes[i];
    this->tapTimes[this->lastTapTime] = now;
  }

lastTapTime is >= TAPTEMPO_AMOUNT_OF_TAPS-1.
If it is (TAPTEMPO_AMOUNT_OF_TAPS-1) then you are fine, but could it be TAPTEMPO_AMOUNT_OF_TAPS or more? in which case you would overflow (I did not check elsewhere when you increment if you always check for bounds)

instead of shifting values, which takes time, you could consider using a circular buffer.

Thanks J-L-M that is great insight :slight_smile:

I fully agree with your last comment but I did check and the code is made that lastTapTime never gets bigger than (or equal to) TAPTEMPO_AMOUNT_OF_TAPS.

The loop() comment is just wrong - my bad, I'll correct that (and edit the post).

Good point about the constructor, I'll add a begin() function (and edit the post as well).

Don’t edit the first post or our conversation becomes kinda irrelevant.. just post an update below

Too late mate - I'll keep it in mind for future edits :wink: