From adefe9164f076d9b9a792f6867359e6ae0179deb Mon Sep 17 00:00:00 2001 From: Jannis Date: Sat, 26 Sep 2026 16:41:45 +0200 Subject: [PATCH] Fix --- .vscode/settings.json | 3 + README.md | 4 + meinBirdNetMic.ino | 261 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 meinBirdNetMic.ino diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ba1456b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "C:/Users/janni/Documents/HomeGIT/BirdNet-goMikrofon/Arduino-audio-tools-main/arduino-audio-tools-main" +} \ No newline at end of file diff --git a/README.md b/README.md index d025159..f371cba 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Dieses Repository enthält die Firmware für einen autarken HTTP-WAV-Audiostream Der Streamer erfasst Audio mit 48 kHz / 16 Bit Mono und stellt es über einen unterbrechungsfreien HTTP-Webserver direkt im Netzwerk bereit. Dieser Stream kann nativ von **BirdNET-Go** (z. B. im Docker-Container auf einem NAS) oder im Browser/VLC verarbeitet werden. +## Mein Code + +In der meinBirdNEtMic.ino ist mein eigener code für das Mikrofon ohne aussetzer! + --- ## Features diff --git a/meinBirdNetMic.ino b/meinBirdNetMic.ino new file mode 100644 index 0000000..b0e2838 --- /dev/null +++ b/meinBirdNetMic.ino @@ -0,0 +1,261 @@ +#include +#include + +// --- WLAN-Konfiguration --- +const char* ssid = "Stockfeld"; +const char* password = "70691554384740972413"; +#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; + } + } + } +}