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

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

  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.