En este tutorial se estudiará los formatos audio digital mediante la implementación práctica de un sistema de grabación y transmisión de audio en tiempo real. El objetivo principal es utilizar el microcontrolador ESP32 junto con el protocolo I2S y el micrófono digital MEMS INMP441 para capturar muestras de audio y enviarlas a un servidor remoto a través de una conexión WiFi.

Este proyecto ofrece a los estudiantes explorar la intersección entre la teoría y la aplicación práctica. Desde la configuración del protocolo I2S hasta la transmisión de datos a través de una red WiFi, tendrán la oportunidad de profundizar en cada etapa del proceso y comprender cómo se integran estos componentes para crear un sistema funcional de grabación y transmisión de audio.

Se presentan los principios fundamentales del audio digital y la comunicación inalámbrica, este proyecto también ofrece la oportunidad de desarrollar habilidades prácticas en el diseño y la implementación de sistemas embebidos. A lo largo del proceso, los estudiantes podrán enfrentarse a desafíos reales, resolver problemas técnicos y adquirir experiencia en la creación de soluciones innovadoras en el campo del audio digital y la comunicación de datos.

Conocimientos previos

  1. Familiaridad con el microcontrolador ESP32: Debes estar familiarizado con las características del ESP32, incluido su entorno de desarrollo, cómo programarlo utilizando el entorno VSCode, el de Arduino o el IDE de ESP-IDF, y cómo realizar conexiones de hardware con periféricos y sensores.
  2. Comprensión de la comunicación I2S: Es fundamental entender los fundamentos de la comunicación Inter-IC Sound (I2S), incluyendo cómo funciona, sus características principales y cómo configurarla adecuadamente en el ESP32 para la interfaz de audio.
  3. Conocimientos sobre el micrófono MEMS INMP441: Debes comprender las especificaciones y características del micrófono digital MEMS INMP441, incluyendo su funcionamiento, sensibilidad, rango dinámico, frecuencia de muestreo y otros aspectos relevantes. Vea el datasheet del dispositivo en datasheet del INMP441.
  4. Herramientas de desarrollo: Asegúrate de tener acceso a las herramientas de desarrollo necesarias, como el entorno VSCode, Arduino o el IDE de ESP-IDF, así como un multímetro para realizar pruebas de continuidad y verificar las conexiones eléctricas.
  5. Redes Wi-Fi: Comprender los principios básicos de las redes Wi-Fi, incluyendo conceptos como SSID (identificador del conjunto de servicios), seguridad Wi-Fi (por ejemplo, WPA2), y configuración de redes locales y conexiones a puntos de acceso.
  6. Protocolo TCP/IP: Familiarizarse con el protocolo de Internet (TCP/IP), que es el conjunto de protocolos de comunicación utilizado en Internet y en muchas redes locales. Esto incluye comprender los protocolos TCP (Protocolo de Control de Transmisión) e IP (Protocolo de Internet), así como la arquitectura cliente-servidor.
  7. Protocolo HTTP: Entender el Protocolo de Transferencia de Hipertexto (HTTP), que se utiliza para la comunicación entre servidores y clientes en la World Wide Web. Esto incluye comprender los métodos HTTP (GET, POST, PUT, DELETE), los códigos de estado HTTP y las cabeceras HTTP.
  8. API REST: Conocer los principios de las API REST (Transferencia de Estado Representacional), que son un estilo de arquitectura de software para sistemas distribuidos. Esto incluye entender los conceptos de recursos, URI (Identificador de Recursos Uniforme), y métodos HTTP utilizados para acceder y manipular estos recursos.
  9. Cliente HTTP en ESP32: Familiarizarse con el cliente HTTP integrado en el ESP32, que permite realizar solicitudes HTTP a servidores remotos y recibir respuestas. Esto incluye comprender cómo configurar el cliente HTTP, realizar solicitudes GET y POST, y manejar las respuestas del servidor.

Arquitectura del proyecto

En el despliegue de la aplicacion se puede observar que el software embebido contiene el software embebido asi como el micrófono INMP441 que captura las muestras de audio y las formatea como un archivo con formato de audio wav (16KHz, 16 bits, monofónico) y lo envía como un flujo por medio de la red hacia un servicio de audio que almacena el archivo.

Configuración del hardware

Lista de materiales

Debe disponer de los siguientes elementos:

Diagrama de conexión

