Created
December 29, 2018 20:42
-
-
Save toolboc/9a2829b2c3c4a1294889232226fd61f8 to your computer and use it in GitHub Desktop.
Talking Web Radio with ESP8266 and Sony Spresense
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
WebRadio Example | |
Very simple HTML app to control web streaming | |
Copyright (C) 2017 Earle F. Philhower, III | |
This program is free software: you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation, either version 3 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
#include <Arduino.h> | |
#ifdef ESP32 | |
#include <WiFi.h> | |
#else | |
#include <ESP8266WiFi.h> | |
#endif | |
#include "AudioFileSourceICYStream.h" | |
#include "AudioFileSourceBuffer.h" | |
#include "AudioGeneratorMP3.h" | |
#include "AudioOutputI2S.h" | |
#include <EEPROM.h> | |
#include <ESP8266SAM.h> | |
// Custom web server that doesn't need much RAM | |
#include "web.h" | |
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload. | |
// Enter your WiFi setup here: | |
const char *SSID = "<YOUR SSID>"; | |
const char *PASSWORD = "<YOUR PASSWORD>"; | |
WiFiServer server(80); | |
AudioGenerator *decoder = NULL; | |
AudioFileSourceICYStream *file = NULL; | |
AudioFileSourceBuffer *buff = NULL; | |
AudioOutputI2S *out = NULL; | |
int volume = 100; | |
char url[256]; | |
char status[24]; | |
bool newUrl = false; | |
int retryms = 0; | |
typedef struct { | |
char url[256]; | |
int16_t volume; | |
int16_t checksum; | |
} Settings; | |
// C++11 multiline string constants are neato... | |
static const char HEAD[] PROGMEM = R"KEWL( | |
<head> | |
<title>Sony Spresense Web Radio</title> | |
</head>)KEWL"; | |
static const char BODY[] PROGMEM = R"KEWL( | |
<body> | |
<h3> Sony Spresense Web Radio </h3> | |
</br> | |
<img src="https:\\www.sony-semicon.co.jp\products_en\spresense\images\spresense.jpg"> | |
<hr> | |
Volume: <input type="range" name="vol" min="1" max="150" steps="10" value="%d" onchange="showValue(this.value)"/> <span id="volspan">%d</span>%% | |
<hr> | |
Status: <span id="statusspan">%s</span> | |
<hr> | |
<form action="changeurl" method="GET"> | |
Current URL: %s<br> | |
Change URL: <input type="text" name="url"> | |
<select name="type"><option value="mp3">MP3</option></select> | |
<input type="submit" value="Change"></form> | |
<form action="stop" method="POST"><input type="submit" value="Stop"></form> | |
</body>)KEWL"; | |
void HandleIndex(WiFiClient *client) | |
{ | |
char buff[sizeof(BODY) + sizeof(status) + sizeof(url) + 3*2]; | |
Serial.printf_P(PSTR("Sending INDEX...Free mem=%d\n"), ESP.getFreeHeap()); | |
WebHeaders(client, NULL); | |
WebPrintf(client, DOCTYPE); | |
client->write_P( PSTR("<html>"), 6 ); | |
client->write_P( HEAD, strlen_P(HEAD) ); | |
sprintf_P(buff, BODY, volume, volume, status, url); | |
client->write(buff, strlen(buff) ); | |
client->write_P( PSTR("</html>"), 7 ); | |
Serial.printf_P(PSTR("Sent INDEX...Free mem=%d\n"), ESP.getFreeHeap()); | |
} | |
void HandleStatus(WiFiClient *client) | |
{ | |
WebHeaders(client, NULL); | |
client->write(status, strlen(status)); | |
} | |
void HandleVolume(WiFiClient *client, char *params) | |
{ | |
char *namePtr; | |
char *valPtr; | |
while (ParseParam(¶ms, &namePtr, &valPtr)) { | |
ParamInt("vol", volume); | |
} | |
Serial.printf_P(PSTR("Set volume: %d\n"), volume); | |
out->SetGain(((float)volume)/100.0); | |
RedirectToIndex(client); | |
} | |
void HandleChangeURL(WiFiClient *client, char *params) | |
{ | |
char *namePtr; | |
char *valPtr; | |
char newURL[sizeof(url)]; | |
char newType[4]; | |
newURL[0] = 0; | |
newType[0] = 0; | |
while (ParseParam(¶ms, &namePtr, &valPtr)) { | |
ParamText("url", newURL); | |
ParamText("type", newType); | |
} | |
if (newURL[0] && newType[0]) { | |
newUrl = true; | |
strncpy(url, newURL, sizeof(url)-1); | |
url[sizeof(url)-1] = 0; | |
strcpy_P(status, PSTR("Changing URL...")); | |
Serial.printf_P(PSTR("Changed URL to: %s(%s)\n"), url, newType); | |
RedirectToIndex(client); | |
} else { | |
WebError(client, 404, NULL, false); | |
} | |
} | |
void RedirectToIndex(WiFiClient *client) | |
{ | |
WebError(client, 301, PSTR("Location: /\r\n"), true); | |
} | |
void StopPlaying() | |
{ | |
if (decoder) { | |
decoder->stop(); | |
delete decoder; | |
decoder = NULL; | |
} | |
if (buff) { | |
buff->close(); | |
delete buff; | |
buff = NULL; | |
} | |
if (file) { | |
file->close(); | |
delete file; | |
file = NULL; | |
} | |
strcpy_P(status, PSTR("Stopped")); | |
} | |
void HandleStop(WiFiClient *client) | |
{ | |
Serial.printf_P(PSTR("HandleStop()\n")); | |
StopPlaying(); | |
RedirectToIndex(client); | |
} | |
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc. | |
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string) | |
{ | |
const char *ptr = reinterpret_cast<const char *>(cbData); | |
(void) isUnicode; // Punt this ball for now | |
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf | |
char s1[32], s2[64]; | |
strncpy_P(s1, type, sizeof(s1)); | |
s1[sizeof(s1)-1]=0; | |
strncpy_P(s2, string, sizeof(s2)); | |
s2[sizeof(s2)-1]=0; | |
Serial.printf("METADATA(%s) '%s' = '%s'\n", ptr, s1, s2); | |
Serial.flush(); | |
} | |
void StatusCallback(void *cbData, int code, const char *string) | |
{ | |
const char *ptr = reinterpret_cast<const char *>(cbData); | |
(void) code; | |
(void) ptr; | |
strncpy_P(status, string, sizeof(status)-1); | |
status[sizeof(status)-1] = 0; | |
} | |
#ifdef ESP8266 | |
const int preallocateBufferSize = 5*1024; | |
const int preallocateCodecSize = 29192; // MP3 codec max mem needed | |
#else | |
const int preallocateBufferSize = 16*1024; | |
const int preallocateCodecSize = 85332; // AAC+SBR codec max mem needed | |
#endif | |
void *preallocateBuffer = NULL; | |
void *preallocateCodec = NULL; | |
void setup() | |
{ | |
// First, preallocate all the memory needed for the buffering and codecs, never to be freed | |
preallocateBuffer = malloc(preallocateBufferSize); | |
preallocateCodec = malloc(preallocateCodecSize); | |
if (!preallocateBuffer || !preallocateCodec) { | |
Serial.begin(115200); | |
Serial.printf_P(PSTR("FATAL ERROR: Unable to preallocate %d bytes for app\n"), preallocateBufferSize+preallocateCodecSize); | |
while (1) delay(1000); // Infinite halt | |
} | |
Serial.begin(115200); | |
delay(1000); | |
Serial.printf_P(PSTR("Connecting to WiFi\n")); | |
WiFi.disconnect(); | |
WiFi.softAPdisconnect(true); | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(SSID, PASSWORD); | |
// Try forever | |
while (WiFi.status() != WL_CONNECTED) { | |
Serial.printf_P(PSTR("...Connecting to WiFi\n")); | |
delay(1000); | |
} | |
Serial.printf_P(PSTR("Connected\n")); | |
Serial.printf_P(PSTR("Go to http://")); | |
Serial.print(WiFi.localIP()); | |
Serial.printf_P(PSTR("/ to control the web radio.\n")); | |
server.begin(); | |
strcpy_P(url, PSTR("none")); | |
strcpy_P(status, PSTR("OK")); | |
file = NULL; | |
buff = NULL; | |
out = new AudioOutputI2S(); | |
decoder = NULL; | |
ESP8266SAM *sam = new ESP8266SAM; | |
sam->SetVoice(sam->SAMVoice::VOICE_SAM); | |
sam->Say(out, "Welcome to Internet Radio powered by So knee Ess Presents!"); | |
delay(500); | |
sam->Say(out, "Let's make some noise!"); | |
delay(500); | |
sam->Say(out, "Connect to me at: "); | |
char ip[16]; | |
sprintf(ip, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3] ); | |
sam->Say(out, ip); | |
delete sam; | |
delete ip; | |
LoadSettings(); | |
} | |
void StartNewURL() | |
{ | |
Serial.printf_P(PSTR("Changing URL to: %s, vol=%d\n"), url, volume); | |
newUrl = false; | |
// Stop and free existing ones | |
Serial.printf_P(PSTR("Before stop...Free mem=%d\n"), ESP.getFreeHeap()); | |
StopPlaying(); | |
Serial.printf_P(PSTR("After stop...Free mem=%d\n"), ESP.getFreeHeap()); | |
SaveSettings(); | |
Serial.printf_P(PSTR("Saved settings\n")); | |
file = new AudioFileSourceICYStream(url); | |
Serial.printf_P(PSTR("created icystream\n")); | |
file->RegisterMetadataCB(MDCallback, NULL); | |
buff = new AudioFileSourceBuffer(file, preallocateBuffer, preallocateBufferSize); | |
Serial.printf_P(PSTR("created buffer\n")); | |
buff->RegisterStatusCB(StatusCallback, NULL); | |
decoder = new AudioGeneratorMP3(preallocateCodec, preallocateCodecSize); | |
Serial.printf_P(PSTR("created decoder\n")); | |
decoder->RegisterStatusCB(StatusCallback, NULL); | |
Serial.printf_P("Decoder start...\n"); | |
out->SetGain(((float)volume)/100.0); | |
decoder->begin(buff, out); | |
if (!decoder->isRunning()) { | |
Serial.printf_P(PSTR("Can't connect to URL")); | |
StopPlaying(); | |
strcpy_P(status, PSTR("Unable to connect to URL")); | |
retryms = millis() + 2000; | |
} | |
Serial.printf_P("Done start new URL\n"); | |
} | |
void LoadSettings() | |
{ | |
// Restore from EEPROM, check the checksum matches | |
Settings s; | |
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s); | |
EEPROM.begin(sizeof(s)); | |
for (size_t i=0; i<sizeof(s); i++) { | |
ptr[i] = EEPROM.read(i); | |
} | |
EEPROM.end(); | |
int16_t sum = 0x1234; | |
for (size_t i=0; i<sizeof(url); i++) sum += s.url[i]; | |
sum += s.volume; | |
if (s.checksum == sum) { | |
strcpy(url, s.url); | |
volume = s.volume; | |
Serial.printf_P(PSTR("Resuming stream from EEPROM: %s, type=%s, vol=%d\n"), url, "MP3", volume); | |
newUrl = true; | |
} | |
} | |
void SaveSettings() | |
{ | |
// Store in "EEPROM" to restart automatically | |
Settings s; | |
memset(&s, 0, sizeof(s)); | |
strcpy(s.url, url); | |
s.volume = volume; | |
s.checksum = 0x1234; | |
for (size_t i=0; i<sizeof(url); i++) s.checksum += s.url[i]; | |
s.checksum += s.volume; | |
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s); | |
EEPROM.begin(sizeof(s)); | |
for (size_t i=0; i<sizeof(s); i++) { | |
EEPROM.write(i, ptr[i]); | |
} | |
EEPROM.commit(); | |
EEPROM.end(); | |
} | |
void PumpDecoder() | |
{ | |
if (decoder && decoder->isRunning()) { | |
strcpy_P(status, PSTR("Playing")); // By default we're OK unless the decoder says otherwise | |
if (!decoder->loop()) { | |
Serial.printf_P(PSTR("Stopping decoder\n")); | |
StopPlaying(); | |
retryms = millis() + 2000; | |
} | |
} | |
} | |
void loop() | |
{ | |
static int lastms = 0; | |
if (millis()-lastms > 1000) { | |
lastms = millis(); | |
Serial.printf_P(PSTR("Running for %d seconds%c...Free mem=%d\n"), lastms/1000, !decoder?' ':(decoder->isRunning()?'*':' '), ESP.getFreeHeap()); | |
} | |
if (retryms && millis()-retryms>0) { | |
retryms = 0; | |
newUrl = true; | |
} | |
if (newUrl) { | |
StartNewURL(); | |
} | |
PumpDecoder(); | |
char *reqUrl; | |
char *params; | |
WiFiClient client = server.available(); | |
PumpDecoder(); | |
char reqBuff[384]; | |
if (client && WebReadRequest(&client, reqBuff, 384, &reqUrl, ¶ms)) { | |
PumpDecoder(); | |
if (IsIndexHTML(reqUrl)) { | |
HandleIndex(&client); | |
} else if (!strcmp_P(reqUrl, PSTR("stop"))) { | |
HandleStop(&client); | |
} else if (!strcmp_P(reqUrl, PSTR("status"))) { | |
HandleStatus(&client); | |
} else if (!strcmp_P(reqUrl, PSTR("setvol"))) { | |
HandleVolume(&client, params); | |
} else if (!strcmp_P(reqUrl, PSTR("changeurl"))) { | |
HandleChangeURL(&client, params); | |
} else { | |
WebError(&client, 404, NULL, false); | |
} | |
} | |
PumpDecoder(); | |
if (client) { | |
client.flush(); | |
client.stop(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment