
What you'll build
This guide walks you through creating a laser harp synthesizer with an ESP32, a set of light-dependent resistors arranged in a row, a piezo speaker, and an OLED display that visualizes which notes are playing. Each LDR sits beneath a beam of light -- from small laser diodes or bright LEDs mounted opposite -- and when you break a beam with your hand, the ESP32 detects the drop in light level and triggers the corresponding musical note through the speaker. Multiple beams can be broken simultaneously to play chords, and the OLED renders a real-time staff notation showing which notes are active.
Building a laser harp teaches you analog sensor reading and calibration on the ESP32's ADC channels, threshold-based event detection that adapts to ambient lighting conditions, tone generation using the ESP32's LEDC PWM peripheral, and simultaneous multi-channel output. You will calibrate each LDR at startup to establish baseline light levels, implement hysteresis to prevent flickering triggers near the threshold, and generate musically accurate frequencies for a full octave of notes. The project also introduces basic concepts of digital audio -- frequency ratios between semitones, square-wave harmonics, and how duty cycle affects timbre.
When complete you will have a playable musical instrument that feels theatrical and impressive despite its simple construction. The code is organized so you can swap the pentatonic scale for any scale you like, add octave shifting with a dedicated button, or replace the piezo with an I2S DAC and amplifier for richer sound. Pair it with the mood lamp project to add synchronized LED animations that respond to the notes being played, creating a full audio-visual performance piece that is perfect for maker faires and classroom demonstrations.
Wiring diagram
Wiring diagram
Components needed
Assembly
Build the LDR voltage dividers
For each LDR, create a voltage divider with a 10 kΩ pull-down resistor. Connect the middle point to GPIO34, GPIO35, and GPIO32. Power each divider from 3.3V.
- Use identical resistor values for all three channels so calibration is consistent.
- GPIO34, 35 are input-only on the ESP32 — this is fine for analog reads.
Set up the light sources
Mount three laser diodes or bright LEDs opposite the LDRs. Align each beam to hit its corresponding sensor directly.
- Use a cardboard frame with holes to keep beams aligned.
- Never look directly into laser beams.
Connect speaker and display
Wire piezo speaker signal to GPIO25 (other lead to GND). Connect OLED VCC to 3.3V, GND to ground, SDA to GPIO21, SCL to GPIO22.
- A small amplifier module between the ESP32 and speaker dramatically improves sound quality.
Calibrate and play
Flash the sketch. The harp auto-calibrates on startup — keep all beams unbroken during the first 2 seconds. Then wave your hand through the beams to play C5, E5, and G5.
- Adjust the HYSTERESIS value in code if the sensors trigger too easily in bright rooms.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 3V3 | laser-ldr-1 VCC | POWER |
| GND | laser-ldr-1 GND | GROUND |
| GPIO 34 | laser-ldr-1 LDR1 | ANALOG |
| GPIO 35 | laser-ldr-1 LDR2 | ANALOG |
| GPIO 32 | laser-ldr-1 LDR3 | ANALOG |
| GND | laser-speaker-1 GND | GROUND |
| GPIO 25 | laser-speaker-1 SIG | PWM |
| 3V3 | laser-oled-1 VCC | POWER |
| GND | laser-oled-1 GND | GROUND |
| GPIO 21 | laser-oled-1 SDA | I2C |
| GPIO 22 | laser-oled-1 SCL | I2C |
Code
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#define LDR_1 34
#define LDR_2 35
#define LDR_3 32
#define BUZZER_PIN 25
#define SDA_PIN 21
#define SCL_PIN 22
Adafruit_SSD1306 display(128, 64, &Wire, -1);
int baseline1 = 0, baseline2 = 0, baseline3 = 0;
const int HYSTERESIS = 200;
const char* noteNames[] = {"C5", "E5", "G5"};
const int noteFreqs[] = {523, 659, 784};
void calibrate() {
long sum1 = 0, sum2 = 0, sum3 = 0;
for (int i = 0; i < 20; i++) {
sum1 += analogRead(LDR_1);
sum2 += analogRead(LDR_2);
sum3 += analogRead(LDR_3);
delay(10);
}
baseline1 = sum1 / 20;
baseline2 = sum2 / 20;
baseline3 = sum3 / 20;
}
void setup() {
Serial.begin(115200);
delay(100);
Wire.begin(SDA_PIN, SCL_PIN);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
pinMode(BUZZER_PIN, OUTPUT);
display.clearDisplay();
display.setCursor(0, 24);
display.println("Calibrating...");
display.display();
calibrate();
Serial.printf("Baselines: %d %d %d\n", baseline1, baseline2, baseline3);
}
void loop() {
int n1 = analogRead(LDR_1);
int n2 = analogRead(LDR_2);
int n3 = analogRead(LDR_3);
bool beams[] = {
n1 < baseline1 - HYSTERESIS,
n2 < baseline2 - HYSTERESIS,
n3 < baseline3 - HYSTERESIS,
};
int activeNote = -1;
for (int i = 0; i < 3; i++) {
if (beams[i]) { activeNote = i; break; }
}
if (activeNote >= 0) {
tone(BUZZER_PIN, noteFreqs[activeNote], 40);
} else {
noTone(BUZZER_PIN);
}
display.clearDisplay();
display.setCursor(0, 0);
display.println("-- Laser Harp --");
display.println();
for (int i = 0; i < 3; i++) {
display.printf("L%d: %4d %s %s\n", i + 1,
i == 0 ? n1 : i == 1 ? n2 : n3,
beams[i] ? ">>>" : " ",
beams[i] ? noteNames[i] : ""
);
}
display.println();
if (activeNote >= 0) {
display.printf("Playing: %s (%dHz)", noteNames[activeNote], noteFreqs[activeNote]);
} else {
display.println("No beam broken");
}
display.display();
delay(20);
}
// Run this and build other cool things at schematik.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Laser Harp Synth.
Open in Schematik →