Hier der zweite Teil zu dem WebRadio Projekt Teil 1.
In diesem Tutorial wird das Radio ein Lautstärke Regler, dann ein OLED Display zur Stationsanzeige erhalten und da der neue Rotary Encoder auch ein Push Button hat, soll dieser beim drücken zur nächsten Radiostation springen.
Das ganze kommt abschließend in ein 3D gedrucktes Gehäuse.

Erweiterung des WebRadio um einen Lautstärkeregler
Nun als erste Erweiterung wird ein Regler zum einstellen der Lautstärke ergänzt. Ich nehme einen Regler mit Drucktaste, damit später auch noch die Stationswahl damit eingestellt werden kann.
Benötigte Hardware:
Drehimpuls Regler
Verdrahtet:
Pin 34 mit CLK
Pin 33 mit DT
Pin 32 mit SW
5V mit + oder VCC
GND an GND

Sketcherweiterung mit Lautstärkeregler:
#include <VS1053Driver.h>
#define UNDEFINED -1
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WiFi.h>
#define VS1053_CS D1
#define VS1053_DCS D0
#define VS1053_DREQ D3
#endif
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#define VS1053_CS 5
#define VS1053_DCS 16
#define VS1053_DREQ 4
#endif
// Lautstärke:
const int encoderPin1 = 34; // Encoder Pin CLK
const int encoderPin2 = 33; // Encoder Pin DT
const int buttonPin = 32; // Button Pin SW
int encoderValue = 70; //start Lautstärke
int lastEncoded = 0;
VS1053 player(VS1053_CS, VS1053_DCS, VS1053_DREQ, UNDEFINED, SPI);
WiFiClient client;
// WiFi Einstellungen
const char *ssid = "euer SSID Name";
const char *password = "SSID Passwort";
//Erfolgreich getestete Sender:
const char *host = "ic.radiomonster.fm";
//const char *path = "/tophits.ultra"; //Tophits Kanal
//const char *path = "/dance.ultra"; //Dance Kanal
const char *path = "/evergreens.ultra"; //Evergreens Kanal
//const char *path = "/schlager.ultra"; //Schlager Kanal
int httpPort = 80;
//const char *host = "icecast.omroep.nl";
//const char *path = "/radio6-bb-mp3";
//int httpPort = 80;
//const char *host = "puma.streemlion.com";
//const char *path = "/stream";
//int httpPort = 1960;
// Die Puffergröße 64 scheint optimal zu sein. Bei 32 und 128 könnte der Klang blechern klingen
uint8_t mp3buff[64];
void setup() {
pinMode(encoderPin1, INPUT);
pinMode(encoderPin2, INPUT);
pinMode(buttonPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(encoderPin1), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPin2), updateEncoder, CHANGE);
Serial.begin(115200);
// 3 Sekunden Pause, bis VS1053 und PAM8403 eingeschaltet sind
// sonst startet das System möglicherweise nicht richtig
delay(3000);
// This can be set in the IDE no need for ext library
// system_update_cpu_freq(160);
Serial.println("\n\nEinfaches Wifi Radio startet");
SPI.begin();
player.beginOutput();
player.setVolume(encoderValue);
Serial.print("Connecting to SSID ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.print("connecting to ");
Serial.println(host);
if (!client.connect(host, httpPort)) {
Serial.println("Connection failed");
return;
}
Serial.print("Requesting stream: ");
Serial.println(path);
client.print(String("GET ") + path + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
}
void loop() {
encoderValue = constrain(encoderValue, 0, 100);
player.setVolume(encoderValue);
if (!client.connected()) {
Serial.println("Reconnecting...");
if (client.connect(host, httpPort)) {
client.print(String("GET ") + path + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
}
}
if (client.available() > 0) {
// Die Puffergröße 64 scheint optimal zu sein. Bei 32 und 128 könnte der Klang blechern klingen
uint8_t bytesread = client.read(mp3buff, 64);
player.playChunk(mp3buff, bytesread);
}
}
void updateEncoder() {
int MSB = digitalRead(encoderPin1); //MSB = Bit mit dem höchsten Stellenwert
int LSB = digitalRead(encoderPin2); //LSB = Bit mit dem niedrigsten Stellenwert
int encoded = (MSB << 1) | LSB; //Umwandlung des 2-Pin-Wertes in eine einzelne Zahl
int sum = (lastEncoded << 2) | encoded; //Hinzufügung zum vorherigen kodierten Wert
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue ++;
if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue --;
lastEncoded = encoded; //diesen Wert für das nächste Mal speichern
}
Damit die Abfrage welche Lautstärke gewünscht ist, nicht störend auf den laufenden Radio Stream auswirkt, wird hier mit Interrupts (attachInterrupt in Zeile 55/56) gearbeitet.
Was ist ein Interrupt? Ein Interrupt ist ein Signal, das den Mikrocontroller dazu bringt, seine aktuelle Aufgabe zu unterbrechen und eine spezielle Funktion (ISR) auszuführen.
Warum sind Interrupts nützlich? Sie ermöglichen es dem Mikrocontroller, auf Ereignisse sofort zu reagieren, ohne ständig nach diesen Ereignissen suchen zu müssen. Das spart Zeit und Ressourcen.
Beispielanwendung: Stell dir vor, wie der hier verwendete Drehgeber, der Impulse sendet. Mit einem Interrupt verpasst der Mikrocontroller keinen Impuls, auch wenn er andere Aufgaben erledigt.
ISR (Interrupt Service Routine): Das ist die Funktion, die ausgeführt wird, wenn ein Interrupt auftritt. Sie sollte kurz und schnell sein, um den Mikrocontroller nicht zu lange zu blockieren.
Erweiterung des WebRadio um einen Display:
Das WebRadio wird hier um ein OLED Display erweitert, zur Anzeige von Senderinformationen.
Der Regler soll nun zusätzlich durch drücken die Sende-Auswahl weiterschalten.
Benötigte Hardware:
OLED Display 128×64 SSD1306
Bibliothek:
Adafruit_SSD1306 incl. aller Abhängigkeiten

Verdrahtet:
Pin 22 mit CLK
Pin21 mit SDA
3,3V mit VCC
GND an GND

Sketch incl. StationsAuswahl:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width
#define SCREEN_HEIGHT 64 // OLED display height
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
#include <VS1053Driver.h>
#define UNDEFINED -1
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WiFi.h>
#define VS1053_CS D1
#define VS1053_DCS D0
#define VS1053_DREQ D3
#endif
#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#define VS1053_CS 5
#define VS1053_DCS 16
#define VS1053_DREQ 4
#endif
// Lautstärke:
const int encoderPin1 = 34;
const int encoderPin2 = 33;
const int buttonPin = 32;
int encoderValue = 70;
int lastEncoded = 0;
VS1053 player(VS1053_CS, VS1053_DCS, VS1053_DREQ, UNDEFINED, SPI);
WiFiClient client;
// WiFi Einstellungen
const char *ssid = "euer SSID Name";
const char *password = "SSID Passwort";
// Liste der Radiostationen, Pfade und Beschreibungen als Array
const char *hosts[] = {
"ic.radiomonster.fm", "ic.radiomonster.fm", "ic.radiomonster.fm", "ic.radiomonster.fm",
"icecast.omroep.nl", "puma.streemlion.com",
"stream.radioparadise.com", "icecast.omroep.nl", "jazzradio.ice.infomaniak.ch", "stream.klassikradio.de",
"ice6.somafm.com", "top40.radio.net", "st02.sslstream.dlf.de", "streams.80s80s.de",
"icecast.omroep.nl", "ice6.somafm.com"
};
const char *paths[] = {
"/evergreens.ultra", "/tophits.ultra", "/dance.ultra", "/schlager.ultra",
"/radio6-bb-mp3", "/stream",
"/aac-320", "/radio6-bb-mp3", "/jazzradio-high.mp3", "/klassikfm-high",
"/groovesalad-128-mp3", "/top40-mp3", "/dlf/02/mid.mp3", "/80s80smp3-high",
"/radio2-bb-mp3", "/secretagent-128-mp3"
};
const int httpPorts[] = { 80, 80, 80, 80, 80, 1960, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80 };
// Beschreibungen der Sender für das Display
const char *descriptions[] = {
"Evergreens", "Top Hits", "Dance Hits", "Schlager",
"Radio 6 NL", "Puma Stream",
"Radio Paradise", "Chill Out Zone", "Jazz Radio", "Klassik Radio",
"Groove Salad", "Top Hits by Radio.net", "DLF Kultur", "80s80s Radio",
"The Rock!", "SomaFM Secret Agent"
};
int currentHostIndex = 0;
const int numHosts = sizeof(hosts) / sizeof(hosts[0]);
// Puffer
uint8_t mp3buff[64];
void setup() {
pinMode(encoderPin1, INPUT);
pinMode(encoderPin2, INPUT);
pinMode(buttonPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(encoderPin1), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPin2), updateEncoder, CHANGE);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
Serial.begin(115200);
delay(3000);
display.clearDisplay();
SPI.begin();
player.beginOutput();
player.setVolume(encoderValue);
Serial.print("Connecting to SSID ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 5);
display.println("Prilchen`s WebRadio");
display.setCursor(0, 20);
display.print("IP: ");
display.println(WiFi.localIP());
display.setCursor(0, 30);
display.print("Internet gefunden");
display.display();
connectToHost();
delay(3000);
}
void loop() {
if (digitalRead(buttonPin) == LOW) {
currentHostIndex = (currentHostIndex + 1) % numHosts;
connectToHost();
delay(1000);
}
encoderValue = constrain(encoderValue, 0, 100);
player.setVolume(encoderValue);
//wenn stream defekt dann nächster sender
if (!client.connected()) {
Serial.println("next sender...");
currentHostIndex = (currentHostIndex + 1) % numHosts; // Nächsten Sender versuchen
connectToHost(); // Versuche erneut, den nächsten Sender zu verbinden
}
if (client.available() > 0) {
uint8_t bytesread = client.read(mp3buff, 64);
player.playChunk(mp3buff, bytesread);
}
}
void connectToHost() {
client.stop();
const char *host = hosts[currentHostIndex];
const char *path = paths[currentHostIndex];
int httpPort = httpPorts[currentHostIndex];
Serial.print("connecting to ");
Serial.println(host);
//wenn stream defekt dann nächster sender
if (!client.connect(host, httpPort)) {
Serial.println("Connection failed");
currentHostIndex = (currentHostIndex + 1) % numHosts; // Nächsten Sender versuchen
connectToHost(); // Versuche erneut, den nächsten Sender zu verbinden
return;
}
Serial.print("Requesting stream: ");
Serial.println(path);
client.print(String("GET ") + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
display.clearDisplay();
display.setCursor(10, 5);
display.println("WebRadioStation:");
display.setCursor(0, 20);
display.print("IP: ");
display.println(WiFi.localIP());
display.setCursor(0, 35);
display.println("Sender:");
display.setCursor(0, 45);
display.println(descriptions[currentHostIndex]);
display.display();
}
void updateEncoder() {
int MSB = digitalRead(encoderPin1); // MSB = Bit mit dem höchsten Stellenwert
int LSB = digitalRead(encoderPin2); // LSB = Bit mit dem niedrigsten Stellenwert
int encoded = (MSB << 1) | LSB; // Umwandlung des 2-Pin-Wertes in eine einzelne Zahl
int sum = (lastEncoded << 2) | encoded; // Hinzufügung zum vorherigen kodierten Wert
if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue++;
if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue--;
lastEncoded = encoded; // diesen Wert für das nächste Mal speichern
}
Sketch Download auch auf GitHub.
Nun sollte das OLED Display die gewählten Sender anzeigen und zur Kontrolle auch die lokale IP-Adresse. Beim Drücken wird die nächste Stream Adresse aus dem erstellten Array gesetzt.
Liste der Radiostationen (Hosts), Pfade und httpPorts als Array beginnen mit Datensatz 0.
Somit ist die letzte Station aus der Beispielliste mit 16 Anbietern der Datensatz Nummer 15.
Erstellung und Einbau in ein 3D Druck Gehäuse:
Erstellt wurde dieses Gehäuse mit Tinkercad,. Es ist 108x49x40mm groß geworden und gedruckt mit PETG Filament.

Dann wurden die 4 Module in das Gehäuse verbaut.
Das VS5310 und der Regler wurden gesteckt, ESP32 mit Doppelklebeband aufgeklebt und Display rechts und links mit Heißkleber befestigt.
Rechts kann nun ein kleines USB Netzteil das Radio mit Strom versorgen und links wird in die Buchse die aktive Lautsprechbox angeschlossen.

Die 3D Bauteile wurden zum Download auf Makerworld und Thingiverse abgelegt.
Viel Spaß beim nachbauen 🙂
Video:
Wer das im Video zu sehende Mini Speaker Paar mit Akku sucht, kann diese hier finden.
Streamtechnik und weitere Stream Sender finden:
Icecast ist eine Open-Source-Streaming-Software, die entwickelt wurde, um Audiodaten in verschiedenen Formaten über das Internet zu übertragen, darunter MP3, Ogg Vorbis und AAC. Ein Icecast-Server funktioniert ähnlich wie ein Webserver, der Daten an Clients (z. B. Webradio-Player) sendet. Der Server nimmt die Streams von den Sendern entgegen und verteilt sie dann an die Zuhörer.
Typische Merkmale von Icecast-Streams:
- Unterstützung mehrerer Formate: Icecast kann gängige Audioformate streamen, darunter MP3, Ogg Vorbis und AAC.
- Direkter Zugriff über URLs: Die Sender können direkt über URL-Adressen (häufig in Form von „http://host:port/path“) angesteuert werden, sodass Webradios und Streaming-Apps sie einfach laden können.
- Unkommerzielle und thematische Sender: Viele Icecast-Stationen sind unkommerzielle Angebote, die thematisch spezielle Genres oder besondere Musikrichtungen anbieten, oft abseits des Mainstreams.
- Live-Streaming: Die Icecast-Technologie ermöglicht eine niedrige Latenz, was Live-Übertragungen unterstützt und ein nahtloses Hörerlebnis ermöglicht.
- Verfügbarkeit von Metadaten: Die meisten Icecast-Streams enthalten Metadaten wie Titel, Künstlername und Genre, die vom Client angezeigt werden können.
Icecast-Sender finden
Um gezielt Icecast-Sender zu finden, kannst du auf Plattformen wie https://dir.xiph.org/ oder https://www.radio-browser.info/ nach Sendern suchen, die explizit Icecast-Streams anbieten. Hier kannst du nach Genres, Ländern oder Bitrate filtern, um den passenden Sender zu finden.