Compare commits
No commits in common. "2290d201ae7212244c1e3d9cd4dbdd5f28e7a78c" and "2316edfd4bc92da4ce28edc46eed566471895fa4" have entirely different histories.
2290d201ae
...
2316edfd4b
3 changed files with 0 additions and 268 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"cmake.sourceDirectory": "C:/Users/janni/Documents/HomeGIT/BirdNet-goMikrofon/Arduino-audio-tools-main/arduino-audio-tools-main"
|
|
||||||
}
|
|
||||||
|
|
@ -4,10 +4,6 @@ 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.
|
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
|
## Features
|
||||||
|
|
|
||||||
|
|
@ -1,261 +0,0 @@
|
||||||
#include <WiFi.h>
|
|
||||||
#include <driver/i2s_std.h>
|
|
||||||
|
|
||||||
// --- 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue