#include #include // --- WLAN-Konfiguration --- const char* ssid = "Nordlicht"; const char* password = "03158968696374163104"; #define HTTP_PORT 80 #define SAMPLE_RATE 48000 // 2400 Samples = 50 ms pro Chunk (statt vorher 10 ms). // Grund: Diagnose zeigte, dass httpClient.write() im Schnitt ~17 ms pro // Aufruf braucht (WLAN-Overhead pro Paket), bei 100 kleinen Paketen/Sekunde // reicht das nicht mehr aus. Größere, seltenere Pakete (20/Sekunde statt // 100/Sekunde) verteilen den Overhead pro Aufruf auf mehr Nutzdaten. #define SAMPLES_PER_PKT 2400 // --- Angepasste Pins für ESP32-WROOM-32U --- #define PIN_I2S_BCLK GPIO_NUM_27 #define PIN_I2S_LRCL GPIO_NUM_26 #define PIN_I2S_Dout GPIO_NUM_25 #define DIGITAL_GAIN 1 // 10 Chunks x 50 ms = 500 ms Audio-Puffer im RAM. // Reicht, um kurze WLAN-Hänger abzufedern, ohne unnötig Heap zu binden. // Wenn regelmäßig mehr als das gebraucht wird, ist das Netzwerk zu langsam - // ein größerer Puffer würde den Ton dann nur weiter verzögern statt das // eigentliche Problem zu lösen. #define AUDIO_QUEUE_LEN 10 // Diagnose-Ausgabe alle N Millisekunden #define DIAG_INTERVAL_MS 5000 WiFiServer httpServer(HTTP_PORT); WiFiClient httpClient; i2s_chan_handle_t rx_chan = NULL; volatile bool isStreaming = false; // Zähler zur Diagnose: zeigen, ob Aufnahme- oder Sendeseite das Problem ist volatile uint32_t droppedChunks = 0; // Queue war voll -> alter Chunk verworfen volatile uint32_t readErrors = 0; // i2s_channel_read() fehlgeschlagen volatile uint32_t sendFailures = 0; // httpClient.write() lieferte 0 Bytes volatile uint32_t maxWriteUs = 0; // längster einzelner write()-Aufruf im aktuellen Fenster volatile uint64_t totalWriteUs = 0; // Summe aller write()-Zeiten im aktuellen Fenster volatile uint32_t writeCalls = 0; // Anzahl write()-Aufrufe im aktuellen Fenster struct AudioChunk { uint16_t sampleCount; int16_t samples[SAMPLES_PER_PKT]; }; QueueHandle_t audioQueue; // Vergrößert den Stack der Arduino-loop()-Task von den standardmäßigen 8192 // Byte auf 16384 Byte. Der ESP32-Arduino-Core ruft diese (schwach gebundene) // Funktion beim Start auf, um die Stackgröße für loopTask festzulegen - so // lässt sie sich ohne Board-Menü/Build-Flags direkt im Sketch anpassen. size_t getArduinoLoopTaskStackSize(void) { return 16384; } // Dein perfekter, unendlicher WAV-Header const uint8_t wavHeader[44] = { 'R', 'I', 'F', 'F', 0xFF, 0xFF, 0xFF, 0xFF, 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ', 16, 0, 0, 0, 1, 0, 1, 0, 0x80, 0xBB, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 2, 0, 16, 0, 'd', 'a', 't', 'a', 0xFF, 0xFF, 0xFF, 0xFF }; bool setupI2S() { i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); chan_cfg.dma_desc_num = 6; chan_cfg.dma_frame_num = 240; if (i2s_new_channel(&chan_cfg, NULL, &rx_chan) != ESP_OK) { Serial.println("FEHLER: i2s_new_channel fehlgeschlagen"); return false; } i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = PIN_I2S_BCLK, .ws = PIN_I2S_LRCL, .dout = I2S_GPIO_UNUSED, .din = PIN_I2S_Dout, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false } } }; std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; if (i2s_channel_init_std_mode(rx_chan, &std_cfg) != ESP_OK) { Serial.println("FEHLER: i2s_channel_init_std_mode fehlgeschlagen"); return false; } if (i2s_channel_enable(rx_chan) != ESP_OK) { Serial.println("FEHLER: i2s_channel_enable fehlgeschlagen"); return false; } return true; } void i2sReaderTask(void *param) { static int32_t i2s_raw[SAMPLES_PER_PKT]; static AudioChunk chunk; for (;;) { size_t bytes_read = 0; esp_err_t res = i2s_channel_read(rx_chan, i2s_raw, sizeof(i2s_raw), &bytes_read, portMAX_DELAY); if (res != ESP_OK || bytes_read == 0) { readErrors++; continue; } int samples = bytes_read / sizeof(int32_t); chunk.sampleCount = samples; for (int i = 0; i < samples; i++) { int32_t val = (i2s_raw[i] >> 16) * DIGITAL_GAIN; if (val > 32767) val = 32767; if (val < -32768) val = -32768; chunk.samples[i] = (int16_t)val; } if (xQueueSend(audioQueue, &chunk, 0) != pdTRUE) { AudioChunk dummy; xQueueReceive(audioQueue, &dummy, 0); xQueueSend(audioQueue, &chunk, 0); droppedChunks++; } } } void setup() { Serial.begin(115200); delay(200); if (!setupI2S()) { Serial.println("I2S-Setup fehlgeschlagen - Neustart in 3s"); delay(3000); ESP.restart(); } WiFi.mode(WIFI_STA); WiFi.setSleep(false); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(250); } audioQueue = xQueueCreate(AUDIO_QUEUE_LEN, sizeof(AudioChunk)); // FIX: Arduino's loop()/WiFi-Netzwerkstack laufen im Arduino-ESP32-Framework // standardmäßig auf Core 1, der WLAN-/LWIP-Treiber dagegen auf Core 0. // Der i2s-Reader gehört deshalb auf Core 1, NICHT auf Core 0 - sonst // konkurriert er mit dem hochprioren WLAN-Treiber um Rechenzeit, was // bei kurzen DMA-Puffern (hier ~30 ms) zu Sample-Verlusten führt. // Seine Priorität (3) liegt trotzdem über der von loop() (Standard: 1), // sodass er loop() bei Bedarf verdrängt. xTaskCreatePinnedToCore(i2sReaderTask, "i2s_reader", 12288, NULL, 3, NULL, 1); httpServer.begin(); Serial.printf("\n=== HTTP Streamer bereit: http://%s/\n", WiFi.localIP().toString().c_str()); } void loop() { static uint32_t lastDiag = 0; uint32_t now = millis(); if (now - lastDiag >= DIAG_INTERVAL_MS) { lastDiag = now; uint32_t avgUs = writeCalls ? (uint32_t)(totalWriteUs / writeCalls) : 0; Serial.printf("[diag] dropped=%u readErr=%u sendFail=%u heap=%u rssi=%d writes=%u maxWriteUs=%u avgWriteUs=%u\n", droppedChunks, readErrors, sendFailures, ESP.getFreeHeap(), WiFi.RSSI(), writeCalls, maxWriteUs, avgUs); maxWriteUs = 0; totalWriteUs = 0; writeCalls = 0; } if (httpServer.hasClient()) { WiFiClient newClient = httpServer.accept(); if (newClient) { if (httpClient) httpClient.stop(); httpClient = newClient; // Verhindert Paket-Stau und Latenzen im WLAN (Anti-Dropout-Befehl!) httpClient.setNoDelay(true); httpClient.setTimeout(3000); isStreaming = false; } } if (!httpClient || !httpClient.connected()) { if (isStreaming) { isStreaming = false; } return; } if (!isStreaming && httpClient.available()) { String req = httpClient.readStringUntil('\n'); req.trim(); while (httpClient.connected()) { String line = httpClient.readStringUntil('\n'); line.trim(); if (line.length() == 0) break; } if (req.startsWith("GET")) { httpClient.print("HTTP/1.0 200 OK\r\n" "Content-Type: audio/wav\r\n" "Connection: keep-alive\r\n\r\n"); httpClient.write(wavHeader, sizeof(wavHeader)); xQueueReset(audioQueue); isStreaming = true; } } if (isStreaming && httpClient.connected()) { static AudioChunk chunk; if (xQueueReceive(audioQueue, &chunk, pdMS_TO_TICKS(10)) == pdTRUE) { size_t bytesToSend = chunk.sampleCount * sizeof(int16_t); const uint8_t* p = (const uint8_t*)chunk.samples; size_t remaining = bytesToSend; while (remaining > 0 && httpClient.connected()) { uint32_t t0 = micros(); size_t written = httpClient.write(p, remaining); uint32_t dt = micros() - t0; if (dt > maxWriteUs) maxWriteUs = dt; totalWriteUs += dt; writeCalls++; if (written == 0) { sendFailures++; httpClient.stop(); isStreaming = false; return; } p += written; remaining -= written; } } } }