Built-in project · Arduino Uno R3

Tap-tempo metronome on an Arduino Uno

Tap a button in time and an Uno copies your tempo. Timer 1 interrupts keep the clicks perfectly even, with a higher click and a red light on the first beat of each bar. The whole build, an Arduino Uno and 7 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $33. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sound
  • Lights
  • Controllers
  • Reading a button
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 9

How it works

Tap a button in time and an Uno copies your tempo. Timer 1 interrupts keep the clicks perfectly even, with a higher click and a red light on the first beat of each bar.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Resistor, 220 Ω, LED, red, LED, green. pinMode sets TAP_PIN as an input with its pull-up on. digitalWrite drives FIRST_LED HIGH. digitalRead reads whether pin is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Pushbutton (1) pin 2aArduino Uno R3 pin GND
Pushbutton (2) pin 2aArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Piezo speaker pin 1Arduino Uno R3 pin 8
Resistor, 220 Ω (2) pin 1Arduino Uno R3 pin 6
Resistor, 220 Ω (1) pin 1Arduino Uno R3 pin 5
Pushbutton (2) pin 1aArduino Uno R3 pin 3
Pushbutton (1) pin 1aArduino Uno R3 pin 2
LED, red pin CArduino Uno R3 pin GND
LED, green pin CArduino Uno R3 pin GND
LED, red pin AResistor, 220 Ω (1) pin 2
LED, green pin AResistor, 220 Ω (2) pin 2

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Tap-tempo metronome: tap in time and it keeps the beat.
//
// Wiring:
//   tap          pin 2 to GND
//   bar          pin 3 to GND: beats per bar, 2, 3, 4 or 6
//   first beat   pin 5 -> 220 ohms -> red LED -> GND
//   other beats  pin 6 -> 220 ohms -> green LED -> GND
//   piezo        pin 8 -> piezo -> GND
//
// Tap four times at the speed you want. The sketch averages the last three gaps
// between taps (a single tap is easily a little early or late, an average is
// not) and turns that into beats per minute.
//
// Steady tempo. A sketch that counts milliseconds in loop() is only as steady
// as loop() is: if the loop is ever busy for a few milliseconds, the click is
// late. Here the beat belongs to Timer 1, a counter inside the chip that runs
// on its own. It counts clock ticks up to OCR1A, raises an interrupt, and starts
// again, and nothing the sketch is doing can make that late. The interrupt
// routine does one small thing: it switches the light and tells loop() to click.
//
//   timer clock       16 MHz / 1024 = 15625 ticks per second
//   ticks per beat    15625 * 60 / BPM       (120 BPM is 7812 ticks, 500 ms)
//
// The first beat of each bar is louder (a higher pitch) and lights the red LED.
//
// Things to try. Tap a fast tempo and a slow one. Change the bar length and
// count along. Try 6 beats and group them in twos.

#include <avr/interrupt.h>

const uint8_t TAP_PIN = 2;
const uint8_t BAR_PIN = 3;
const uint8_t FIRST_LED = 5;
const uint8_t OTHER_LED = 6;
const uint8_t PIEZO_PIN = 8;

const uint8_t BAR_CHOICES[] = {2, 3, 4, 6};
uint8_t barChoice = 2;  // 4 beats to the bar

uint16_t bpm = 120;

// the interrupt counts beats; loop() clicks for each one it has not heard yet
volatile uint16_t beatsRung = 0;
uint16_t beatsHeard = 0;
volatile uint8_t beatInBar = 0;  // 0 is the first beat

// taps: when the last few happened
uint32_t tapAt[4];
uint8_t taps = 0;

uint32_t ledsOffAt = 0;

bool tapWas = false, barWas = false;
uint32_t tapChanged = 0, barChanged = 0;

static void setTempo(uint16_t newBpm)
{
  if (newBpm < 40) {
    newBpm = 40;
  }
  if (newBpm > 240) {
    newBpm = 240;
  }
  bpm = newBpm;
  uint16_t ticks = (uint16_t)(15625UL * 60 / bpm) - 1;
  // change the compare value and start the count again from zero, with
  // interrupts off so the timer cannot fire half way through
  cli();
  OCR1A = ticks;
  TCNT1 = 0;
  sei();
}

ISR(TIMER1_COMPA_vect)
{
  uint8_t bar = BAR_CHOICES[barChoice];
  beatInBar = (beatInBar + 1) % bar;
  // the light goes on right here, on the beat, not whenever loop() gets round to it
  if (beatInBar == 0) {
    digitalWrite(FIRST_LED, HIGH);
  } else {
    digitalWrite(OTHER_LED, HIGH);
  }
  beatsRung++;
}

