P3 ( Espectro de voz con INMP441 , esp32 y FFT )

Introducción a la FFT (Transformada Rápida de Fourier)

La Transformada Rápida de Fourier (FFT) es un algoritmo fundamental en el procesamiento de señales. Para entenderlo, primero debemos comprender la diferencia entre dos dominios:

  • Señal en el tiempo: Es la información que captura el micrófono: cómo varía la presión del aire (amplitud) a lo largo del tiempo. Es una forma de onda.

  • Señal en la frecuencia: Es la información que queremos visualizar: qué tonos (frecuencias) están presentes en ese sonido y con qué intensidad.

La FFT es el algoritmo que convierte de manera eficiente una señal del dominio del tiempo al dominio de la frecuencia . En nuestro proyecto, el ESP32 tomará las muestras de audio del micrófono (señal en el tiempo) y, mediante la librería ArduinoFFT, calculará la magnitud de las diferentes frecuencias presentes para mostrarlas como un gráfico de barras en la OLED .

Conexiones del Hardware

Antes de cargar el código, conecta los componentes siguiendo esta tabla. Es muy importante alimentar todo correctamente, ya que el INMP441 necesita 3.3V.

Tabla de conexiones definitiva

Para que no haya dudas, aquí está la tabla que debes seguir al pie de la letra:

Pin INMP441 Nombre en mi código Pin ESP32
VDD 3.3V 3.3V
GND GND GND
L/R GND (para canal izquierdo) GND
SD (o DOUT) GPIO 32 (DOUT) GPIO 32
SCK (o BCLK) GPIO 26 (BCLK) GPIO 26
WS GPIO 25 (Word Select) GPIO 25

Pin OLED (I2C) Pin ESP32
VCC 3.3V
GND GND
SCL GPIO 22
SDA GPIO 21

Nota sobre LR: Conectar LR a GND selecciona el canal izquierdo y a 3.3V el derecho. Como solo usamos un micrófono, cualquiera de las dos opciones funciona. En este código, asumiremos que está conectado a GND.

Código Completo para ESP32

Para este proyecto, necesitarás instalar las siguientes librerías en tu IDE de Arduino:

cpp
#include <Arduino.h>
#include <driver/i2s.h>
#include <arduinoFFT.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
// ============================================
// CONFIGURACIÓN DE PINES Y HARDWARE
// ============================================
// — Pantalla OLED —
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// — Micrófono INMP441 (I2S) —
#define I2S_WS   25   // Word Select (LRCLK)
#define I2S_SD   32   // Data Out (DOUT)
#define I2S_SCK  26   // Bit Clock (BCLK)
#define I2S_PORT I2S_NUM_0
// ============================================
// CONFIGURACIÓN DE MUESTREO Y FFT
// ============================================
#define SAMPLE_RATE     16000  // Frecuencia de muestreo (Hz)
#define SAMPLES         512    // Número de muestras (potencia de 2)
// Arrays para FFT
double vReal[SAMPLES];
double vImag[SAMPLES];
// Objeto FFT (versión 2.x de la librería)
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, SAMPLES, SAMPLE_RATE);
// Buffer para muestras del micrófono
int32_t sampleBuffer[SAMPLES];
// ============================================
// CONFIGURACIÓN DE BANDAS DE FRECUENCIA
// ============================================
#define NUM_BANDS 12  // Reducido a 12 para barras más gruesas
// Bandas de frecuencia (logarítmicas de 20Hz a 8kHz)
const int bandFreqs[NUM_BANDS + 1] = {
  20, 60, 120, 200, 350, 550,
  850, 1300, 2000, 3000, 4500, 7000, 8000
};
// Arrays para visualización
float bandValues[NUM_BANDS] = {0};
float peakValues[NUM_BANDS] = {0};
float bandMaxValues[NUM_BANDS] = {0};  // Para normalización dinámica
// ============================================
// CONFIGURACIÓN I2S
// ============================================
void i2s_install() {
  const i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 1024,
    .use_apll = false
  };
  i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
}
void i2s_setpins() {
  const i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = -1,
    .data_in_num = I2S_SD
  };
  i2s_set_pin(I2S_PORT, &pin_config);
}
// ============================================
// CONFIGURACIÓN OLED
// ============================================
void setupOLED() {
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F(«Error: No se encuentra pantalla OLED»));
    for(;;);
  }
  // Configuración inicial
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.cp437(true);  // Usar caracteres extendidos
  // Mostrar mensaje de inicio
  display.setCursor(0, 0);
  display.println(«Espectro Audio»);
  display.println(«INMP441 + FFT»);
  display.println(«Iniciando…»);
  display.display();
  delay(2000);
}
// ============================================
// SETUP
// ============================================
void setup() {
  Serial.begin(115200);
  Serial.println(«Iniciando Analizador de Espectro…»);
  // Inicializar I2S
  i2s_install();
  i2s_setpins();
  i2s_start(I2S_PORT);
  Serial.println(«✅ I2S inicializado»);
  // Inicializar OLED
  setupOLED();
  Serial.println(«✅ OLED inicializado»);
  // Dibujar marco inicial
  display.clearDisplay();
  display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
  display.display();
  delay(500);
}
// ============================================
// FUNCIÓN PARA NORMALIZAR BANDAS
// ============================================
void normalizeBands() {
  // Encontrar el valor máximo actual
  float maxVal = 0.1;  // Valor pequeño para evitar división por cero
  for (int b = 0; b < NUM_BANDS; b++) {
    if (bandValues[b] > maxVal) {
      maxVal = bandValues[b];
    }
  }
  // Actualizar máximos históricos con caída gradual
  for (int b = 0; b < NUM_BANDS; b++) {
    if (bandValues[b] > bandMaxValues[b]) {
      bandMaxValues[b] = bandValues[b];
    } else {
      bandMaxValues[b] *= 0.999;  // Caída muy lenta
      if (bandMaxValues[b] < 0.1) bandMaxValues[b] = 0.1;
    }
  }
  // Normalizar usando el máximo actual (respuesta rápida)
  float globalMax = maxVal;
  if (globalMax < 0.1) globalMax = 0.1;
  for (int b = 0; b < NUM_BANDS; b++) {
    bandValues[b] = (bandValues[b] / globalMax) * 50;  // Normalizar a 0-50
    if (bandValues[b] > 50) bandValues[b] = 50;
  }
}
// ============================================
// LOOP PRINCIPAL
// ============================================
void loop() {
  size_t bytesRead;
  // — 1. LEER MICRÓFONO —
  esp_err_t result = i2s_read(I2S_PORT, &sampleBuffer, sizeof(sampleBuffer), &bytesRead, portMAX_DELAY);
  if (result != ESP_OK) {
    Serial.println(«Error leyendo I2S»);
    delay(100);
    return;
  }
  int samplesRead = bytesRead / sizeof(int32_t);
  // — 2. PREPARAR DATOS PARA FFT —
  // Eliminar DC offset y escalar
  int32_t sum = 0;
  for (int i = 0; i < samplesRead; i++) {
    sum += sampleBuffer[i];
  }
  int32_t avg = sum / samplesRead;
  for (int i = 0; i < samplesRead; i++) {
    // Restar DC offset y escalar apropiadamente
    vReal[i] = (double)((sampleBuffer[i] – avg) >> 12);
    vImag[i] = 0.0;
  }
  // — 3. CALCULAR FFT —
  FFT.windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.compute(vReal, vImag, SAMPLES, FFT_FORWARD);
  FFT.complexToMagnitude(vReal, vImag, SAMPLES);
  // — 4. LIMPIAR BANDAS —
  for (int i = 0; i < NUM_BANDS; i++) {
    bandValues[i] = 0;
  }
  // — 5. AGRUPAR EN BANDAS —
  int counts[NUM_BANDS] = {0};  // Para promediar
  for (int i = 2; i < (SAMPLES/2); i++) {
    float freq = (i * 1.0 * SAMPLE_RATE) / SAMPLES;
    // Ignorar frecuencias muy bajas (ruido)
    if (freq < 30) continue;
    for (int b = 0; b < NUM_BANDS; b++) {
      if (freq >= bandFreqs[b] && freq < bandFreqs[b+1]) {
        // Usar magnitud al cuadrado para mejor respuesta
        bandValues[b] += (vReal[i] * vReal[i]);
        counts[b]++;
        break;
      }
    }
  }
  // Promediar las bandas
  for (int b = 0; b < NUM_BANDS; b++) {
    if (counts[b] > 0) {
      bandValues[b] = sqrt(bandValues[b] / counts[b]);  // RMS
    }
  }
  // — 6. APLICAR SUAVIZADO —
  static float smoothBands[NUM_BANDS] = {0};
  float smoothingFactor = 0.4;  // 0.3-0.7, más alto = más suave
  for (int b = 0; b < NUM_BANDS; b++) {
    smoothBands[b] = smoothBands[b] * smoothingFactor +
                     bandValues[b] * (1 – smoothingFactor);
    bandValues[b] = smoothBands[b];
  }
  // — 7. NORMALIZAR —
  normalizeBands();
  // — 8. ACTUALIZAR PICOS —
  for (int b = 0; b < NUM_BANDS; b++) {
    if (bandValues[b] > peakValues[b]) {
      peakValues[b] = bandValues[b];
    } else {
      peakValues[b] -= 0.8;  // Caída del pico
      if (peakValues[b] < 0) peakValues[b] = 0;
    }
  }
  // — 9. DIBUJAR EN OLED —
  display.clearDisplay();
  // Dibujar líneas guía horizontales (opcional)
  for (int y = 0; y < SCREEN_HEIGHT; y += 16) {
    for (int x = 0; x < SCREEN_WIDTH; x += 4) {
      display.drawPixel(x, y, SSD1306_WHITE);
    }
  }
  int barWidth = SCREEN_WIDTH / NUM_BANDS;
  int maxBarHeight = SCREEN_HEIGHT – 8;  // Dejar margen superior
  for (int b = 0; b < NUM_BANDS; b++) {
    int barHeight = (int)bandValues[b];
    if (barHeight > maxBarHeight) barHeight = maxBarHeight;
    int x = b * barWidth + 2;  // +2 para margen izquierdo
    int y = SCREEN_HEIGHT – barHeight – 4;  // -4 para margen inferior
    // Dibujar barra con degradado (más intensidad = más blanco)
    for (int h = 0; h < barHeight; h++) {
      int intensity = map(h, 0, barHeight, 0, 255);
      if (intensity > 240) {
        display.drawFastHLine(x, y + h, barWidth – 4, SSD1306_WHITE);
      }
    }
    // Dibujar contorno de la barra
    display.drawRect(x, y, barWidth – 4, barHeight, SSD1306_WHITE);
    // Dibujar pico (punto blanco)
    int peakY = SCREEN_HEIGHT – (int)peakValues[b] – 4;
    if (peakY > y && peakY < SCREEN_HEIGHT – 4) {
      display.fillRect(x + (barWidth/2) – 2, peakY – 1, 4, 3, SSD1306_WHITE);
    }
  }
  // Dibujar texto informativo (opcional)
  display.setCursor(0, 0);
  display.print(«FFT»);
  display.setCursor(SCREEN_WIDTH – 30, 0);
  display.print(«dB»);
  display.display();
  // Pequeña pausa para estabilidad
  delay(30);
}

 Explicación del Código

  1. Inclusión de Librerías: Se incluyen las necesarias para I2S, FFT y la OLED.

  2. Configuración de Pines: Se definen los pines GPIO para la comunicación I2S con el micrófono y para la OLED .

  3. Configuración de Muestreo: Se define la frecuencia de muestreo (SAMPLE_RATE) y el número de muestras (SAMPLES). Una frecuencia de 16kHz y 512 muestras ofrecen un buen rendimiento. La resolución en frecuencia será SAMPLE_RATE / SAMPLES = 31.25 Hz.

  4. Inicialización I2S: Las funciones i2s_install() y i2s_setpins() configuran el periférico I2S del ESP32 para recibir datos del INMP441 correctamente .

  5. Bucle Principal (loop):

    • Lectura: Se lee un bloque de audio del micrófono y se almacena en sampleBuffer.

    • Preparación: Los datos de 24 bits se convierten a double y se colocan en vReal para la FFT .

    • Cálculo FFT: Se aplica una ventana (Hamming) para mejorar la precisión y se calcula la FFT. Luego, se obtiene la magnitud de cada frecuencia .

    • Agrupación en Bandas: Las frecuencias se agrupan en 16 bandas log-root, imitando cómo percibimos el sonido.

    • Escalado: Los valores se escalan logarítmicamente para que sean más visibles en la pequeña pantalla OLED.

    • Visualización: Se dibujan barras para cada banda y un punto que representa el pico de la banda, que decae con el tiempo .

 Bandas de frecuencia

Las 12 bandas están optimizadas para voz y música:

Banda Rango (Hz) Tipo de sonido
1 20-60 Subgraves
2 60-120 Graves
3 120-200 Bajos medios
4 200-350 Medios
5 350-550 Medios
6 550-850 Medios altos
7 850-1300 Vocales
8 1300-2000 Vocales
9 2000-3000 Armónicos
10 3000-4500 Agudos
11 4500-7000 Agudos altos
12 7000-8000 Muy agudos

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *