streaming_podcast_player.ino
  1  // SPDX-FileCopyrightText: 2020 Limor Fried/Ladyada for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  //
  5  // Stream MP3s over WiFi on Metro M4 Express and play via music maker shield
  6  
  7  //#define DEBUG_OUTPUT
  8  
  9  #include <ArduinoHttpClient.h>
 10  #include <WiFiNINA.h>
 11  #include <CircularBuffer.h>  // From Agileware
 12  #include "Adafruit_MP3.h"
 13  #include "arduino_secrets.h" 
 14  
 15  ///////please enter your sensitive data in the Secret tab/arduino_secrets.h
 16  char ssid[] = SECRET_SSID;        // your network SSID (name)
 17  char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)
 18  
 19  // Use ~64Kbps streams if possible, 128kb+ is too much data ;)
 20  
 21  // CircuitPython weekly
 22  //const char *xmlfeed = "http://adafruit-podcasts.s3.amazonaws.com/circuitpython_weekly_meeting/audio-podcast.xml";
 23  const char *xmlfeed = "http://www.2600.com/oth.xml"; // yay they have a 16 kbps
 24  
 25  // Too high bitrate!
 26  //const char *xmlfeed = "http://feeds.soundcloud.com/users/soundcloud:users:93913472/sounds.rss";
 27  //const char *xmlfeed = "https://theamphour.com/feed/";
 28  
 29  /************* WiFi over ESP32 client for MP3 datastream */
 30  WiFiClient http_client; // Use WiFiClient class to create HTTP/TCP connection
 31  WiFiSSLClient https_client; // Use WiFiClient class to create HTTP/TCP connection
 32  char *stream_host, *stream_path;
 33  int stream_port = 80;
 34  
 35  /************* Native MP3 decoding supported on M4 chips */
 36  Adafruit_MP3 player;  // The MP3 player
 37  #define BUFFER_SIZE 8000     // we need a lot of buffer to keep from underruns! but not too big?
 38  CircularBuffer<uint8_t, BUFFER_SIZE> buffer;
 39  bool paused = true;
 40  float gain = 1;
 41  
 42  void setup() {
 43    Serial.begin(115200);
 44    while (!Serial);
 45    delay(100);
 46    Serial.println("\nAdafruit Native MP3 Podcast Radio");
 47  
 48    /************************* INITIALIZE WIFI */
 49    Serial.print("Connecting to SSID "); Serial.println(ssid);
 50    WiFi.begin(ssid, pass);
 51    
 52    while (WiFi.status() != WL_CONNECTED) {
 53      delay(500);
 54      Serial.print(".");
 55    }
 56    Serial.print("WiFi connected. ");  
 57    Serial.print("My IP address: ");  Serial.println(WiFi.localIP());
 58  
 59  
 60    char *mp3_stream, *pubdate;
 61    getLatestMP3(xmlfeed, &mp3_stream, &pubdate);
 62    Serial.println(mp3_stream);
 63    Serial.println(pubdate);
 64  
 65    splitURL(mp3_stream, &stream_host, &stream_port, &stream_path);
 66    if (stream_port == 443) {
 67      Serial.println("We don't support SSL MP3 playback, defaulting back to port 80");
 68      stream_port = 80;
 69    }
 70    player.begin();
 71  
 72    //do this when there are samples ready
 73    player.setSampleReadyCallback(writeDacs);
 74  
 75    //do this when more data is required
 76    player.setBufferCallback(getMoreData);
 77  
 78    analogWrite(A0, 2048);
 79    player.play();
 80    player.pause();
 81    
 82  
 83    connectStream();
 84  }
 85  
 86  bool getLatestMP3(String xmlfeed, char **mp3_url, char **date_str) {
 87    char *xml_host, *xml_path;
 88    int xml_port = 80;
 89  
 90    bool found_mp3=false, found_date=false;
 91  
 92    splitURL(xmlfeed, &xml_host, &xml_port, &xml_path);
 93          
 94    Serial.print("XML Server: "); Serial.println(xml_host);
 95    Serial.print("XML Port #"); Serial.println(xml_port);  
 96    Serial.print("XML Path: "); Serial.println(xml_path);
 97  
 98    WiFiClient *xml_client;
 99    if (xml_port == 443) {
100        xml_client = &https_client;
101    } else {
102        xml_client = &http_client;
103    }
104    
105    if (!xml_client->connect(xml_host, xml_port)) {
106      Serial.println("Connection failed");
107      return false;
108    }
109  
110    // We now create a URI for the request
111    Serial.print("Requesting XML URL: "); Serial.println(xml_path);
112    
113    // This will send the request to the server
114    xml_client->print(String("GET ") + xml_path + " HTTP/1.1\r\n" +
115                      "Host: " + xml_host + "\r\n" + 
116                      "Connection: close\r\n\r\n");
117  
118    while (xml_client->connected()) {
119      if (!xml_client->available()) { continue; }
120      char c = xml_client->read();    
121      Serial.print(c);
122      if (c == '<') {
123        String tag = xml_client->readStringUntil('>');
124        Serial.print(tag);
125  
126        if (!found_mp3 && (tag.indexOf("enclosure") != -1)) {  // get first enclosure
127          int i = tag.indexOf("url=\"");
128          if (i == -1) continue;
129          tag = tag.substring(i+5);
130          int end = tag.indexOf("\"");
131          if (end == -1) continue;
132          tag = tag.substring(0, end);
133          *mp3_url = (char *)malloc(tag.length()+1);
134          tag.toCharArray(*mp3_url, tag.length()+1);
135          // Serial.print("****"); Serial.println(*mp3_url);
136          found_mp3 = true;
137        }
138        
139        if (!found_date && (tag.indexOf("pubDate") != -1)) {  // get first pubdate
140          String date = xml_client->readStringUntil('<');
141          *date_str = (char *)malloc(date.length()+1);
142          date.toCharArray(*date_str, date.length()+1);
143          //  Serial.print("****"); Serial.println(*date_str);
144          found_date = true;
145        }
146      }
147      if (found_date && found_mp3) {
148         break;
149      }
150    }
151    xml_client->stop();
152    return (found_date && found_mp3);
153  }
154  
155  void connectStream(void) {
156    http_client.stop();
157    /************************* INITIALIZE STREAM */
158    Serial.print("Stream Server: "); Serial.println(stream_host);
159    Serial.print("Stream Port #"); Serial.println(stream_port);  
160    Serial.print("Stream Path: "); Serial.println(stream_path);
161    
162    if (!http_client.connect(stream_host, stream_port)) {
163      Serial.println("Connection failed");
164      while (1);
165    }
166    
167    // We now create a URI for the request
168    Serial.print("Requesting URL: "); Serial.println(stream_path);
169    
170    // This will send the request to the server
171    http_client.print(String("GET ") + stream_path + " HTTP/1.1\r\n" +
172                      "Host: " + stream_host + "\r\n" + 
173                      "Connection: close\r\n\r\n");
174  }
175  
176  
177  void loop() {
178    if (!http_client.connected()) {
179      connectStream();
180    }
181  #ifdef DEBUG_OUTPUT
182    Serial.print("Client Avail: "); Serial.print(http_client.available());
183    Serial.print("\tBuffer Avail: "); Serial.println(buffer.available());
184  #endif
185    int ret = player.tick();
186  
187    if (ret != 0) {   // some error, best to pause & rebuffer
188      Serial.print("MP3 error: "); Serial.println(ret);
189      player.pause(); paused = true;
190    }
191    if ( paused && (buffer.size() > 6000)) {  // buffered, restart!
192      player.resume(); paused = false;
193    }
194  
195    // Prioritize reading data from the ESP32 into the buffer (it sometimes stalls)
196    if (http_client.available() && buffer.available()) {
197     
198      uint8_t minibuff[BUFFER_SIZE];
199  
200      int bytesread = http_client.read(minibuff, buffer.available());
201  #ifdef DEBUG_OUTPUT
202      Serial.print("Client read: "); Serial.print(bytesread);
203  #endif
204  
205      noInterrupts();
206      for (int i=0; i<bytesread; i++) {
207        buffer.push(minibuff[i]);      // push every byte we read
208      }
209      interrupts();
210  #ifdef DEBUG_OUTPUT
211      Serial.println(" OK");
212  #endif
213    }
214  }
215  
216  
217  void writeDacs(int16_t l, int16_t r){
218    uint16_t val = map(l, -32768, 32767, 0, 4095 * gain);
219    analogWrite(A0, val);
220  }
221  
222  
223  int getMoreData(uint8_t *writeHere, int thisManyBytes){
224  #ifdef DEBUG_OUTPUT
225    Serial.print("Wants: "); Serial.print(thisManyBytes);
226  #endif 
227    int toWrite = min(buffer.size(), thisManyBytes);
228  #ifdef DEBUG_OUTPUT
229    Serial.print(" have: "); Serial.println(toWrite);
230  #endif 
231    // this is a bit of a hack but otherwise the decoder chokes
232    if (toWrite < 128) {
233      //return 0;    // we'll try again later!
234    }
235    for (int i=0; i<toWrite; i++) {
236      writeHere[i] = buffer.shift();
237    }
238    return toWrite;
239  }
240  
241  
242  bool splitURL(String url, char **host, int *port, char **path){
243    Serial.println(url);
244    if (url.startsWith("http://")) {
245      Serial.println("Regular HTTP stream");
246      url = url.substring(7);
247      *port = 80;
248    }
249    if (url.startsWith("https://")) {
250      Serial.println("Secure HTTPS stream");
251      url = url.substring(8);
252      *port = 443;
253    }
254  
255    // extract host before port
256    int colon = url.indexOf(':');
257    int slash = url.indexOf('/');
258    if ((slash != -1) && (colon != -1) && (colon < slash)) {
259      String host_str = url.substring(0, colon);
260      *host = (char *)malloc(host_str.length()+1);
261      host_str.toCharArray(*host, host_str.length()+1);
262      url = url.substring(colon+1);
263      // extract port before /
264      slash = url.indexOf('/');
265      if (slash != -1) {
266        String port_str = url.substring(0, slash);
267        *port = port_str.toInt();
268      } else {
269        Serial.println("Couldn't locate path /");
270        return false;
271      }
272      url = url.substring(slash);
273    } else {
274      // extract host before /
275      slash = url.indexOf('/');
276      if (slash != -1) {
277        String host_str = url.substring(0, slash);
278        *host = (char *)malloc(host_str.length()+1);
279        host_str.toCharArray(*host, host_str.length()+1);
280        url = url.substring(slash);
281      } else {
282        Serial.println("Couldn't locate path /");
283        return false;
284      }
285    }
286    *path = (char *)malloc(url.length()+1);
287    url.toCharArray(*path, url.length()+1);
288    return true;
289  }