static bool pressed(uint8_t pin, bool &was, uint32_t &changed)
{
  bool down = digitalRead(pin) == LOW;
  uint32_t now = millis();
  if (down != was && now - changed > 20) {
    was = down;
    changed = now;
    return down;
  }
  return false;
}

static void onTap()
{
  uint32_t now = millis();
  // a long pause means a new count: start again
  if (taps > 0 && now - tapAt[taps - 1] > 2000) {
    taps = 0;
  }
  if (taps == 4) {
    for (uint8_t i = 1; i < 4; i++) {
      tapAt[i - 1] = tapAt[i];
    }
    taps = 3;
  }
  tapAt[taps++] = now;
  if (taps >= 2) {
    uint32_t average = (tapAt[taps - 1] - tapAt[0]) / (taps - 1);
    setTempo((uint16_t)(60000UL / average));
    Serial.print("Tempo ");
    Serial.print(bpm);
    Serial.println(" BPM");
  }
}

void setup()
{
  Serial.begin(9600);
  pinMode(TAP_PIN, INPUT_PULLUP);
  pinMode(BAR_PIN, INPUT_PULLUP);
  pinMode(FIRST_LED, OUTPUT);
  pinMode(OTHER_LED, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);

  // Timer 1 in CTC mode ("clear on compare"): count up to OCR1A, interrupt, start over
  cli();
  TCCR1A = 0;
  TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);  // CTC, clock / 1024
  TIMSK1 = (1 << OCIE1A);
  sei();
  setTempo(bpm);
  Serial.println("Tap-tempo metronome ready: 120 BPM, 4 beats");
}

void loop()
{
  if (beatsHeard != beatsRung) {
    beatsHeard = beatsRung;
    uint8_t bar = BAR_CHOICES[barChoice];
    uint8_t beat = beatInBar;
    bool first = beat == 0;
    tone(PIEZO_PIN, first ? 1800 : 1200, 30);
    ledsOffAt = millis() + 60;
    Serial.print("Beat ");
    Serial.print(beat + 1);
    Serial.print("/");
    Serial.print(bar);
    Serial.print(" ");
    Serial.print(bpm);
    Serial.println(" BPM");
  }
  if (ledsOffAt != 0 && millis() >= ledsOffAt) {
    digitalWrite(FIRST_LED, LOW);
    digitalWrite(OTHER_LED, LOW);
    ledsOffAt = 0;
  }

  if (pressed(TAP_PIN, tapWas, tapChanged)) {
    onTap();
  }
  if (pressed(BAR_PIN, barWas, barChanged)) {
    barChoice = (barChoice + 1) % sizeof(BAR_CHOICES);
    beatInBar = BAR_CHOICES[barChoice] - 1;  // the next beat is the first of a bar
    Serial.print("Bar ");
    Serial.print(BAR_CHOICES[barChoice]);
    Serial.println(" beats");
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup()
{
  Serial.begin(9600);
  pinMode(TAP_PIN, INPUT_PULLUP);
  pinMode(BAR_PIN, INPUT_PULLUP);
  pinMode(FIRST_LED, OUTPUT);
  pinMode(OTHER_LED, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);

  // Timer 1 in CTC mode ("clear on compare"): count up to OCR1A, interrupt, start over
  cli();
  TCCR1A = 0;
  TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);  // CTC, clock / 1024
  TIMSK1 = (1 << OCIE1A);
  sei();
  setTempo(bpm);
  Serial.println("Tap-tempo metronome ready: 120 BPM, 4 beats");
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop()
{
  if (beatsHeard != beatsRung) {
    beatsHeard = beatsRung;
    uint8_t bar = BAR_CHOICES[barChoice];
    uint8_t beat = beatInBar;
    bool first = beat == 0;
    tone(PIEZO_PIN, first ? 1800 : 1200, 30);
    ledsOffAt = millis() + 60;
    Serial.print("Beat ");
    Serial.print(beat + 1);
    Serial.print("/");
    Serial.print(bar);
    Serial.print(" ");
    Serial.print(bpm);
    Serial.println(" BPM");
  }
  if (ledsOffAt != 0 && millis() >= ledsOffAt) {
    digitalWrite(FIRST_LED, LOW);
    digitalWrite(OTHER_LED, LOW);
    ledsOffAt = 0;
  }

  if (pressed(TAP_PIN, tapWas, tapChanged)) {
    onTap();
  }
  if (pressed(BAR_PIN, barWas, barChanged)) {
    barChoice = (barChoice + 1) % sizeof(BAR_CHOICES);
  // ... 6 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
  • Sound is very quiet or the pin seems overloaded. Piezo speaker
    Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.