
Convertir un eclipse en sonido: que una persona ciega pueda “verlo” escuchándolo con Arduino y Python
El 12 de agosto de 2026 España vive un eclipse solar total, y hay una forma de vivirlo que no depende de los ojos: convertir la luz en sonido, para que quien no puede ver el cielo oscurecerse, pueda oírlo oscurecerse. Eso es exactamente lo que hemos construido estos días con un Arduino, un sensor de luz que cuesta menos que un café, y varias tardes de conversación con una IA hasta dar con un sonido que de verdad se pudiera escuchar sin cansancio durante horas.
Esta entrada no es el resumen bonito de un proyecto que salió bien a la primera. Es la aventura real: cuatro versiones distintas del mismo programa, cada una nacida de un problema muy concreto que solo apareció al probar la anterior en voz alta. Y creo que esa aventura —con sus prompts, sus “esto no me convence” y sus vueltas atrás— enseña más sobre cómo trabajar con IA que cualquier resultado final pulido.
El punto de partida: un sensor de luz que ya estaba mandando datos
La estación que teníamos montada ya recogía varias cosas a la vez con un Arduino: temperatura y humedad con un sensor DHT, temperatura, presión y altitud con un sensor BMP, y la luminosidad con un sensor de luz de Keyestudio —de los baratitos, un fotorresistor que da un valor analógico entre 0 (oscuridad total) y 1023 (luz máxima)—. Todo esto viaja por el puerto serie en una línea de texto con este formato:
DATOS,temp_dht,humedad,temp_bmp,presion,altitud,luz,uv_adc,uv_voltios
El ordenador, con un script en Python, lee esa línea, la guarda en un CSV y la sube a ThingSpeak para tener un registro online. Hasta ahí, nada nuevo: es una estación meteorológica de las que se montan en cualquier clase de tecnología. Lo nuevo empieza cuando decidimos que ese mismo dato de luz, además de guardarse, tenía que sonar.
De momento hemos dejado fuera el sensor de UV que también manda datos por esa misma línea —esa es la siguiente capa del proyecto, para más adelante—. Esta primera vuelta se centra solo en la luz.
Si no tienes ya el Arduino enviando datos: el código mínimo
Todos los scripts de Python de esta entrada esperan recibir por el puerto serie una línea con ese formato de 9 campos. Si todavía no tienes montada la estación completa y solo quieres probar la sonificación con el sensor de luz, este código de Arduino es suficiente: lee el sensor de luz de Keyestudio (conectado al pin analógico A0) y manda la trama con el campo de luz relleno de verdad y el resto a 0, para que los scripts de Python la entiendan sin tocar nada en ellos.
cpp
// Lectura del sensor de luz (Keyestudio) y envío por el puerto serie
// Conecta el pin de salida del sensor al pin analógico A0 del Arduino
const int PIN_SENSOR_LUZ = A0;
void setup() {
Serial.begin(115200); // Mismos baudios que esperan los scripts de Python
}
void loop() {
int valorLuz = analogRead(PIN_SENSOR_LUZ); // Valor entre 0 (oscuridad) y 1023 (luz máxima)
// Enviamos la misma trama de 9 campos que leen los scripts de Python.
// Solo el campo de luz (posición 6) lleva un dato real; el resto va a 0
// porque este montaje mínimo no tiene sensores de DHT, BMP ni UV.
Serial.print("DATOS,0,0,0,0,0,");
Serial.print(valorLuz);
Serial.println(",0,0");
delay(2000); // Una lectura cada 2 segundos, igual que en el resto del proyecto
}
Para que el ordenador y el Arduino se encuentren, hay que decirle al script en qué puerto está conectado la placa: en el código de Python verás la línea PUERTO_SERIE = 'TU_PUERTO_ARDUINO', que hay que sustituir por el puerto real. En Windows se ve en el Administrador de dispositivos, dentro de “Puertos (COM y LPT)” (suele ser algo como COM3 o COM4); en Mac o Linux se ve ejecutando ls /dev/tty.* en la terminal, o mirando el menú Herramientas → Puerto del propio IDE de Arduino, que en cualquier sistema muestra el puerto correcto con la placa conectada.
Prompt 1: todo junto, sonido incluido
La primera petición fue directa: seguir el estándar que usa todo el mundo en sonificación (luz alta = tono agudo, oscuridad = tono grave) y añadir esa lógica al script que ya guardaba en CSV y enviaba a ThingSpeak, sin tocar nada de lo que ya funcionaba.
La IA propuso generar una onda sinusoidal en tiempo real con las librerías numpy y sounddevice, mapeando el valor de luz (0-1023) a un rango de frecuencias audibles: 180 Hz para la oscuridad total y 1500 Hz para el sol pleno. Un detalle importante desde el primer momento: la onda lleva una “envolvente” (un suave fundido de entrada y salida) para que el sonido no chasquee como un clic al empezar y terminar cada tono.
python
import serial
import time
import requests
import csv
import os
from datetime import datetime
import numpy as np
import sounddevice as sd
# ====================================================
# CONFIGURACIÓN DEL PUERTO SERIE Y ARCHIVO
# ====================================================
PUERTO_SERIE = 'TU_PUERTO_ARDUINO' # En Windows algo como 'COM3' o 'COM4'; en Mac/Linux algo como '/dev/ttyUSB0'
BAUDIOS = 115200
NOMBRE_ARCHIVO_CSV = 'datos_estacion.csv'
THINGSPEAK_URL = 'https://api.thingspeak.com/update'
WRITE_API_KEY = 'ESCRIBE_AQUI_TU_API_KEY'
INTERVALO_THINGSPEAK = 30 # Enviar a ThingSpeak cada 30 segundos
# ====================================================
# CONFIGURACIÓN DE LA SONIFICACIÓN (AUDIO)
# ====================================================
# Rango de frecuencias audibles (en Hertz)
FREQ_MIN_OSCURIDAD = 180.0 # Tono grave (C3 aprox.) para eclipse total/oscuridad
FREQ_MAX_SOL = 1500.0 # Tono agudo (F6 aprox.) para luz máxima
# Rango esperado del sensor de luz (LDR)
LUZ_MIN = 0
LUZ_MAX = 1023
VOLUMEN = 0.15 # Volumen general (0.0 a 1.0)
SAMPLE_RATE = 44100 # Frecuencia de muestreo estándar de audio
def calcular_frecuencia(valor_luz):
"""Mapea el valor de luz (0-1023) a un rango de frecuencia audible (Hz)."""
val_clamped = max(LUZ_MIN, min(LUZ_MAX, valor_luz))
# Mapeo lineal: Luz alta = Frecuencia alta (Agudo)
frecuencia = FREQ_MIN_OSCURIDAD + (val_clamped / LUZ_MAX) * (FREQ_MAX_SOL - FREQ_MIN_OSCURIDAD)
return frecuencia
def reproducir_tono(frecuencia, duracion=0.3):
"""Genera y reproduce una onda sinusoidal limpia sin bloquear el programa."""
t = np.linspace(0, duracion, int(SAMPLE_RATE * duracion), False)
# Suavizado de bordes (envelope) para evitar chasquidos tipo "pop" al inicio/fin del sonido
fading = np.sin(np.linspace(0, np.pi, len(t)))
onda = np.sin(2 * np.pi * frecuencia * t) * fading * VOLUMEN
# Reproduce el audio de fondo
sd.play(onda.astype(np.float32), SAMPLE_RATE)
# ====================================================
# CREACIÓN / VERIFICACIÓN DEL ARCHIVO CSV LOCAL
# ====================================================
if not os.path.exists(NOMBRE_ARCHIVO_CSV):
with open(NOMBRE_ARCHIVO_CSV, mode='w', newline='', encoding='utf-8') as f:
escritor = csv.writer(f)
escritor.writerow([
"Fecha", "Hora", "TemperaturaDHT", "Humedad",
"TemperaturaBMP", "Presion", "Altitud",
"Luminosidad", "UV_ADC", "UV_Voltios"
])
print(f"Archivo local '{NOMBRE_ARCHIVO_CSV}' creado con éxito.")
# ====================================================
# INICIALIZACIÓN DEL PUERTO SERIE
# ====================================================
try:
arduino = serial.Serial(PUERTO_SERIE, BAUDIOS, timeout=2)
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)
print(f"Conectado a Arduino en {PUERTO_SERIE}.")
print("Iniciando sistema de sonificación para el Eclipse...")
time.sleep(2)
except Exception as e:
print(f"Error al abrir el puerto serie: {e}")
exit()
ultimo_envio_ts = 0
# ====================================================
# BUCLE PRINCIPAL
# ====================================================
while True:
try:
if arduino.in_waiting > 0:
linea_raw = arduino.readline()
if linea_raw:
linea = linea_raw.decode('utf-8', errors='ignore').strip()
if linea.startswith("DATOS,"):
partes = linea.split(",")
if len(partes) == 9:
temp_dht = partes[1]
humedad = partes[2]
temp_bmp = partes[3]
presion = partes[4]
altitud = partes[5]
luz = partes[6]
uv_adc = partes[7]
uv_voltios = partes[8]
ahora = datetime.now()
fecha_str = ahora.strftime('%Y-%m-%d')
hora_str = ahora.strftime('%H:%M:%S')
# 1. EMITIR TONO DE SONIFICACIÓN
try:
val_luz = int(luz)
freq_actual = calcular_frecuencia(val_luz)
reproducir_tono(freq_actual, duracion=0.4)
sintesis_str = f"Freq: {freq_actual:.1f} Hz"
except ValueError:
sintesis_str = "Freq: N/A"
# 2. GUARDAR EN ARCHIVO CSV LOCAL
with open(NOMBRE_ARCHIVO_CSV, mode='a', newline='', encoding='utf-8') as f:
escritor = csv.writer(f)
escritor.writerow([
fecha_str, hora_str, temp_dht, humedad,
temp_bmp, presion, altitud, luz, uv_adc, uv_voltios
])
print(f"[{fecha_str} {hora_str}] Luz: {luz} | {sintesis_str} -> Registrado OK")
# 3. ENVIAR A THINGSPEAK
tiempo_actual = time.time()
if tiempo_actual - ultimo_envio_ts >= INTERVALO_THINGSPEAK:
ultimo_envio_ts = tiempo_actual
payload = {
'api_key': WRITE_API_KEY,
'field1': temp_dht,
'field2': humedad,
'field3': temp_bmp,
'field4': presion,
'field5': altitud,
'field6': luz,
'field7': uv_adc,
'field8': uv_voltios
}
respuesta = requests.post(THINGSPEAK_URL, data=payload)
if respuesta.status_code == 200 and respuesta.text != '0':
print(f" └─> ¡Enviado a ThingSpeak! ID: {respuesta.text}")
else:
print(f" └─> Error ThingSpeak: {respuesta.text}")
except KeyboardInterrupt:
print("\nPrograma detenido por el usuario.")
arduino.close()
break
except Exception as e:
print(f"Error en el bucle: {e}")
time.sleep(1)
La primera prueba fue tapar el sensor con la mano: el tono cae hacia un grave profundo, alrededor de 180 Hz. Al acercarle la linterna del móvil, sube hasta un agudo claro, cerca de 1500 Hz. Funcionaba. Pero enseguida apareció el primer “esto no me convence”.
Prompt 2: separar el sonido de todo lo demás
Con el script anterior, cualquier prueba de sonido implicaba tocar un programa que también estaba guardando datos y subiéndolos a ThingSpeak. Demasiado riesgo para simplemente ir probando tonos. El siguiente prompt fue justo ese: sacar la sonificación a un script totalmente independiente, que solo escuche la luz y la convierta en sonido, para integrarla más adelante cuando estuviera afinada.
El resultado fue sonificacion_luz.py: la misma lógica de mapeo luz-frecuencia, pero sola, y con un añadido muy útil para depurar de oído y de vista a la vez —una barra de progreso en la consola que crece y decrece con el nivel de luz—.
python
import serial
import time
import numpy as np
import sounddevice as sd
# ====================================================
# CONFIGURACIÓN DEL PUERTO SERIE
# ====================================================
PUERTO_SERIE = 'TU_PUERTO_ARDUINO' # En Windows algo como 'COM3' o 'COM4'; en Mac/Linux algo como '/dev/ttyUSB0'
BAUDIOS = 115200
# ====================================================
# CONFIGURACIÓN DEL SONIDO (SONIFICACIÓN)
# ====================================================
# Mapeo de frecuencias audibles (Hz)
FREQ_MIN_OSCURIDAD = 180.0 # Tono grave (C3) -> Eclipse total / Sombra
FREQ_MAX_SOL = 1500.0 # Tono agudo (F6) -> Pleno sol
# Rango del sensor de luz (LDR 0-1023)
LUZ_MIN = 0
LUZ_MAX = 1023
VOLUMEN = 0.15 # Volumen del tono (0.0 a 1.0)
SAMPLE_RATE = 44100 # Frecuencia de muestreo estándar de audio
def calcular_frecuencia(valor_luz):
"""Mapea la luz (0-1023) a una frecuencia audible (Hz)."""
val_clamped = max(LUZ_MIN, min(LUZ_MAX, valor_luz))
# Luz alta = Frecuencia alta (Agudo) | Luz baja = Frecuencia baja (Grave)
frecuencia = FREQ_MIN_OSCURIDAD + (val_clamped / LUZ_MAX) * (FREQ_MAX_SOL - FREQ_MIN_OSCURIDAD)
return frecuencia
def reproducir_tono(frecuencia, duracion=0.3):
"""Genera una onda senoidal suave con envolvente para evitar chasquidos."""
t = np.linspace(0, duracion, int(SAMPLE_RATE * duracion), False)
# Suavizado de bordes (envelope) tipo seno para fluidez en las transiciones
fading = np.sin(np.linspace(0, np.pi, len(t)))
onda = np.sin(2 * np.pi * frecuencia * t) * fading * VOLUMEN
# Reproduce el audio en segundo plano
sd.play(onda.astype(np.float32), SAMPLE_RATE)
# ====================================================
# INICIALIZACIÓN
# ====================================================
try:
arduino = serial.Serial(PUERTO_SERIE, BAUDIOS, timeout=2)
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)
print("==================================================")
print(" SISTEMA DE SONIFICACIÓN DE LUZ (ECLIPSE 2026) ")
print("==================================================")
print(f"Conectado a Arduino en {PUERTO_SERIE}.")
print("Escuchando nivel de luz...")
print("Presiona Ctrl + C para salir.\n")
time.sleep(2)
except Exception as e:
print(f"Error al abrir el puerto serie: {e}")
exit()
# ====================================================
# BUCLE PRINCIPAL
# ====================================================
while True:
try:
if arduino.in_waiting > 0:
linea_raw = arduino.readline()
if linea_raw:
linea = linea_raw.decode('utf-8', errors='ignore').strip()
# Buscamos únicamente la trama de datos
if linea.startswith("DATOS,"):
partes = linea.split(",")
if len(partes) == 9:
# Extraemos la luminosidad (Posición 6)
luz_str = partes[6]
try:
val_luz = int(luz_str)
freq_actual = calcular_frecuencia(val_luz)
# Emitimos el tono correspondiente a esta lectura
reproducir_tono(freq_actual, duracion=0.4)
# Indicador visual simple por consola
barras = "█" * int((val_luz / LUZ_MAX) * 30)
print(f"[Luz: {val_luz:4d} / 1023] | Freq: {freq_actual:6.1f} Hz | [{barras:<30}]")
except ValueError:
pass
except KeyboardInterrupt:
print("\nSonificación finalizada por el usuario.")
arduino.close()
break
except Exception as e:
print(f"Error: {e}")
time.sleep(1)
Con este script suelto ya se podía jugar sin miedo: tapar el sensor y ver cómo caía el valor mientras el tono se hundía hacia el grave, o iluminarlo con una linterna y ver la barra dispararse hacia arriba junto con un agudo. Fue justo escuchándolo con calma, durante un rato largo, cuando apareció el problema de verdad.
Prompt 3: el sonido se cortaba, y eso lo estropeaba todo
El Arduino manda una lectura nueva cada dos segundos, y cada tono duraba 0.4 segundos. Es decir: 0.4 segundos de sonido y 1.6 segundos de silencio, una y otra vez. Al oído, eso no suena como “un dato cada dos segundos”: suena como un fallo, un parpadeo, algo que se corta y no debería cortarse. La pregunta que disparó el siguiente cambio fue exactamente esa: si el silencio entre lecturas resultaba molesto, ¿no sería mejor que el sonido no se detuviera nunca?
La respuesta fue sí, de forma clara: en sonificación de datos, el sonido no se apaga. Ya existen proyectos reales de astronomía accesible que funcionan así, pensados para que una persona con discapacidad visual pueda seguir un eclipse por el oído sin cortes ni sobresaltos, y este es el mismo principio. La solución técnica consiste en montar un hilo de audio en segundo plano —un sounddevice.OutputStream con una función de callback— que suena de forma continua al 100% del tiempo. Cada dos segundos, cuando llega un dato nuevo del Arduino, el programa no cambia el tono de golpe: desliza la frecuencia actual hacia la nueva poco a poco (un 5% de la diferencia en cada bloque de audio), como un glissando. El resultado es una nota que fluye sin parar y se curva suavemente hacia arriba o hacia abajo según cambia la luz, en vez de dar saltos.
python
import serial
import time
import numpy as np
import sounddevice as sd
# ====================================================
# CONFIGURACIÓN DEL PUERTO SERIE
# ====================================================
PUERTO_SERIE = 'TU_PUERTO_ARDUINO' # En Windows algo como 'COM3' o 'COM4'; en Mac/Linux algo como '/dev/ttyUSB0'
BAUDIOS = 115200
# ====================================================
# CONFIGURACIÓN DEL SONIDO CONTINUO
# ====================================================
FREQ_MIN_OSCURIDAD = 180.0 # Tono grave (C3) -> Eclipse total
FREQ_MAX_SOL = 1500.0 # Tono agudo (F6) -> Pleno sol
LUZ_MIN = 0
LUZ_MAX = 1023
VOLUMEN = 0.12 # Volumen (0.0 a 1.0)
SAMPLE_RATE = 44100 # Frecuencia de muestreo (Hz)
# Variables globales para la síntesis de audio continua
frecuencia_objetivo = 440.0 # Frecuencia a la que queremos llegar
frecuencia_actual = 440.0 # Frecuencia actual sonando
fase = 0.0 # Fase de la onda para mantener la continuidad limpia
def callback_audio(outdata, frames, time_info, status):
"""
Función que el motor de sonido llama continuamente en segundo plano.
Mantiene el sonido sonando sin interrupción.
"""
global frecuencia_objetivo, frecuencia_actual, fase
# Transición suave (Glissando) entre la frecuencia anterior y la nueva
# Cambia un 5% de la diferencia en cada bloque de audio para un deslizamiento fluido
factor_suavizado = 0.05
t = np.arange(frames)
# Creamos un vector de frecuencias que se desliza gradualmente hacia el objetivo
frecuencias = np.linspace(frecuencia_actual,
frecuencia_actual + (frecuencia_objetivo - frecuencia_actual) * factor_suavizado,
frames)
# Actualizamos la frecuencia actual para el siguiente bloque
frecuencia_actual = frecuencias[-1]
# Calculamos la fase continua para evitar chasquidos de audio
fases = fase + 2 * np.pi * np.cumsum(frecuencias) / SAMPLE_RATE
fase = fases[-1] % (2 * np.pi)
# Generamos la onda senoidal continua
onda = np.sin(fases) * VOLUMEN
# Enviamos los datos al canal de audio (estéreo)
outdata[:] = onda.reshape(-1, 1)
def calcular_frecuencia(valor_luz):
"""Mapea la luz (0-1023) a la frecuencia audible correspondiente."""
val_clamped = max(LUZ_MIN, min(LUZ_MAX, valor_luz))
return FREQ_MIN_OSCURIDAD + (val_clamped / LUZ_MAX) * (FREQ_MAX_SOL - FREQ_MIN_OSCURIDAD)
# ====================================================
# INICIALIZACIÓN DEL PUERTO Y MOTOR DE AUDIO
# ====================================================
try:
arduino = serial.Serial(PUERTO_SERIE, BAUDIOS, timeout=2)
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)
print("==================================================")
print(" SONIFICACIÓN CONTINUA DE LUZ (ECLIPSE 2026) ")
print("==================================================")
print(f"Conectado a Arduino en {PUERTO_SERIE}.")
# Inicia el flujo de audio continuo en segundo plano
stream_audio = sd.OutputStream(channels=1, callback=callback_audio, samplerate=SAMPLE_RATE)
stream_audio.start()
print("Audio continuo activado. Escuchando sensor...")
print("Presiona Ctrl + C para salir.\n")
time.sleep(1)
except Exception as e:
print(f"Error al inicializar: {e}")
exit()
# ====================================================
# BUCLE PRINCIPAL (Solo lee el serie y actualiza el objetivo)
# ====================================================
try:
while True:
if arduino.in_waiting > 0:
linea_raw = arduino.readline()
if linea_raw:
linea = linea_raw.decode('utf-8', errors='ignore').strip()
if linea.startswith("DATOS,"):
partes = linea.split(",")
if len(partes) == 9:
luz_str = partes[6]
try:
val_luz = int(luz_str)
# Actualizamos el objetivo de frecuencia (el motor de audio hace el resto de forma fluida)
frecuencia_objetivo = calcular_frecuencia(val_luz)
barras = "█" * int((val_luz / LUZ_MAX) * 30)
print(f"[Luz: {val_luz:4d} / 1023] | Tono Objetivo: {frecuencia_objetivo:6.1f} Hz | [{barras:<30}]")
except ValueError:
pass
except KeyboardInterrupt:
print("\nDeteniendo sistema de audio...")
stream_audio.stop()
stream_audio.close()
arduino.close()
print("Sistema cerrado correctamente.")
except Exception as e:
print(f"Error en ejecución: {e}")
Con esto desaparecieron los cortes: el sonido no calla nunca, y cada nuevo dato del Arduino curva la nota hacia arriba o hacia abajo en vez de dar un salto brusco. Ya era usable de verdad. Pero quedaba un problema distinto: la onda sinusoidal pura, sonando sin parar durante horas, resultaba bastante desagradable al oído.
Prompt 4: de pitido de laboratorio a sonido cálido y musical
El último prompt fue el más sencillo de plantear y el que más cambió la experiencia final: pedir un sonido “menos desagradable”, recordando que antes se había hablado de otros tipos de onda. La respuesta llegó con dos ideas que combinadas transforman por completo la sensación:
La primera es añadir un segundo armónico suave a la onda principal —una frecuencia el doble de alta, mezclada a menor volumen—, lo que le da un timbre cálido, parecido a una flauta o un órgano suave, en vez del pitido plano de un sintetizador de laboratorio.
La segunda es más interesante todavía: en lugar de recorrer todas las frecuencias intermedias en Hertz (que muchas veces caen en tonos “desafinados” al oído humano), el valor de luz elige la nota musical afinada más cercana dentro de una escala pentatónica mayor —Do, Re, Mi, Sol, La, repetida en varias octavas—. Es la escala más usada en música por una razón muy concreta: cualquier combinación de sus notas suena agradable, sin notas que choquen entre sí. Así, el eclipse entero se recorre como una melodía suave que siempre asciende o desciende de forma musical, nunca como un pitido que sube y baja sin más.
python
import serial
import time
import numpy as np
import sounddevice as sd
# ====================================================
# CONFIGURACIÓN DEL PUERTO SERIE
# ====================================================
PUERTO_SERIE = 'TU_PUERTO_ARDUINO' # En Windows algo como 'COM3' o 'COM4'; en Mac/Linux algo como '/dev/ttyUSB0'
BAUDIOS = 115200
# ====================================================
# CONFIGURACIÓN AUDIO: ESCALA PENTATÓNICA CÁLIDA
# ====================================================
# Frecuencias de notas musicales reales (Hz) en escala Pentatónica (C3 a C6)
# Suena armónico y agradable en cualquier nivel de luz
NOTAS_PENTATONICAS = [
130.81, 146.83, 164.81, 196.00, 220.00, # C3, D3, E3, G3, A3 (Grave)
261.63, 293.66, 329.63, 392.00, 440.00, # C4, D4, E4, G4, A4
523.25, 587.33, 659.25, 783.99, 880.00, # C5, D5, E5, G5, A5
1046.50 # C6 (Agudo)
]
LUZ_MIN = 0
LUZ_MAX = 1023
VOLUMEN = 0.10 # Volumen suave
SAMPLE_RATE = 44100
frecuencia_objetivo = NOTAS_PENTATONICAS[0]
frecuencia_actual = NOTAS_PENTATONICAS[0]
fase1 = 0.0
fase2 = 0.0
def callback_audio_calido(outdata, frames, time_info, status):
"""
Sintetizador de sonido con armónicos suaves (Timbre tipo flauta/órgano).
"""
global frecuencia_objetivo, frecuencia_actual, fase1, fase2
# Transición de glissando suave entre notas
factor_suavizado = 0.04
frecuencias = np.linspace(frecuencia_actual,
frecuencia_actual + (frecuencia_objetivo - frecuencia_actual) * factor_suavizado,
frames)
frecuencia_actual = frecuencias[-1]
# Fundamental (Nota base)
fases1 = fase1 + 2 * np.pi * np.cumsum(frecuencias) / SAMPLE_RATE
fase1 = fases1[-1] % (2 * np.pi)
onda_base = np.sin(fases1)
# Segundo armónico más suave (Añade calidez/timbre de instrumento)
fases2 = fase2 + 2 * np.pi * np.cumsum(frecuencias * 2) / SAMPLE_RATE
fase2 = fases2[-1] % (2 * np.pi)
onda_armonico = np.sin(fases2) * 0.25
# Mezcla final de ondas
onda_final = (onda_base + onda_armonico) * VOLUMEN
outdata[:] = onda_final.reshape(-1, 1)
def obtener_nota_musical(valor_luz):
"""Mapea el valor de luz a la nota musical afinada más cercana."""
val_clamped = max(LUZ_MIN, min(LUZ_MAX, valor_luz))
# Calculamos el índice dentro de la lista de notas disponibles
num_notas = len(NOTAS_PENTATONICAS)
indice = int((val_clamped / LUZ_MAX) * (num_notas - 1))
return NOTAS_PENTATONICAS[indice]
# ====================================================
# INICIALIZACIÓN
# ====================================================
try:
arduino = serial.Serial(PUERTO_SERIE, BAUDIOS, timeout=2)
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)
print("==================================================")
print(" SONIFICACIÓN MUSICAL CÁLIDA (ECLIPSE 2026) ")
print("==================================================")
print(f"Conectado a Arduino en {PUERTO_SERIE}.")
# Iniciar flujo de audio continuo
stream_audio = sd.OutputStream(channels=1, callback=callback_audio_calido, samplerate=SAMPLE_RATE)
stream_audio.start()
print("Sintetizador cálido activado. Escuchando luz...")
print("Presiona Ctrl + C para salir.\n")
time.sleep(1)
except Exception as e:
print(f"Error al inicializar: {e}")
exit()
# ====================================================
# BUCLE PRINCIPAL
# ====================================================
try:
while True:
if arduino.in_waiting > 0:
linea_raw = arduino.readline()
if linea_raw:
linea = linea_raw.decode('utf-8', errors='ignore').strip()
if linea.startswith("DATOS,"):
partes = linea.split(",")
if len(partes) == 9:
luz_str = partes[6]
try:
val_luz = int(luz_str)
# Selecciona la nota armónica correspondiente
frecuencia_objetivo = obtener_nota_musical(val_luz)
barras = "█" * int((val_luz / LUZ_MAX) * 30)
print(f"[Luz: {val_luz:4d} / 1023] | Nota: {frecuencia_objetivo:6.1f} Hz | [{barras:<30}]")
except ValueError:
pass
except KeyboardInterrupt:
print("\nDeteniendo sintetizador...")
stream_audio.stop()
stream_audio.close()
arduino.close()
print("Sistema cerrado.")
except Exception as e:
print(f"Error: {e}")
La diferencia se nota al instante: al pasar de sombra a luz ya no se oyen frecuencias intermedias desafinadas, sino una escala musical relajante, y el armónico añadido le quita la aspereza del “pitido de laboratorio” para acercarlo a un instrumento real.
Lo que queda por hacer: sumar el sensor de UV
El Arduino ya manda también la lectura del sensor de UV en la misma línea de datos, pero de momento se ha dejado fuera de la sonificación a propósito, para no mezclar dos variables en el mismo sonido antes de tener bien afinada la de luz. La siguiente vuelta del proyecto será decidir cómo añadir esa segunda capa —quizá como un segundo timbre, o como un cambio de instrumento cuando la radiación UV caiga en picado durante la totalidad— sin estropear lo que ya funciona bien.
La idea para llevarte a clase
Ninguna de las cuatro versiones salió de pedir “hazme un programa que convierta la luz en sonido” y quedarse con la primera respuesta. Cada cambio real vino de probar el resultado anterior con calma —tapando el sensor, escuchándolo un rato largo, notando el corte entre lecturas, notando la aspereza del tono— y de convertir esa sensación en un prompt concreto: “que sea continuo”, “que suene menos desagradable”. Ese es, quizá, el aprendizaje de fondo detrás de todo el código: la IA no acierta el diseño final a la primera, pero itera muy bien contigo si le describes exactamente qué es lo que no te convence de lo que acabas de escuchar o ver.
Si tienes un Arduino, un sensor de luz barato y ganas de que alguien pueda vivir el eclipse del 12 de agosto sin necesidad de mirarlo, ya tienes el punto de partida completo: cuatro scripts, cuatro problemas resueltos, y una nota musical que sube y baja con el sol.




