Breadboard ultrasonic theremin build with ESP32-C3 SuperMini, HC-SR04 ultrasonic sensor, and buzzer, hand hovering over sensor

Beginner

Build an Ultrasonic Theremin

Sensors

Wave a hand closer or farther from a sensor and hear the pitch change in real time, a real musical instrument built entirely from a distance sensor and a buzzer, almost pure software creativity.

3 real parts included

Total for this kit: 1,050.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

Named after the classic hands-free electronic instrument, this project turns hand distance into pitch: move a hand closer to the sensor and the tone rises, move it away and the tone falls. It uses only two parts, and the entire instrument is really just one line of math.

What the Controller Does

It continuously reads the current distance, maps that distance onto a musical pitch range, and drives the buzzer at that frequency, dozens of times a second, so pitch changes feel smooth and responsive rather than stepped.

What the Ultrasonic Sensor Does

It measures the distance to whatever is closest in front of it, typically a hand, dozens of times per second, giving the controller a continuously updating input to translate into sound.

What the Buzzer Does

It plays whatever frequency the controller sends it, and that's the entire instrument: no strings, no keys, just a tone that tracks hand position in real time.

Software and Library Concepts

The core of this project is a single mapping function: take a distance reading within a chosen range, say 5 to 40 centimeters, and scale it linearly onto a pitch range, say 200 to 2000 Hz. Everything else is just reading the sensor and calling that function in a loop.

Testing and Troubleshooting

  • The pitch jumps around erratically, ultrasonic readings can be noisy at very close range, average the last few readings together rather than reacting to every single one instantly.
  • The playable range feels too narrow or too wide, adjust the minimum and maximum distance values in the mapping function until the gesture feels natural for your own arm's reach.
  • No sound at very close or very far distances, that's expected, clamp readings outside your chosen range to silence rather than letting them produce an out-of-range tone.

Optional Upgrade

Add a second ultrasonic sensor controlling volume instead of pitch, closer to true theremin behavior, one hand for pitch, the other for volume.

Wiring

  • HC-SR04 VCC → ESP32-C3 5V
  • HC-SR04 GND → ESP32-C3 GND
  • HC-SR04 TRIG → ESP32-C3 GPIO2 (output)
  • HC-SR04 ECHO → 1kΩ/2kΩ voltage divider → ESP32-C3 GPIO3 (input)
  • Buzzer signal → ESP32-C3 GPIO4
  • Buzzer GND → ESP32-C3 GND

Important: a true "active" buzzer has its own internal oscillator and can only be turned fully on or off with `digitalWrite` — it cannot play variable pitch, so it will not work as a theremin. This code drives the pitch electrically via `ledcWriteTone`, which requires a passive buzzer or bare piezo element in that slot instead of a fixed-tone active buzzer. Distance range is clamped to 2–40cm in this example; widen `MAX_DISTANCE_CM` for a bigger playing area.

Wiring Diagram

VCC → 5V GND TRIG → GPIO2 ECHO GPIO3 signal → GPIO4 GND ESP32-C3 HC-SR04 1kΩ/2kΩ voltage divider Buzzer

Starter Code

#include <Arduino.h>

// --- Pin definitions: adjust to match your ESP32-C3 SuperMini board if different ---
#define TRIG_PIN    2
#define ECHO_PIN    3   // through a 1k/2k voltage divider - HC-SR04 ECHO is 5V logic
#define BUZZER_PIN  4   // PASSIVE buzzer / piezo required - see note above

#define PWM_CHANNEL 0
#define PWM_RES_BITS 8

const int MIN_DISTANCE_CM = 2;
const int MAX_DISTANCE_CM = 40;
const int MIN_FREQ_HZ = 200;
const int MAX_FREQ_HZ = 2000;

long readDistanceCM() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 25000);
  if (duration == 0) return -1;
  return duration * 0.0343 / 2;
}

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  ledcSetup(PWM_CHANNEL, MIN_FREQ_HZ, PWM_RES_BITS);
  ledcAttachPin(BUZZER_PIN, PWM_CHANNEL);
}

void loop() {
  long distance = readDistanceCM();

  if (distance > 0) {
    long clamped = constrain(distance, MIN_DISTANCE_CM, MAX_DISTANCE_CM);
    // closer hand = higher pitch
    int freq = map(clamped, MIN_DISTANCE_CM, MAX_DISTANCE_CM, MAX_FREQ_HZ, MIN_FREQ_HZ);
    ledcWriteTone(PWM_CHANNEL, freq);
  } else {
    ledcWriteTone(PWM_CHANNEL, 0); // out of range - silence
  }

  delay(30);
}

Not sure about a step? Ask us