Brody's Builds
← All projects
Air Piano
ArduinoEasy30 minutes

Air Piano

Eight notes you play by holding your hand at different heights over a sensor: every 2 inches is the next note, from C up to high C.

Board: Arduino UNO (Elegoo UNO R3)

What you need

What it is

An invisible keyboard in the air above a sensor. Hold your hand at different heights and it plays a different note. Take your hand away and it stops.

Hand heightNote
2 to 4 inC
4 to 6 inD
6 to 8 inE
8 to 10 inF
10 to 12 inG
12 to 14 inA
14 to 16 inB
16 to 18 inhigh C

Try "Mary Had a Little Lamb": E D C D E E E.

Each 2 inches of height is the next note
Each 2 inches of height is the next note

How it works

  • The ultrasonic sensor measures how far away your hand is about 30 times a second, and the program uses the middle of the last 3 readings so one bad echo doesn't play a wrong note.
  • Each 2-inch band is one note. Near the edge of a band it waits until your hand is clearly past it (0.4 in), so the note doesn't flicker between two.
  • The notes are high ones (C6 to C7) because the little buzzer is loudest up there.
  • With a screen, the top line shows the note with music symbols and the bottom line shows a keyboard, with the key you're playing lit up.

Build it

1Upload the code

Upload the code below (see Getting started).

2Wire it

Same wiring as the Air Theremin:

Wiring: sensor on 5V, 8, 7, GND and the buzzer on 13 and GND
Wiring: sensor on 5V, 8, 7, GND and the buzzer on 13 and GND
  • Sensor: Vcc → 5V, Trig → 8, Echo → 7, Gnd → GND (Trig and Echo can be either way round).
  • Buzzer: one leg → pin 13, the other → the GND right next to pin 13 (either way round).
  • Screen (optional): wired like the Distance Meter.

Commands

TypeWhat it does
mMute / unmute (the screen still shows the notes)
?Is the sensor connected, and how far away is the nearest thing
fBlink the L light 3 times
c 40, b 160Screen contrast and backlight

If it doesn't work

  • No sound: type ? to check the sensor. Make sure it's the passive buzzer (no sticker).
  • Wrong notes: keep your hand flat and level over the sensor, and keep other things out of its way.
  • Want it smooth instead? Upload the Air Theremin: same wiring, and it has this piano mode built in too.

The code

Copy it into the Arduino app, or download the file. New to this? Start with Getting started.

