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

Wiring map: Photon Theremin module connections to the Arduino.
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

  1. 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.
  2. 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.
  3. 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.
  4. 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 PluggableUSBMIDI class built in.
  5. 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.
  6. Jam. Close Serial, connect powered speakers, and sweep your hand to play. Flip the MIDI_ENABLE constant 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.