#include <WiFi.h>
//Includes from ESP8266audio
#include "AudioFileSourceICYStream.h" //input stream
#include "AudioFileSourceBuffer.h" //input buffer
#include "AudioGeneratorMP3.h" //decoder
#include "AudioOutputI2S.h" //output stream
//library for LCD display
#include <LiquidCrystal_I2C.h>
//library for rotary encoder
#include "AiEsp32RotaryEncoder.h"
//esp32 library to save preferences in flash
#include <Preferences.h>
//WLAN access fill with your credentials
#define SSID "Wokwi-GUEST"
#define PSK ""
//used pins for rotary encoder
#define ROTARY_ENCODER_A_PIN 33
#define ROTARY_ENCODER_B_PIN 32
#define ROTARY_ENCODER_BUTTON_PIN 34
#define ROTARY_ENCODER_VCC_PIN -1 /* 27 put -1 of Rotary encoder Vcc is connected directly to 3,3V; else you can use declared output pin for powering rotary encoder */
//depending on your encoder - try 1,2 or 4 to get expected behaviour
//#define ROTARY_ENCODER_STEPS 1
//#define ROTARY_ENCODER_STEPS 2
#define ROTARY_ENCODER_STEPS 4
//structure for station list
typedef struct {
char * url; //stream url
char * name; //stations name
} Station;
#define STATIONS 1 //number of stations in the list
Station stationlist[STATIONS] PROGMEM = {
{"http://absolut-hot.live-sm.absolutradio.de/absolut-hot/stream/mp3","Absolut-HOT"},
};
//buffer size for stream buffering
const int preallocateBufferSize = 80*1024;
const int preallocateCodecSize = 29192; // MP3 codec max mem needed
//pointer to preallocated memory
void *preallocateBuffer = NULL;
void *preallocateCodec = NULL;
//instance of prefernces
Preferences pref;
//instance for rotary encoder
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
//instance for LCD display
LiquidCrystal_I2C lcd(0x27,16,2); // set the LCD address to 0x27 for a 16 chars and 2 line display
//instances for audio components
AudioGenerator *decoder = NULL;
AudioFileSourceICYStream *file = NULL;
AudioFileSourceBuffer *buff = NULL;
AudioOutputI2S *out;
//Special character to show a speaker icon for current station
uint8_t speaker[8] = {0x3,0x5,0x19,0x11,0x19,0x5,0x3};
//global variables
uint8_t curStation = 0; //index for current selected station in stationlist
uint8_t actStation = 0; //index for current station in station list used for streaming
uint32_t lastchange = 0; //time of last selection change
//callback function will be called if meta data were found in input stream
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();
}
//stop playing the input stream release memory, delete instances
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;
}
}
//start playing a stream from current active station
void startUrl() {
stopPlaying(); //first close existing streams
//open input file for selected url
Serial.printf("Active station %s\n",stationlist[actStation].url);
file = new AudioFileSourceICYStream(stationlist[actStation].url);
//register callback for meta data
file->RegisterMetadataCB(MDCallback, NULL);
//create a new buffer which uses the preallocated memory
buff = new AudioFileSourceBuffer(file, preallocateBuffer, preallocateBufferSize);
Serial.printf_P(PSTR("sourcebuffer created - Free mem=%d\n"), ESP.getFreeHeap());
//create and start a new decoder
decoder = (AudioGenerator*) new AudioGeneratorMP3(preallocateCodec, preallocateCodecSize);
Serial.printf_P(PSTR("created decoder\n"));
Serial.printf_P("Decoder start...\n");
decoder->begin(buff, out);
}
//show name of current station on LCD display
//show the speaker symbol in front if current station = active station
void showStation() {
lcd.clear();
if (curStation == actStation) {
lcd.home();
lcd.print(char(1));
}
lcd.setCursor(2,0);
String name = String(stationlist[curStation].name);
if (name.length() < 15)
lcd.print(name);
else {
uint8_t p = name.lastIndexOf(" ",15); //if name does not fit, split line on space
lcd.print(name.substring(0,p));
lcd.setCursor(0,1);
lcd.print(name.substring(p+1,p+17));
}
}
//handle events from rotary encoder
void rotary_loop()
{
//dont do anything unless value changed
if (rotaryEncoder.encoderChanged())
{
uint16_t v = rotaryEncoder.readEncoder();
Serial.printf("Station: %i\n",v);
//set new currtent station and show its name
if (v < STATIONS) {
curStation = v;
showStation();
lastchange = millis();
}
}
//if no change happened within 10s set active station as current station
if ((lastchange > 0) && ((millis()-lastchange) > 10000)){
curStation = actStation;
lastchange = 0;
showStation();
}
//react on rotary encoder switch
if (rotaryEncoder.isEncoderButtonClicked())
{
//set current station as active station and start streaming
actStation = curStation;
Serial.printf("Active station %s\n",stationlist[actStation].name);
pref.putUShort("station",curStation);
startUrl();
//call show station to display the speaker symbol
showStation();
}
}
//interrupt handling for rotary encoder
void IRAM_ATTR readEncoderISR()
{
rotaryEncoder.readEncoder_ISR();
}
//setup
void setup() {
Serial.begin(115200);
delay(1000);
//reserve buffer für for decoder and stream
preallocateBuffer = malloc(preallocateBufferSize); // Stream-file-buffer
preallocateCodec = malloc(preallocateCodecSize); // Decoder- buffer
if (!preallocateBuffer || !preallocateCodec)
{
Serial.printf_P(PSTR("FATAL ERROR: Unable to preallocate %d bytes for app\n"), preallocateBufferSize+preallocateCodecSize);
while(1){
yield(); // Infinite halt
}
}
//start rotary encoder instance
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, STATIONS, true); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
rotaryEncoder.disableAcceleration();
//init WiFi
Serial.println("Connecting to WiFi");
WiFi.disconnect();
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PSK);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
//create I2S output do use with decoder
//the second parameter 1 means use the internal DAC
out = new AudioOutputI2S(0,1);
//init the LCD display
lcd.init();
lcd.backlight();
lcd.createChar(1, speaker);
//set current station to 0
curStation = 0;
//start preferences instance
pref.begin("radio", false);
//set current station to saved value if available
if (pref.isKey("station")) curStation = pref.getUShort("station");
Serial.printf("Gespeicherte Station %i von %i\n",curStation,STATIONS);
if (curStation >= STATIONS) curStation = 0;
//set active station to current station
//show on display and start streaming
//curStation = 6;
actStation = curStation;
showStation();
startUrl();
}
void loop() {
//check if stream has ended normally not on ICY streams
if (decoder->isRunning()) {
if (!decoder->loop()) {
decoder->stop();
}
} else {
Serial.printf("MP3 done\n");
// Restart ESP when streaming is done or errored
delay(10000);
ESP.restart();
}
//read events from rotary encoder
rotary_loop();
}