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 }