Built-in project · Arduino Uno R3

Digital piano on an Arduino Uno

Eight keys and a piezo play an octave from middle C on an Uno. tone() keeps the keys live while a note sounds, and the piano plays back your last 32 notes. The whole build, an Arduino Uno and 9 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.

Beginner About 20 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

  • Reading buttons with internal pull-ups
  • Mapping keys to note frequencies
  • Playing tones with tone while still reading inputs
  • Storing a short recording in arrays
  • Debouncing with a small delay
  • Difficulty: Beginner
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 11

How it works

Eight pushbuttons connect pins 2 to 9 to ground. The sketch reads them with the internal pull-up resistors, so a pressed key reads LOW and no extra resistors are needed. Each key maps to a note from C4 to C5, played on a piezo on pin 10.

It plays one note at a time. If you hold two keys, the highest one wins. The piano also remembers your last 32 notes with their timing. Press the lowest and highest keys together and it plays them back.

Parts list

11 parts, plus the jumper wires. They retail for about $33; on Mokxi they're free.

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
Pushbutton (3) pin 2aArduino Uno R3 pin GND
Pushbutton (4) pin 2aArduino Uno R3 pin GND
Pushbutton (5) pin 2aArduino Uno R3 pin GND
Pushbutton (6) pin 2aArduino Uno R3 pin GND
Pushbutton (7) pin 2aArduino Uno R3 pin GND
Pushbutton (8) pin 2aArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Piezo speaker pin 1Arduino Uno R3 pin 10
Pushbutton (8) pin 1aArduino Uno R3 pin 9
Pushbutton (7) pin 1aArduino Uno R3 pin 8
Pushbutton (6) pin 1aArduino Uno R3 pin 7
Pushbutton (5) pin 1aArduino Uno R3 pin 6
Pushbutton (4) pin 1aArduino Uno R3 pin 5
Pushbutton (3) pin 1aArduino Uno R3 pin 4
Pushbutton (2) pin 1aArduino Uno R3 pin 3
Pushbutton (1) pin 1aArduino Uno R3 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
// Digital piano: eight keys and a piezo, one octave from middle C.
//
// Wiring:
//   keys     a pushbutton from each of pins 2 to 9 to GND (C D E F G A B C)
//   piezo    pin 10 -> piezo -> GND
//
// Every key is a switch to ground read with INPUT_PULLUP, so pressed is LOW
// and no resistors are needed. tone() plays a square wave from a timer, so
// the sketch keeps reading the keys while a note sounds.
//
// Hold a key and the note holds; press another and it takes over, the way a
// one-voice synth does. The serial monitor names each note.
//
// The piano remembers the last 32 notes you played. Press the lowest and the
// highest key together and it plays them back, with your own timing.

const uint8_t PIEZO_PIN = 10;
const uint8_t KEYS = 8;
const uint8_t KEY_PIN[KEYS] = {2, 3, 4, 5, 6, 7, 8, 9};
// C4 to C5, in hertz
const uint16_t NOTE_HZ[KEYS] = {262, 294, 330, 349, 392, 440, 494, 523};
const char *const NOTE_NAME[KEYS] = {"C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"};

const uint8_t MEMORY = 32;
uint8_t played[MEMORY];
uint16_t gapBefore[MEMORY];  // milliseconds since the note before, capped
uint16_t heldFor[MEMORY];
uint8_t count = 0;

int8_t sounding = -1;  // the key whose note is playing, or -1
uint32_t noteStart = 0;
uint32_t lastNoteEnd = 0;

static int8_t keyDown()
{
  // The highest key held wins, like a one-voice synth.
  for (int8_t k = KEYS - 1; k >= 0; k--) {
    if (digitalRead(KEY_PIN[k]) == LOW) {
      return k;
    }
  }
  return -1;
}

static void remember(int8_t key, uint32_t start, uint32_t end)
{
  if (count == MEMORY) {
    for (uint8_t i = 1; i < MEMORY; i++) {
      played[i - 1] = played[i];
      gapBefore[i - 1] = gapBefore[i];
      heldFor[i - 1] = heldFor[i];
    }
    count--;
  }
  uint32_t gap = count == 0 ? 0 : start - lastNoteEnd;
  played[count] = (uint8_t)key;
  gapBefore[count] = (uint16_t)(gap > 2000 ? 2000 : gap);
  heldFor[count] = (uint16_t)(end - start > 2000 ? 2000 : end - start);
  count++;
  lastNoteEnd = end;
}

