En otro de mis posts describí cómo transmitir datos UDP desde la ESP32-CAM para comprobar que toda la red funcionara, y ahora no vamos a enviar datos estáticos, sino transmitir audio en tiempo real desde un micrófono usando la ESP32-CAM y el micrófono ICS-43434.

ESP32-CAM montada en Protoboard. Un micrófono ICS-4343 está conectado a ella, y se está transmitiendo el audio en tiempo real por UDP

El receptor será un script de Python en una computadora con Windows, pero obviamente puedes hacerlo con cualquier lenguaje de programación y tecnología, incluso debe ser posible escucharlo en otra esp32-cam

Te recomiendo que primero leas el post sobre la transmisión de datos estáticos para que puedas hacer una prueba a la vez, o si quieres saltemos de una vez a la transmisión del audio de un micrófono desde la ESP32-CAM usando UDP.

¿Por qué UDP?

Cuando usaba TCP con WebSockets funcionaba pero en ocasiones la recepción era lenta y la cola se llenaba, por ello opté por UDP.

A mí me interesa la velocidad, no la garantía de entrega. UDP es tan rápido como para darme la opción de decir “Disculpe, no lo escuché, por favor repita lo que dijo” al receptor en tiempo real por si no lo logro escuchar.

El sample rate en Hertz

He probado 3 calidades:

  • 10khz
  • 44.1khz
  • 48khz

La mejor es, obviamente, la de 48khz, pero entre mejor calidad habrá más datos que enviar por la red, así que toma en cuenta ese factor.

Yo he dejado la calidad en 10khz y funciona bien para la voz humana.

Este sample rate se cambia en SAMPLE_RATE_MICROFONO de microfono.h y en SAMPLE_RATE = 48000 del script de Python. Se especifica en Hertz.

Inicializar micrófono y pines I2s

La ESP32-CAM tiene pocos pines GPIO ya que varios de ellos están ocupados por la cámara. Yo planeo usar la cámara en conjunto con el micrófono más adelante, pero no planeo usar la Micro SD así que he usado algunos pines exclusivos.

El archivo microfono.h queda como se ve a continuación.


#include "driver/i2s.h"
#include "Arduino.h"
#define I2S_WS_MICROFONO 13   // LRCK
#define I2S_SD_MICROFONO 12   // DATA
#define I2S_SCK_MICROFONO 15 // BCL
#define SAMPLE_RATE_MICROFONO 48000
#define PIN_I2S_MICROFONO I2S_NUM_1
#define PUERTO_RECEPTOR_ICS 12345
#define IP_RECEPTOR_ICS "192.168.0.5"

void inicializar_i2s_microfono()
{

  i2s_config_t i2s_config = {
      .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
      .sample_rate = SAMPLE_RATE_MICROFONO,
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_STAND_I2S,
      .intr_alloc_flags = 0,
      .dma_buf_count = 4,
      .dma_buf_len = 256,
      .use_apll = false};
  ;

  i2s_pin_config_t pin_config = {
      .bck_io_num = I2S_SCK_MICROFONO,
      .ws_io_num = I2S_WS_MICROFONO,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = I2S_SD_MICROFONO};

  esp_err_t err = i2s_driver_install(PIN_I2S_MICROFONO, &i2s_config, 0, NULL);
  if (err != ESP_OK)
  {
    Serial.printf("Error al instalar I2S mic: %d\n", err);
  }
  err = i2s_set_pin(PIN_I2S_MICROFONO, &pin_config);
  if (err != ESP_OK)
  {

    Serial.printf("Error al configurar pines mic: %d\n", err);
  }
  Serial.println("Driver I2S instalado y configurado mic");
}

La asignación de pines queda así:

ESP32-CAMICS-43434Definición en código
GPIO 12SDI2S_SD_MICROFONO
GPIO 13WSI2S_WS_MICROFONO
GPIO 15SCKI2S_SCK_MICROFONO
GNDGND
VCCVIN

Credenciales conexión internet

Coloca tus credenciales en credenciales.h que se ve así:

#define NOMBRE_RED_WIFI "nombre"
#define PASSWORD_RED_WIFI "contraseña"

Código de la ESP32-CAM

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include "credenciales.h"
#include "microfono.h"
#define PUERTO_UDP 12345
#define UDP_PACKET_SIZE 1024
WiFiUDP udp;

void conectar_wifi()
{
  Serial.println("Conectando wifi...");
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(true, true);
  delay(100);
  wl_status_t resultado = WiFi.begin(NOMBRE_RED_WIFI, PASSWORD_RED_WIFI);
  Serial.printf("El resultado al conectar wifi es %d", resultado);
  WiFi.setTxPower(WIFI_POWER_8_5dBm);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(300);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP());
}

void setup()
{
  Serial.begin(115200);
  ledcSetup(0, 5000, 8);
  ledcAttachPin(4, 0);
  conectar_wifi();
  inicializar_i2s_microfono();
  udp.begin(PUERTO_UDP);
  ledcWrite(0, 128);
  delay(200);
  ledcWrite(0, 1);
}

