Creadores: Rodward Gabriel Mago Ayerde y Gabriel Jaimes kogan
Objetivo de proyecto:
Este proyecto consiste en la creación de un instrumento musical electrónico sin contacto basado en el principio del Theremin, pero utilizando tecnología moderna de sensores de tiempo de vuelo ToF y el uso de un altavoz conctado al MAX98357A. El sistema utiliza un microcontrolador ESP32-S3 para procesar datos de distancia en tiempo real y convertirlos en señales de audio mediante síntesis digital. Todo controlado mediante Mu con CircuitPython.
Funcionamiento Técnico:
El instrumento utiliza dos sensores láser VL53L0X que miden la distancia exacta de las manos del intérprete:
Mano Derecha (Control de Tono): La distancia se mapea para cambiar la frecuencia de la nota. Cuanto más cerca está la mano, más agudo es el sonido (rango de aproximadamente 130Hz a 1100Hz).
Mano Izquierda (Control de Vibrato): En lugar de controlar solo el volumen, este sensor añade una capa de expresión musical llamada vibrato. Al acercar la mano, la frecuencia oscila rápidamente, emulando la técnica de un cantante de ópera o un violinista.
Componentes Principales:
Placa:ESP32-S3 (Dual-core, optimizado para tareas de audio y sensores).
Sensores: 2x VL53L0X (Tecnología láser infrarroja de alta precisión).
Audio: Amplificador I2S (MAX98357A) y altavoz (Salida de audio digital de alta fidelidad).
Otros: Caja de plastico, protoboard, cable USB-microUSB, cables,...
Codigo, Conecciones y descripcion
import board
import busio
import audiobusio
import synthio
import adafruit_vl53l0x
import time
import array
import math
# --- 1. SENSORES ---
i2c_tono = busio.I2C(board.IO13, board.IO14)
i2c_vol = busio.I2C(board.IO5, board.IO4)
tof_t = tof_v = None
try:
tof_t = adafruit_vl53l0x.VL53L0X(i2c_tono)
tof_t.measurement_timing_budget = 20000
print("✅ Tono (Derecha) OK")
except: print("❌ Error Tono")
try:
tof_v = adafruit_vl53l0x.VL53L0X(i2c_vol)
tof_v.measurement_timing_budget = 20000
print("✅ Vibrato (Izquierda) OK")
except: print("❌ Error Vibrato")
# --- 2. AUDIO ---
audio = audiobusio.I2SOut(board.IO1, board.IO2, board.IO3)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)
# Onda Triangular
samples = 512
waveform_triangle = array.array('h', [0] * samples)
for i in range(samples):
if i < samples // 2: waveform_triangle[i] = -32767 + (i * 128 * 2)
else: waveform_triangle[i] = 32767 - ((i - samples // 2) * 128 * 2)
nota = synthio.Note(frequency=440, amplitude=0.4, waveform=waveform_triangle)
synth.press(nota)
# Variables de control
freq_base = 440
vibrato_intensidad = 0.0
vibrato_index = 0.0
while True:
# --- MANO DERECHA (TONO) ---
if tof_t:
d1 = tof_t.range
if 40 < d1 < 500:
target_f = 1100 - (d1 * 2)
freq_base = (freq_base * 0.8) + (target_f * 0.2)
nota.amplitude = 0.4
else:
nota.amplitude = 0.0
# --- MANO IZQUIERDA (VIBRATO EXAGERADO) ---
if tof_v:
d2 = tof_v.range
# Si la mano está entre 4cm y 40cm
if 40 < d2 < 400:
# Mapeo: 0.0 (lejos) a 1.0 (cerca)
vibrato_intensidad = 1.0 - ((d2 - 40) / 360)
vibrato_intensidad = max(0.0, min(vibrato_intensidad, 1.0))
else:
vibrato_intensidad = 0.0
# --- APLICAR EFECTO ---
vibrato_index += 0.5 # Velocidad del temblor
# Multiplicamos por 60 para que la nota salte hasta 60Hz arriba y abajo (muy notable)
oscilacion = math.sin(vibrato_index) * (vibrato_intensidad * 60)
nota.frequency = max(100, freq_base + oscilacion)
# MONITOR (Para ver si el sensor funciona)
print(f"Nota: {int(freq_base)}Hz | Vibrato: {int(vibrato_intensidad*100)}% ", end="\r")
time.sleep(0.01)
Conecciones:
Componente
Pin del Sensor
Pin del Microcontrolador
Función
Sensor Tono (Derecha)
VCC
3.3V
Alimentación
Sensor Tono (Derecha)
GND
GND
Tierra
Sensor Tono (Derecha)
SCL
IO13
Reloj I2C
Sensor Tono (Derecha)
SDA
IO14
Datos I2C
Sensor Tono (Derecha)
XSHUT
3.3V
Siempre encendido
Sensor Vibrato (Izquierda)
VCC
3.3V
Alimentación
Sensor Vibrato (Izquierda)
GND
GND
Tierra
Sensor Vibrato (Izquierda)
SCL
IO5
Reloj I2C
Sensor Vibrato (Izquierda)
SDA
IO4
Datos I2C
Sensor Vibrato (Izquierda)
XSHUT
3.3V
Siempre encendido
Audio I2S
BCK
IO1
Bit Clock
Audio I2S
WS / LRC
IO2
Word Select
Audio I2S
DIN
IO3
Datos de audio
Explicaciones:
Este programa crea un theremin avanzado con dos sensores: uno controla el tono con la mano derecha y el otro controla el vibrato (temblor) con la mano izquierda.
Parte 1: Configuración de los sensores
Se crean dos buses I2C separados:
i2c_tono = busio.I2C(board.IO13, board.IO14) → para la mano derecha
i2c_vol = busio.I2C(board.IO5, board.IO4) → para la mano izquierda
Luego intenta conectar cada sensor:
tof_t = adafruit_vl53l0x.VL53L0X(i2c_tono) → Sensor de tono
tof_v = adafruit_vl53l0x.VL53L0X(i2c_vol) → Sensor de vibrato
También acelera la lectura con: tof_t.measurement_timing_budget = 20000
Parte 2: Audio y sonido
Crea la salida de audio: audio = audiobusio.I2SOut(board.IO1, board.IO2, board.IO3)
Genera una onda triangular personalizada para que suene más como un theremin clásico.
Luego crea la nota: nota = synthio.Note(frequency=440, amplitude=0.4, waveform=waveform_triangle)
Empezamos con probando los componentes individualmente viendo si funcionaban
Probamos el altavoz mediante conectando los cables del altavoz a los polos de una bateria
Miramos si funcioaba los ToF funcionaba con un simple codigo:
import board
import busio
import adafruit_vl53l0x
import time
# 1. Setup I2C (using your pins)
i2c = busio.I2C(board.IO47, board.IO21)
# 2. Initialize the sensor
tof = adafruit_vl53l0x.VL53L0X(i2c)
print("Sensor Ready!")
# 3. Main Loop
while True:
# Read distance in millimeters
mm = tof.range
# Print the result
print("Distance:", mm, "mm")
# Wait half a second so it doesn't spam the screen
time.sleep(0.5)
Las primeras líneas: cargan las herramientas necesarias.
i2c = ...: Conecta el sensor usando los pines IO47 e IO21.
tof = ...: Activa el sensor.
while True:: Repite forever (bucle infinito).
tof.range: Mide la distancia.
print(...): Muestra la distancia en la pantalla.
time.sleep(0.5): Espera 0.5 segundos antes de medir otra vez.
2. Un ToF + Altavoz
Conecciones:
MAX98357A Pin
ESP32-S3 Pin (IO)
Descripción
Vin
5V (o 3.3V)
El 5V da más potencia al altavoz.
GND
GND
Tierra.
LRC (Left/Right Clock)
IO17
Word Select (WS).
BCLK (Bit Clock)
IO16
Bit Clock (BCK).
DIN (Data In)
IO18
Data Out del ESP32.
Gain / SD
No conectar
Configuración por defecto.
Codigo:
import board
import busio
import audiobusio
import synthio
import adafruit_vl53l0x
import time
# --- CONFIGURACIÓN I2C (Sensor ToF) ---
i2c = busio.I2C(board.IO47, board.IO21)
tof = adafruit_vl53l0x.VL53L0X(i2c)
# --- CONFIGURACIÓN I2S (Audio Amp) ---
# Bit Clock (BCK), Word Select (LRC), Data (DIN)
audio = audiobusio.I2SOut(board.IO16, board.IO17, board.IO18)
synth = synthio.Synthesizer(sample_rate=44100)
audio.play(synth)
# --- NOTA INICIAL ---
nota = synthio.Note(frequency=440, amplitude=0.1)
synth.press(nota)
print("🔊 Audio iniciado. Mueve la mano frente al sensor!")
while True:
dist = tof.range # Distancia en mm
# Mapeo: Si la mano está entre 50mm y 400mm
if 50 < dist < 400:
# Convertimos distancia en frecuencia (Hz)
# Cuanto más cerca (50), más agudo (800Hz)
# Cuanto más lejos (400), más grave (200Hz)
frecuencia = 800 - ((dist - 50) * 2)
nota.frequency = max(200, min(frecuencia, 800))
nota.amplitude = 0.2 # Volumen moderado
else:
nota.amplitude = 0.0 # Silencio si está muy lejos
time.sleep(0.01) # Respuesta rápida
Lee la distancia con el sensor láser.
Si detecta tu mano (entre 5 cm y 40 cm):
Cambia el tono: más cerca = más agudo
Si no detecta mano → silencio.
Suena por el amplificador conectado por I2S.
3. Dos ToF (volument y tono) y Altavoz
Primero escaneamos las conecciones si esta bien conectado:
import board
import busio
import digitalio
import time
# 1. Setup XSHUT for the sensor that HAS the cable
# (Make sure this matches the IO pin you plugged it into!)
xshut = digitalio.DigitalInOut(board.IO10)
xshut.direction = digitalio.Direction.OUTPUT
xshut.value = True # Wake it up!
# 2. Setup I2C (Using the "Safe" pins from before)
i2c = busio.I2C(board.IO9, board.IO8)
print("Scanning I2C Bus...")
while True:
if i2c.try_lock():
devices = i2c.scan()
i2c.unlock()
if devices:
print("Detected addresses:", [hex(d) for d in devices])
else:
print("No sensors found. Check SDA/SCL cables.")
time.sleep(1)
Donde solo aparecia 0x30
Intentamos cambiar el codigo y las conecciones ya que anteriormente habia un time of flight que no estaba conectado a un xshut:
import board
import busio
import audiobusio
import synthio
import adafruit_vl53l0x
import time
# --- 1. CONFIGURACIÓN DE HARDWARE ---
# Intentamos abrir el bus I2C una sola vez
try:
i2c = busio.I2C(board.IO5, board.IO4)
except Exception:
print("Error crítico: No se puede iniciar el bus I2C")
# --- 2. INICIALIZACIÓN DE SENSORES ---
tof_t = None
tof_v = None
print("Buscando sensores...")
# Intentar Sensor 1 (Tono)
try:
tof_t = adafruit_vl53l0x.VL53L0X(i2c, address=0x29)
print("✅ Tono: OK")
except Exception:
print("❌ Tono: NO DETECTADO")
# NOTA: Si solo tienes un sensor funcionando ahora mismo,
# el código usará ese para el tono y dejará el volumen fijo.
# --- 3. CONFIGURACIÓN DE AUDIO ---
try:
audio = audiobusio.I2SOut(board.IO15, board.IO16, board.IO17)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)
nota = synthio.Note(frequency=440, amplitude=0.2)
synth.press(nota)
except Exception as e:
print("Error de Audio:", e)
# --- 4. BUCLE PRINCIPAL ---
while True:
# Lógica de Tono
if tof_t:
try:
dist = tof_t.range
if 50 < dist < 400:
f = 800 - (dist * 1.5)
nota.frequency = max(100, min(f, 1000))
# Si solo hay un sensor, el volumen se queda en 0.2
nota.amplitude = 0.2
except Exception:
print("Error leyendo Tono")
time.sleep(0.01)
Usa estos pines, son los más estables del ESP32-S3:
Audio: DIN (17), LRC (16), BCLK (15).
I2C (Ambos sensores): SDA (IO4), SCL (IO5).
Energía: Todos los VIN y XSHUT a 3.3V (en filas separadas).
Ahora que funciona lo que necestimaos es conectar el Amplificador:
Conecta tu amplificador/altavoz a estos pines exactamente:
El codigo suena pero con poca variacion entonces ultilizamos ahora el rago completo de audio de 16 bits desde -32767 hasta 32767, luego vemos que el volumen control no funciona entonces cambiamos el volumen por vibrato
import board
import busio
import audiobusio
import synthio
import adafruit_vl53l0x
import time
import array
import math
# --- 1. SENSORES ---
i2c_tono = busio.I2C(board.IO13, board.IO14)
i2c_vol = busio.I2C(board.IO5, board.IO4)
tof_t = tof_v = None
try:
tof_t = adafruit_vl53l0x.VL53L0X(i2c_tono)
tof_t.measurement_timing_budget = 20000
print("✅ Tono (Derecha) OK")
except: print("❌ Error Tono")
try:
tof_v = adafruit_vl53l0x.VL53L0X(i2c_vol)
tof_v.measurement_timing_budget = 20000
print("✅ Vibrato (Izquierda) OK")
except: print("❌ Error Vibrato")
# --- 2. AUDIO ---
audio = audiobusio.I2SOut(board.IO1, board.IO2, board.IO3)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)
# Onda Triangular
samples = 512
waveform_triangle = array.array('h', [0] * samples)
for i in range(samples):
if i < samples // 2: waveform_triangle[i] = -32767 + (i * 128 * 2)
else: waveform_triangle[i] = 32767 - ((i - samples // 2) * 128 * 2)
nota = synthio.Note(frequency=440, amplitude=0.4, waveform=waveform_triangle)
synth.press(nota)
# Variables de control
freq_base = 440
vibrato_intensidad = 0.0
vibrato_index = 0.0
while True:
# --- MANO DERECHA (TONO) ---
if tof_t:
d1 = tof_t.range
if 40 < d1 < 500:
target_f = 1100 - (d1 * 2)
freq_base = (freq_base * 0.8) + (target_f * 0.2)
nota.amplitude = 0.4
else:
nota.amplitude = 0.0
# --- MANO IZQUIERDA (VIBRATO EXAGERADO) ---
if tof_v:
d2 = tof_v.range
# Si la mano está entre 4cm y 40cm
if 40 < d2 < 400:
# Mapeo: 0.0 (lejos) a 1.0 (cerca)
vibrato_intensidad = 1.0 - ((d2 - 40) / 360)
vibrato_intensidad = max(0.0, min(vibrato_intensidad, 1.0))
else:
vibrato_intensidad = 0.0
# --- APLICAR EFECTO ---
vibrato_index += 0.5 # Velocidad del temblor
# Multiplicamos por 60 para que la nota salte hasta 60Hz arriba y abajo (muy notable)
oscilacion = math.sin(vibrato_index) * (vibrato_intensidad * 60)
nota.frequency = max(100, freq_base + oscilacion)
# MONITOR (Para ver si el sensor funciona)
print(f"Nota: {int(freq_base)}Hz | Vibrato: {int(vibrato_intensidad*100)}% ", end="\r")
time.sleep(0.01)