Instrucciones de conexión

  1. Identifica los pines correspondientes en ambos dispositivos: En el ESP32, los pines para la comunicación I2S se seleccionan de acuerdo con la configuración del driver y las preferencias del usuario. Por ejemplo, asumiendo que se utilizan los pines por defecto para el driver I2S, los pines son:
    • SCK (Serial Clock): Pin 18
    • WS (Word Select): Pin 15
    • SD (Serial Data): Pin 13
    En el caso del micrófono INMP441, los pines correspondientes son generalmente SCK, WS, y SD, que se conectan a los pines SCK, WS y SD del ESP32, respectivamente.
  2. Realiza las conexiones físicas: Utiliza cables o un protoboard para conectar los pines del ESP32 a los pines correspondientes del micrófono INMP441. Asegúrate de hacer coincidir correctamente los pines SCK, WS y SD entre ambos dispositivos.
  3. Verifica la orientación de los pines: Es importante asegurarse de que los pines estén correctamente orientados y conectados para evitar posibles problemas de conexión o daños en los dispositivos.
  4. Realiza la conexión a tierra (GND): Conecta el pin de tierra (GND) del ESP32 al pin de tierra del micrófono INMP441 para proporcionar una referencia común de voltaje.
  5. Verifica la conexión: Antes de continuar con la programación y el uso del sistema, verifica visualmente todas las conexiones para asegurarte de que estén correctamente realizadas y que no haya cables sueltos o conexiones incorrectas.

