Skip to content

Project: Touch Piano

The ESP32 can feel your touch on ten of its pins, with no buttons needed. In this project you'll wire seven of them to anything that conducts electricity (squares of foil, coins, even bananas) and turn them into the keys of a piano.

Objective

  • Seven touch keys play the notes C D E F G A B.
  • A note plays for as long as you hold its key, and stops when you let go.
  • If you touch several keys at once, the lowest note wins.
  • Optional: a NeoPixel stick lights up in a different colour for each note.

You'll need:

  • 1 × passive piezo buzzer
  • 7 × jumper wires (male-to-male, or with crocodile clips)
  • 7 × things to touch: foil squares, coins, pieces of fruit…
  • Tape
  • Optional: 1 × NeoPixel stick (8 LEDs)

This project borrows pins from the plan

The ESP32 only has ten touch-capable pins, and many are already used in the pin plan. Unplug the servo (13), Button A (14), the LED (27), the motion sensor (33) and Button C (32) before you start.

Before you start

This project uses what you learned in Touch Sensors and Piezo Buzzers. It doesn't need any library files.

Wiring

Key Note Connects to
1 C GPIO 4
2 D GPIO 15
3 E GPIO 13
4 F GPIO 14
5 G GPIO 27
6 A GPIO 33
7 B GPIO 32
Buzzer + leg GPIO 26
– leg GND
Optional: NeoPixel stick DIN / 5V / GND GPIO 23 / VIN / GND

Tape one end of each key's jumper wire to its foil square (or push it into the fruit), and lay the keys out in a row on the table, lowest note on the left.

Making good keys

Make each key about the size of a fingertip or bigger. Keep the wires apart from each other, and don't let the keys touch. Your body is part of the circuit, so you don't need a ground wire: just touch the key.

Plan it out

  1. Set up each component. Make a list called KEYS holding each note's name, pin and frequency. Create a TouchPad for every key and a PWM output for the buzzer.
  2. Break the job into functions.
    • calibrate() measures every key while nobody is touching it (its baseline).
    • pressed_key() checks the keys from lowest to highest and returns the first one that's being touched.
    • show_colour() lights the NeoPixels, if you have them.
  3. Call the functions from the main loop. Find out which key is touched. If it's different from last time, start the new note (or stop the sound).

Code

Save this to your ESP32 as main.py so it runs every time the board powers up. Or just click Run in Thonny to try it out.

touch_piano.py
# Touch Piano: seven touch keys play a scale on the buzzer.
# Keys can be foil squares, coins, or even pieces of fruit!
import time
from machine import Pin, PWM, TouchPad

BUZZER_PIN = 26
VOLUME = 32768      # duty_u16: 0 is silent, 32768 (50%) is loudest
SENSITIVITY = 0.7   # a key counts as touched below 70% of its baseline

# (note name, touch pin, frequency in Hz), lowest note first
KEYS = [
    ("C", 4, 262),
    ("D", 15, 294),
    ("E", 13, 330),
    ("F", 14, 349),
    ("G", 27, 392),
    ("A", 33, 440),
    ("B", 32, 494),
]

# Optional: light a NeoPixel stick in a different colour for each note.
# Set USE_PIXELS = True if you have one plugged into GPIO 23.
USE_PIXELS = False
PIXEL_PIN = 23
NUM_PIXELS = 8
COLOURS = [(60, 0, 0), (60, 25, 0), (60, 60, 0), (0, 60, 0),
           (0, 50, 60), (0, 0, 60), (40, 0, 60)]

# Step 1: set up each component
buzzer = PWM(Pin(BUZZER_PIN), freq=262, duty_u16=0)
pads = [TouchPad(Pin(pin)) for name, pin, freq in KEYS]

pixels = None
if USE_PIXELS:
    from neopixel import NeoPixel
    pixels = NeoPixel(Pin(PIXEL_PIN, Pin.OUT), NUM_PIXELS)


# Step 2: small functions for each job
def calibrate():
    # Learn what each key reads when nobody is touching it
    print("Calibrating... keep your hands off the keys!")
    baselines = []
    for pad in pads:
        total = 0
        for i in range(20):
            total += pad.read()
            time.sleep_ms(5)
        baselines.append(total // 20)
    print("Baselines:", baselines)
    return baselines


def pressed_key():
    # Return the index of the lowest touched key, or None
    for i in range(len(pads)):
        if pads[i].read() < baselines[i] * SENSITIVITY:
            return i
    return None


def show_colour(key):
    if pixels is None:
        return
    colour = (0, 0, 0) if key is None else COLOURS[key]
    for i in range(NUM_PIXELS):
        pixels[i] = colour
    pixels.write()


# Step 3: play whichever key is being touched
baselines = calibrate()
print("Ready! Touch a key.")
current = None

while True:
    key = pressed_key()
    if key != current:           # only act when something changes
        if key is None:
            buzzer.duty_u16(0)
        else:
            name, pin, freq = KEYS[key]
            buzzer.freq(freq)
            buzzer.duty_u16(VOLUME)
            print(name)
        show_colour(key)
        current = key
    time.sleep_ms(10)

How it works

  • Every key is different. A long wire, a big foil square and a banana all give different readings, so one threshold for all of them wouldn't work. calibrate() averages 20 readings per key to find each one's own baseline. A key counts as touched when its reading drops below 70 % of its baseline (SENSITIVITY).
  • Keep your hands off at the start! If you're touching a key while it calibrates, that key's baseline will be too low and it'll never trigger. Press the board's EN/RST button to calibrate again.
  • pads = [TouchPad(Pin(pin)) for name, pin, freq in KEYS] is a list comprehension: a compact way of building a list with a loop. It makes one TouchPad for every entry in KEYS.
  • current remembers which key was playing last time. The buzzer is only changed when the key changes, which keeps the sound clean instead of restarting it 100 times a second.
  • To turn on the optional NeoPixels, change USE_PIXELS = False to True. The neopixel module is only imported if you use it.

Result

Touch the first key and the buzzer plays a C. Slide your finger along the keys and it plays up the scale. Try Twinkle Twinkle Little Star: C C G G A A G, F F E E D D C.

Make it your own

  • Fruit keyboard: swap the foil for bananas, apples or oranges. It works, and it's much funnier.
  • Tune your sensitivity: if keys trigger when your hand is nearby, lower SENSITIVITY to 0.6. If they're hard to trigger, raise it to 0.8.
  • Octave switch: make the lowest key a "shift" key that doubles every other note's frequency while it's held (an octave higher).
  • Record and play back: store (note, milliseconds) pairs in a list as you play, then replay your tune when you touch two keys at once.