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.

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-CAM | ICS-43434 | Definición en código |
|---|---|---|
| GPIO 12 | SD | I2S_SD_MICROFONO |
| GPIO 13 | WS | I2S_WS_MICROFONO |
| GPIO 15 | SCK | I2S_SCK_MICROFONO |
| GND | GND | |
| VCC | VIN |
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.