Built-in project · Arduino Nano

Ultrasonic theremin on an Arduino Nano

Ultrasonic theremin on an Arduino Nano: wave a hand over an HC-SR04 to play notes of a pentatonic scale, or glide the pitch like a real theremin. The whole build, an Arduino Nano and 3 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 Runs in your browser. Free, and no account needed.

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

Ultrasonic theremin · Arduino Nanolive0.000 s 0.00x
Press Run, then drag the HC-SR04 distance between 5 and 50 cm
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

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 Nano
// Ultrasonic theremin: wave your hand over an HC-SR04 and a piezo plays a
// note for how far away it is. Closer is higher. On an Arduino Nano.
//
//   HC-SR04   TRIG -> D11, ECHO -> D10, VCC -> 5V, GND -> GND
//   piezo     D5 and GND
//   button    D2 and GND (INPUT_PULLUP): switches between the two modes
//
// Two ways to play:
//
//   scale   the hand picks one of eleven notes of the C major pentatonic
//           scale (C D E G A), two octaves from C5 to C7. Any notes from
//           this scale sound good together, so anything you play is a tune.
//   glide   the pitch slides smoothly with the hand, like a real theremin,
//           from 523 Hz at 50 cm to 2093 Hz at 5 cm.
//
// Farther than 50 cm (or nothing there at all) is silence, so take your hand
// away to stop.
//
// # Smoothing the readings
//
// An ultrasonic reading jumps around a little, and a hand is never quite
// still. The sketch keeps the last three readings and plays their median,
// the middle one, which throws away a single bad reading without lagging
// behind a hand that really moved.

const uint8_t TRIG = 11;
const uint8_t ECHO = 10;
const uint8_t PIEZO = 5;
const uint8_t BUTTON = 2;

const int NEAR_CM = 5;
const int FAR_CM = 50;

// C major pentatonic, C5 to C7.
const unsigned int SCALE[11] = {523, 587, 659, 784, 880, 1047, 1175, 1319, 1568, 1760, 2093};
const char *const NOTE_NAME[11] = {"C5", "D5", "E5", "G5", "A5", "C6", "D6", "E6", "G6", "A6", "C7"};

bool glide = false;
bool lastButton = HIGH;
int readings[3] = {999, 999, 999};
uint8_t next = 0;
int lastNote = -1;

int measureCm() {
  digitalWrite(TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  // Give up after 6 ms: past about a meter, nobody is playing.
  unsigned long us = pulseIn(ECHO, HIGH, 6000UL);
  if (us == 0) return 999;
  return us / 58;
}

int median3(int a, int b, int c) {
  if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
  if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
  return c;
}

void setup() {
  Serial.begin(115200);
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  pinMode(PIEZO, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.println("Ultrasonic theremin: scale mode. Hand 5 to 50 cm above the sensor.");
}

void loop() {
  bool button = digitalRead(BUTTON);
  if (button == LOW && lastButton == HIGH) {
    glide = !glide;
    lastNote = -1;
    Serial.println(glide ? "glide mode" : "scale mode");
    delay(30);  // ride out the contact bounce
  }
  lastButton = button;

  readings[next] = measureCm();
  next = (next + 1) % 3;
  int cm = median3(readings[0], readings[1], readings[2]);

  if (cm > FAR_CM) {
    noTone(PIEZO);
    if (lastNote != -2) Serial.println("silence");
    lastNote = -2;
  } else {
    if (cm < NEAR_CM) cm = NEAR_CM;
    if (glide) {
      // Two octaves over the range: each 22.5 cm closer doubles the pitch.
      float octaves = 2.0 * (FAR_CM - cm) / (FAR_CM - NEAR_CM);
      unsigned int hz = 523.25 * pow(2.0, octaves);
      tone(PIEZO, hz);
      lastNote = -1;
    } else {
      int note = map(cm, FAR_CM, NEAR_CM, 0, 10);
      tone(PIEZO, SCALE[note]);
      if (note != lastNote) {
        Serial.print(NOTE_NAME[note]);
        Serial.print(" (");
        Serial.print(SCALE[note]);
        Serial.print(" Hz) at ");
        Serial.print(cm);
        Serial.println(" cm");
        lastNote = note;
      }
    }
  }
  delay(30);  // let the last ping's echoes die away before the next one
}

Parts list

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

How it is wired

8 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Nano pin 2; Arcade button pin 2
  • Arduino Nano pin 5; Piezo speaker pin 1
  • Arduino Nano pin 10; Ultrasonic ranger pin ECHO
  • Arduino Nano pin 11; Ultrasonic ranger pin TRIG
  • 5 V: Ultrasonic ranger pin VCC
  • Ground: Ultrasonic ranger pin GND
  • Ground: Arcade button pin 1
  • Ground: Piezo speaker pin 2

Change it and keep it

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