Además de la configuración del software en el lado del ESP32, es necesario establecer un servicio del lado del servidor que pueda recibir y procesar los archivos de audio enviados desde el dispositivo. Aquí se detallan los pasos necesarios para configurar este servicio utilizando Express.js, un marco de aplicación web muy popular para Node.js:

  1. Instalación de Node.js y Express.js:
    • Antes de comenzar, los estudiantes deben asegurarse de tener Node.js instalado en su sistema. Node.js es un entorno de ejecución de JavaScript que permite ejecutar código JavaScript en el lado del servidor.
    • Se crea la carpeta del proyecto:
    mkdir servicio-audio
    cd servicio-audio
    
    • Se inicializa el proyecto:
    npm init
    
    Ahora llene los datos que se solicitan asi:
    This utility will walk you through creating a package.json file.
    It only covers the most common items, and tries to guess sensible defaults.
    
    See npm help init for definitive documentation on these fields
    and exactly what they do.
    
    Use npm install <pkg> afterwards to install a package and
    save it as a dependency in the package.json file.
    
    Press ^C at any time to quit.
    description: Servicio de recepcion de archivos de audio wav
    entry point: (index.js)
    test command:
    git repository:
    keywords:
    author: Alvaro Salazar
    license: (ISC) MIT
    About to write to C:\Users\lashe\temporal\package.json:
    
    {
      "name": "servicio-audio",
      "version": "1.0.0",
      "description": "Servicio de recepcion de archivos de audio wav",
      "main": "index.js",
      "scripts": {
        t": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "Alvaro Salazar",
      "license": "MIT"
    }
    
    
    Is this OK? (yes)
    
    ``
    • Luego, debe instalar Express.js, un marco de aplicación web para Node.js, utilizando npm (Node Package Manager), que generalmente se instala junto con Node.js. Esto se puede hacer ejecutando el siguiente comando en la terminal:
    npm install express
    
  2. Configuración del servidor Express:
    • Una vez que Express.js esté instalado, puedes comenzar a configurar el servidor. Esto implica crear un archivo JavaScript (por ejemplo, app.js) y configurar un servidor Express básico en él. Puedes usar el bloc de notas o aplicacion similar:
    notepad app.js
    
    Se pregunta si desea crear el archivo, de clic en Si.
    • En el archivo app.js, debes importar Express y crear una instancia de la aplicación Express. Luego, puedes definir las rutas y los manejadores de solicitudes necesarios para manejar las solicitudes entrantes.
    const express = require('express');
    const app = express();
    const fs = require('fs');
    const port = 8888;
    
    app.use(express.raw({type: 'audio/wav', limit: '50mb'}));
    
    • Por ejemplo, puedes definir una ruta POST para manejar las solicitudes de carga de archivos de audio desde el ESP32 al servidor (/uploadAudio). Después de procesar el archivo de audio, el servidor debe enviar una respuesta al ESP32 para indicar que la solicitud se procesó correctamente.
      app.post('/uploadAudio', (req, res) => {
        const audioData = req.body;
        console.log(`Recibido audio con tamaño: ${audioData.length}`);
        fs.writeFileSync('audio_received.wav', audioData);
        res.send('Audio recibido con exito');
      });
    
  3. Ahora debes iniciar el servicio poniéndolo a escuchar peticiones de clientes asi:
      app.listen(port, () => {
        console.log(`Servidor escuchando en http://localhost:${port}`);
      });
    
  4. Enviar respuesta al ESP32:
    • Después de procesar los archivos de audio, el servidor debe enviar una respuesta al ESP32 para indicar si la solicitud se procesó correctamente o si hubo algún error.
     // Middleware de manejo de errores
     app.use((err, req, res, next) => {
       console.error('Se ha producido un error:', err);
       if (err.type === 'entity.too.large') 
         return res.status(413).send('Archivo demasiado grande');
    
       // Si no se ha manejado el error especificamente, envia una respuesta generica
       return res.status(500).send('Ocurrio un error en el servidor');
     });
    
    Se debe guardar el archivo para su posterior ejecución.
  5. Iniciar el servicio:
    • Se inicia el servicio invocando la siguiente linea:
    node app.js
    
    `` Si el sistema solicita permisos para redes publicas y privadas seleccione todo y acepte. Debe salir la siguiente salida en pantalla:
    Servidor escuchando en http://localhost:8888
    
    ``
  6. Pruebas y depuración:
    • Una vez configurado el servidor Express, debes realizar pruebas exhaustivas para asegurarse de que funcione según lo previsto. Esto implica enviar solicitudes de prueba desde el ESP32 y verificar si el servidor responde correctamente.
    • Crea un archivo llamado archivo.wav en la carpeta del proyecto:
    notepad archivo.wav
    
    Coloca cualquier contenido y guarda el archivo.
    • Ahora puedes usar el cmdlet de PowerShell llamado Invoke-WebRequest (También puedes usar programas para pruebas de integración como Postman, Curl, etc.):
    Invoke-WebRequest -Uri http://localhost:8888/uploadAudio -Method Post -Headers @{"Content-Type" = "audio/wav"} -InFile "archivo.wav"
    
    Y deberás recibir el archivo en la carpeta, además obtendrás una salida similar a esta:
    StatusCode        : 200
    StatusDescription : OK
    Content           : Audio recibido con exito
    RawContent        : HTTP/1.1 200 OK
                        Connection: keep-alive
                        Keep-Alive: timeout=5
                        Content-Length: 24
                        Content-Type: text/html; charset=utf-8
                        Date: Tue, 16 Apr 2024 02:54:51 GMT
                        ETag: W/"18-TWR+47Z6W0ligVp9pm1UkjaqyRU...
    Forms             : {}
    Headers           : {[Connection, keep-alive], [Keep-Alive, timeout=5], [Content-Length, 24], [Content-Type,
                        text/html; charset=utf-8]...}
    Images            : {}
    InputFields       : {}
    Links             : {}
    ParsedHtml        : mshtml.HTMLDocumentClass
    RawContentLength  : 24
    
    ``
    • Si hay algún problema, debes utilizar herramientas de depuración como console.log() en Node.js y herramientas de desarrollo web del navegador para identificar y solucionar cualquier error.

La siguiente es la estructura del programa diseñado para el ESP32 que envia continuamente el flujo de audio capturado y lo envia como un archivo wav a un servicio HTTP:

Ahora el diagrama de flujo de la aplicacion embebida es la siguiente:

/**
 *  MIT License

    Copyright (c) 2024 Alvaro Salazar

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
*/


#include "Arduino.h"
#include "driver/i2s.h"
#include "WiFi.h"

// Definición de pines para el micrófono INMP441
#define I2S_SCK_PIN  18             // Pin Serial Clock (SCK) 
#define I2S_WS_PIN   15             // Pin Word Select (WS)
#define I2S_SD_PIN   13             // Pin Serial Data (SD)

// Configuración del driver I2S para el micrófono INMP441
#define I2S_PORT           I2S_NUM_0 // I2S port number (0)
#define SAMPLE_BUFFER_SIZE 1024      // Tamaño del buffer de muestras (1024)
#define I2S_SAMPLE_RATE    (16000)   // Frecuencia de muestreo (16 kHz)
#define RECORD_TIME        (10)      // segundos de grabación 

// Credenciales de la red WiFi
const char* ssid = "virus2";         // Reemplaza con el nombre de tu red WiFi
const char* password = "a1b2c3d4";   // Reemplaza con tu contraseña WiFi

// Servidor HTTP (IP y puerto)
const char* serverName = "192.168.137.1";
const int port = 8888;

// Constantes para la cabecera del archivo WAV
const int HEADERSIZE = 44;

// Cliente WiFi y cliente HTTP
WiFiClient cliente;

// Prototipos de funciones
void micTask(void* parameter);
void setWavHeader(uint8_t* header, int wavSize);

// Buffer de muestras de audio (32 bits) y buffer de muestras de audio (8 bits)
int32_t i2s_read_buffer[SAMPLE_BUFFER_SIZE];
int8_t i2s_read_buff8[SAMPLE_BUFFER_SIZE*sizeof(int32_t)];

// Estructura de parámetros para la tarea del micrófono
struct MicTaskParameters {
    int duracion;
    int frecuencia;
    int bufferSize;
} micParams;



// Implementacion de las funciones


/**
 * @brief Función de configuración del driver I2S para el micrófono INMP441
 */
void i2s_config_setup() {
    const i2s_config_t i2s_config = {
        .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),  // Modo RX (recepción)
        .sample_rate = I2S_SAMPLE_RATE, // Frecuencia de muestreo
        .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // Ajustado para alineación de 32 bits
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // El INMP441 es mono, usar solo canal izquierdo
        .communication_format = I2S_COMM_FORMAT_STAND_I2S, // Ajustado para alineación de 32 bits (MSB)
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt level 1 (1-7) significa prioridad baja
        .dma_buf_count = 40,    // Número de buffers, 8
        .dma_buf_len = 1024,    // Tamaño de cada buffer
        .use_apll = false,      // No usar APLL para la frecuencia de muestreo (no es necesario, pero es más preciso)
    };

    // Configuración de pines para I2S (I2S0) en el ESP32
    const i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_SCK_PIN,          // Serial Clock (SCK): Señal de reloj de bits
        .ws_io_num = I2S_WS_PIN,            // Word Select (WS): Señal de reloj de muestreo (LRCLK)
        .data_out_num = I2S_PIN_NO_CHANGE,  // No se usa en modo RX (recepción): DOUT
        .data_in_num = I2S_SD_PIN  // DIN   // Serial Data (SD): Datos de audio
    };

    // Instalar el driver de I2S con la configuración anterior y sin buffer de eventos (0) ni callback (NULL)
    if(ESP_OK != i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL)) {
        Serial.println("i2s_driver_install: error");
    }
    
    // Configurar los pines del ESP32 para el I2S (I2S0) con la configuración anterior (pin_config)
    if(ESP_OK != i2s_set_pin(I2S_PORT, &pin_config)) {
        Serial.println("i2s_set_pin: error");
    }
}

/**
 * @brief Función de configuración de la board ESP32 y conexión a la red WiFi
 * Se ejecuta una sola vez al inicio del programa
 */
void setup() {

    Serial.begin(230400);                   // Iniciar el puerto serie a 230400 baudios

    WiFi.begin(ssid, password);             // Conectar a la red WiFi con las credenciales proporcionadas
    Serial.println("Conectando a WiFi");

    while (WiFi.status() != WL_CONNECTED) { // Esperar a que se conecte a la red WiFi
        delay(1000);                        // Esperar un segundo
        Serial.println(".");                // Imprimir un punto cada segundo
    }
    Serial.println("Conectado a WiFi");     // Imprimir mensaje de conexión exitosa
    Serial.print("IP local: ");             // Imprimir mensaje de dirección IP local
    Serial.println(WiFi.localIP());         

    if(!cliente.connect(serverName, port)){ // Conectar al servidor HTTP en el puerto 8888 (HTTP)
     Serial.println("Conexion fallida!");   // Imprimir mensaje de error si la conexión falla
     delay(10000);                          // Esperar 10 segundos
     return;                                // Salir de la función setup()
   }
   Serial.println("Conectado al servidor"); // Imprimir mensaje de conexión exitosa

   // Crear una estructura de parámetros para la tarea del micrófono
    
    micParams.duracion = RECORD_TIME;  // Duración de la grabación en segundos
    micParams.frecuencia = I2S_SAMPLE_RATE;  // Frecuencia de muestreo de 16 kHz (16000 muestras por segundo)
    micParams.bufferSize = SAMPLE_BUFFER_SIZE;  // Tamaño del buffer de muestras (1024 muestras)
    Serial.println("Configuracion de la tarea del microfono");
    Serial.println("Duracion: " + String(micParams.duracion));
    Serial.println("Frecuencia: " + String(micParams.frecuencia));
    Serial.println("Tamaño del buffer: " + String(micParams.bufferSize));
   // Crea tarea para leer muestras de audio del micrófono y enviarlas al servidor
   // - Asigna 10000 bytes de stack, los bytes de stack deben ser suficientes para la tarea y sirven 
   //   para guardar las variables locales de la tarea
   // - Los parámetros de la tarea se pasan como un puntero en la estructura micParams
   // - La prioridad 1 es la más baja (0-24), y el núcleo asignado a la tarea es el núcleo 1 del ESP32. 
   //   (La prioridad 0 se reserva para el sistema)
   xTaskCreatePinnedToCore(micTask, "micTask", 10000, &micParams, 1, NULL, 1);

}


/**
 * @brief Función principal del programa (bucle infinito)
 */
void loop() {
  // No se hace nada en el loop
}


/**
 * @brief Tarea para leer muestras de audio del micrófono I2S y enviarlas a un servidor HTTP
 * @param parameter Puntero a los parámetros de la tarea (no se usa)
 */
void micTask(void* parameter) {
    struct MicTaskParameters {
        int duracion;
        int frecuencia;
        int bufferSize;
    };
    // Convertir el puntero a los parámetros de la tarea a un puntero de tipo MicTaskParameters
    MicTaskParameters * params = (MicTaskParameters *) parameter;
    int duracion = params->duracion;        // Duración de la grabación en segundos
    int frecuencia = params->frecuencia;    // Frecuencia de muestreo de 16 kHz (16000 muestras por segundo)
    int bufferSize = params->bufferSize;    // Tamaño del buffer de muestras (1024 muestras)
    int numBuffers = (int)(params->duracion * params->frecuencia / bufferSize);  // Número de buffers a enviar al servidor
    int recordBytes = numBuffers * params->bufferSize * sizeof(int16_t);  
    
    i2s_config_setup();             // Configurar el I2S para la lectura de muestras de audio
    
    size_t elements_read=0;         // Número de elementos leídos del I2S
    uint8_t header[HEADERSIZE];     // Buffer para la cabecera del archivo WAV (44 bytes)

    setWavHeader(header, recordBytes); // Configurar la cabecera del archivo WAV (44 bytes)

    // Enviar la petición HTTP POST al servicio indicando el tamaño del archivo WAV
    cliente.println("POST /uploadAudio HTTP/1.1");                          // Método POST y ruta del servidor 
    cliente.println("Content-Type: audio/wav");                             // Tipo de contenido de audio WAV
    cliente.println("Content-Length: " + String(recordBytes+HEADERSIZE));   // Usa el tamaño del archivo aqui
    cliente.println("Host: " + String(serverName));   // Especifica el host al que se está realizando la solicitud.
    cliente.println("Connection: keep-alive");        // Establece la conexión para mantenerse viva después de que se complete la solicitud actual.
    cliente.println();                 // Línea en blanco para indicar el fin de las cabeceras HTTP
    cliente.write(header, HEADERSIZE); // Este es el body de la petición HTTP POST (la cabecera del archivo WAV)
    
    //Eliminamos los primeros 8 buffers para que no se envie ruido (se descartan los primeros 512ms de grabacion)
    for(int i=0; i<8; i++){
        i2s_read(I2S_PORT, &i2s_read_buffer, SAMPLE_BUFFER_SIZE*sizeof(int32_t), &elements_read, portMAX_DELAY);
    }
    
    // Leer muestras de audio del micrófono y enviarlas al servidor
    for(int j = 0; j < numBuffers; j++ ){
        // Leer muestras de audio del micrófono (I2S) y enviarlas al servidor (HTTP) 
        // un total de SAMPLE_BUFFER_SIZE veces es decir 1024 veces ya que el buffer es de 1024 muestras de 32 bits
        // cada una y un portMAX_DELAY para que espere indefinidamente hasta que se llene el buffer de muestras
        esp_err_t result = i2s_read(I2S_PORT, &i2s_read_buffer, SAMPLE_BUFFER_SIZE*sizeof(int32_t), &elements_read, portMAX_DELAY);
        if (result != ESP_OK) {
            Serial.println("Error en la lectura de I2S");
            while(1);
        } else {
            for(int i=0; i<SAMPLE_BUFFER_SIZE; i++){                            // Convertimos los datos de 32 bits a 16 bits (2 bytes por muestra)
                i2s_read_buff8[2*i]   = (int8_t) (i2s_read_buffer[i]>>24&0xFF); // Convertimos los datos de 32 bits (8 bits mas altos) a 8 bits MSB
                i2s_read_buff8[2*i+1] = (int8_t) (i2s_read_buffer[i]>>16&0xFF); // Convertimos los datos de 32 bits (8 bits del bit 23 al 16) a 8 bits LSB
            }
            cliente.write((uint8_t *) i2s_read_buff8, SAMPLE_BUFFER_SIZE*sizeof(int16_t));
        }
    }   
    cliente.flush();

    Serial.println("Se enviaron " + String(recordBytes+HEADERSIZE) + " bytes al servicio");

    unsigned long tiempoInicial = millis();     // Guardar el tiempo actual en milisegundos
    while(cliente.available()==0){              // Mientras no haya datos disponibles
        if(millis() - tiempoInicial > 5000){    // Reviso si ya pasaron mas de 5 segundos
            Serial.println("Expiro el tiempo de espera");
            cliente.stop();                     // Detengo la conexion al servicio
            while(1);                           // Me quedo en un bucle infinito
        }
    }

    while(cliente.available()){                         // Mientras haya datos disponibles
        String linea = cliente.readStringUntil('\r');   // Leer una línea de texto hasta el enter
        Serial.println(linea);                          // Imprimir la línea de texto
    }
    Serial.println("Fin de conexion");
    cliente.stop();                             // Detener la conexión al servidor
    while(1);                                   // Bucle infinito para detener la tarea
}



