Photon Theremin
A VL53L0X time-of-flight sensor measures hand distance from 30 mm to about 1.2 m. We map that reading to both pitch and filter sweep, driving a piezo buzzer (for quick tests) or a 3.5 mm jack feeding a powered speaker. Optional MIDI-over-USB lets you pipe the data into a DAW.
Bill of Materials
- Arduino Micro or Leonardo (native USB for MIDI option)
- VL53L0X distance sensor breakout
- Piezo buzzer (active) or small 8 Ω speaker + simple RC low-pass
- 10 kΩ potentiometer (for scale shift)
- 3.5 mm TRS jack (optional line out)
- Enclosure + acrylic window for the ToF sensor
Wiring Map
| Connection | Arduino | Notes |
|---|---|---|
| VL53L0X VIN | 5V | Module regulates down to 2.8 V internally. |
| VL53L0X GND | GND | Keep wiring short; ToF sensors hate noise. |
| VL53L0X SDA | SDA (D2) | On Micro/Leonardo, SDA = D2. |
| VL53L0X SCL | SCL (D3) | SCL = D3 on the Micro. |
| Buzzer + | D9 | Use a 100 Ω resistor in series for speakers. |
| Buzzer − | GND | Share ground with audio jack sleeve. |
| Pot wiper | A0 | Outer legs to 5V/GND. Adjusts musical scale. |
| 3.5 mm jack tip | D9 via 100 Ω + 10 µF | AC-couple for line-level output. |
Build Steps
- Mount the sensor. Place the VL53L0X behind a thin acrylic window flush with the enclosure so stray light doesn’t leak in. If you use standoffs, keep them non-metallic to avoid reflections.
- Stage audio. For the quickest test, wire a piezo directly to D9/GND. For higher fidelity, route D9 through a 100 Ω resistor, then a 10 µF capacitor into the tip of the 3.5 mm jack, with sleeve to ground.
- Add the pitch potentiometer. Mount the 10 kΩ potentiometer on the front panel; route the wiper to A0. This lets you decide whether the theremin plays a minor pentatonic, major, or chromatic scale.
- Install libraries. In Library Manager search for “Adafruit VL53L0X” and install it (it pulls in Adafruit Unified Sensor automatically). On a Micro/Leonardo you already have the
PluggableUSBMIDIclass built in. - Calibrate distance. Upload the sketch and open Serial Plotter. Move your hand from 5 cm to 50 cm to confirm smooth readings. If you see
-1, the sensor is saturating; tilt it slightly or extend the hood. - Jam. Close Serial, connect powered speakers, and sweep your hand to play. Flip the
MIDI_ENABLEconstant if you want the board to enumerate as a MIDI synth instead of using the buzzer.
Sketch
#include <Wire.h>
#include <Adafruit_VL53L0X.h>
#ifdef USBCON
#include <MIDIUSB.h>
#endif
constexpr bool MIDI_ENABLE = false;
constexpr uint8_t AUDIO_PIN = 9;
constexpr uint8_t SCALE_POT = A0;
constexpr uint16_t MIN_MM = 40;
constexpr uint16_t MAX_MM = 600;
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
uint16_t currentNote = 0;
const uint8_t MAJOR_SCALE[] = {0, 2, 4, 5, 7, 9, 11};
const uint8_t MINOR_PENTA[] = {0, 3, 5, 7, 10};
const uint8_t CHROMATIC[] = {0,1,2,3,4,5,6,7,8,9,10,11};
void setup() {
pinMode(AUDIO_PIN, OUTPUT);
analogWrite(AUDIO_PIN, 0);
Wire.begin();
if (!lox.begin()) {
while (true) {
tone(AUDIO_PIN, 220, 200);
delay(400);
}
}
}
void loop() {
VL53L0X_RangingMeasurementData_t measure;
lox.rangingTest(&measure, false);
if (measure.RangeStatus != 4) {
uint16_t dist = constrain(measure.RangeMilliMeter, MIN_MM, MAX_MM);
float norm = 1.0 - ((dist - MIN_MM) / float(MAX_MM - MIN_MM));
uint16_t freq = mapFrequency(norm);
if (!MIDI_ENABLE) {
tone(AUDIO_PIN, freq, 10);
} else {
#ifdef USBCON
if (freq != currentNote) {
if (currentNote) midiNoteOff(currentNote);
midiNoteOn(freq);
currentNote = freq;
}
#endif
}
}
delay(5);
}
uint16_t mapFrequency(float ratio) {
ratio = constrain(ratio, 0.0, 1.0);
int scaleChoice = map(analogRead(SCALE_POT), 0, 1023, 0, 2);
const uint8_t* scale;
size_t scaleLen;
switch (scaleChoice) {
case 0: scale = MINOR_PENTA; scaleLen = sizeof(MINOR_PENTA); break;
case 1: scale = MAJOR_SCALE; scaleLen = sizeof(MAJOR_SCALE); break;
default: scale = CHROMATIC; scaleLen = sizeof(CHROMATIC); break;
}
float noteSpan = ratio * (scaleLen * 5); // 5 octaves of fun
int index = int(noteSpan) % scaleLen;
int octave = int(noteSpan) / int(scaleLen);
int midiNote = 48 + scale[index] + octave * 12; // base C3
return midiToFreq(midiNote);
}
uint16_t midiToFreq(int note) {
return uint16_t(440.0 * powf(2.0, (note - 69) / 12.0));
}
#ifdef USBCON
void midiNoteOn(int note) {
midiEventPacket_t noteOn = {0x09, 0x90, (uint8_t)note, 0x64};
MidiUSB.sendMIDI(noteOn);
MidiUSB.flush();
}
void midiNoteOff(int note) {
midiEventPacket_t noteOff = {0x08, 0x80, (uint8_t)note, 0x00};
MidiUSB.sendMIDI(noteOff);
MidiUSB.flush();
}
#endif
Play Modes
Use the potentiometer to slide between scales mid-performance. For example, dial fully counter-clockwise for moody minor pentatonic drones, center for a bright major scale, and clockwise for chromatic sweeps.
Troubleshooting. Flickering or jittery distance readings usually mean the sensor is too close to reflective surfaces. Add a simple cardboard hood or change the polling rate (
delay(5)) to delay(20) to give the sensor time to settle. If MIDI mode appears silent, check that your OS enumerated “Arduino Micro MIDI” and that your DAW armed the track.