2026-09-22 19:23:42 +02:00
|
|
|
#include <WiFi.h>
|
|
|
|
|
#include <driver/i2s_std.h>
|
|
|
|
|
|
|
|
|
|
// --- WLAN-Konfiguration ---
|
|
|
|
|
const char* ssid = "Stockfeld";
|
|
|
|
|
const char* password = "70691554384740972413"; // Hier dein echtes Passwort eintragen
|
2026-09-24 00:02:27 +02:00
|
|
|
#define HTTP_PORT 8080
|
|
|
|
|
#define SAMPLE_RATE 48000
|
|
|
|
|
// 480 Samples = 960 Bytes (Extrem sichere Größe für den WLAN-Puffer)
|
|
|
|
|
#define SAMPLES_PER_PKT 480
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
#define PIN_I2S_BCLK GPIO_NUM_0 // D0
|
|
|
|
|
#define PIN_I2S_WS GPIO_NUM_1 // D1
|
|
|
|
|
#define PIN_I2S_DIN GPIO_NUM_2 // D2
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
#define DIGITAL_GAIN 1
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
// 100 Chunks = exakt 1 Sekunde Audio-Puffer im RAM
|
|
|
|
|
#define AUDIO_QUEUE_LEN 100
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
WiFiServer httpServer(HTTP_PORT);
|
|
|
|
|
WiFiClient httpClient;
|
2026-09-22 19:23:42 +02:00
|
|
|
i2s_chan_handle_t rx_chan = NULL;
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
volatile bool isStreaming = false;
|
|
|
|
|
|
|
|
|
|
struct AudioChunk {
|
|
|
|
|
uint16_t sampleCount;
|
|
|
|
|
int16_t samples[SAMPLES_PER_PKT];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QueueHandle_t audioQueue;
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
2026-09-22 19:23:42 +02:00
|
|
|
|
|
|
|
|
void 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;
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
i2s_new_channel(&chan_cfg, NULL, &rx_chan);
|
2026-09-22 19:23:42 +02:00
|
|
|
|
|
|
|
|
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_WS,
|
|
|
|
|
.dout = I2S_GPIO_UNUSED,
|
|
|
|
|
.din = PIN_I2S_DIN,
|
|
|
|
|
.invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
i2s_channel_init_std_mode(rx_chan, &std_cfg);
|
|
|
|
|
i2s_channel_enable(rx_chan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void i2sReaderTask(void *param) {
|
|
|
|
|
static int32_t i2s_raw[SAMPLES_PER_PKT];
|
|
|
|
|
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) 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;
|
|
|
|
|
}
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
if (xQueueSend(audioQueue, &chunk, 0) != pdTRUE) {
|
|
|
|
|
AudioChunk dummy;
|
|
|
|
|
xQueueReceive(audioQueue, &dummy, 0);
|
|
|
|
|
xQueueSend(audioQueue, &chunk, 0);
|
|
|
|
|
}
|
2026-09-22 19:23:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
void handleHTTP() {
|
|
|
|
|
if (httpServer.hasClient()) {
|
|
|
|
|
WiFiClient newClient = httpServer.accept();
|
2026-09-22 19:23:42 +02:00
|
|
|
if (newClient) {
|
2026-09-24 00:02:27 +02:00
|
|
|
if (httpClient) httpClient.stop();
|
|
|
|
|
httpClient = newClient;
|
|
|
|
|
|
|
|
|
|
// 3 Sekunden Timeout auf Socket-Ebene
|
|
|
|
|
httpClient.setTimeout(3000);
|
|
|
|
|
|
2026-09-22 19:23:42 +02:00
|
|
|
isStreaming = false;
|
2026-09-24 00:02:27 +02:00
|
|
|
Serial.printf("Client verbunden: %s (Heap: %u Bytes)\n",
|
|
|
|
|
httpClient.remoteIP().toString().c_str(), ESP.getFreeHeap());
|
2026-09-22 19:23:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
if (!httpClient || !httpClient.connected()) {
|
|
|
|
|
if (isStreaming) {
|
|
|
|
|
Serial.println("Client getrennt.");
|
|
|
|
|
isStreaming = false;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-09-22 19:23:42 +02:00
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
if (!isStreaming && httpClient.available()) {
|
|
|
|
|
String req = httpClient.readStringUntil('\n');
|
2026-09-22 19:23:42 +02:00
|
|
|
req.trim();
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
while (httpClient.connected()) {
|
|
|
|
|
String line = httpClient.readStringUntil('\n');
|
2026-09-22 19:23:42 +02:00
|
|
|
line.trim();
|
|
|
|
|
if (line.length() == 0) break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
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);
|
2026-09-22 19:23:42 +02:00
|
|
|
isStreaming = true;
|
2026-09-24 00:02:27 +02:00
|
|
|
Serial.println("Streaming aktiv (Sende-Schleife aktiviert).");
|
2026-09-22 19:23:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void streamAudio() {
|
2026-09-24 00:02:27 +02:00
|
|
|
if (!isStreaming || !httpClient || !httpClient.connected()) return;
|
|
|
|
|
|
|
|
|
|
AudioChunk chunk;
|
|
|
|
|
if (xQueueReceive(audioQueue, &chunk, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
|
|
|
size_t bytesToSend = chunk.sampleCount * sizeof(int16_t);
|
|
|
|
|
const uint8_t* p = (const uint8_t*)chunk.samples;
|
|
|
|
|
size_t remaining = bytesToSend;
|
|
|
|
|
|
|
|
|
|
// KERN-FIX: Sende-Schleife! Behandelt 'Partial Writes' nun sauber,
|
|
|
|
|
// ohne die Verbindung versehentlich selbst zu kappen.
|
|
|
|
|
while (remaining > 0 && httpClient.connected()) {
|
|
|
|
|
size_t written = httpClient.write(p, remaining);
|
|
|
|
|
|
|
|
|
|
if (written == 0) {
|
|
|
|
|
Serial.println("Fataler Netzwerk-Timeout -> Abbruch.");
|
|
|
|
|
httpClient.stop();
|
|
|
|
|
isStreaming = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p += written;
|
|
|
|
|
remaining -= written;
|
|
|
|
|
}
|
2026-09-22 19:23:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setup() {
|
|
|
|
|
Serial.begin(115200);
|
2026-09-24 00:02:27 +02:00
|
|
|
delay(200);
|
|
|
|
|
|
2026-09-22 19:23:42 +02:00
|
|
|
setupI2S();
|
|
|
|
|
|
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
|
WiFi.begin(ssid, password);
|
|
|
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
|
|
|
delay(250);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 00:02:27 +02:00
|
|
|
WiFi.setSleep(false);
|
|
|
|
|
|
|
|
|
|
audioQueue = xQueueCreate(AUDIO_QUEUE_LEN, sizeof(AudioChunk));
|
|
|
|
|
|
|
|
|
|
xTaskCreate(i2sReaderTask, "i2s_reader", 8192, NULL, 3, NULL);
|
|
|
|
|
|
|
|
|
|
httpServer.begin();
|
|
|
|
|
Serial.printf("\n=== HTTP Streamer bereit: http://%s:%d/stream ===\n",
|
|
|
|
|
WiFi.localIP().toString().c_str(), HTTP_PORT);
|
2026-09-22 19:23:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop() {
|
2026-09-24 00:02:27 +02:00
|
|
|
handleHTTP();
|
2026-09-22 19:23:42 +02:00
|
|
|
streamAudio();
|
2026-09-24 00:02:27 +02:00
|
|
|
}
|