/**
 * @brief Función para configurar el encabezado de un archivo WAV
 * @param header Puntero a la cabecera del archivo WAV (un arreglo de 44 bytes)
 * @param wavSize Tamaño del archivo WAV en bytes (sin incluir la cabecera)
 */
void setWavHeader(uint8_t* header, int wavSize){
  int fileSize = wavSize + HEADERSIZE - 8; // Tamaño del archivo WAV en bytes (sin incluir los primeros 8 bytes)
  Serial.println("Tamaño del archivo: " + String(fileSize));
  // Formato de la cabecera del archivo WAV (44 bytes)
  header[0] = 'R';  // RIFF: Marca el archivo como archivo riff (Resource Interchange File Format)
  header[1] = 'I';  // |
  header[2] = 'F';  // |
  header[3] = 'F';  // |______
  header[4] = (uint8_t)(fileSize & 0xFF);         // Tamaño de todo el archivo - 8 bytes, en bytes (32-bit integer)
  header[5] = (uint8_t)((fileSize >> 8) & 0xFF);  // |
  header[6] = (uint8_t)((fileSize >> 16) & 0xFF); // |
  header[7] = (uint8_t)((fileSize >> 24) & 0xFF); // |______   
  header[8] = 'W';  // WAVE: Cabecera del tipo de archivo
  header[9] = 'A';  // |
  header[10] = 'V'; // |
  header[11] = 'E'; // |______
  header[12] = 'f'; // fmt: Marcador de fragmento de formato. Incluye null al final
  header[13] = 'm'; // |
  header[14] = 't'; // |
  header[15] = ' '; // |______
  header[16] = 0x10; // Longitud del formato de los datos (16 bytes)
  header[17] = 0x00; // |
  header[18] = 0x00; // |
  header[19] = 0x00; // |______
  header[20] = 0x01; // Tipo de formato 1 (PCM: Pulse Code Modulation)
  header[21] = 0x00; // |______
  header[22] = 0x01; // Numero de canales: 1
  header[23] = 0x00; // |______
  header[24] = 0x80; // Frecuencia de muestreo: 00003E80 (16000 Hz)
  header[25] = 0x3E; // |
  header[26] = 0x00; // |
  header[27] = 0x00; // |______
  header[28] = 0x00; // (Frecuencia de muestreo * (Numero de Bits por muestra) * Numero de Canales) / 8.
  header[29] = 0x7D; // | 0x00007D00 = 32000 = 16000 * 16 * 1 / 8
  header[30] = 0x00; // | 
  header[31] = 0x00; // |______
  header[32] = 0x02; // (Numero de bits por muestra * Numero de canales) / 8 = (1: 8 bit mono) (2: 8 bit stereo o 16 bit mono) (4: 16 bit stereo)
  header[33] = 0x00; // |______
  header[34] = 0x10; // Bits por muestra: 16
  header[35] = 0x00; // |______
  header[36] = 'd';  // Data: Marca el comienzo de una seccion de datos
  header[37] = 'a';  // |
  header[38] = 't';  // |
  header[39] = 'a';  // |______
  header[40] = (uint8_t)(wavSize & 0xFF);         // Tamaño de la seccion de datos, en bytes (32-bit integer)
  header[41] = (uint8_t)((wavSize >> 8) & 0xFF);  // |
  header[42] = (uint8_t)((wavSize >> 16) & 0xFF); // |
  header[43] = (uint8_t)((wavSize >> 24) & 0xFF); // |______
  
}

Explicación general del código del sistema embebido

Funciones principales del programa

