// Arreglo de pines para las 12 notas (Escala cromática) en ESP32.
const int pinsNotas[] = {13, 12, 14, 27, 26, 25, 33, 32, 19, 18, 5, 17};
const int pinSubir = 4, pinBajar = 16, pinAudio = 2;
// Nombres de las notas y sus frecuencias base (octava 4 como referencia).
const char* nombresNotas[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
const float frecuenciasBase[] = {261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88};
// Tablas precalculadas de frecuencias para 9 octavas con fines de optimización.
float tablaFrecuencias[9][12]; // Frecuencias en Hertz (Hz).
float tablaPeriodos[9][12]; // Periodos de onda en milisegundos (ms).
// Variables de estado generales ("recuerdos").
int octavaActual = 4; // Octava de inicio (referencia estándar).
int ultimaNotaImpresa = -2; // Control para evitar que la última nota accionada se imprima infinitas veces.
unsigned long tiempoNotaSerial = 0; // Marca de tiempo para controlar la duración de notas por teclado. Sin esta variable, el piano quedaría sonando para siempre hasta ejecutar otra acción.
const int duracionNotaSerial = 200; // Tiempo (ms) que dura una nota activada por pulsación única de teclado.
const int tiempoEntreNotasSerie = 400; // Tiempo (ms) entre notas en reproducción automática (secuenciador).
int notaSerialActiva = -1; // Índice de la nota que se está reproduciendo vía Serial (teclado).
String bufferSerial = ""; // Almacén temporal (buffer) para comandos y secuencias recibidas vía Serial.
// Variables de estado y debounce (necesario si deseamos implementar el circuito en físico) para botones físicos de cambio de octava.
bool lastSubir = HIGH, lastBajar = HIGH;
unsigned long lastDebounceSubir = 0, lastDebounceBajar = 0;
// Generación y cálculo de las tablas de frecuencias y periodos para todas las octavas.
void generarTablas() {
for (int oct = 0; oct < 9; oct++) {
for (int nota = 0; nota < 12; nota++) {
float f = frecuenciasBase[nota] * pow(2, oct - 4);
tablaFrecuencias[oct][nota] = f;
tablaPeriodos[oct][nota] = (1.0 / f) * 1000.0;
}
}
}
// Inicialización del sistema (configuración de pines: periféricos, PWM y estructura de datos (tablas)).
void setup() {
Serial.begin(115200);
generarTablas();
// Configuración de pines de entrada para las notas (Usando resistencia Pull-up interna).
for (int i = 0; i < 12; i++) {
pinMode(pinsNotas[i], INPUT_PULLUP);}
pinMode(pinSubir, INPUT_PULLUP);
pinMode(pinBajar, INPUT_PULLUP);
// Configuración del canal PWM para la salida de audio (API de ESP32).
ledcAttach(pinAudio, 2000, 10); // Frecuencia inicial 2kHz, resolución 10 bits.
ledcWriteTone(pinAudio, 0); // Iniciar en silencio.
// Bienvenida e instrucciones de uso.
Serial.println("Piano Cromático: Botones en Tiempo Real + Secuenciador.");
Serial.println("Toca en tiempo real con: a(C), w(C#), s(D), e(D#), d(E), f(F), t(F#) ,g(G), y(G#), h(A), u(A#), j(B)");
Serial.println("Escribe series (ej: asdf...) o (ej: a+a-a...) y presiona ENTER para reproducir.");
}
// Ciclo principal: lectura de entradas y control de reproducción (salida de audio).
void loop() {
leerSerial(); // Revisa comandos del teclado de la PC.
controlarOctava(); // Revisa botones físicos (Wokwi) de octava.
int notaHardware = leerNota();
int notaFinal = -1;
// Prioridad 1: Botones Físicos (Sonido continuo mientras se mantienen presionados).
if (notaHardware != -1) {
notaFinal = notaHardware;
notaSerialActiva = -1; // Se cancela cualquier nota serial pendiente.
}
// Prioridad 2: Entrada por teclado serial (Dura el tiempo predefinido en "duracionNotaSerial").
else if (notaSerialActiva != -1) {
if (millis() - tiempoNotaSerial < duracionNotaSerial) {
notaFinal = notaSerialActiva;
} else {
notaSerialActiva = -1; // Fin del tiempo de la nota serial.
}
}
reproducirNota(notaFinal);
delay(5); // Únicamente con fines de estabilidad.
}
// FUNCIONES DE ENTRADA: Gestión de la comunicación serial: diferencia entre notas únicas (ej. "a") y secuencias (ej. "ab c").
void leerSerial() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n') { // Al recibir ENTER, se procesa lo "acumulado".
if (bufferSerial.length() > 1) {
ejecutarSerie(bufferSerial);
} else if (bufferSerial.length() == 1) {
procesarTeclaUnica(bufferSerial[0]);
}
bufferSerial = "";
}
else if (c != '\r') {
bufferSerial += c;
}
}
}
// Procesamiento de las pulsaciones individuales del teclado de la PC (ej. "a").
void procesarTeclaUnica(char t) {
int mapeo = mapearTeclado(t);
notaSerialActiva = mapeo;
tiempoNotaSerial = millis();
// Cambios rápidos de octava vía teclado.
if (t == '+') { if(octavaActual < 8) octavaActual++; Serial.printf(">>> Octava: %d\n", octavaActual); }
if (t == '-') { if(octavaActual > 0) octavaActual--; Serial.printf(">>> Octava: %d\n", octavaActual); }
}
// Secuenciador: Reproduce una cadena de caracteres como una melodía (ej. "ab c").
void ejecutarSerie(String serie) {
Serial.printf("\n>>> Ejecutando secuencia: %s\n", serie.c_str());
for (int i = 0; i < serie.length(); i++) {
char t = serie[i];
// Cambios de octava dentro de la serie (sin necesidad de parar).
if (t == '+') {
if(octavaActual < 8) octavaActual++;
continue;
}
if (t == '-') {
if(octavaActual > 0) octavaActual--;
continue;
}
int nota = mapearTeclado(t);
if (nota == -1) {
ledcWriteTone(pinAudio, 0);
Serial.println("[Silencio]");
} else {
reproducirNota(nota);
}
delay(tiempoEntreNotasSerie);
ledcWriteTone(pinAudio, 0);
delay(40); // Pequeño espacio de tiempo (ms) para separar notas repetidas.
ultimaNotaImpresa = -1;
}
Serial.println(">>> Secuencia terminada <<<\n");
ultimaNotaImpresa = -1;
}
// Diccionario de mapeo: Teclas del PC -> Índice de la escala cromática asignado previamente.
int mapearTeclado(char t) {
switch (t) {
case 'a': return 0; case 'w': return 1; case 's': return 2;
case 'e': return 3; case 'd': return 4; case 'f': return 5;
case 't': return 6; case 'g': return 7; case 'y': return 8;
case 'h': return 9; case 'u': return 10; case 'j': return 11;
case ' ': return -1;
default: return -1;
}
}
// FUNCIONES DE SALIDA: Actualiza la frecuencia del PWM e imprime datos técnicos (nota, frec, prdo).
void reproducirNota(int nota) {
if (nota == ultimaNotaImpresa) return; // Evita ruidos y spam en el monitor si la nota no ha cambiado.
if (nota == -1) {
ledcWriteTone(pinAudio, 0);
if (ultimaNotaImpresa != -1) {
Serial.println("[Silencio]");
}
} else {
float f = tablaFrecuencias[octavaActual][nota];
float p = tablaPeriodos[octavaActual][nota];
float w = 2.0 * PI * f; // Cálculo de la frecuencia angular (ω) en radianes por segundo a partir de la frecuencia de cada nota en Hz.
ledcWriteTone(pinAudio, f);
Serial.printf("Nota: %s%d | Frecuencia: %.2f Hz | Frecuencia angular: %.2f rad/s | Periodo: %.3f ms\n", nombresNotas[nota], octavaActual, f, w, p);
}
ultimaNotaImpresa = nota;
}
// Lectura de periféricos: Escaneo de los botones de la octava actual (hardware).
int leerNota() {
for (int i = 0; i < 12; i++) {
if (digitalRead(pinsNotas[i]) == LOW) return i; // Lógica inversa por PULLUP.
}
return -1;
}
// Gestión de hardware (botones) para el cambio de octava con debounce (eliminación de rebotes).
void controlarOctava() {
unsigned long now = millis();
bool subir = digitalRead(pinSubir), bajar = digitalRead(pinBajar);
// Decide si la pulsación del dedo sobre el botón es válida para cambiar de octava. Es el "filtro" del estado actual del botón (si está accionado o no), de la memoria (para evitar subir o bajar infinitamente de octava) y de tiempo (debounce).
if (subir == LOW && lastSubir == HIGH && (now - lastDebounceSubir > 50)) {
if (octavaActual < 8) octavaActual++;
lastDebounceSubir = now;
Serial.printf("\n>>> Cambio a Octava: %d <<<\n", octavaActual);
}
if (bajar == LOW && lastBajar == HIGH && (now - lastDebounceBajar > 50)) {
if (octavaActual > 0) octavaActual--;
lastDebounceBajar = now;
Serial.printf("\n>>> Cambio a Octava: %d <<<\n", octavaActual);
}
lastSubir = subir; lastBajar = bajar; // El "pasado" se vuelve el nuevo "presente".
}