Nach meinen bisherigen Webradio-Projekten mit dem VS1053-Hardwaredecoder war es für mich Zeit für einen kleinen technologischen Generationswechsel: weg vom spezialisierten Audio-Decoder und hin zu einer Lösung, bei der deutlich mehr Arbeit direkt vom ESP32 übernommen wird.
In diesem Aufbau übernimmt der leistungsfähige ESP32-S3 die Audio-Dekodierung in Software. Die digitalen Audiodaten werden anschließend über den I2S-Bus an den PCM5102 weitergegeben. Dieser arbeitet als Digital-Analog-Wandler (DAC) und wandelt das digitale Audiosignal in ein analoges Signal um, das anschließend beispielsweise an einen Verstärker weitergegeben werden kann.
Klanglich gefällt mir diese Kombination sehr gut und sie wirkt für mich deutlich besser als meine bisherige Lösung mit dem VS1053-Modul. Nun läuft hier auch zeitgleich das auslesen der Metadaten. Interpret, Titel und weitere Informationen werden auf dem Display angezeigt. Die Streams bereiten mir somit nicht nur einen guten Hörgenuss, sondern zeigen auch was da gerade läuft.
Für dieses Projekt verwende ich außerdem die R16N8-Variante des ESP32-S3 mit 16 MB Flashspeicher und 8 MB PSRAM. Der zusätzliche Speicher ermöglicht einen großzügigen Audio-Puffer, der Schwankungen der WLAN-Verbindung besser abfangen soll. In der Theorie sollten kurze Netzwerkprobleme dadurch seltener zu hörbaren Aussetzern führen. Bisher störungsfrei.
- Bussysteme: Wir nutzen I2C für das Display und I2S für Audio.
- Ressourcenmanagement: Konfiguration des PSRAM
- Interrupt-Handling: Der Rotary Encoder steuert die Lautstärke unterbrechungsfrei parallel zur Musikwiedergabe.
So sieht der derzeitige Aufbau aus:

Das Hardware-Setup & Die Lötbrücken
Hardware:
ESP32-S3 N16R8
PCM5102
Encoder Zum Tippen und regeln
Display SH1107 mit 128 x 128 Pixel
Vorbereitung des PCM5102:
Damit der DAC mit dem ESP32-S3 zusammenarbeitet, musst du auf der Rückseite des Moduls vier kleine Lötbrücken (H1L bis H4L) setzen. Verbinde dazu jeweils den mittleren Kontakt mit der Seite, die als (L) LOW markiert ist:
H1 (FLT) auf LOW: Stellt den Filter auf Normal (bester Klang für Webradio).
H2 (DEMP) auf LOW: De-Emphasis aus (unverfälschter Sound).
H3 (XSMT) auf HIGH: Das ist der entscheidende Punkt! XSMT ist „Active Low Mute“. Das heißt: Wenn du H3 auf LOW (GND) lötest, bleibt der Chip stumm geschaltet. Er muss auf HIGH (3.3V) liegen, damit wir etwas hören!
H4 (FMT) auf LOW: Stellt den Chip auf den I2S-Modus, den wir in der main.cpp nutzen.

