Proyecto ESP32-Theremin

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:

  1. 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).
  2. 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:


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:

Luego intenta conectar cada sensor:

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)

Parte 3: El bucle principal

Mano derecha (Tono):

d1 = tof_t.range
target_f = 1100 - (d1 * 2)
freq_base = (freq_base * 0.8) + (target_f * 0.2)

Cuanto más cerca la mano → más agudo el sonido. Usa suavizado para que no cambie de golpe.

Mano izquierda (Vibrato):

vibrato_intensidad = 1.0 - ((d2 - 40) / 360)

Cuanto más cerca la mano izquierda → más fuerte tiembla la nota.

Aplicar el efecto:

oscilacion = math.sin(vibrato_index) * (vibrato_intensidad * 60)
nota.frequency = freq_base + oscilacion

Aquí se calcula el temblor usando math.sin() y se combina con la frecuencia base.

Al final imprime información útil:
print(f"Nota: {int(freq_base)}Hz | Vibrato: {int(vibrato_intensidad*100)}% ", end="\r")


4. Galeria de imagenes y videos

Video 1 Video 2
https://drive.google.com/file/d/1cxJwvNkfWBde6f4S-sKTNcfZgZF7OAI6/view?usp=sharing https://drive.google.com/file/d/1JL0RetyjdboU_4BA93_UHVyQzwxlYzR_/view?usp=sharing
Pasted image 20260514192911.png|230 Pasted image 20260514192726.png|265 Pasted image 20260514192932.png|248

5. Proceso de creación:

1.Pruebas

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:

  • BCLK (Bit Clock) -> IO11
  • LRC (Word Select / Left-Right Clock) -> IO12
  • DIN (Data In) -> IO13
  • VIN -> 5V o 3.3V (según tu ampli)
  • GND -> GND
import board
import busio
import audiobusio
import synthio
import adafruit_vl53l0x
import time

# --- 1. SENSORES (Pines que ya te funcionan) ---
i2c_tono = busio.I2C(board.IO13, board.IO14)
i2c_vol = busio.I2C(board.IO5, board.IO4)

tof_t = None
tof_v = None

try:
    tof_t = adafruit_vl53l0x.VL53L0X(i2c_tono)
    print("✅ Sensor Tono OK")
except:
    print("❌ Fallo Tono")

try:
    tof_v = adafruit_vl53l0x.VL53L0X(i2c_vol)
    print("✅ Sensor Volumen OK")
except:
    print("❌ Fallo Volumen")

# --- 2. AUDIO (Tus pines originales) ---
# BCLK -> IO1 | LRC -> IO2 | DIN -> IO3
audio = audiobusio.I2SOut(board.IO1, board.IO2, board.IO3)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)

nota = synthio.Note(frequency=440, amplitude=0.0)
synth.press(nota)

print("🎶 ¡A TOCAR! Usando pines 1, 2 y 3 para audio.")

# --- 3. BUCLE ---
while True:
    if tof_t:
        d1 = tof_t.range
        if 50 < d1 < 450:
            f = 900 - (d1 * 1.5)
            nota.frequency = max(150, min(f, 1000))
    
    if tof_v:
        d2 = tof_v.range
        if 50 < d2 < 400:
            v = 0.6 - ((d2 - 50) / 600)
            nota.amplitude = max(0.0, min(v, 0.6))
        else:
            nota.amplitude = 0.0
    else:
        nota.amplitude = 0.2

    time.sleep(0.01)

4. Mejorar el sonido

  • 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)
Librería Para qué se usa
busio Crear las conexiones I2C (para los sensores)
adafruit_vl53l0x Controlar los sensores de distancia VL53L0X
audiobusio Salida de audio digital I2S (para que suene)
synthio Crear sonidos y notas musicales (sintetizador)
Librerias de CircuitPython: https://circuitpython.org/libraries

Mu: https://codewith.mu/

Links a diferentes fuentes de informacion:

Powered by Forestry.md