En este post te voy a hablar de mi experiencia con la ESP32-S3 WROOM N16R8 y sobre algunos experimentos que he llevado a cabo con ella.

Tal vez escriba una guía más adelante. Por ahora todo el código presentado aquí funciona correctamente pero se puede mejorar y limpiar en muchos aspectos.

Por cierto, fue muy complejo configurar la cámara y encontrar los pines adecuados. Desconozco si fue error mío o que el fabricante no deja la documentación clara, pero no te preocupes, aquí describiré todos los problemas que encontré y las soluciones.

Algunas cosas que resaltar son:

  1. Tiene función de I2S así que se puede grabar y reproducir audio
  2. Tiene función de tarjeta SD así que se puede leer y escribir
  3. Obviamente tiene WiFi, se puede usar como cliente o como servidor
  4. Trae cámara OV2640 integrada
  5. Tiene un LED RGB WS2812 muy guapo
  6. Tiene sensor de touch para cuando tocas un pin con tu dedo
  7. He logrado mandar fotos a Telegram, stremear el audio y reproducir audio en el mismo código
  8. Puedes conectarla como si fuera un teclado o dispositivo de interfaz humana, esto abre la puerta a automatizaciones

Una cosa muy importante es que mi modelo específico no cuenta con SRAM adicional, así que solo tiene los 512KB de fábrica.

Si tú vas a comprar esta tarjeta te recomiendo que preguntes al vendedor si realmente tiene SRAM.

Previamente he programado la ESP32-CAM y la NodeMCU ESP8266, así que solo he instalado el CH340, y no el CH343 como dice.

Programando ESP32-S3 WROOM por primera vez

La conecté encendió el LED verde.

Utilicé PlatformIO para programarla, pero no lo hice de la manera normal. Hice una nueva carpeta y coloqué ahí:

  1. Una carpeta llamada src
  2. Un archivo llamado main.cpp dentro de la carpeta creada en el paso 1
  3. Un archivo llamado platformio.ini con el siguiente contenido

Después abrí esa carpeta desde PlatformIO.

[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

board_build.flash_size = 16MB
board_build.psram = disabled

lib_deps =
  adafruit/Adafruit NeoPixel

Código de main.cpp:

void loop() {
 led.begin();
  led.show();
  led.setPixelColor(0, led.Color(0,150,0)); led.show(); delay(400);
  led.setPixelColor(0, led.Color(150,150,150)); led.show(); delay(600);
  led.setPixelColor(0, led.Color(150,0,0)); led.show(); delay(400);
  led.clear(); led.show();
}

Y todo bien

Grabar audio con el 43434 o algo así

#include <driver/i2s.h>
#include "SD_MMC.h"

#define I2S_WS 5  // LRCK
#define I2S_SD 6  // DATA
#define I2S_SCK 7 // BCLK

const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const int RECORD_TIME = 10; // segundos
File audioFile;

void writeWavHeader(File &file, int sampleRate, int bitsPerSample, int channels, uint32_t dataSize)
{
  uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
  uint16_t blockAlign = channels * bitsPerSample / 8;
  uint8_t header[44] = {
      'R', 'I', 'F', 'F',
      0, 0, 0, 0, // tamaño total - 8
      'W', 'A', 'V', 'E',
      'f', 'm', 't', ' ',
      16, 0, 0, 0, // fmt chunk size
      1, 0,        // audio format PCM
      (uint8_t)channels, 0,
      (uint8_t)(sampleRate & 0xFF), (uint8_t)((sampleRate >> 8) & 0xFF),
      (uint8_t)((sampleRate >> 16) & 0xFF), (uint8_t)((sampleRate >> 24) & 0xFF),
      (uint8_t)(byteRate & 0xFF), (uint8_t)((byteRate >> 8) & 0xFF),
      (uint8_t)((byteRate >> 16) & 0xFF), (uint8_t)((byteRate >> 24) & 0xFF),
      (uint8_t)(blockAlign & 0xFF), (uint8_t)((blockAlign >> 8) & 0xFF),
      (uint8_t)bitsPerSample, 0,
      'd', 'a', 't', 'a',
      0, 0, 0, 0 // data chunk size
  };

  // tamaño total
  uint32_t fileSize = dataSize + 36;
  header[4] = fileSize & 0xFF;
  header[5] = (fileSize >> 8) & 0xFF;
  header[6] = (fileSize >> 16) & 0xFF;
  header[7] = (fileSize >> 24) & 0xFF;

  // tamaño data
  header[40] = dataSize & 0xFF;
  header[41] = (dataSize >> 8) & 0xFF;
  header[42] = (dataSize >> 16) & 0xFF;
  header[43] = (dataSize >> 24) & 0xFF;

  file.seek(0);
  file.write(header, 44);
}

void setup()
{
  Serial.begin(115200);
  // Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
  bool ok = SD_MMC.setPins(39, 38, 40);
  Serial.printf("set pins: %d", ok);
  if (!SD_MMC.begin("/sdcard", true))
  { 
    Serial.println("Error al inicializar SD_MMC");
    while (1)
      ;
  }

  Serial.println("SD inicializada correctamente!");

  // Configurar I2S
  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_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_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,
      .ws_io_num = I2S_WS,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = I2S_SD};

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);

  audioFile = SD_MMC.open("/test.wav", FILE_WRITE);
  if (!audioFile)
  {
    Serial.println("No se pudo crear archivo");
    while (1)
      ;
  }

  // Escribir header temporal
  uint8_t emptyHeader[44] = {0};
  audioFile.write(emptyHeader, 44);
  Serial.println("Grabando...");
}

void loop()
{
  static uint32_t bytesWritten = 0;
  const uint32_t maxBytes = RECORD_TIME * SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);

  int16_t buffer[256];
  size_t bytesRead;

  i2s_read(I2S_NUM_0, buffer, sizeof(buffer), &bytesRead, portMAX_DELAY);

  if (bytesWritten + bytesRead > maxBytes)
  {
    bytesRead = maxBytes - bytesWritten;
  }

  audioFile.write((uint8_t *)buffer, bytesRead);
  bytesWritten += bytesRead;

  if (bytesWritten >= maxBytes)
  {
    Serial.println("Grabación terminada!");
    audioFile.flush();
    writeWavHeader(audioFile, SAMPLE_RATE, BITS_PER_SAMPLE, CHANNELS, bytesWritten);
    audioFile.close();
    while (1)
      ;
  }
}

Reproducir MP3

Este ejemplo toma una canción llamada z.mp3 de la Micro SD y la reproduce infinitamente usando un MAX98357A e I2S.

Por cierto, necesitas earlephilhower/ESP8266Audio@^2.0.0

#include <Arduino.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"

#define I2S_DOUT 5
#define I2S_BCLK 6
#define I2S_LRC 7

AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;

void setup()
{
    Serial.begin(115200);
    Serial.println("Iniciando reproductor de MP3 con ESP32-S3...");

    // Inicialización de la tarjeta SD con SD_MMC
    // Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
    bool ok = SD_MMC.setPins(39, 38, 40);
    if (!SD_MMC.begin("/sdcard", true))
    {
        Serial.println("Error al inicializar la tarjeta SD.");
        return;
    }
    Serial.println("Tarjeta SD inicializada correctamente.");

    out = new AudioOutputI2S();
    out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    out->SetChannels(2);
    out->SetGain(1);

    file = new AudioFileSourceFS(SD_MMC, "/z.mp3");
    mp3 = new AudioGeneratorMP3();
    mp3->begin(file, out);
}

void loop()
{
    if (mp3->isRunning())
    {
        if (!mp3->loop())
        {
            Serial.println("Reproducción terminada. Reiniciando...");
            mp3->stop();
            delete mp3;
            delete file;

            file = new AudioFileSourceFS(SD_MMC, "/z.mp3");
            mp3 = new AudioGeneratorMP3();
            mp3->begin(file, out);
        }
    }
}

La conexión es:

MAX98357AESP32-S3 WROOM
LRC (Left/right clock)7
BCLK (Bit clock input)6
DIN (Digital input signal)5
GAINNinguno
SDNinguno
GNDGND
Vin3V3

Crear servidor, recibir MP3, almacenar en SD y reproducir

Mi platformio.ini va quedando así:

[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
board_build.flash_size = 16MB
board_build.psram = disabled
monitor_speed = 115200
lib_deps = 
	earlephilhower/ESP8266Audio@^2.0.0
	esp32async/AsyncTCP@^3.3.6
	esp32async/ESPAsyncWebServer@^3.7.2

El código es:

#include <Arduino.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

#define I2S_DOUT 5
#define I2S_BCLK 6
#define I2S_LRC 7
const char *ssid = "Tu ssid";
const char *password = "Tu contraseña";
AsyncWebServer server(80);

AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;

void setup()
{
    Serial.begin(115200);
    Serial.println("Iniciando reproductor de MP3 con ESP32-S3...");

    // Inicialización de la tarjeta SD con SD_MMC
    // Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
    bool ok = SD_MMC.setPins(39, 38, 40);
    if (!SD_MMC.begin("/sdcard", true))
    {
        Serial.println("Error al inicializar la tarjeta SD.");
        return;
    }
    Serial.println("Tarjeta SD inicializada correctamente.");

    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(300);
        Serial.print('.');
    }
    Serial.println("\nWiFi connected");
    Serial.print("Servidor corriendo en: http://");
    Serial.println(WiFi.localIP());
    out = new AudioOutputI2S();
    out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    out->SetChannels(2);
    out->SetGain(0.15);

    server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
              {
      static File uploadFile;

      if(index == 0){
          Serial.println("Comenzando upload de: " + filename);
          uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
      }

      if(uploadFile){
          uploadFile.write(data, len);
      }

      if(final){
          uploadFile.close();
          Serial.println("Upload completado, iniciando reproducción...");

          // iniciar reproducción
          if(file) delete file;
          if(mp3) delete mp3;

          file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
          mp3 = new AudioGeneratorMP3();
          mp3->begin(file, out);
      } });
    server.begin();
}

void loop()
{
    if (mp3 && mp3->isRunning())
    {
        if (!mp3->loop())
        {
            mp3->stop();
            delete mp3;
            delete file;
            mp3 = nullptr;
            file = nullptr;
        }
    }
}

Para probar podemos usar cURL:

curl -v -F "file=@t_.mp3" http://192.168.0.9/upload

En este caso t_.mp3 es la canción que quiero subir y 192.168.0.9 es la IP de la ESP32-S3 que me imprimió en el monitor Serial.

Por cierto, uso esas versiones de las dependencias (AsyncTCP y ESPAsyncWebServer) porque al usar las versiones más recientes me decía cosas como error: ‘ip_addr_t’ {aka ‘struct ip_addr’} has no member named ‘addr’; did you mean ‘u_addr’?

Crear servidor para stremear audio usando HTTP y ICS 43434

En el lugar donde lo compré dice: Módulo Micrófono Omnidireccional Mh-et I2s Ics 43434 Esp32

#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#define I2S_WS 5  // LRCK
#define I2S_SD 6  // DATA
#define I2S_SCK 7 // BCLK
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AsyncWebServer server(80);

void setup()
{
    Serial.begin(115200);
    // Configurar I2S
    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_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_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,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_SD};

    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
    // Wifi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(300);
        Serial.print('.');
    }
    Serial.println("\nWiFi connected");
    Serial.print("Servidor corriendo en: http://");
    Serial.println(WiFi.localIP());
    server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Hola mundo"); });
    server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
              {
  AsyncWebServerResponse *response = request->beginChunkedResponse(
      "audio/wav",
      [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
        static bool headerSent = false;
         //float gain = 2.0f; // 2x volumen = +6dB
        if (!headerSent) {
          headerSent = true;
          // Header WAV "infinito"
          uint32_t sampleRate = 16000;
          uint16_t bitsPerSample = 16;
          uint16_t channels = 1;
          uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
          uint16_t blockAlign = channels * bitsPerSample / 8;
          uint8_t header[44] = {
              'R','I','F','F',
              0xFF,0xFF,0xFF,0x7F, // tamaño fake
              'W','A','V','E',
              'f','m','t',' ',
              16,0,0,0,
              1,0,
              (uint8_t)channels,0,
              (uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
              (uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
              (uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
              (uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
              (uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
              (uint8_t)bitsPerSample,0,
              'd','a','t','a',
              0xFF,0xFF,0xFF,0x7F
          };
          memcpy(buffer, header, 44);
          return 44;
        }

        // Leer audio del I2S
        size_t bytesRead = 0;
        i2s_read(I2S_NUM_0, buffer, maxLen, &bytesRead, portMAX_DELAY);
        //Serial.printf("Bytes read: %d\n", bytesRead);
        // Aplicar ganancia
        /*
        int16_t *samples = (int16_t *)buffer;
        size_t nSamples = bytesRead / 2;
        for (size_t i = 0; i < nSamples; i++) {
          int32_t s = samples[i] * gain;
          if (s > 32767) s = 32767;
          if (s < -32768) s = -32768;
          samples[i] = (int16_t)s;
        }
*/
        return bytesRead;
      });
  request->send(response); });
    server.begin();
}

void loop() {}

La conexión es:

ICS43434ESP32-S3 WROOM
SD6
SCK7
WS5
L/RNinguno
GNDGND
VDD3V3

En el navegador no sirve pero sí sirve con VLC > Medio > Abrir emisión de red > http://192.168.0.9/stream > reproducir.

Aquí 192.168.0.9 es la IP que se muestra en el monitor serial

Crear servidor stremeando audio y también reproduciendo audio

Notar que es como la combinación de los anteriores pero aquí he usado el I2S_NUM_1 para el micrófono porque infiero que el reproductor de audio usa el I2S_NUM_0 ya que antes de cambiarlo me daba un error que decía: E (54214) I2S: i2s_write(2129): TX mode is not enabled

Y bueno, aquí el código. Por cierto, he notado que le pasa algo al stream de audio cuando reproduce una canción muy larga, desconozco la razón, pero el “hola” sí responde aunque más tardado de lo normal

#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 5  // LRCK
#define I2S_SD 6  // DATA
#define I2S_SCK 7 // BCL
// Pines DAC
#define I2S_DOUT 11
#define I2S_BCLK 9
#define I2S_LRC 10
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);

void setup()
{
    Serial.begin(115200);
    out = new AudioOutputI2S();
    out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    out->SetChannels(2);
    out->SetGain(1);
    // Configurar I2S
    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_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_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,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_SD};

    i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_1, &pin_config);
    bool ok = SD_MMC.setPins(39, 38, 40);
    if (!SD_MMC.begin("/sdcard", true))
    {
        Serial.println("Error al inicializar la tarjeta SD.");
        return;
    }
    Serial.println("Tarjeta SD inicializada correctamente.");
    // Wifi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(300);
        Serial.print('.');
    }
    Serial.println("\nWiFi connected");
    Serial.print("Servidor corriendo en: http://");
    Serial.println(WiFi.localIP());
    server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
              {
      static File uploadFile;

      if(index == 0){
          //Serial.println("Comenzando upload de: " + filename);
          uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
      }

      if(uploadFile){
          uploadFile.write(data, len);
      }

      if(final){
          uploadFile.close();
          //Serial.println("Upload completado, iniciando reproducción...");

          /*Aquí reproducimos pero solo quiero probar*/
          // iniciar reproducción
          if(file) delete file;
          if(mp3) delete mp3;

          file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
          mp3 = new AudioGeneratorMP3();
          mp3->begin(file, out);
      } });
    server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Hola mundo"); });
    server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
              {
  AsyncWebServerResponse *response = request->beginChunkedResponse(
      "audio/wav",
      [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
        static bool headerSent = false;
         //float gain = 2.0f; // 2x volumen = +6dB
        if (!headerSent) {
          headerSent = true;
          // Header WAV "infinito"
          uint32_t sampleRate = 16000;
          uint16_t bitsPerSample = 16;
          uint16_t channels = 1;
          uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
          uint16_t blockAlign = channels * bitsPerSample / 8;
          uint8_t header[44] = {
              'R','I','F','F',
              0xFF,0xFF,0xFF,0x7F, // tamaño fake
              'W','A','V','E',
              'f','m','t',' ',
              16,0,0,0,
              1,0,
              (uint8_t)channels,0,
              (uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
              (uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
              (uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
              (uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
              (uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
              (uint8_t)bitsPerSample,0,
              'd','a','t','a',
              0xFF,0xFF,0xFF,0x7F
          };
          memcpy(buffer, header, 44);
          return 44;
        }

        // Leer audio del I2S
        size_t bytesRead = 0;
        i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
        //Serial.printf("Bytes read: %d\n", bytesRead);
        // Aplicar ganancia
        /*
        int16_t *samples = (int16_t *)buffer;
        size_t nSamples = bytesRead / 2;
        for (size_t i = 0; i < nSamples; i++) {
          int32_t s = samples[i] * gain;
          if (s > 32767) s = 32767;
          if (s < -32768) s = -32768;
          samples[i] = (int16_t)s;
        }
*/
        return bytesRead;
      });
  request->send(response); });
    server.begin();
}

void loop()
{
    if (mp3 && mp3->isRunning())
    {
        if (!mp3->loop())
        {
            mp3->stop();
            delete mp3;
            delete file;
            mp3 = nullptr;
            file = nullptr;
        }
    }
}

Servidor que stremea y reproduce audio intentando al mismo tiempo

Es casi lo mismo que la anterior porque el stream se pausa o corta cuando se reproduce audio, pero al menos sigue conectado dicho stream

Por cierto, probé con una grabación de 172KB duración 20 segundos y funciona bien. Realmente no sé cuánto pese una grabación real porque esta la convertí de ogg a mp3 con ffmpeg -i grabación.ogg salida.mp3 ya que usé Telegram para grabar y enviar el audio.

#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 5  // LRCK
#define I2S_SD 6  // DATA
#define I2S_SCK 7 // BCL
// Pines DAC
#define I2S_DOUT 11
#define I2S_BCLK 9
#define I2S_LRC 10
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void Task1code(void *parameter)
{
    Serial.println("Ejecutando tarea");
    while (1)
    {
        if (nuevaCancion)
        {
            if (file)
            {
                delete file;
                file = nullptr;
            }
            if (mp3)
            {
                delete mp3;
                mp3 = nullptr;
            }

            file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
            mp3 = new AudioGeneratorMP3();
            mp3->begin(file, out);

            nuevaCancion = false;
        }

        if (mp3 && mp3->isRunning())
        {
            if (!mp3->loop())
            {
                mp3->stop();
                delete mp3;
                mp3 = nullptr;
                delete file;
                file = nullptr;
            }
        }
        vTaskDelay(1);
    }
}
void setup()
{

    Serial.begin(115200);
    out = new AudioOutputI2S();
    out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    out->SetChannels(2);
    out->SetGain(1);
    // Configurar I2S
    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_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_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,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_PIN_NO_CHANGE,
        .data_in_num = I2S_SD};

    i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_1, &pin_config);
    bool ok = SD_MMC.setPins(39, 38, 40);
    if (!SD_MMC.begin("/sdcard", true))
    {
        Serial.println("Error al inicializar la tarjeta SD.");
        return;
    }
    Serial.println("Tarjeta SD inicializada correctamente.");
    // Wifi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(300);
        Serial.print('.');
    }
    Serial.println("\nWiFi connected");
    Serial.print("Servidor corriendo en: http://");
    Serial.println(WiFi.localIP());
    server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
              {
      static File uploadFile;

      if(index == 0){
          //Serial.println("Comenzando upload de: " + filename);
          uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
      }

      if(uploadFile){
          uploadFile.write(data, len);
      }

      if(final){
          uploadFile.close();
    nuevaCancion = true;
          //Serial.println("Upload completado, iniciando reproducción...");

          /*Aquí reproducimos pero solo quiero probar*/
          // iniciar reproducción
          //if(file) delete file;
          //if(mp3) delete mp3;

          //file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
          //mp3 = new AudioGeneratorMP3();
          //mp3->begin(file, out);
      } });
    server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
    server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
              {
  AsyncWebServerResponse *response = request->beginChunkedResponse(
      "audio/wav",
      [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
        static bool headerSent = false;
         //float gain = 2.0f; // 2x volumen = +6dB
        if (!headerSent) {
          headerSent = true;
          // Header WAV "infinito"
          uint32_t sampleRate = 16000;
          uint16_t bitsPerSample = 16;
          uint16_t channels = 1;
          uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
          uint16_t blockAlign = channels * bitsPerSample / 8;
          uint8_t header[44] = {
              'R','I','F','F',
              0xFF,0xFF,0xFF,0x7F, // tamaño fake
              'W','A','V','E',
              'f','m','t',' ',
              16,0,0,0,
              1,0,
              (uint8_t)channels,0,
              (uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
              (uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
              (uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
              (uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
              (uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
              (uint8_t)bitsPerSample,0,
              'd','a','t','a',
              0xFF,0xFF,0xFF,0x7F
          };
          memcpy(buffer, header, 44);
          return 44;
        }

        // Leer audio del I2S
        size_t bytesRead = 0;
        i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
        //Serial.printf("Bytes read: %d\n", bytesRead);
        // Aplicar ganancia
        /*
        int16_t *samples = (int16_t *)buffer;
        size_t nSamples = bytesRead / 2;
        for (size_t i = 0; i < nSamples; i++) {
          int32_t s = samples[i] * gain;
          if (s > 32767) s = 32767;
          if (s < -32768) s = -32768;
          samples[i] = (int16_t)s;
        }
*/
        return bytesRead;
      });
  request->send(response); });

    xTaskCreate(
        Task1code,
        "ReproducirMp3",
        10000,
        NULL,
        0,
        &TareaReproducirMP3);

    server.begin();
}

void loop()
{
    /*
if (mp3 && mp3->isRunning())
        {
            if (!mp3->loop())
            {
                mp3->stop();
                delete mp3;
                delete file;
                mp3 = nullptr;
                file = nullptr;
            }
        }
*/
}

Inicializar cámara (todavía no he tomado foto)

Intenté revisando el pinout y no pude, no funcionaba. Revisé todo y me salía:

E (109) cam_hal: cam_dma_config(301): frame buffer malloc failed
E (109) cam_hal: cam_config(385): cam_dma_config failed
E (109) gdma: gdma_disconnect(299): no peripheral is connected to the channel
E (116) camera: Camera config failed with error 0xffffffff

Me parece que esta tarjeta no trae PSRAM (aunque su modelo es N16R8 y la R es de 8 MB de RAM) así que hay que modificar otro ajuste:

    config.fb_location = CAMERA_FB_IN_DRAM;

Con la ESP32-CAM ni siquiera toqué ese ajuste porque esa sí trae PSRAM, pero esta tarjeta, desafortunadamente, no.

    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sccb_sda = SIOD_GPIO_NUM;
    config.pin_sccb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 10000000;
    config.frame_size = FRAMESIZE_96X96;
    config.pixel_format = PIXFORMAT_JPEG; // for streaming
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_DRAM;
    config.jpeg_quality = 30;
    config.fb_count = 1;
    int res = esp_camera_init(&config);
    Serial.printf("Init: %d\n", res);

Salida:

El pwdn -1
Init: 0

Y con eso ya pude hacerle el init correcto. Por cierto, los pines quedan así:

#define PWDN_GPIO_NUM    -1
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    4
#define SIOD_GPIO_NUM    18
#define SIOC_GPIO_NUM    23

#define Y9_GPIO_NUM      36
#define Y8_GPIO_NUM      37
#define Y7_GPIO_NUM      38
#define Y6_GPIO_NUM      39
#define Y5_GPIO_NUM      35
#define Y4_GPIO_NUM      14
#define Y3_GPIO_NUM      13
#define Y2_GPIO_NUM      34
#define VSYNC_GPIO_NUM   5
#define HREF_GPIO_NUM    27
#define PCLK_GPIO_NUM    25

De tantos sketches que he visto ya ni sé en cuál de ellos me basé, pero solo como pista, supuestamente esta tarjeta tiene la CAMERA_MODEL_ESP_EYE. Los pines NO coinciden con el pinout de la tienda donde compré la tarjeta , ya que ahí los CAM_ (por ejemplo CAM_Y8) son distintos.

Solo como ejemplo, en el código donde sí funciona el init tengo el Y9_GPIO_NUM en 36 mientras que el pinout de la tienda dice que es el GPIO16 y el SIOC_GPIO_NUM en el código es 23 pero en la imagen del pinout el CAM_SIOC es el GPIO5

Seguiré revisando para ver si puedo tomar la foto, ya que el init funciona hasta el momento pero no sé si lo hará el tomado de la foto.

El init no indica que todo está bien

Con el código de arriba no funcionaba la toma de foto con esp_camera_fb_get ya que me devolvía NULL así que me puse a depurar, al final terminé transcribiendo otra vez el pinout quedando así:

    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = 11;
    config.pin_d1 = 9;
    config.pin_d2 = 8;
    config.pin_d3 = 10;
    config.pin_d4 = 12;
    config.pin_d5 = 18;
    config.pin_d6 = 17;
    config.pin_d7 = 16;
    config.pin_xclk = 15;
    config.pin_pclk = 13;
    config.pin_vsync = 6;
    config.pin_href = 7;
    config.pin_sccb_sda = 4;
    config.pin_sccb_scl = 5;
    config.pin_pwdn = -1;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.frame_size = FRAMESIZE_CIF;
    config.pixel_format = PIXFORMAT_GRAYSCALE; 
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_DRAM;
    config.jpeg_quality = 30;
    config.fb_count = 1;

RESET_GPIO_NUM es -1. Los demás pines los dejé así como lo dice la imagen del pinout.

Nota que ahora no me voy a fijar en calidad de foto, solo quiero una foto en cualquier formato y saber si la cámara funciona.

Pues lo siguiente para saber si la conexión es correcta es obtener información del sensor. Veamos el siguiente fragmento de código:

  sensor_t *s = esp_camera_sensor_get();
  if (!s) {
    Serial.println("No se pudo obtener el sensor!");
    return;
  }

Intencionalmente desconecté la cámara y me da justamente ese error junto con algo como un error de memoria:

E (1028) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
E (1028) gdma: gdma_disconnect(299): no peripheral is connected to the channel
Es distinto a ok
No se pudo obtener el sensor!
Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x4200223f  PS      : 0x00060630  A0      : 0x82004079  A1      : 0x3fcebea0  
A2      : 0x3fc96814  A3      : 0x00000000  A4      : 0x00000014  A5      : 0x00000004
A6      : 0x3fcec0c0  A7      : 0x80000001  A8      : 0x8200223f  A9      : 0x3fcebe80  
A10     : 0x00000000  A11     : 0x00000000  A12     : 0xf56d182c  A13     : 0x3fcebe3c
A14     : 0x3fc96b64  A15     : 0x3fcebe3c  SAR     : 0x0000001a  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000002  LBEG    : 0x400556d5  LEND    : 0x400556e5  LCOUNT  : 0xffffffff


Backtrace: 0x4200223c:0x3fcebea0 0x42004076:0x3fcebf40

El mensaje de Es distinto a ok es por lo siguiente:

    int res = esp_camera_init(&config);
    if (res != ESP_OK)
    {
        Serial.printf("Es distinto a ok");
    }

Y el del sensor ya lo expliqué más arriba. Como lo dije, esto fue intencional para saber si realmente detecta errores. Volví a conectar la cámara, reinicié y ahora sí puse todo en la función:

void checkCamera() {
  sensor_t *s = esp_camera_sensor_get();
  if (!s) {
    Serial.println("No se pudo obtener el sensor!");
    return;
  }

  Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
  Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
  Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}

La salida es:

Sensor PID: 0x26
Sensor VER: 0x42
Sensor MIDH: 0x7f, MIDL: 0xa2

Así que no basta con que el esp_camera_init te devuelva ESP_OK, también es importante revisar el sensor solo para saber si la conexión es correcta.

Hay que invocar a checkCamera después de esp_camera_init obviamente.

Solo para comprobar que realmente todo está ok, voy a modificar la configuración y poner lo siguiente:

    config.pin_sccb_sda = 18; // Originalmente es 4

Me da la siguiente salida:

E (1028) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
E (1028) gdma: gdma_disconnect(299): no peripheral is connected to the channel
Es distinto a okNo se pudo obtener el sensor!
Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x420021e7  PS      : 0x00060630  A0      : 0x82003fd9  A1      : 0x3fcebea0
A2      : 0x3fc96810  A3      : 0x00000000  A4      : 0x00000014  A5      : 0x00000004
A6      : 0x3fcec0c0  A7      : 0x80000001  A8      : 0x820021e7  A9      : 0x3fcebe80
A10     : 0x00000000  A11     : 0x3fc96810  A12     : 0x7a7bcd8e  A13     : 0x3fcebe3c
A14     : 0x3fc96b60  A15     : 0x3fcebe3c  SAR     : 0x0000001a  EXCCAUSE: 0x0000001c
EXCVADDR: 0x00000002  LBEG    : 0x400556d5  LEND    : 0x400556e5  LCOUNT  : 0xffffffff


Backtrace: 0x420021e4:0x3fcebea0 0x42003fd6:0x3fcebf40

Así que sí: esp_camera_init y esp_camera_sensor_get en conjunto nos ayudan a descartar errores.

Lo único que queda es probar si la foto es correcta.

Seguimos con pruebas: tomar foto y ver size

Sé que lo más obvio sería guardar en la SD o mostrar la foto en un servidor web, pero vamos paso por paso ya que no quiero cometer un error. Me ha costado llegar hasta aquí.

No sé si el problema está en la cantidad de RAM, por ello es que quiero hacer pruebas muy mínimas.

El siguiente código ya funciona y me muestra el tamaño del búfer. Es un código limpio donde ya se puede ver la conexión correcta que al menos no da error para el sensor ni para el init:

#include "esp_camera.h"
#include "Arduino.h"

void checkCamera()
{
    sensor_t *s = esp_camera_sensor_get();
    if (!s)
    {
        Serial.println("No se pudo obtener el sensor!");
        return;
    }

    Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
    Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
    Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = 11;
    config.pin_d1 = 9;
    config.pin_d2 = 8;
    config.pin_d3 = 10;
    config.pin_d4 = 12;
    config.pin_d5 = 18;
    config.pin_d6 = 17;
    config.pin_d7 = 16;
    config.pin_xclk = 15;
    config.pin_pclk = 13;
    config.pin_vsync = 6;
    config.pin_href = 7;
    config.pin_sccb_sda = 4;
    config.pin_sccb_scl = 5;
    config.pin_pwdn = -1;
    config.pin_reset = -1;
    config.xclk_freq_hz = 20000000;
    config.frame_size = FRAMESIZE_CIF;
    config.pixel_format = PIXFORMAT_JPEG;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_DRAM;
    config.jpeg_quality = 30;
    config.fb_count = 1;
    int res = esp_camera_init(&config);
    if (res != ESP_OK)
    {
        Serial.printf("Es distinto a ok");
    }
    checkCamera();
    camera_fb_t *b = esp_camera_fb_get();

    if (b == NULL)
    {
        Serial.println("b es null!");
    }
    else
    {
        Serial.printf("Foto funciona hasta el momento. A ver el size del buffer? %d\n", b->len);
    }
}

void loop()
{
}

La salida es:

Sensor PID: 0x26
Sensor VER: 0x42
Sensor MIDH: 0x7f, MIDL: 0xa2
Foto funciona hasta el momento. A ver el size del buffer? 5096

Y siempre me da medidas aproximadas a 5KB.

Mostrar una foto en servidor web

Hagamos un servidor web que muestre la foto a ver qué sale. El código queda así:

#include "esp_camera.h"
#include "Arduino.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <WiFi.h>
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AsyncWebServer server(80);
void handlePhoto(AsyncWebServerRequest *request)
{
    camera_fb_t *fb = esp_camera_fb_get();
    if (!fb)
    {
        request->send(500, "text/plain", "No se pudo capturar la foto");
        return;
    }

    AsyncWebServerResponse *response = request->beginResponse_P(
        200, "image/jpeg", fb->buf, fb->len);
    response->addHeader("Content-Disposition", "inline; filename=capture.jpg");
    request->send(response);

    esp_camera_fb_return(fb);
}
void checkCamera()
{
    sensor_t *s = esp_camera_sensor_get();
    if (!s)
    {
        Serial.println("No se pudo obtener el sensor!");
        return;
    }

    Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
    Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
    Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = 11;
    config.pin_d1 = 9;
    config.pin_d2 = 8;
    config.pin_d3 = 10;
    config.pin_d4 = 12;
    config.pin_d5 = 18;
    config.pin_d6 = 17;
    config.pin_d7 = 16;
    config.pin_xclk = 15;
    config.pin_pclk = 13;
    config.pin_vsync = 6;
    config.pin_href = 7;
    config.pin_sccb_sda = 4;
    config.pin_sccb_scl = 5;
    config.pin_pwdn = -1;
    config.pin_reset = -1;
    config.xclk_freq_hz = 20000000;
    config.frame_size = FRAMESIZE_CIF;
    config.pixel_format = PIXFORMAT_JPEG;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_DRAM;
    config.jpeg_quality = 30;
    config.fb_count = 1;
    int res = esp_camera_init(&config);
    if (res != ESP_OK)
    {
        Serial.printf("Es distinto a ok");
    }
    checkCamera();

    WiFi.begin(ssid, password);
    Serial.printf("Conectando a %s", ssid);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.printf("\nConectado! IP: %s\n", WiFi.localIP().toString().c_str());
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/html",
                              "<h1>ESP32-S3 Cam</h1><img src='/photo'>"); });
    server.on("/photo", HTTP_GET, handlePhoto);

    server.begin();
}

void loop()
{
}

Lo cargué, apunté a mi laptop y aquí tenemos lo más esperado:

Foto tomada con ESP32-S3 WROOM

Así que la cámara, la conexión y todo lo demás funciona. Ya a partir de aquí solo es cuestión de ajustar todos los parámetros para obtener la mejor calidad.

Solo para que quede el registro. Cambié el grab_mode y el pixel_format así:

    config.frame_size = FRAMESIZE_UXGA;
    config.grab_mode = CAMERA_GRAB_LATEST;

Me dio el siguiente error:

E (108) cam_hal: cam_dma_config(301): frame buffer malloc failed
E (108) cam_hal: cam_config(385): cam_dma_config failed
E (108) gdma: gdma_disconnect(299): no peripheral is connected to the channel
E (114) camera: Camera config failed with error 0xffffffff
Es distinto a okNo se pudo obtener el sensor!

Me imagino que es por el tamaño de la foto. Lo cambié a:

    config.frame_size = FRAMESIZE_XGA;

Pero me daba fotos mal formadas (aunque eso sí, en mejor calidad), por ejemplo:

Foto 2 tomada con ESP32-S3 WROOM. Imagen más grande pero malformada

Imagino que es el grab_mode, voy a probar.

Después de cambiarlo la foto sigue igual. Voy a bajar la calidad a FRAMESIZE_SVGA dejando config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;.