Verdrahtungsplan:
| Komponente | Pin am Bauteil | ESP32-S3 GPIO | Funktion / Anmerkung |
| PCM5102 (I2S) | VCC | 3.3V | Stromversorgung |
| GND | GND | Masse | |
| BCK | GPIO 7 | Bit Clock | |
| LCK (WS) | GPIO 6 | Word Select / Left-Right Clock | |
| DIN / DOUT | GPIO 5 | Data In | |
| SCK | GND | WICHTIG: Muss auf GND liegen! | |
| SH1107 (I2C) | VCC | 3.3V | Stromversorgung Display |
| GND | GND | Masse | |
| SDA | GPIO 8 | I2C Data | |
| SCL | GPIO 9 | I2C Clock | |
| Encoder | CLK | GPIO 4 | Dreh-Signal A |
| DT | GPIO 1 | Dreh-Signal B | |
| SW | GPIO 2 | Taster (Click) | |
| + / VCC | 3.3V | Versorgung Encoder | |
| GND | GND | Masse |
Library:
ESP-audioI2S-master von Schreibfaul (Wolle)
Rotary Encoder von Matthias Hertel
U82g Library von olikraus
Code:
#include <WiFi.h>
#include <Audio.h>
#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include <Wire.h>
#define WIFI_SSID "xxx"
#define WIFI_PASS "xxx"
#define OLED_SDA 8
#define OLED_SCL 9
#define ENCODER_CLK 1
#define ENCODER_DT 4
#define ENCODER_SW 2
#define I2S_BCK 7
#define I2S_DOUT 5
#define I2S_WS 6
// --- STATIONEN LISTE ---
struct Station { const char* name; const char* url; };
Station stationList[] = {
{"WDR 4", "http://wdr-wdr4-live.icecast.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3"},
{"1LIVE", "http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3"},
{"Deutschlandfunk", "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3"},
{"Antenne Bayern", "http://stream.antenne.de/antenne/stream/mp3"},
{"Radio Swiss Pop", "http://stream.srg-ssr.ch/m/rsp/mp3_128"},
{"ROCK ANTENNE 80er", "http://stream.rockantenne.de/80er-rock/stream/mp3"},
{"Radio BOB! 80s Rock", "http://streams.radiobob.de/bob-80errock/mp3-192/mediaplayer"},
{"80s80s Rock", "http://streams.80s80s.de/rock/mp3-192/"}
};
const int maxStations = sizeof(stationList) / sizeof(stationList[0]);
int currentStationIndex = 0;
// --- OBJEKTE ---
Audio audio;
U8G2_SH1107_128X128_F_HW_I2C u8g2(U8G2_R3, U8X8_PIN_NONE, OLED_SCL, OLED_SDA);
RotaryEncoder encoder(ENCODER_CLK, ENCODER_DT, RotaryEncoder::LatchMode::TWO03);
// --- VARIABLEN ---
int volume = 10;
String currentTitle = "Warte auf Titel...";
unsigned long lastDisplayUpdate = 0;
// --- DISPLAY UPDATE ---
void updateDisplay() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13B_tf);
u8g2.drawUTF8(0, 25, stationList[currentStationIndex].name);
u8g2.drawHLine(0, 28, 128);
u8g2.setFont(u8g2_font_6x12_tf);
if (currentTitle.length() > 20) {
u8g2.drawUTF8(0, 45, currentTitle.substring(0, 20).c_str());
if (currentTitle.length() > 40) {
u8g2.drawUTF8(0, 60, currentTitle.substring(20, 40).c_str());
u8g2.drawUTF8(0, 70, currentTitle.substring(40, 60).c_str());
} else {
u8g2.drawUTF8(0, 60, currentTitle.substring(20).c_str());
}
} else {
u8g2.drawUTF8(0, 55, currentTitle.c_str());
}
u8g2.drawHLine(0, 105, 128);
u8g2.setFont(u8g2_font_6x12_tf);
char buf[40];
int totalBuf = audio.getInBufferSize();
int fillPercent = 0;
if (totalBuf > 0) {
fillPercent = (audio.inBufferFilled() * 100) / totalBuf;
}
if (fillPercent > 100) fillPercent = 100;
snprintf(buf, sizeof(buf), "Vol:%d | Buf:%d%%", volume, fillPercent);
u8g2.drawStr(0, 118, buf);
u8g2.sendBuffer();
}
// --- NEUER ZENTRALER CALLBACK ---
// Ersetzt audio_info / audio_showstation / audio_showstreamtitle / audio_showicy
// Muss global definiert sein und exakt diese Signatur haben!
void my_audio_info(Audio::msg_t m) {
switch (m.e) {
case Audio::evt_streamtitle:
Serial.printf("TITEL ERKANNT: %s\n", m.msg);
currentTitle = String(m.msg);
break;
case Audio::evt_name:
Serial.printf("Sender gefunden: %s\n", m.msg);
currentTitle = "Stream laeuft...";
break;
case Audio::evt_bitrate:
Serial.printf("Bitrate: %s\n", m.msg);
break;
case Audio::evt_info:
Serial.printf("Audio-Info: %s\n", m.msg);
break;
case Audio::evt_icydescription:
Serial.printf("ICY-Description: %s\n", m.msg);
break;
case Audio::evt_icyurl:
Serial.printf("ICY-URL: %s\n", m.msg);
break;
case Audio::evt_lasthost:
Serial.printf("Letzter Host: %s\n", m.msg);
break;
case Audio::evt_eof:
Serial.println("Stream EOF");
break;
default:
// Optional: Alle anderen Events zum Debuggen ausgeben
// Serial.printf("Event [%d]: %s\n", m.e, m.msg ? m.msg : "(null)");
break;
}
}
// --- SETUP ---
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n--- PRILCHEN WEBRADIO V4 START ---");
// 0. WICHTIG: Callback registrieren BEVOR Audio benutzt wird
Audio::audio_info_callback = my_audio_info;
Serial.println("Metadaten-Callback registriert.");
// 1. Hardware Check
Serial.printf("Chip Modell: %s\n", ESP.getChipModel());
Serial.printf("Flash Größe: %d MB\n", ESP.getFlashChipSize() / (1024 * 1024));
if (psramFound()) {
Serial.printf("PSRAM erkannt: %d MB\n", ESP.getPsramSize() / (1024 * 1024));
} else {
Serial.println("ACHTUNG: Kein PSRAM erkannt! Bitte PSRAM in IDE aktivieren.");
}
// 2. I2C & Display
Wire.begin(OLED_SDA, OLED_SCL);
Wire.setClock(400000);
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13B_tf);
u8g2.drawStr(25, 45, "Webradio");
u8g2.drawStr(10, 65, "Prilchen LABS");
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 100, "WLAN verbinden...");
u8g2.sendBuffer();
// 3. Encoder Button
pinMode(ENCODER_SW, INPUT_PULLUP);
// 4. WiFi
WiFi.begin(WIFI_SSID, WIFI_PASS);
int wifiTimeout = 0;
while (WiFi.status() != WL_CONNECTED && wifiTimeout < 40) {
delay(250);
Serial.print(".");
wifiTimeout++;
}
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13B_tf);
u8g2.drawStr(25, 45, "Webradio");
u8g2.drawStr(10, 65, "Prilchen LABS");
u8g2.setFont(u8g2_font_6x12_tf);
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi Verbunden! IP: " + WiFi.localIP().toString());
u8g2.drawStr(0, 100, "WLAN Erfolgreich!");
u8g2.drawStr(0, 110, WiFi.localIP().toString().c_str());
} else {
Serial.println("\nWiFi Fehler!");
u8g2.drawStr(0, 80, "WLAN Fehler!");
u8g2.drawStr(0, 100, "Bitte Reset druecken");
}
u8g2.sendBuffer();
delay(3000);
// 5. Audio Init
audio.setPinout(I2S_BCK, I2S_WS, I2S_DOUT);
if (psramFound()) {
Serial.println("PSRAM aktiv: Audio-Bibliothek nutzt erweiterten Puffer automatisch.");
}
audio.setVolume(volume);
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Verbinde zu: " + String(stationList[currentStationIndex].name));
audio.connecttohost(stationList[currentStationIndex].url);
}
}
// --- LOOP ---
void loop() {
audio.loop();
encoder.tick();
if (millis() - lastDisplayUpdate > 1000) {
updateDisplay();
lastDisplayUpdate = millis();
}
// Lautstärke
static int lastPos = 0;
int newPos = encoder.getPosition();
if (lastPos != newPos) {
if (newPos > lastPos) volume = min(volume + 1, 21);
else volume = max(volume - 1, 0);
audio.setVolume(volume);
lastPos = newPos;
updateDisplay();
}
// Senderwechsel
static bool lastBtnState = HIGH;
bool currentBtnState = digitalRead(ENCODER_SW);
if (lastBtnState == HIGH && currentBtnState == LOW) {
delay(50);
if (digitalRead(ENCODER_SW) == LOW) {
currentStationIndex++;
if (currentStationIndex >= maxStations) currentStationIndex = 0;
Serial.println("Wechsle Sender zu: " + String(stationList[currentStationIndex].name));
currentTitle = "Lade Sender...";
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_8x13B_tf);
u8g2.drawStr(0, 60, "Lade Sender...");
u8g2.drawUTF8(0, 80, stationList[currentStationIndex].name);
u8g2.sendBuffer();
audio.connecttohost(stationList[currentStationIndex].url);
}
}
lastBtnState = currentBtnState;
}Arduino IDE Einstellungen:
Für ein generisches ESP32-S3 N16R8-Board habe ich in der Arduino IDE diese Einstellungen verwendet:
| Arduino IDE → Werkzeuge | Einstellung |
|---|---|
| Board | ESP32S3 Dev Module |
| USB CDC On Boot | Enabled |
| CPU Frequency | 240 MHz (WiFi) |
| Core Debug Level | None |
| USB DFU On Boot | Disabled |
| Erase All Flash Before Sketch Upl | Disabled |
| Events Run On | Core 1 |
| Flash Mode | QIO 80 MHz |
| Flash Size | 16MB (128Mb) |
| JTAG Adapter | Disabled |
| Arduino Runs On | Core 1 |
| USB | Disabled |
| Partition Scheme | z. B. 16M Flash (3MB APP/9.9MB FATFS) |
| PSRAM | OPI PSRAM |
| Upload Mode | UART0 / Hardware CDC |
| Upload Speed | 921600 |
| USB Mode | Hardware CDC and JTA |
Fazit
Es klingt sehr angenehm aus den kleinen Aktivboxen. Da auch hier deutlich mehr RAM zu Verfügung steht, wurde es mal Zeit auch die Metadaten der gespielten Titel abzufragen und auf dem Display anzeigen zu lassen. Dann sollte ich mich mal dran machen ein passendes Gehäuse zu erstellen.
