MP3 Webradio

Das abspielen eines MP3 Streams von einem Webradiosenden ist eigentlich ganz einfach. (So dachte ich zumindest)

Der ESP32 wird mit dem VS1053 Board verkabelt. Man öffnet den MP3 stream des Servers mit dem ESP32, schickt die Datenpakete an den VS1053 und voila Musique!
Ein einfaches Script funktioniert jedoch nur wenn der Webserver direkt MP3-Daten liefert.
Ein paar Webradio-Sender (zu denen auch mein Lieblings Radiosender gehört) liefern jedoch nur weiterleitungs-Informationen welche die Adresse des Streaming-Server beinhalten. Das ist ein sogenannter "redirect".
Das MP3-Streaming-script erwartet jedoch MP3-Datenpakete und kann mit einem "redirect" nichts anfangen. Die Lösung:
Aufruf des MP3-Streams und prüfen ob MP3-Datenpakete gesendet werden. Wenn keine MP3-Daten gesendet werden, guckt man in die header-Daten der Antwort. Hier sollten sich redirect-Informationen und die richtige Server-Adresse befinden.
Aufruf des MP3-Stream von der neuen Server-Adresse.

Voila Musique!
Für einen Webradiowecker wird ein Zusatzmodul benötigt das MP3 Stream dekodieren kann.
Das VS1053 Modul (ca. 15 Euro) empfängt MP3-Daten vom ESP32 und gibt diesen Audiostream aus.

Komponenten:
ESP32
VS1053-Modul
Lautsprecher

Anschlüsse:
5V -> 5V
MISO -> 19
SCK -> 18
XRST -> EN
XDSC -> 33
XCS -> 32
DREQ -> 35
MOSI -> 23
DGND -> GND

#include <VS1053.h> //https://github.com/baldram/ESP_VS1053_Library
#include <WiFi.h>
#include <HTTPClient.h>
#include <esp_wifi.h>

#define VS1053_CS  32 
#define VS1053_DCS  33  
#define VS1053_DREQ 35 

#define VOLUME 90 // volume level 0-100

int connected = 0; 

char ssid[] = "networkid";      // your network SSID (name) 
char pass[] = "pass";    // your network password

char host[512] = "st03.sslstream.dlf.de";
char path[512] = "/dlf/03/128/mp3/stream.mp3?aggregator=web";
int port = 80;

int status = WL_IDLE_STATUS;
WiFiClient client;
uint8_t mp3buff[32];  // vs1053 likes 32 bytes at a time

VS1053 player(VS1053_CS, VS1053_DCS, VS1053_DREQ);

void setup () {
 Serial.begin(115200);
 delay(500);
 Serial.println("init");
 SPI.begin();
 Serial.print("MP3");
 initMP3Decoder();
 Serial.print("WiFi");
 connectToWIFI();
}

void loop() {
 int reply = 0;
  if ( !connected )
  {
   // Keep trying until connected (allows us to handle redirections)
   do
   {
     reply = station_connect();
   } while ( reply == 0 );
   Serial.print("Connected to ");
  }
 if (client.available() > 0)
 {
  uint8_t bytesread = client.read(mp3buff, 32);
  player.playChunk(mp3buff, bytesread);
 }
}

int station_connect( )
{
  // Connect to the current station, returns ZERO if did NOT connect.
  // Try again - host/path may have been updated if redirect.
  String newhost;
  char hdrbuf[1024] = "";
  int hdrposn = 0;
  int twonewlines = 0;
  int thisbyte = 0;
  int lastbyte = 0;

  Serial.print("Connecting to ");
  Serial.println(hdrbuf);
  
  // Open a connection
  if ( client.connect(host,port) )
  {
   Serial.println("Port opened, sending request."); 

   client.print(String("GET ") + path + " HTTP/1.1\r\n" +
             "Host: " + host + "\r\n" +
             "Connection: close\r\n\r\n");
    
   // Now we need to read header lines and extract data
   delay(500); // wait for some data to come back
   if (client.available() == 0)
   {
     delay(2500); // delay before returning failed
     return 0;
   }

   // Keep reading until we read two LF bytes in a row
   while ( !twonewlines )
   {
     // Read a line
     hdrposn = 0;
     hdrbuf[0] = '\0';
     do
     {
      // Read a byte
      thisbyte = client.read();
      if ( thisbyte > 31 )
      {
        // If a printable, add it to the buffer
        hdrbuf[hdrposn++] = thisbyte;
        hdrbuf[hdrposn] = '\0';
        lastbyte = thisbyte;
        if ( hdrposn > 512 )
         hdrposn = 0;
      }
      else
      {
        // It's a control character - is it an LF?
        if ( thisbyte == 10 )
        {
         // If this byte is LF and so was the last one...
         if ( lastbyte == thisbyte )
           twonewlines = 1; // flag we've reached the end of the headers
         lastbyte = thisbyte;
        }
      }
     } while ( thisbyte != 10 );
     // We have a header line, so let's work out what it is
     Serial.println(hdrbuf);
     String header;
     header = hdrbuf;
     // Deal with "Location:" header for redirection
     header.toLowerCase();
     if ( header.startsWith("location: http://") )
     {
      String newhdr;
      String newhost;
      String newpath;
      int  newport = 80; // default to HTTP
      int  posn;

      // Work out where we're supposed to point to now
      Serial.print("Redirection -> ");
      header = hdrbuf;
      newhdr = header.substring(17);
      Serial.println(newhdr);

      // Look to split URI into host and path
      posn = newhdr.indexOf("/");
      if ( posn > 0 )
      {
        newpath = newhdr.substring( posn );
        newhost = newhdr.substring( 0, posn );
      }
      // Look to split host into host and port number
      posn = newhdr.indexOf(":");
      if ( posn > 0 )
      {
        newport = newhdr.substring( posn + 1 ).toInt();
        newhost = newhdr.substring( 0, posn );
      }
      
      Serial.println("Specifying new host " +
              newhost +
              " at " +
              newpath);

      strncpy(host, newhost.c_str(), 512);
      strncpy(path, newpath.c_str(), 512);
       
      port = newport;

      // Don't try closing the connection, it'll already have been done
      // and doing it ourselves will cause the device to hang (great code!)
      Serial.println("About to redirect");

      delay(500); // half a second so message can be seen (briefly!)

      return 0;
     }
   }
  }
  else
  {
   Serial.println("Connection failed.");
   delay(1500);
   return 0;
  }

  // Assume by now that we're connected
  connected = 1;
  return 1; // connected!
}

void connectToWIFI()
{
 WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
   delay(500);
   Serial.print(".");
  }
  Serial.println("WiFi connected");
}

void initMP3Decoder()
{
 player.begin();
 player.switchToMp3Mode(); // optional, some boards require this
 player.setVolume(VOLUME);
}


herzlichen dank an Ricks-Webblog, auf dem ich viele Erklärungen und die Basis für diesen Code gefunden habe:
https://heyrick.eu/blog/index.php?diary=20190203&keitai=0