Sigue igual. Voy a bajarla de nuevo a FRAMESIZE_CIF a ver si así ya respeta.

Correcto, con FRAMESIZE_CIF ya muestra imágenes bien formadas.

  • Probando con HVGA (480x320). También funciona.
  • Probando con VGA (640x480). Salen con colores distintos y malformadas

Entonces parece que el máximo es 480x320. Eso sí: nunca bajé la calidad JPG, siempre estuvo en 30 y recordemos que entre más bajo es mayor calidad.

Bajé la calidad a 45 y lo dejé en VGA. El resultado es el mismo: fotos malformadas la mayoría de veces aunque el servidor muestra la foto más rápido.

Stremear cámara

He probado con los siguientes ajustes, no va tan rápido como uno quisiera pero al menos sí se ve decente. No he probado el máximo pero imagino que esto roza el límite.

#include "esp_camera.h"
#include "Arduino.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <WiFi.h>
const char *ssid = "Tu ssid";
const char *password = "Tu contraseña";
AsyncWebServer server(80);
void checkCamera()
{
    sensor_t *s = esp_camera_sensor_get();
    if (!s)
    {
        Serial.println("No se pudo obtener el sensor!");
        return;
    }

    Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
    Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
    Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = 11;
    config.pin_d1 = 9;
    config.pin_d2 = 8;
    config.pin_d3 = 10;
    config.pin_d4 = 12;
    config.pin_d5 = 18;
    config.pin_d6 = 17;
    config.pin_d7 = 16;
    config.pin_xclk = 15;
    config.pin_pclk = 13;
    config.pin_vsync = 6;
    config.pin_href = 7;
    config.pin_sccb_sda = 4;
    config.pin_sccb_scl = 5;
    config.pin_pwdn = -1;
    config.pin_reset = -1;
    config.xclk_freq_hz = 20000000;
    config.frame_size = FRAMESIZE_VGA;
    config.pixel_format = PIXFORMAT_JPEG;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_DRAM;
    config.jpeg_quality = 45;
    config.fb_count = 1;
    int res = esp_camera_init(&config);
    if (res != ESP_OK)
    {
        Serial.printf("Es distinto a ok");
    }
    checkCamera();

    WiFi.begin(ssid, password);
    Serial.printf("Conectando a %s", ssid);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.printf("\nConectado! IP: %s\n", WiFi.localIP().toString().c_str());
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->send(200, "text/html",
                              "<h1>ESP32-S3 Cam</h1><img src='/photo'>"); });
    server.on("/photo", HTTP_GET, [](AsyncWebServerRequest *request)
              {
  struct MJPEGState {
    camera_fb_t *fb = nullptr;
    size_t hdr_pos = 0;
    size_t jpg_pos = 0;
    bool tail_sent = false;
    std::string header;
  };
  auto state = std::make_shared<MJPEGState>();

  auto cb = [state](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
    if (!state->fb) {
      state->fb = esp_camera_fb_get();
      if (!state->fb) return 0;
      char h[128];
      int hlen = snprintf(h, sizeof(h),
                          "--frame\r\n"
                          "Content-Type: image/jpeg\r\n"
                          "Content-Length: %u\r\n\r\n",
                          (unsigned)state->fb->len);
      state->header.assign(h, hlen);
      state->hdr_pos = 0;
      state->jpg_pos = 0;
      state->tail_sent = false;
    }

    if (state->hdr_pos < state->header.size()) {
      size_t toCopy = std::min(maxLen, state->header.size() - state->hdr_pos);
      memcpy(buffer, state->header.data() + state->hdr_pos, toCopy);
      state->hdr_pos += toCopy;
      return toCopy;
    }

    if (state->jpg_pos < state->fb->len) {
      size_t rem = state->fb->len - state->jpg_pos;
      size_t toCopy = std::min(maxLen, rem);
      memcpy(buffer, state->fb->buf + state->jpg_pos, toCopy);
      state->jpg_pos += toCopy;
      return toCopy;
    }

    if (!state->tail_sent) {
      if (maxLen < 2) return 0;
      buffer[0] = '\r';
      buffer[1] = '\n';
      state->tail_sent = true;
      esp_camera_fb_return(state->fb);
      state->fb = nullptr;
      return 2;
    }

    return 0;
  };

  AsyncWebServerResponse *response = request->beginChunkedResponse(
    "multipart/x-mixed-replace; boundary=frame", cb);
  request->send(response); });

    server.begin();
}