static void playBack()
{
  noTone(PIEZO_PIN);
  Serial.print("Playing back ");
  Serial.print(count);
  Serial.println(" notes");
  // wait for both keys to come up first
  while (digitalRead(KEY_PIN[0]) == LOW || digitalRead(KEY_PIN[KEYS - 1]) == LOW) {
    delay(5);
  }
  delay(300);
  for (uint8_t i = 0; i < count; i++) {
    delay(gapBefore[i]);
    tone(PIEZO_PIN, NOTE_HZ[played[i]]);
    delay(heldFor[i]);
    noTone(PIEZO_PIN);
  }
  Serial.println("Done. Play something new.");
  count = 0;
  sounding = -1;
}

void setup()
{
  Serial.begin(9600);
  for (uint8_t k = 0; k < KEYS; k++) {
    pinMode(KEY_PIN[k], INPUT_PULLUP);
  }
  noTone(PIEZO_PIN);
  delay(20);
  Serial.println("Piano ready: eight keys, C4 to C5");
}

void loop()
{
  if (digitalRead(KEY_PIN[0]) == LOW && digitalRead(KEY_PIN[KEYS - 1]) == LOW) {
    if (sounding >= 0) {
      remember(sounding, noteStart, millis());
    }
    playBack();
    return;
  }

  int8_t key = keyDown();
  if (key != sounding) {
    uint32_t now = millis();
    if (sounding >= 0) {
      remember(sounding, noteStart, now);
    }
    if (key >= 0) {
      tone(PIEZO_PIN, NOTE_HZ[key]);
      Serial.println(NOTE_NAME[key]);
      noteStart = now;
    } else {
      noTone(PIEZO_PIN);
    }
    sounding = key;
  }
  delay(5);  // a key bounces for a few milliseconds; this rides it out
}

Code walkthrough

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

keyDown sketch.ino

Scans the eight keys from the highest down and returns the first one reading LOW. That is why the highest held key wins. It returns -1 when nothing is pressed.

static int8_t keyDown()
{
  // The highest key held wins, like a one-voice synth.
  for (int8_t k = KEYS - 1; k >= 0; k--) {
    if (digitalRead(KEY_PIN[k]) == LOW) {
      return k;
    }
  }
  return -1;
}

remember sketch.ino

Stores a finished note: which key, the gap since the last note and how long it was held. Gaps and lengths are capped at 2000 ms. When all 32 slots are full, it shifts everything down one place first.

static void remember(int8_t key, uint32_t start, uint32_t end)
{
  if (count == MEMORY) {
    for (uint8_t i = 1; i < MEMORY; i++) {
      played[i - 1] = played[i];
      gapBefore[i - 1] = gapBefore[i];
      heldFor[i - 1] = heldFor[i];
    }
    count--;
  }
  uint32_t gap = count == 0 ? 0 : start - lastNoteEnd;
  played[count] = (uint8_t)key;
  gapBefore[count] = (uint16_t)(gap > 2000 ? 2000 : gap);
  heldFor[count] = (uint16_t)(end - start > 2000 ? 2000 : end - start);
  count++;
  lastNoteEnd = end;
}

playBack sketch.ino

Waits for the two end keys to be released, then plays every stored note with its original gap and hold time. Afterward it clears the memory so you can record something new.

static void playBack()
{
  noTone(PIEZO_PIN);
  Serial.print("Playing back ");
  Serial.print(count);
  Serial.println(" notes");
  // wait for both keys to come up first
  while (digitalRead(KEY_PIN[0]) == LOW || digitalRead(KEY_PIN[KEYS - 1]) == LOW) {
    delay(5);
  }
  delay(300);
  for (uint8_t i = 0; i < count; i++) {
    delay(gapBefore[i]);
    tone(PIEZO_PIN, NOTE_HZ[played[i]]);
    delay(heldFor[i]);
    noTone(PIEZO_PIN);
  }
  Serial.println("Done. Play something new.");
  count = 0;
  sounding = -1;
}

Troubleshooting

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

  • A key does nothing. This project
    Each button needs one side on its pin (2 to 9) and the other side on GND.
  • Silence from the piezo. This project
    The piezo's positive pin goes to pin 10 and the other pin to GND.
  • 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.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

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