6.1 Musical Light Theremin
Build your own magical musical instrument that plays music by waving your hand in the air! No buttons, no keys - just pure hand gestures creating beautiful melodies.
What’s a Theremin? It’s a mysterious electronic instrument played without touching it. Move your hand closer or farther from the sensor, and it plays different musical notes - like conducting an invisible orchestra!
Our version: Instead of using expensive antennas like real theremins, we’ll use a simple light sensor. Cover the sensor with your hand for low notes, move away for high notes. It’s like having a light-controlled piano that responds to shadows and brightness!
The magic: This project combines light sensing, sound generation, and musical scales to create an instrument that’s both educational and incredibly fun to play.
Component List
Raspberry Pi Pico W x1
MicroUSB cable x1
830 Tie-Points Breadboard x1
LED x1
Transistor S8050 x1
Resistor 220Ω, 1kΩ, 10kΩ x1
Passive Buzzer x1
Photoresistor x1
Jumper Wire Several
How it works - The Science Made Simple:
🔧 The Setup: - Light Sensor (GP28): Detects how much light hits it - like a digital eye! - Buzzer (GP15): Makes musical sounds when powered through the transistor - Status LED (GP16): Shows when the system is calibrating (learning your playing style) - Transistor S8050: Acts like an electronic switch to control the buzzer volume
🎵 The Magic Formula: - Hand closer = Less light hits sensor = Lower musical notes - Hand farther = More light hits sensor = Higher musical notes - No hand = Maximum light = Highest pitch
🎯 Calibration Process: When you first run the program, the LED lights up for 5 seconds. During this time, wave your hand over the sensor from very close to very far. This teaches the system your “playing range” so it can map your movements to the full musical scale.
Connect
Code
Note
Open the
6.1_musical_light_theremin.pyfile under the path ofUltimate-Starter-Kit-for-Pico-W\Python\1.Projector copy this code into Thonny, then click “Run Current Script” or simply press F5 to run it.Don’t forget to click on the “MicroPython (Raspberry Pi Pico)” interpreter in the bottom right corner.
After running the code, get ready for your musical adventure in 3 easy steps:
🚀 Step 1: Automatic Calibration (5 seconds) - The LED lights up - calibration has started! - Wave your hand over the sensor like you’re conducting an orchestra - Move from very close (almost touching) to very far (arm’s length away) - This teaches your theremin your personal playing style
🎵 Step 2: Listen for the Welcome Melody - After 5 seconds, the LED turns off - You’ll hear a beautiful startup melody - this means your theremin is ready! - The system has learned your hand movement range
🎼 Step 3: Start Playing! - Hover your hand over the light sensor - Move closer for deeper, bass notes 🎶 - Move away for higher, treble notes 🎵 - Experiment with different heights and speeds - Try creating melodies by moving your hand smoothly up and down!
🎯 Pro Playing Tips: - Start with slow, gentle movements to hear the note changes clearly - The system uses a pentatonic scale (like traditional Asian music) - every note sounds good together! - Try “playing” simple songs like “Twinkle Twinkle Little Star” by moving your hand to different heights
The following is the program code:
"""
Musical Light Theremin v2.0
Enhanced with standard musical notes and improved user experience
Based on the tutorial:
https://sg.cytron.io/tutorial/build-a-light-theremin-using-maker-pi-pico-and-circuitpython
Key improvements over original:
- Uses pentatonic scale for more pleasant sounds
- Enhanced with proper exception handling
- Simplified user interface (no buttons required)
- Added startup melody and status feedback
- Safe buzzer shutdown when program stops
Hardware Requirements:
- Raspberry Pi Pico or compatible board
- Photoresistor on ADC pin 28
- Buzzer with PWM support on pin 15
- Status LED on pin 16
"""
import machine
import utime
# Hardware pin definitions
LED_PIN = 16 # Status LED pin
PHOTORESISTOR_PIN = 28 # Light sensor ADC pin
BUZZER_PIN = 15 # PWM buzzer pin
# Calibration timing constants
CALIBRATION_TIME_MS = 5000 # 5 seconds for light range calibration
INITIAL_LIGHT_LOW = 65535 # Maximum ADC value (16-bit) - starting point
INITIAL_LIGHT_HIGH = 0 # Minimum ADC value - starting point
# Audio configuration constants
BUZZER_DUTY_CYCLE = 32768 # 50% duty cycle for optimal sound quality
DEFAULT_NOTE_DURATION_MS = 100 # Default note length in milliseconds
LOOP_DELAY_MS = 10 # Main loop delay for responsiveness
# Musical note frequencies (in Hz) - Based on A4 = 440Hz standard tuning
NOTE_FREQUENCIES = {
'C3': 131, 'D3': 147, 'E3': 165, 'F3': 175, 'G3': 196, 'A3': 220, 'B3': 247,
'C4': 262, 'D4': 294, 'E4': 330, 'F4': 349, 'G4': 392, 'A4': 440, 'B4': 494,
'C5': 523, 'D5': 587, 'E5': 659, 'F5': 698, 'G5': 784, 'A5': 880, 'B5': 988,
'C6': 1047, 'D6': 1175, 'E6': 1319, 'F6': 1397, 'G6': 1568, 'A6': 1760
}
# Musical scale for theremin - Pentatonic + Major scale for pleasant harmonics
THEREMIN_NOTES = [
# Pentatonic scale (naturally harmonious - avoids dissonant half-steps)
'C3', 'D3', 'E3', 'G3', 'A3', # Lower octave pentatonic
'C4', 'D4', 'E4', 'G4', 'A4', # Middle octave pentatonic
'C5', 'D5', 'E5', 'G5', 'A5', # Higher octave pentatonic
# Major scale notes for additional variety
'F3', 'B3', 'F4', 'B4', 'F5', 'B5',
# Extended high range for full musical expression
'C6', 'D6', 'E6', 'G6', 'A6'
]
TOTAL_NOTES = len(THEREMIN_NOTES) # Total number of available musical notes
# Initialize hardware components
led = machine.Pin(LED_PIN, machine.Pin.OUT) # Status indicator LED
photoresistor = machine.ADC(PHOTORESISTOR_PIN) # Light level sensor
buzzer = machine.PWM(machine.Pin(BUZZER_PIN)) # PWM-controlled buzzer
# System state variables for operation
light_range_min = INITIAL_LIGHT_LOW # Minimum light value from calibration
light_range_max = INITIAL_LIGHT_HIGH # Maximum light value from calibration
note_duration_ms = DEFAULT_NOTE_DURATION_MS # Current note duration setting
def map_to_note_index(light_value, min_light, max_light):
"""
Map light sensor value to a discrete note index
Args:
light_value: Current ADC reading from photoresistor
min_light: Minimum light value from calibration
max_light: Maximum light value from calibration
Returns:
Integer index (0 to TOTAL_NOTES-1) for note selection
"""
if max_light <= min_light:
return 0
# Clamp light value to calibrated range to prevent index overflow
light_value = max(min_light, min(light_value, max_light))
# Normalize to 0-1 range, then map to discrete note indices
normalized = (light_value - min_light) / (max_light - min_light)
note_index = int(normalized * (TOTAL_NOTES - 1))
return max(0, min(note_index, TOTAL_NOTES - 1))
def play_musical_note(note_name, duration_ms):
"""
Play a musical note by name for specified duration
Args:
note_name: String name of note (e.g., 'C4', 'A5')
duration_ms: How long to play the note in milliseconds
"""
try:
if note_name in NOTE_FREQUENCIES:
frequency = NOTE_FREQUENCIES[note_name]
buzzer.freq(frequency)
buzzer.duty_u16(BUZZER_DUTY_CYCLE)
utime.sleep_ms(duration_ms)
buzzer.duty_u16(0) # Turn off buzzer after note
else:
# Silent pause for unknown notes
utime.sleep_ms(duration_ms)
except:
# Ensure buzzer stops on any error to prevent continuous noise
buzzer.duty_u16(0)
def stop_buzzer():
"""
Safely stop the buzzer and release PWM resources
Called when program terminates to ensure clean shutdown
"""
try:
buzzer.duty_u16(0) # Set duty cycle to 0 (silent)
buzzer.deinit() # Release PWM hardware resource
except:
pass # Ignore errors during cleanup
def get_current_note(light_value):
"""
Get the current musical note based on light sensor reading
Args:
light_value: Current ADC reading from photoresistor
Returns:
String name of the musical note to play
"""
note_index = map_to_note_index(light_value, light_range_min, light_range_max)
return THEREMIN_NOTES[note_index]
def read_light_sensor():
"""
Read current light level from photoresistor
Returns:
Integer ADC value (0-65535 for 16-bit ADC)
"""
return photoresistor.read_u16()
def calibrate_light_sensor():
"""
Calibrate light sensor by recording min/max light values over calibration period
User should create varied lighting conditions during this time for optimal range
Updates global variables:
light_range_min: Minimum light value detected
light_range_max: Maximum light value detected
"""
global light_range_min, light_range_max
print("=== Starting Light Sensor Calibration ===")
print("Wave your hand over the sensor for 5 seconds...")
print("Create bright and dark conditions for best musical range")
# Reset calibration values to starting extremes
light_range_min = INITIAL_LIGHT_LOW
light_range_max = INITIAL_LIGHT_HIGH
# Run calibration for specified time with visual feedback
start_time = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start_time) < CALIBRATION_TIME_MS:
current_light = read_light_sensor()
# Track minimum and maximum light values encountered
if current_light > light_range_max:
light_range_max = current_light
if current_light < light_range_min:
light_range_min = current_light
# Provide visual feedback during calibration (LED blinks every 250ms)
led.value((utime.ticks_ms() // 250) % 2)
utime.sleep_ms(50)
led.value(0) # Turn off LED after calibration complete
# Display calibration results
print(f"Calibration complete!")
print(f"Light range detected: {light_range_min} - {light_range_max}")
print(f"Musical notes available: {TOTAL_NOTES} (Pentatonic + Major scale)")
print("Theremin ready - wave your hand to play beautiful music!")
def play_startup_melody():
"""
Play a pleasant pentatonic melody to indicate system initialization complete
Uses ascending pentatonic scale for harmonious startup sound
"""
startup_notes = ['C4', 'D4', 'E4', 'G4', 'A4', 'C5'] # Rising pentatonic melody
for note in startup_notes:
play_musical_note(note, 180) # Play each note for 180ms
utime.sleep_ms(50) # Brief pause between notes
# === System Initialization ===
print("=== Musical Light Theremin v2.0 ===")
print("Enhanced with standard musical notes!")
# Perform light sensor calibration to establish musical range
calibrate_light_sensor()
# Play welcome melody to confirm system ready
play_startup_melody()
print("\n=== Theremin Ready ===")
print("Simply wave your hand over the sensor to play beautiful music!")
print("Each light level corresponds to a different musical note")
# === Main Control Loop ===
# Track current and previous notes to provide smooth playback
current_note = None
last_note = None
try:
while True:
# Read current light level and determine corresponding musical note
current_light = read_light_sensor()
current_note = get_current_note(current_light)
# Only play new note if it changed (prevents continuous note restart)
if current_note != last_note:
print(f"Playing: {current_note} (Light: {current_light})")
play_musical_note(current_note, note_duration_ms)
last_note = current_note
else:
# Continue current note with shorter duration for smooth sound
play_musical_note(current_note, note_duration_ms // 2)
# Brief delay for system responsiveness and note timing
utime.sleep_ms(LOOP_DELAY_MS)
except KeyboardInterrupt:
# Handle Ctrl+C or Stop button gracefully
print("\nTheremin stopped by user")
except Exception as e:
# Handle any unexpected errors
print(f"\nError occurred: {e}")
finally:
# Critical: Always stop buzzer when program ends to prevent continuous noise
print("Stopping buzzer...")
stop_buzzer()
led.value(0) # Turn off status LED
print("Theremin safely stopped")