void loop()
{
}

Enviar foto a Telegram cuando tocan un pin, stremear micrófono y reproducir audio

Este código:

  • Envía una foto a Telegram cuando el usuario toca el pin 14
  • Stremea el micrófono en ip/stream
  • Reproduce MP3 subidos a /upload

Hay que calibrar bien el valor de touchRead porque a veces dice que alguien lo está tocando cuando no es cierto.

Primero lo dejé en 20 mil, pero luego decidí dejarlo en 30 mil (30000) y ya no me dio errores. Lo mejor es calibrarlo tú mismo.

#include <driver/i2s.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 1   
#define I2S_SD 2   
#define I2S_SCK 42 
// Pines DAC
#define I2S_DOUT 48
#define I2S_BCLK 47
#define I2S_LRC 21 
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
const char *telegramHost = "api.telegram.org";
String botToken = "tu token de Telegram";
String chatID = "tu id de chat";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void checkCamera()
{
  sensor_t *s = esp_camera_sensor_get();
  if (!s)
  {
    Serial.println("No se pudo obtener el sensor!");
    return;
  }

  Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
  Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
  Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void sendPhoto()
{
  Serial.println("Enviando foto");
  /*Descartamos el primero simplemente*/
  camera_fb_t *fb = esp_camera_fb_get();
  if (fb)
  {
    Serial.println("Descartado!");
    esp_camera_fb_return(fb);
  }
  // Tomamos el real
  Serial.println("Tomando real");
  fb = esp_camera_fb_get();
  if (!fb)
  {
    Serial.println("Error al tomar foto");
    return;
  }

  WiFiClientSecure client;
  client.setInsecure();

  if (!client.connect(telegramHost, 443))
  {
    Serial.println("Conexión fallida");
    esp_camera_fb_return(fb);
    return;
  }

  String boundary = "----ESP32CamBoundary";

  // Parte inicial (chat_id y encabezado de archivo)
  String head = "--" + boundary + "\r\n"
                                  "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
                chatID + "\r\n"
                         "--" +
                boundary + "\r\n"
                           "Content-Disposition: form-data; name=\"photo\"; filename=\"esp32s3.jpg\"\r\n"
                           "Content-Type: image/jpeg\r\n\r\n";

  String tail = "\r\n--" + boundary + "--\r\n";

  uint32_t totalLen = head.length() + fb->len + tail.length();

  // Headers HTTP
  client.println("POST /bot" + botToken + "/sendPhoto HTTP/1.1");
  client.println("Host: " + String(telegramHost));
  client.println("Content-Type: multipart/form-data; boundary=" + boundary);
  client.println("Content-Length: " + String(totalLen));
  client.println("Connection: close");
  client.println();

  // Cuerpo
  client.print(head);
  client.write(fb->buf, fb->len);
  client.print(tail);

  esp_camera_fb_return(fb);

  // Leer respuesta
  String response;
  while (client.connected())
  {
    while (client.available())
    {
      char c = client.read();
      response += c;
    }
  }

  Serial.println("Respuesta Telegram:");
  Serial.println(response);
}
void Task1code(void *parameter)
{
  Serial.println("Ejecutando tarea");
  while (1)
  {
    if (nuevaCancion)
    {
      if (file)
      {
        delete file;
        file = nullptr;
      }
      if (mp3)
      {
        delete mp3;
        mp3 = nullptr;
      }

      file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
      mp3 = new AudioGeneratorMP3();
      mp3->begin(file, out);

      nuevaCancion = false;
    }

    if (mp3 && mp3->isRunning())
    {
      if (!mp3->loop())
      {
        mp3->stop();
        delete mp3;
        mp3 = nullptr;
        delete file;
        file = nullptr;
      }
      else
      {
        vTaskDelay(10);
      }
    }
  }
}
void setup()
{

  Serial.begin(115200);
  out = new AudioOutputI2S();
  out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  out->SetChannels(2);
  out->SetGain(1);
  // Configurar I2S
  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_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_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,
      .ws_io_num = I2S_WS,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = I2S_SD};

  i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_1, &pin_config);
  bool ok = SD_MMC.setPins(39, 38, 40);
  if (!SD_MMC.begin("/sdcard", true))
  {
    Serial.println("Error al inicializar la tarjeta SD.");
    return;
  }
  Serial.println("Tarjeta SD inicializada correctamente.");
  // Wifi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(300);
    Serial.print('.');
  }
  Serial.println("\nWiFi connected");
  Serial.print("Servidor corriendo en: http://");
  Serial.println(WiFi.localIP());
  checkCamera();
  // Cámara
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 11;
  config.pin_d1 = 9;
  config.pin_d2 = 8;
  config.pin_d3 = 10;
  config.pin_d4 = 12;
  config.pin_d5 = 18;
  config.pin_d6 = 17;
  config.pin_d7 = 16;
  config.pin_xclk = 15;
  config.pin_pclk = 13;
  config.pin_vsync = 6;
  config.pin_href = 7;
  config.pin_sccb_sda = 4;
  config.pin_sccb_scl = 5;
  config.pin_pwdn = -1;
  config.pin_reset = -1;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_CIF;
  config.pixel_format = PIXFORMAT_JPEG;
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_location = CAMERA_FB_IN_DRAM;
  config.jpeg_quality = 45;
  config.fb_count = 1;
  int res = esp_camera_init(&config);
  if (res != ESP_OK)
  {
    Serial.printf("Es distinto a ok");
  }

  // Termina cámara
  server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
            { request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
            {
      static File uploadFile;

      if(index == 0){
          //Serial.println("Comenzando upload de: " + filename);
          uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
      }

      if(uploadFile){
          uploadFile.write(data, len);
      }

      if(final){
          uploadFile.close();
    nuevaCancion = true;
          //Serial.println("Upload completado, iniciando reproducción...");

          /*Aquí reproducimos pero solo quiero probar*/
          // iniciar reproducción
          //if(file) delete file;
          //if(mp3) delete mp3;

          //file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
          //mp3 = new AudioGeneratorMP3();
          //mp3->begin(file, out);
      } });
  server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
  server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
            {
  AsyncWebServerResponse *response = request->beginChunkedResponse(
      "audio/wav",
      [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
        static bool headerSent = false;
         //float gain = 2.0f; // 2x volumen = +6dB
        if (!headerSent) {
          headerSent = true;
          // Header WAV "infinito"
          uint32_t sampleRate = 16000;
          uint16_t bitsPerSample = 16;
          uint16_t channels = 1;
          uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
          uint16_t blockAlign = channels * bitsPerSample / 8;
          uint8_t header[44] = {
              'R','I','F','F',
              0xFF,0xFF,0xFF,0x7F, // tamaño fake
              'W','A','V','E',
              'f','m','t',' ',
              16,0,0,0,
              1,0,
              (uint8_t)channels,0,
              (uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
              (uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
              (uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
              (uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
              (uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
              (uint8_t)bitsPerSample,0,
              'd','a','t','a',
              0xFF,0xFF,0xFF,0x7F
          };
          memcpy(buffer, header, 44);
          return 44;
        }

        // Leer audio del I2S
        size_t bytesRead = 0;
        i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
        //Serial.printf("Bytes read: %d\n", bytesRead);
        // Aplicar ganancia
        /*
        int16_t *samples = (int16_t *)buffer;
        size_t nSamples = bytesRead / 2;
        for (size_t i = 0; i < nSamples; i++) {
          int32_t s = samples[i] * gain;
          if (s > 32767) s = 32767;
          if (s < -32768) s = -32768;
          samples[i] = (int16_t)s;
        }
*/
        return bytesRead;
      });
  request->send(response); });

  xTaskCreate(
      Task1code,
      "ReproducirMp3",
      10000,
      NULL,
      0,
      &TareaReproducirMP3);

  server.begin();
}

void loop()
{
  if (touchRead(14) > 20000)
  {
    sendPhoto();
  }
  /*
if (mp3 && mp3->isRunning())
      {
          if (!mp3->loop())
          {
              mp3->stop();
              delete mp3;
              delete file;
              mp3 = nullptr;
              file = nullptr;
          }
      }
*/
}

Falta revisar bien el stream del micrófono pues cuando se desconecta es un poco complejo volver a conectar

Optimizando un poco lo anterior

Aumentando el delay de la reproducción de audio. Me parece que 15 es el límite, ya que si vas más abajo es perceptible para el oído.

También aumenté la calidad y resolución de la cámara al máximo, cambié el umbral de activación del pin y no recuerdo si fue aquí o antes que manejo las desconexiones para enviar el header wav siempre que se conecta alguien nuevo

#include <driver/i2s.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
// Micrófono funciona bien con esta configuración
#define I2S_WS 1   // LRCK Nuevo: 1
#define I2S_SD 2   // DATA nuevo 2
#define I2S_SCK 42 // BCL nuevo 42
// Pines DAC
#define I2S_DOUT 48 // Nuevo 47
#define I2S_BCLK 47 // Nuevo 21
#define I2S_LRC 21  // Nuevo 20
#define UMBRAL_SENSOR_TOUCH 30000
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
const char *telegramHost = "api.telegram.org";
String botToken = "tu token";
String chatID = "tu id de chat";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void checkCamera()
{
  sensor_t *s = esp_camera_sensor_get();
  if (!s)
  {
    Serial.println("No se pudo obtener el sensor!");
    return;
  }

  Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
  Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
  Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void sendPhoto()
{
  Serial.println("Enviando foto");
  /*Descartamos el primero simplemente*/
  camera_fb_t *fb = esp_camera_fb_get();
  if (fb)
  {
    Serial.println("Descartado!");
    esp_camera_fb_return(fb);
  }
  // Tomamos el real
  Serial.println("Tomando real");
  fb = esp_camera_fb_get();
  if (!fb)
  {
    Serial.println("Error al tomar foto");
    return;
  }

  WiFiClientSecure client;
  client.setInsecure();

  if (!client.connect(telegramHost, 443))
  {
    Serial.println("Conexión fallida");
    esp_camera_fb_return(fb);
    return;
  }

  String boundary = "----ESP32CamBoundary";

  // Parte inicial (chat_id y encabezado de archivo)
  String head = "--" + boundary + "\r\n"
                                  "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
                chatID + "\r\n"
                         "--" +
                boundary + "\r\n"
                           "Content-Disposition: form-data; name=\"photo\"; filename=\"esp32s3.jpg\"\r\n"
                           "Content-Type: image/jpeg\r\n\r\n";

  String tail = "\r\n--" + boundary + "--\r\n";

  uint32_t totalLen = head.length() + fb->len + tail.length();

  // Headers HTTP
  client.println("POST /bot" + botToken + "/sendPhoto HTTP/1.1");
  client.println("Host: " + String(telegramHost));
  client.println("Content-Type: multipart/form-data; boundary=" + boundary);
  client.println("Content-Length: " + String(totalLen));
  client.println("Connection: close");
  client.println();

  // Cuerpo
  client.print(head);
  client.write(fb->buf, fb->len);
  client.print(tail);

  esp_camera_fb_return(fb);

  // Leer respuesta
  String response;
  while (client.connected())
  {
    while (client.available())
    {
      char c = client.read();
      response += c;
    }
  }

  Serial.println("Respuesta Telegram:");
  Serial.println(response);
}
void Task1code(void *parameter)
{
  Serial.println("Ejecutando tarea");
  while (1)
  {
    if (nuevaCancion)
    {
      if (file)
      {
        delete file;
        file = nullptr;
      }
      if (mp3)
      {
        delete mp3;
        mp3 = nullptr;
      }

      file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
      mp3 = new AudioGeneratorMP3();
      mp3->begin(file, out);

      nuevaCancion = false;
    }

    if (mp3 && mp3->isRunning())
    {
      if (!mp3->loop())
      {
        mp3->stop();
        delete mp3;
        mp3 = nullptr;
        delete file;
        file = nullptr;
      }
      else
      {
        vTaskDelay(15);
      }
    }
  }
}
void setup()
{

  Serial.begin(115200);
  out = new AudioOutputI2S();
  out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  out->SetChannels(2);
  out->SetGain(1);
  // Configurar I2S
  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_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_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,
      .ws_io_num = I2S_WS,
      .data_out_num = I2S_PIN_NO_CHANGE,
      .data_in_num = I2S_SD};

  i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_1, &pin_config);
  bool ok = SD_MMC.setPins(39, 38, 40);
  if (!SD_MMC.begin("/sdcard", true))
  {
    Serial.println("Error al inicializar la tarjeta SD.");
    return;
  }
  Serial.println("Tarjeta SD inicializada correctamente.");
  // Wifi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(300);
    Serial.print('.');
  }
  Serial.println("\nWiFi connected");
  Serial.print("Servidor corriendo en: http://");
  Serial.println(WiFi.localIP());
  checkCamera();
  // Cámara
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 11;
  config.pin_d1 = 9;
  config.pin_d2 = 8;
  config.pin_d3 = 10;
  config.pin_d4 = 12;
  config.pin_d5 = 18;
  config.pin_d6 = 17;
  config.pin_d7 = 16;
  config.pin_xclk = 15;
  config.pin_pclk = 13;
  config.pin_vsync = 6;
  config.pin_href = 7;
  config.pin_sccb_sda = 4;
  config.pin_sccb_scl = 5;
  config.pin_pwdn = -1;
  config.pin_reset = -1;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_SVGA;
  config.pixel_format = PIXFORMAT_JPEG;
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_location = CAMERA_FB_IN_DRAM;
  config.jpeg_quality = 25; // Originalmente 30, ya vi que con 25 sí aguanta
  config.fb_count = 1;
  int res = esp_camera_init(&config);
  if (res != ESP_OK)
  {
    Serial.printf("Es distinto a ok");
  }

  // Termina cámara
  server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
            { request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
            {
      static File uploadFile;

      if(index == 0){
          //Serial.println("Comenzando upload de: " + filename);
          uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
      }

      if(uploadFile){
          uploadFile.write(data, len);
      }

      if(final){
          uploadFile.close();
    nuevaCancion = true;
          //Serial.println("Upload completado, iniciando reproducción...");

          /*Aquí reproducimos pero solo quiero probar*/
          // iniciar reproducción
          //if(file) delete file;
          //if(mp3) delete mp3;

          //file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
          //mp3 = new AudioGeneratorMP3();
          //mp3->begin(file, out);
      } });
  server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
  server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
            {
  static bool headerSent = false;

  auto *response = request->beginChunkedResponse(
      "audio/wav",
      [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
        static const uint8_t header[44] = {
            'R','I','F','F',
            0xFF,0xFF,0xFF,0x7F,
            'W','A','V','E',
            'f','m','t',' ',
            16,0,0,0,
            1,0,
            1,0, // canales
            0x80,0x3E,0,0, // 16000 Hz
            0x00,0x7D,0,0, // byteRate = 32000
            2,0,            // blockAlign
            16,0,           // bits/sample
            'd','a','t','a',
            0xFF,0xFF,0xFF,0x7F
        };

        if (!headerSent) {
          headerSent = true;
          memcpy(buffer, header, 44);
          return 44;
        }

        size_t bytesRead = 0;
        i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, 10 / portTICK_PERIOD_MS);
        return bytesRead;
      });

  request->onDisconnect([&]() {
    headerSent = false;
  });

  request->send(response); });

  xTaskCreate(
      Task1code,
      "ReproducirMp3",
      10000,
      NULL,
      0,
      &TareaReproducirMP3);

  server.begin();
}

void loop()
{
  // Serial.printf("touch read es %d\n", touchRead(14));

  if (touchRead(14) > UMBRAL_SENSOR_TOUCH)
  {
    sendPhoto();
  }
  /*
if (mp3 && mp3->isRunning())
      {
          if (!mp3->loop())
          {
              mp3->stop();
              delete mp3;
              delete file;
              mp3 = nullptr;
              file = nullptr;
          }
      }
*/
}

Conclusión

Me parece que es una tarjeta con muchas características muy buenas, pero sin PSRAM no vamos a poder sacarle mucho partido a la cámara.

Comparado con la ESP32-CAM que tiene 4MB de RAM, esta se queda corta. Una ventaja que le puedo ver es que tiene más pines disponibles que la ESP32-CAM.

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