AirPiano.ino · 428 lines Download
// Air piano: hold your hand over the ultrasonic sensor and move it up and down.// Each 2-inch step is a different note, from C (closest) up to the next C// (furthest). Take your hand away and it goes quiet.////   2 to 4 in = C    4 to 6 = D    6 to 8 = E    8 to 10 = F//  10 to 12 = G     12 to 14 = A  14 to 16 = B  16 to 18 = high C//// Wiring is the same as the distance meter, plus the buzzer:// Ultrasonic sensor (HC-SR04), 4 female-to-male wires://   Vcc -> 5V     Trig -> pin 8     Echo -> pin 7     Gnd -> GND//   (Trig and Echo can go in 7 and 8 either way round, the program checks.)// Screen (LCD1602), pin by pin. Pin 1 is the one marked VSS://    1 VSS -> GND         2 VDD -> A0          3 V0 -> pin 9       4 RS -> pin 12//    5 RW  -> A1          6 E   -> pin 11      7 to 10 -> nothing//   11 D4  -> pin 5      12 D5  -> pin 4      13 D6 -> pin 3      14 D7 -> pin 2//   15 A   -> pin 10     16 K   -> A2// Buzzer (the starter kit's passive one, no sticker), 2 female-to-male wires://   one leg -> pin 13, the other leg -> the GND right next to pin 13.//   Either way round works. The board's "L" light blinks along with the notes.// The screen is optional: without it the piano still plays.//// From the computer://   "c 40"   screen contrast, 0 to 255 (lower = darker letters)//   "b 160"  backlight brightness, 0 to 255//   "m"      mute / unmute (the screen still shows the notes)//   "?"      is the sensor connected, and how far away is the nearest thing//   "f"      blink the "L" light 3 times (to find this board)// Contrast, backlight and mute are saved.#include <EEPROM.h>// ---------- Pins ----------const byte SENSOR_PINS[2] = {7, 8};byte trigPin = 7;byte echoPin = 8;const byte BUZZER = 13;const byte LCD_POWER = A0;const byte LCD_RW = A1;const byte LCD_GROUND = A2;  // the backlight's K pinconst byte LCD_RS = 12;const byte LCD_E = 11;const byte LCD_DATA[4] = {5, 4, 3, 2};  // the screen's D4, D5, D6, D7const byte LCD_CONTRAST = 9;const byte LCD_LIGHT = 10;// ---------- Notes ----------// One octave of C major. These are an octave above middle C's octave, because// the little buzzer is much louder at higher pitches.const byte NOTE_COUNT = 8;const char NOTE_NAMES[NOTE_COUNT] = {'C', 'D', 'E', 'F', 'G', 'A', 'B', 'C'};const unsigned int NOTE_HZ[NOTE_COUNT] = {1047, 1175, 1319, 1397, 1568, 1760, 1976, 2093};const float FIRST_NOTE_AT = 2.0;  // inches from the sensor where the first C startsconst float NOTE_WIDTH = 2.0;     // inches per noteconst float WOBBLE = 0.4;         // a hand has to move this far past a line to change noteconst int NO_NOTE = -1;// ---------- Measuring ----------const float MICROS_PER_INCH = 148.0;  // sound out and back, per inch, at room temperatureconst unsigned long LISTEN_MICROS = 4500;  // stop listening after ~30 inchesconst unsigned long PING_GAP_MS = 30;      // short rest so old echoes die awayconst unsigned long QUIET_AFTER_MS = 150;  // a hand has to be gone this long before it goes quiet// ---------- Saved settings ----------struct Settings {  byte check;  byte contrast;  byte backlight;  byte muted;};const byte SETTINGS_CHECK = 0xA9;Settings settings;int playing = NO_NOTE;unsigned long lastHandSeen = 0;float recent[3] = {0, 0, 0};  // last 3 readings, so one stray echo can't jump the notebyte recentNext = 0;          // which of the 3 gets the next readingbyte recentCount = 0;         // how many fresh readings there are (stops at 3)void setup() {  Serial.begin(9600);  Serial.println("Air piano starting");  EEPROM.get(0, settings);  if (settings.check != SETTINGS_CHECK) {    settings = {SETTINGS_CHECK, 40, 160, 0};    EEPROM.put(0, settings);  }  // Pins 9 and 10 flicker very fast (31,000 times a second) to set contrast  // and brightness without the screen shimmering.  TCCR1B = (TCCR1B & 0b11111000) | 0b001;  pinMode(LCD_CONTRAST, OUTPUT);  pinMode(LCD_LIGHT, OUTPUT);  analogWrite(LCD_CONTRAST, settings.contrast);  analogWrite(LCD_LIGHT, settings.backlight);  pinMode(LCD_RW, OUTPUT);  digitalWrite(LCD_RW, LOW);  pinMode(LCD_GROUND, OUTPUT);  digitalWrite(LCD_GROUND, LOW);  pinMode(LCD_POWER, OUTPUT);  digitalWrite(LCD_POWER, HIGH);  delay(100);  lcdBegin();  makeNoteSymbol();  pinMode(BUZZER, OUTPUT);  findSensorPins();  playStartTune();  showIdle();  Serial.println(settings.muted ? "Ready (muted)" : "Ready");}void loop() {  readComputer();  float inches = readInches();  if (inches > 0) {    recent[recentNext] = inches;    recentNext = (recentNext + 1) % 3;    if (recentCount < 3) recentCount++;  } else {    findSensorPins();  // in case the wires were swapped or plugged in late  }  int note = NO_NOTE;  if (inches > 0 && recentCount >= 3) {    note = pickNote(middleOf3());  }  if (note != NO_NOTE) {    lastHandSeen = millis();    if (note != playing) startNote(note);  } else if (playing != NO_NOTE && millis() - lastHandSeen > QUIET_AFTER_MS) {    stopNote();  }  delay(PING_GAP_MS);}// ---------- Notes ----------// Turns a distance into a note number (0 = low C ... 7 = high C), or NO_NOTE.// Near a line between two notes it keeps the current note, so it doesn't flutter.int pickNote(float inches) {  if (playing != NO_NOTE) {    float low = FIRST_NOTE_AT + playing * NOTE_WIDTH - WOBBLE;    float high = FIRST_NOTE_AT + (playing + 1) * NOTE_WIDTH + WOBBLE;    if (inches >= low && inches < high) return playing;  }  if (inches < FIRST_NOTE_AT) return NO_NOTE;  int note = (int)((inches - FIRST_NOTE_AT) / NOTE_WIDTH);  return note < NOTE_COUNT ? note : NO_NOTE;}void startNote(int note) {  playing = note;  if (!settings.muted) tone(BUZZER, NOTE_HZ[note]);  showNote(note);  Serial.print("Note ");  Serial.print(NOTE_NAMES[note]);  if (note == NOTE_COUNT - 1) Serial.print(" (high)");  Serial.print("  at ");  Serial.print(middleOf3(), 1);  Serial.println(" in");}void stopNote() {  playing = NO_NOTE;  noTone(BUZZER);  digitalWrite(BUZZER, LOW);  recentCount = 0;  showIdle();  Serial.println("Quiet");}void playStartTune() {  if (settings.muted) return;  const byte tune[] = {0, 2, 4, 7};  for (byte i = 0; i < sizeof(tune); i++) {    tone(BUZZER, NOTE_HZ[tune[i]], 110);    delay(140);  }  noTone(BUZZER);}// ---------- Measuring ----------// Pins 7 and 8: with a pull-up on, the Echo pin still reads LOW because the// sensor holds it there; the Trig pin reads HIGH. If it can't tell, it keeps// the last choice.void findSensorPins() {  pinMode(SENSOR_PINS[0], INPUT_PULLUP);  pinMode(SENSOR_PINS[1], INPUT_PULLUP);  delay(2);  bool low0 = digitalRead(SENSOR_PINS[0]) == LOW;  bool low1 = digitalRead(SENSOR_PINS[1]) == LOW;  if (low0 != low1) {    echoPin = low0 ? SENSOR_PINS[0] : SENSOR_PINS[1];    trigPin = low0 ? SENSOR_PINS[1] : SENSOR_PINS[0];  }  pinMode(echoPin, INPUT);  pinMode(trigPin, OUTPUT);  digitalWrite(trigPin, LOW);}// One ping. Returns the distance in inches, or 0 if nothing answered within ~30 in.float readInches() {  digitalWrite(trigPin, LOW);  delayMicroseconds(2);  digitalWrite(trigPin, HIGH);  delayMicroseconds(10);  digitalWrite(trigPin, LOW);  unsigned long microseconds = pulseIn(echoPin, HIGH, LISTEN_MICROS);  return microseconds == 0 ? 0 : microseconds / MICROS_PER_INCH;}float middleOf3() {  float a = recent[0], b = recent[1], c = recent[2];  if ((a <= b && b <= c) || (c <= b && b <= a)) return b;  if ((b <= a && a <= c) || (c <= a && a <= b)) return a;  return c;}// ---------- Screen ----------//   top row:     ♪  E  ♪          (or "Air Piano" when quiet)//   bottom row:  C D E F G A B C   with a solid block on the note being played// The music note lives in the screen's custom character slot 0. The screen// shows slot 0 for code 0 or code 8; using 8 keeps it from ending the text.const byte NOTE_SYMBOL_SLOT = 0;const char NOTE_SYMBOL = 8;void showNote(int note) {  char top[17] = "                ";  top[4] = NOTE_SYMBOL;  top[11] = NOTE_SYMBOL;  top[7] = NOTE_NAMES[note];  if (note == NOTE_COUNT - 1) {  // high C    top[7] = 'C';    top[8] = '\'';  }  lcdWake();  lcdPrintRow(0, top);  lcdPrintRow(1, keyboardRow(note));}void showIdle() {  lcdWake();  lcdPrintRow(0, settings.muted ? " Air Piano  mute" : "   Air Piano");  lcdPrintRow(1, "Hand 2-18in away");}const char *keyboardRow(int note) {  static char row[17];  for (byte i = 0; i < NOTE_COUNT; i++) {    row[i * 2] = ' ';    row[i * 2 + 1] = (i == note) ? (char)0xFF : NOTE_NAMES[i];  // 0xFF = solid block  }  row[16] = '\0';  return row;}// A little music note, drawn dot by dot (5 wide, 8 tall).void makeNoteSymbol() {  const byte rows[8] = {    0b00100,    0b00110,    0b00101,    0b00100,    0b00100,    0b01100,    0b11100,    0b01000,  };  lcdSend(0x40 | (NOTE_SYMBOL_SLOT << 3), false);  for (byte i = 0; i < 8; i++) lcdSend(rows[i], true);  lcdSend(0x80, false);  // back to writing letters}// "?" from the computer: is the sensor there, and what does it see right now?// With a pull-up on, a powered sensor holds its Echo pin LOW; with no sensor// (or no power), both pins read HIGH.void reportStatus() {  pinMode(SENSOR_PINS[0], INPUT_PULLUP);  pinMode(SENSOR_PINS[1], INPUT_PULLUP);  delay(2);  bool low0 = digitalRead(SENSOR_PINS[0]) == LOW;  bool low1 = digitalRead(SENSOR_PINS[1]) == LOW;  findSensorPins();  Serial.print("Sensor: ");  if (low0 != low1) {    Serial.print("found (Trig ");    Serial.print(trigPin);    Serial.print(", Echo ");    Serial.print(echoPin);    Serial.print(")");  } else if (low0 && low1) {    Serial.print("NOT found: pins 7 and 8 both pulled LOW (wires in, but no power? check 5V/GND)");  } else {    Serial.print("NOT found: pins 7 and 8 both HIGH (Trig/Echo wires not connected?)");  }  delay(60);  float inches = readInches();  Serial.print("   sees: ");  if (inches > 0) {    Serial.print(inches, 1);    Serial.print(" in");  } else {    Serial.print("nothing within 30 in");  }  Serial.println(settings.muted ? "   sound: muted" : "   sound: on");}// ---------- Settings from the computer ----------void readComputer() {  static char line[20];  static byte length = 0;  while (Serial.available()) {    char ch = Serial.read();    if (ch == '\n' || ch == '\r') {      line[length] = '\0';      if (length > 0) handleCommand(line);      length = 0;    } else if (length < sizeof(line) - 1) {      line[length++] = ch;    }  }}void handleCommand(const char *line) {  int value = constrain(atoi(line + 1), 0, 255);  if (line[0] == 'c') {    settings.contrast = value;    analogWrite(LCD_CONTRAST, settings.contrast);  } else if (line[0] == 'b') {    settings.backlight = value;    analogWrite(LCD_LIGHT, settings.backlight);  } else if (line[0] == '?') {    reportStatus();    return;  } else if (line[0] == 'f') {    // Three quick blinks of the "L" light, to find which board this is.    noTone(BUZZER);    for (byte i = 0; i < 3; i++) {      digitalWrite(BUZZER, HIGH);      delay(150);      digitalWrite(BUZZER, LOW);      delay(150);    }    if (playing != NO_NOTE && !settings.muted) tone(BUZZER, NOTE_HZ[playing]);    Serial.println("Blinked");    return;  } else if (line[0] == 'm') {    settings.muted = !settings.muted;    if (settings.muted) noTone(BUZZER);    else if (playing != NO_NOTE) tone(BUZZER, NOTE_HZ[playing]);    if (playing == NO_NOTE) showIdle();  } else {    return;  }  EEPROM.put(0, settings);  Serial.print("Saved: contrast ");  Serial.print(settings.contrast);  Serial.print(", backlight ");  Serial.print(settings.backlight);  Serial.println(settings.muted ? ", muted" : ", sound on");}// ---------- Screen driver ----------// The screen takes letters 4 bits at a time on its D4-D7 pins. RS picks// between a command (LOW) and a letter (HIGH), and a pulse on E sends it.void lcdBegin() {  pinMode(LCD_RS, OUTPUT);  pinMode(LCD_E, OUTPUT);  for (byte i = 0; i < 4; i++) pinMode(LCD_DATA[i], OUTPUT);  digitalWrite(LCD_E, LOW);  digitalWrite(LCD_RS, LOW);  delay(50);  lcdWake();  lcdSend(0x01, false);  // clear the screen  delay(2);}// The screen's start-up routine. Repeating it before each update gets a// screen that had a glitch back in step, without clearing it.void lcdWake() {  digitalWrite(LCD_RS, LOW);  lcdNibble(0x3);  delay(5);  lcdNibble(0x3);  delayMicroseconds(150);  lcdNibble(0x3);  delayMicroseconds(150);  lcdNibble(0x2);  delayMicroseconds(150);  lcdSend(0x28, false);  // 2 lines of text  lcdSend(0x0C, false);  // screen on, no cursor  lcdSend(0x06, false);  // move right after each letter}void lcdNibble(byte bits) {  for (byte i = 0; i < 4; i++) digitalWrite(LCD_DATA[i], (bits >> i) & 1);  digitalWrite(LCD_E, HIGH);  delayMicroseconds(1);  digitalWrite(LCD_E, LOW);  delayMicroseconds(100);}void lcdSend(byte value, bool isLetter) {  digitalWrite(LCD_RS, isLetter ? HIGH : LOW);  lcdNibble(value >> 4);  lcdNibble(value & 0x0F);}// Writes a whole row: the text, then spaces to fill all 16 places.void lcdPrintRow(byte row, const char *text) {  lcdSend(0x80 | (row ? 0x40 : 0), false);  bool ended = false;  for (byte i = 0; i < 16; i++) {    if (text[i] == '\0') ended = true;    lcdSend(ended ? ' ' : text[i], true);  }}

Related projects