Etapas de la aplicación

  1. Configuración de pines y constantes: Se definen los pines utilizados para la comunicación I2S con el micrófono INMP441, así como algunas constantes para la configuración del driver I2S, el tamaño del búfer de muestras, la frecuencia de muestreo y el tiempo de grabación.
  2. Credenciales de la red WiFi y servidor HTTP: Se especifican el nombre de la red WiFi y la contraseña, así como la dirección IP y el puerto del servidor HTTP al que se enviarán los datos de audio.
  3. Inicialización y conexión a la red WiFi: En la función setup(), se inicia la comunicación serial y se intenta conectar el ESP32 a la red WiFi especificada. Se espera hasta que la conexión sea exitosa antes de continuar.
  4. Configuración del micrófono y del driver I2S: Se configura el driver I2S del ESP32 para la comunicación con el micrófono INMP441. Esto incluye la configuración de la frecuencia de muestreo, el formato de los datos y otros parámetros relevantes.
  5. Creación de la tarea para la captura y envío de audio: Se crea una tarea (micTask) que se encarga de capturar las muestras de audio del micrófono y enviarlas al servidor HTTP. Se utilizan parámetros como la duración de la grabación, la frecuencia de muestreo y el tamaño del búfer de muestras.
  6. Configuración de la cabecera del archivo WAV: Antes de enviar los datos de audio al servidor, se configura la cabecera del archivo WAV que acompañará a los datos de audio. Esta cabecera contiene información sobre el formato del archivo y la longitud de los datos de audio.
  7. Captura y envío de datos de audio al servidor: En la tarea micTask, se leen las muestras de audio del micrófono utilizando el driver I2S y se envían al servidor HTTP a través de una conexión TCP. Se utilizan bucles para leer y enviar los datos de audio en bloques de muestras.
  8. Gestión de errores y finalización de la tarea: Se implementa una lógica de manejo de errores para detectar problemas durante la captura y el envío de audio. Si se produce un error, se detiene la tarea y se imprime un mensaje de error.

Explicación de la conversión de datos

Bucle de lectura y envío de datos:

El bucle for recorre un número determinado de veces (numBuffers), que representa la cantidad de bloques de muestras de audio que se van a leer para que cumplan con la duración requerida y sean enviados al servidor.

		
// Leer muestras de audio del micrófono y enviarlas al servidor
for(int j = 0; j < numBuffers; j++ ){
	// Leer muestras de audio del micrófono (I2S) y enviarlas al servidor (HTTP) 
	// un total de SAMPLE_BUFFER_SIZE veces es decir 1024 veces ya que el buffer es de 1024 muestras de 32 bits
	// cada una y un portMAX_DELAY para que espere indefinidamente hasta que se llene el buffer de muestras
	esp_err_t result = i2s_read(I2S_PORT, &i2s_read_buffer, SAMPLE_BUFFER_SIZE*sizeof(int32_t), &elements_read, portMAX_DELAY);
	if (result != ESP_OK) {
		Serial.println("Error en la lectura de I2S");
		while(1);
	} else {
		for(int i=0; i<SAMPLE_BUFFER_SIZE; i++){                            // Convertimos los datos de 32 bits a 16 bits (2 bytes por muestra)
			i2s_read_buff8[2*i]   = (int8_t) (i2s_read_buffer[i]>>24&0xFF); // Convertimos los datos de 32 bits (8 bits mas altos) a 8 bits MSB
			i2s_read_buff8[2*i+1] = (int8_t) (i2s_read_buffer[i]>>16&0xFF); // Convertimos los datos de 32 bits (8 bits del bit 23 al 16) a 8 bits LSB
		}
		cliente.write((uint8_t *) i2s_read_buff8, SAMPLE_BUFFER_SIZE*sizeof(int16_t));
	}
}
		
  1. Lectura de muestras de audio del micrófono:
    • Se utiliza la función i2s_read() para leer muestras de audio del micrófono a través del protocolo I2S.
    • Esta función espera hasta que el buffer de muestras esté lleno (portMAX_DELAY), lo que significa que esperará indefinidamente hasta que se capturen suficientes muestras.
    • Las muestras de audio se almacenan en el búfer i2s_read_buffer.
  2. Verificación de errores:
    • Se verifica si la lectura de muestras de audio fue exitosa mediante la variable result.
    • Si ocurre un error durante la lectura de I2S (indicado por result != ESP_OK), se imprime un mensaje de error y el programa entra en un bucle infinito (while(1)). Esto detiene la ejecución del programa y permite identificar y solucionar el problema.
  3. Conversión de formato de datos:
    • Si la lectura de muestras de audio fue exitosa, se procede a convertir las muestras de formato de 32 bits a 16 bits. El microfono INMP441 entrega 24 bits pero el completa con ceros hasta completar 32 bits. Como solo manejamos 16 bits, se van a descartar 8 bits mas bajos de los 24 bits.
    • Esto se realiza mediante un bucle for que recorre todas las muestras en el buffer i2s_read_buffer.
    • Cada muestra de 32 bits se divide en dos muestras de 8 bits: los 8 bits más altos (MSB) y los 8 bits más bajos (LSB). Con estas dos muestras tenemos el numero de 16 bits de audio.
    • Estas muestras de 8 bits se almacenan en el buffer i2s_read_buff8, que está formateado como un arreglo de 8 bits. Una muestra de 16 bits correspondería a las posiciones 0 y 1, la siguiente en las posiciones 2 y 3, y asi sucesivamente.
  4. Envío de datos al servidor:
    • Una vez que todas las muestras de audio han sido convertidas al formato adecuado, se utilizan las funciones cliente.write() para enviar los datos al servidor HTTP.
    • Se envía el búfer i2s_read_buff8, que contiene las muestras de audio convertidas a 16 bits, con un tamaño específico que corresponde al tamaño de un bloque de muestras (SAMPLE_BUFFER_SIZE = 1024) multiplicado por 2 ya que se requieren 2 bytes por muestra (sizeof(int16_t)) obteniendo asi el tamaño del buffer a enviar.