static uint8_t buffer[UDP_PACKET_SIZE];
void loop()
{
 
  // Comienza envío de audio

  size_t bytesLeidosDeMicrofono = 0;
  esp_err_t resultadoAlGrabarMicrofono = i2s_read(PIN_I2S_MICROFONO, buffer, UDP_PACKET_SIZE, &bytesLeidosDeMicrofono, portMAX_DELAY);
  if (resultadoAlGrabarMicrofono != ESP_OK)
  {
    Serial.printf("Error leyendo mic: %d\n", resultadoAlGrabarMicrofono);
  }
  if (bytesLeidosDeMicrofono == UDP_PACKET_SIZE)
  {
    udp.beginPacket(IP_RECEPTOR_ICS, PUERTO_RECEPTOR_ICS);
    udp.write(buffer, UDP_PACKET_SIZE);
    udp.endPacket();
  }
  else
  {

    Serial.printf("bytesRead != maxLen\n");
  }
  // Si el delay es muy grande no te vas a escuchar
  delay(10);
  //Serial.println("Escrito");
}

El fragmento importante es cuando leemos del micrófono con i2s_read:

esp_err_t resultadoAlGrabarMicrofono = i2s_read(PIN_I2S_MICROFONO, buffer, UDP_PACKET_SIZE, &bytesLeidosDeMicrofono, portMAX_DELAY);

Y luego lo enviamos por UDP:

udp.beginPacket(IP_RECEPTOR_ICS, PUERTO_RECEPTOR_ICS);
udp.write(buffer, UDP_PACKET_SIZE);
udp.endPacket();

Todo reside en buffer ya que primero lo llenamos con i2s_read y luego lo enviamos con udp.write

Recuerda que en este ejemplo el tamaño del paquete enviado por UDP es de 1024 bytes, si tienes problemas con la red recomiendo ajustarlo sin sobrepasar el MTU.

Código Python receptor

El código es muy parecido a cuando recibíamos los bytes estáticos, solo que ahora usamos pyaudio para reproducir los bytes que nos van llegando.

Recuerda que tanto el código del receptor como el del emisor deben tener la misma configuración del tamaño de paquete, frecuencia, etcétera.

import socket
import numpy as np
import pyaudio
import struct
# Debe coincidir con:
# udp.beginPacket("192.168.0.5", PUERTO_RECEPTOR_ICS);
LOCAL_IP = "192.168.0.5"
# Debe coincidir con
# #define PUERTO_RECEPTOR_ICS 12345
LOCAL_PORT = 12345
# Debe coincidir con
# #define UDP_PACKET_SIZE 2
BUFFER_SIZE = 1024
# Debe coincidir con #define SAMPLE_RATE_MICROFONO 10000
SAMPLE_RATE = 48000


sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

try:
    sock.bind((LOCAL_IP, LOCAL_PORT))
    print(f"Servidor UDP iniciado y escuchando en {LOCAL_IP}:{LOCAL_PORT}")
except Exception as e:
    print(f"Error al asignar el socket: {e}")
    exit()

p = pyaudio.PyAudio()

stream = p.open(format=pyaudio.paInt16,
                channels=1,
                rate=SAMPLE_RATE,
                output=True,
                frames_per_buffer=BUFFER_SIZE // p.get_sample_size(pyaudio.paInt16))

GAIN_FACTOR_PYTHON = 5
while True:
    try:
        bytes_data, address = sock.recvfrom(
            BUFFER_SIZE * 2)  # Tal vez remover el *2?

        if len(bytes_data) == BUFFER_SIZE:
            audio_array = np.frombuffer(bytes_data, dtype=np.int16)

            
            amplified_array = audio_array * GAIN_FACTOR_PYTHON

            amplified_bytes = amplified_array.astype(np.int16).tobytes()

            # 4. Escribir los bytes amplificados al stream
            stream.write(amplified_bytes)

            # Escribir los datos binarios directamente al stream de audio
            # stream.write(bytes_data)

            print(f"Recibidos {len(bytes_data)} bytes de audio de {address}")

        else:
            print(
                f"Paquete recibido con tamaño inesperado: {len(bytes_data)} bytes. Esperado: {BUFFER_SIZE}")

        # print(f"Recibimos {len(bytes_data)} bytes de {address} y son {bytes_data}")

    except KeyboardInterrupt:
        print("\nCerrando servidor UDP...")
        break
    except Exception as e:
        print(f"Error durante la recepción: {e}")
        break

sock.close()

Y solo para que quede claro, la IP 192.168.0.5 es la IP que tiene mi PC en donde recibo los datos, hay que configurarla en Python y en la ESP32-CAM. Lo mismo pasa con el puerto. Obviamente en tu caso la IP podría cambiar

Conclusión

Ya he escrito un post sobre cómo transmitir la cámara OV2640 en tiempo real y cómo stremear audio y video en tiempo real con la ESP32-CAM

Me gusta dejar todo por separado porque ese es el proceso que sigo al empezar las pruebas sobre qué tan lejos se puede llegar con la tarjeta.

Si el post ha sido de tu agrado te invito a que me sigas para saber cuando haya escrito un nuevo post, haya actualizado algún sistema o publicado un nuevo software. Facebook | X | Instagram | Telegram | También estoy a tus órdenes para cualquier contratación en mi página de contacto