// --- LIBRERÍAS ---
/*
Preamplificador:
Encoder: CLK a pin D2, DT a D3, SW a D4.
Potenciómetros Digitales (MCP):
SCK a D13 (SPI Clock)
MOSI a D11 (SPI MOSI)
CS_MCP1 a D10
CS_MCP2 a D9
LCD I2C: SDA a A4, SCL a A5.
VU-Meter:
Entrada de audio Izquierda: A la patilla A0.
Entrada de audio Derecha: A la patilla A1.
*/
#include <SPI.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h> // Necesario para la función sqrt() del vúmetro
// --- DEFINICIONES DEL PREAMPLIFICADOR (CÓDIGO 1) ---
#define ENCODER_CLK 2
#define ENCODER_DT 3
#define ENCODER_SW 4
#define CS_MCP1 10
#define CS_MCP2 9
#define MUTE 8
// --- DEFINICIONES DEL VU-METER (CÓDIGO 2) ---
#define IN_LEFT A0 // Entrada analógica para el canal izquierdo
#define IN_RIGHT A1 // Entrada analógica para el canal derecho
#define T_REFRESH 100 // ms - Frecuencia de refresco del vúmetro
#define T_PEAKHOLD (5 * T_REFRESH) // ms - Tiempo que se mantiene el pico
// --- CONFIGURACIÓN DEL LCD ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- VARIABLES GLOBALES DEL PREAMPLIFICADOR ---
const int MAX_STEPS = 127;
const float MAX_DB = 0.0;
const float MIN_DB = -80.0;
const byte WIPER0_ADDRESS = 0x00;
volatile int masterAttenuation = MAX_STEPS;
int lastMasterAttenuation = -1;
int stepMcp1 = 0;
int stepMcp2 = 0;
bool isMuted = false;
int preMuteMasterAttenuation;
// --- VARIABLES GLOBALES DEL VU-METER ---
int lmax[2]; // Almacena el valor máximo (pico) para cada canal (L y R)
int dly[2]; // Almacena el retardo para la caída del pico
long lastVuRefreshTime = 0; // Temporizador para el refresco del vúmetro
// --- VARIABLES PARA LA LÓGICA DE UNIÓN ---
enum DisplayMode { PREAMP, VU_METER }; // Define los dos modos de pantalla posibles
DisplayMode currentDisplayMode = VU_METER; // El modo inicial es el vúmetro
unsigned long lastActivityTime = 0; // Almacena el tiempo de la última interacción con el encoder
const unsigned long INACTIVITY_TIMEOUT = 5000; // 5 segundos de inactividad para volver al vúmetro
// --- CARACTERES PERSONALIZADOS PARA EL VU-METER ---
// Estos arrays definen los patrones de píxeles para los caracteres personalizados del LCD
byte block[8][8] = {
{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // 1/5 de bloque
{0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18}, // 2/5 de bloque
{0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C}, // 3/5 de bloque
{0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E}, // 4/5 de bloque
{0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}, // Bloque completo (5/5)
// Caracteres para la marca de pico
{0x00, 0x00, 0x11, 0x11, 0x00, 0x00, 0x00, 0x00}, // Pico en el medio
{0x00, 0x00, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00}, // Pico más tenue
{0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00} // Pico más tenue
};
// Estos arrays mapean un nivel de llenado o pico al caracter personalizado correspondiente
// El primer elemento es un espacio en blanco ' ' (0x20) para cuando no hay barra.
byte fill[6] = {0x20, 0, 1, 2, 3, 4}; // Caracteres para la barra (0-4 son los primeros 5 bloques)
byte peak[7] = {0x20, 5, 6, 7, 6, 5, 0x20}; // Caracteres para la marca de pico
// =========================================================================
// === FUNCIÓN SETUP ===
// =========================================================================
void setup() {
Serial.begin(9600);
SPI.begin();
pinMode(MUTE, OUTPUT);
digitalWrite(MUTE, HIGH);
// --- Inicialización de Preamplificador ---
pinMode(CS_MCP1, OUTPUT);
pinMode(CS_MCP2, OUTPUT);
digitalWrite(CS_MCP1, HIGH);
digitalWrite(CS_MCP2, HIGH);
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
pinMode(ENCODER_SW, INPUT_PULLUP);
// La interrupción del encoder se activa al girar el knob
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), updateEncoder, FALLING);
// --- Inicialización del LCD ---
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Patro Dj");
lcd.setCursor(0, 1);
lcd.print(" Iniciando...");
// --- Creación de Caracteres Personalizados para el VU-Meter ---
for (int i = 0; i < 8; i++) {
lcd.createChar(i, block[i]);
}
delay(1500);
// --- Estado Inicial ---
Serial.println("Sistema listo.");
updatePotentiometers(); // Ajusta los potenciómetros al valor inicial
lastMasterAttenuation = masterAttenuation; // Sincroniza el valor
lastActivityTime = millis(); // Inicia el temporizador de actividad
currentDisplayMode = VU_METER; // Inicia en modo vúmetro
lcd.clear(); // Limpia la pantalla de inicio
digitalWrite(MUTE, LOW);
}
// =========================================================================
// === FUNCIÓN LOOP ===
// =========================================================================
void loop() {
// 1. Revisa si se ha presionado el botón del encoder
checkMuteButton();
// 2. Revisa si el valor del encoder ha cambiado (por la interrupción)
if (masterAttenuation != lastMasterAttenuation) {
switchToPreampMode(); // Cambia al modo de preamp si no lo está ya
updatePotentiometers();
updatePreampDisplay();
lastMasterAttenuation = masterAttenuation;
}
// 3. Comprueba el temporizador de inactividad
if (currentDisplayMode == PREAMP && (millis() - lastActivityTime > INACTIVITY_TIMEOUT)) {
switchToVuMeterMode(); // Vuelve al modo vúmetro si ha pasado el tiempo
}
// 4. Actualiza la pantalla según el modo actual
if (currentDisplayMode == VU_METER) {
updateVuMeterDisplay();
}
}
// =========================================================================
// === FUNCIONES DE CAMBIO DE MODO ===
// =========================================================================
void switchToPreampMode() {
lastActivityTime = millis(); // Reinicia el temporizador de inactividad
if (currentDisplayMode != PREAMP) {
currentDisplayMode = PREAMP;
lcd.clear(); // Limpia la pantalla solo una vez al cambiar de modo
Serial.println("Cambiando a modo Preamplificador");
}
}
void switchToVuMeterMode() {
currentDisplayMode = VU_METER;
lcd.clear(); // Limpia la pantalla solo una vez al cambiar de modo
Serial.println("Cambiando a modo VU-Meter por inactividad");
}
// =========================================================================
// === FUNCIONES DEL PREAMPLIFICADOR ===
// =========================================================================
void checkMuteButton() {
// Usa un simple debounce por software
static unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
if (digitalRead(ENCODER_SW) == LOW && (millis() - lastDebounceTime) > debounceDelay) {
toggleMute();
lastDebounceTime = millis();
}
}
void toggleMute() {
switchToPreampMode(); // Activar el modo preamp al presionar mute
isMuted = !isMuted;
if (isMuted) {
digitalWrite(MUTE, HIGH);
Serial.println("MUTE ACTIVADO");
preMuteMasterAttenuation = masterAttenuation;
} else {
digitalWrite(MUTE, LOW);
Serial.println("MUTE DESACTIVADO");
masterAttenuation = preMuteMasterAttenuation;
}
updatePotentiometers();
updatePreampDisplay();
lastMasterAttenuation = masterAttenuation;
}
// ESTA FUNCIÓN ES UNA INTERRUPCIÓN (ISR). DEBE SER RÁPIDA.
void updateEncoder() {
// Ignora el giro del encoder si está en Mute
if (isMuted) {
return;
}
// Actualiza el temporizador de actividad y el modo desde la interrupción
lastActivityTime = millis();
currentDisplayMode = PREAMP; // Cambia a modo preamp al girar
if (digitalRead(ENCODER_DT) != digitalRead(ENCODER_CLK)) {
masterAttenuation--;
} else {
masterAttenuation++;
}
// Limita el valor entre 0 y MAX_STEPS
masterAttenuation = constrain(masterAttenuation, 0, MAX_STEPS);
}
void updatePotentiometers() {
if (isMuted) {
stepMcp1 = 127;
stepMcp2 = 127;
} else {
// Lógica de atenuación escalonada
if (masterAttenuation >= 32) {
stepMcp1 = masterAttenuation;
stepMcp2 = map(masterAttenuation, 127, 32, 96, 0);
} else {
stepMcp1 = masterAttenuation;
stepMcp2 = 0;
}
}
setWiper(CS_MCP1, stepMcp1);
setWiper(CS_MCP2, stepMcp2);
Serial.print("Master: "); Serial.print(masterAttenuation);
Serial.print(" | MCP1(paso): "); Serial.print(stepMcp1);
Serial.print(" | MCP2(paso): "); Serial.print(stepMcp2);
Serial.print(" | Mute: "); Serial.println(isMuted ? "ON" : "OFF");
}
void setWiper(int csPin, byte value) {
digitalWrite(csPin, LOW);
SPI.transfer(WIPER0_ADDRESS);
SPI.transfer(value);
digitalWrite(csPin, HIGH);
}
void updatePreampDisplay() {
// Calcula la ganancia en dB para mostrar
float gain_dB = map(masterAttenuation, 0, MAX_STEPS, MAX_DB, MIN_DB);
lcd.clear(); // Limpia para redibujar
lcd.setCursor(1, 0);
lcd.print("Gain:");
lcd.setCursor(7, 0);
// Formato para alinear los números
if (gain_dB > -10.0) {
lcd.print(" ");
}
lcd.print(gain_dB, 1);
lcd.print(" dB");
lcd.setCursor(0, 1);
if (isMuted) {
lcd.print("<<<< Muting >>>>");
} else {
// Dibuja la barra de volumen
int barWidth = map(masterAttenuation, MAX_STEPS, 0, 0, 16);
for (int i = 0; i < barWidth; i++) {
lcd.write(byte(255)); // Usamos el caracter de bloque completo (índice 4)
}
}
}
// =========================================================================
// === FUNCIONES DEL VU-METER ===
// =========================================================================
void updateVuMeterDisplay() {
// Controla el refresco del vúmetro para no saturar el loop
if(millis() < lastVuRefreshTime) return;
lastVuRefreshTime += T_REFRESH;
// Lee y escala los valores analógicos. sqrt() da una respuesta más logarítmica (realista).
int anL = map(sqrt(analogRead(IN_LEFT) * 16), 0, 128, 0, 75);
int anR = map(sqrt(analogRead(IN_RIGHT) * 16), 0, 128, 0, 75);
drawBar(0, anL); // Dibuja la barra del canal Izquierdo (fila 0)
drawBar(1, anR); // Dibuja la barra del canal Derecho (fila 1)
}
void drawBar(int row, int level) {
lcd.setCursor(0, row);
lcd.write(row ? 'R' : 'L'); // Imprime 'L' o 'R'
// Dibuja la barra y la marca de pico
for(int i = 1; i < 16; i++) {
// Nivel de llenado del bloque actual (0-5)
int fillLevel = constrain(level - (i-1)*5, 0, 5);
// Posición del marcador de pico (0-6)
int peakLevel = constrain(lmax[row] - (i-1)*5, 0, 6);
lcd.setCursor(i, row);
if(fillLevel > 0) {
lcd.write(fill[fillLevel]);
} else {
lcd.write(peak[peakLevel]);
}
}
// Lógica para actualizar y hacer caer el pico
if(level > lmax[row]) {
lmax[row] = level;
dly[row] = -(T_PEAKHOLD) / T_REFRESH;
} else {
if(dly[row] > 0)
lmax[row] -= dly[row];
if(lmax[row] < 0)
lmax[row] = 0;
else
dly[row]++;
}
}