En la imagen se muestra un diagrama de timing con la estructura de cada muestra y como se procesa hasta obtener los bytes a enviar.

El codigo de ambos, del sistema embebido y del servicio de audio lo puede clonar desde el siguiente repositorio Repositorio del proyecto.

Clonación del proyecto completo:

  1. Instalación de Git:
    • Descarga e instala Git desde git-scm.com.
    • Durante la instalación, asegúrate de seleccionar la opción para agregar Git al PATH del sistema.
  2. Clonar el Repositorio:
    • Abre el PowerShell o la terminal de tu sistema.
    • Ejecuta el siguiente comando para clonar el repositorio:
      git clone https://github.com/alvaro-salazar/esp32-audio-i2s-wav-http
      
  3. Abrir Proyecto ESP32 en VSCode:
    • Abre Visual Studio Code.
    • Ve al menú "File" y selecciona "Open Folder" (Abrir Carpeta).
    • Navega hasta la carpeta "I2S_INPUT" dentro del repositorio clonado.
    • Selecciona la carpeta "I2S_INPUT" y haz clic en "Abrir" para abrir el proyecto PlatformIO para ESP32.
  4. Abrir Proyecto de Servicio de Audio en VSCode:
    • Abre una nueva instancia de Visual Studio Code.
    • Ve al menú "File" y selecciona "Open Folder" (Abrir Carpeta).
    • Navega hasta la carpeta "audioService" dentro del repositorio clonado.
    • Selecciona la carpeta "audioService" y haz clic en "Abrir" para abrir el proyecto de servicio de audio hecho con Express (Node.js).
    • Seleccione el menu Terminal y de clic en New Terminal.
    • En la linea de comandos ejecute npm install.

Ejecución del proyecto

  1. Conexión del ESP32:
    • Se debe asegurar que el ESP32 esté conectado correctamente al puerto USB del computador.
  2. Compilación del Código:
    • Abrir el proyecto en Visual Studio Code.
    • Abrir el archivo platformio.ini y verificar su configuración para la board ESP32.
    • Hacer clic en el icono de PlatformIO en la barra lateral izquierda.
    • Seleccionar la opción "Build" para compilar el código. Esto generará el firmware necesario para cargar en el ESP32.
  3. Carga del Firmware:
    • Una vez compilado con éxito, conectar el ESP32 a su computador mediante un cable USB.
    • Hacer clic en el icono de PlatformIO en la barra lateral izquierda.
    • Seleccionar la opción "Upload" para cargar el firmware en el ESP32. Esto transferirá el firmware compilado al ESP32 y lo ejecutará automáticamente.
  4. Observación de la Ejecución:
    • Debe estar ejecutándose previamente el servicio de audio, revise el numero IP de la maquina donde se ejecuta, este numero debe ingresarlo en codigo del firmware del ESP32.
    • Abrir el monitor serial en Visual Studio Code para observar la salida del programa y verificar que se esté ejecutando correctamente.
    • Verificar que el ESP32 esté conectado a la red WiFi según lo configurado en el código.
    • Observar cualquier mensaje de error o información de depuración que se imprima en el monitor serial.
  5. Pruebas Funcionales:
    • Verificar que el ESP32 pueda capturar muestras de audio del micrófono INMP441.
    • Asegurarse de que las muestras de audio se estén enviando correctamente al servidor HTTP especificado. Abra la carpeta del servicio de audio y allí deberá encontrar el archivo de audio. Ábralo con un programa como Audacity.
    • Realizar pruebas adicionales según los requisitos específicos del proyecto para validar la funcionalidad del programa.