Rick's b.log - 2019/02/03 |
|
It is the 21st of November 2024 You are 3.140.196.5, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
When I got myself an ESP32, I wasn't exactly sure what I wanted to do with it. However I realised fairly quickly that it was a device with flexible GPIO ports that could connect to the Internet via WiFi, and it had a reasonable amount of memory onboard and a fast enough processor that, may be... just maybe...
An on-line radio station that I like listening to is Eagle 80s (an offshoot of Eagle FM on 96.4 in the Guildford area, where I used to live in the UK). Now Eagle makes an app to make tuning in on Android quick and simple, but I don't like using the app. Not because there is anything wrong with the app, but simply because using my phone implies plugging it into the charger and that is the part I'm not so good at (which reminds me.....).
So I wondered, how does one build a net radio (or web radio)? Well, actually it's really easy...
There is also an all-singing all-dancing ESP32-Radio that plays MP3s off SD card, supports playlists, has a web server for remote control, supports half a dozen types of display, has extremely flexible and controllable I/O setup, uses the second core for smoother playback, blah blah blah.
So here's Rick's NetRadioシ.
There's no other bull. Rick's NetRadioシ won't offer to suggest stations you might like, it won't make tea, and it won't tell you that your favourite genre sucks. It will offer what's necessary to be a simple and to-the-point webradio player, along with simple and to-the-point code should you feel like tweaking things.
Other things you will require (assumed you'll have around):
You might want this:
And, of course:
But not forgetting:
Okay, the requirements do sound like a lot, I'll give you that. So here's a picture from the back so you can see how it all fits together.
If you can't get as far as successfully building the code, you'll need to tinker with the IDE until it works.
Notes:
After uploading is done, the ESP32 should reset. On the serial monitor (it's in Tools in the IDE) you should see messages saying that Rick's NetRadioシ is initialising, failing to connect to WiFi, and rebooting itself. That's okay, this is normal.
Not all ESP32 modules are alike, so pay attention to the pin names and not the placement on the diagram.
The first thing you may need to do is to solder the IIC backpack to the LCD panel. This is done in such a way that the IIC connector on the backpack is to the side of the LCD and not in the middle.
The first thing to do is to plug a jumper cable into 0v (GND) and (using whatever method you like) split this into three wires.
Connect a jumper wire to VIN on the ESP32 and split this two wires.
Optional (but recommended): Find a suitable way to hook an electrolytic capacitor between the 5V and GND. Beware of polarity, you must hook it up the right way around (the stripe is -ve, which means GND).
You may also want to hook a capacitor to the 3.3V power in the same way. The way I did this was to take a jumper cable, cut off the header from each end, and insert the capacitor into that.
Why the capacitors? Because the ESP32 has some insanely erratic power requirements, so may notice the LCD flickering like crazy without the capacitors and if the voltage drops too low the processor will crash. This seems, in my experience, to be related to WiFi signal strength - if it's weak than the ESP32 has to shout louder and that takes more power...
Now... Let's hook up the VS1053 first as it's the complicated one. Because this is a high speed serial protocol, it's best to keep these connections short. You might not have much choice if your jumper wires are pre-built.
Now for the LCD, this is simple:
We're almost done! Just this:
Hook a button to the final wire of the GND split.
Now meter to see which poles of the button are joined when the button is pressed (contrary to what may seem logical, it's probably going to be the legs closest to each other and not the legs farthest).
Hook one button to D13 on the ESP32. This will be the
Think you're done? Think again!
Think you're done? Think again!
Now you're done. ☺
Power up the ESP32. You should see:
Followed by:
Followed by:
If you see this, you're good to go with modifying the program that your ESP32 will be running.
Load "RicksNetRadio" into the Arduino IDE and look just after all the #define lines to find this:
That done, you can now rebuild and upload the program to your ESP32. Let it start up.
You will briefly see a message about having connected to the WiFi router, see if you can catch the signal strength report.
It's the negative number followed by "dBm". Explaining what that actually means involves a pile of hard physics and scary equations, so I'll just give you a chart:
You will then see:
Followed by:
You may briefly see:
Take a moment to enjoy whatever Eagle '80s is playing. I was born in 1973 (you might have guessed that from my email address!) so I was a teenager in the '80s. This is the stuff of my childhood. I never made a mix tape for a girl as I went to an all-boys boarding school. Made some mix tapes for myself, recording off the radio. My school being near the south coast, the radio I used to listen to then was Radio Mercury (which in time was related to Eagle).
Audio glitches but no or minimal LCD flickering?
The display says "No data,retrying" several times.
There's no track name showing on the display.
Tapping
Holding down
Tapping/holding the
The station list wraps, so pressing
Your radio will receive regular bits of "metadata" to indicate what the current title/artist is. However if reception is poor and data is lost, the metadata synchronisation will be lost. If this happens, your radio will try to reconnect to the station (to resync). If this happens too often and moving to a better reception area is not possible, then simply briefly press both buttons at the same time. This will toggle whether or not metadata is requested.
"Out of the box", your radio will provide five stations:
Note that JPopsuki often provides title/artist names in UTF-8 and written in Japanese. This is not something that can sensibly be translated into something readable, so you'll probably see something like this on the display:
Look in the source code until you see:
Simply edit the information as follows. The first line is the station name that will be shown on the LCD (16 chars max for the LCD). The second line is the host that supplies the stream. The third line is the path to the stream (note that some stations, such as Eagle) use a redirection. Do not store the redirection address as it may be time limited. The radio software understands redirections, so just give it the path quoted.
If you have more or less than five stations, change the '5' in the station definition (it's in square brackets), and then look down a little in the source code until you see:
Your ESP32 module has plenty of memory, you can add lots of stations, but note that the practical limitation of how many you can add will depend upon how many times you're willing to press the button to get to the one you want to listen to.
Your current station is not remembered from session to session. When the radio starts up, it will play the first station in the list. So put your favourite station there!
When you have defined all your favourite stations, rebuild the software and upload it to your ESP32. Job done!
The host is the bit after "http://" and before the next "/". In this case, "streaming.ukrd.com" and the path is everything after that, in other words "/eagle-80s.mp3".
For other stations, they may offer a WinAmp playlist (a pls file). Load this into a text editor and you'll see something like this:
How to tell if the IP address matches the domain?
Go to the command line and ping the domain...
Okay, if you aren't a geek, you can stop reading. You have a working netradio/webradio that will run autonomously and just get the job done.
I'm now going to take the software apart and explain how it works. So, seriously, if you aren't a geek, it's time to turn on your shiny new radio and turn off your browser. ☺
Note that Rick's NetRadio is open source, provided under the EUPL licence version 1.1 - you can read the licence in the European language of your preference at http://ec.europa.eu/idabc/eupl.html.
Here is the WiFi access point configuration. This must be amended to match your router's credentials...
Here is the list of radio stations. There are five stations built in by default, you can have as many as you like...
More things set up.
The first job to do is see if there's an LCD connected. There should be, but better to specifically look for it.
Now to set up some GPIO. There are three things that need to be done here. The first is to set GPIO2 to be an output, and set it to low. GPIO controls the blue LED which will light up when the radio is connected to a station.
Now we set up the SPI system, and call a function to set up the MP3 player.
We're almost done now, all that remains is to connect to the access point.
For those of you used to traditional C coding, there is no main() function. In the Arduino way of working, we begin with setup(), and then execute loop() repeatedly. There are no 'programs', everything goes into the one big lump of code, as the devices in question have a flat addressing model, it's closer to firmware than application ware.
As it says, because we fetch and play the data in 32 byte chunks, adding much more into loop() will likely require some sort of buffering to be implemented. A 32 byte buffer means that the radio is not tolerant of lag, hiccups, or irregular streaming speed. This could be greatly aided by a buffer in the order of 32K (which is only two seconds at 128kbit). Our 32 byte buffer is sufficient for 0.002s (a 500th of a second!).
If the radio is not connected, then loop until connected...
Now for a more complicated thing. If there is data available (there should be), then read it. Why it is complicated is because we must work in one of two modes. Either we're not reading metadata (in which case we can simply read 32 bytes and send them directly to the audio chip) or we are reading metadata.
If we are reading metadata, then it will appear in the stream at a server refined rate (usually something from 4096 bytes to 16000 bytes). The metadata does not have any header frame, it is usually a single byte with a value of zero (means no more data to follow), usually around four times per second. Because it is a single byte, we are obliged to count how many bytes have been received in order that we may handle the metadata at the right time. So what we do is read 32 bytes and write them to the audio chip until our remaining number of bytes is less than 32, in which case we read however many bytes remain. Then, when the remain counter is zero, we read the metadata before resetting the byte count for the next loop.
After a chunk of data has been played, we check the state of the buttons.
If there is a pop-up message on the LCD (like the current volume), it will have set a timeout value, so if this timeout has expired then restore the data originally on the LCD (either track/artist names, or station name).
First, set up the initial state, ensure the LED is off, etc.
Open a connection to the server...
Now send a GET request to the server to begin fetching the MP3 stream. If we want to use metadata, we must insert "<1> Rick's NetRadioシ
But wait, isn't there already software to do this?
Yes. There's a simple ESP32_Web_Radio which looks to be as if the VS1053 streaming radio example was modified to talk to some sort of programmable TFT screen. It's very basic, otherwise. It doesn't support redirections or...
Unfortunately the SoftAP is bugged (it threw an error about unhandled event when I tried to connect), it's hard to configure correctly, and when I managed to get it running (failing to find the VS1053 audio chip), trying to edit the settings via the web server resulted in it instead deleting them and having it revert to the broken SoftAP mode.
Given that building ESP32-Radio took longer than building the entirety of RISC OS from scratch on a Raspberry Pi, sorry, life's too short...
Excerpt of serial trace when trying to connect to ESP32-Radio:
D: WiFi Failed! Trying to setup AP with name ESP32Radio and password ESP32Radio.
D: IP = 192.168.4.1
D: Start server for commands
D: Rotary encoder is disabled (-1/-1/-1)
E (35716) event: mismatch or invalid event, id=63
E (35717) event: default event handler failed!
It talks to a 1602 LCD (that's two lines of sixteen characters). Restricted, but sufficient. Right now, mine says:
It's enough.Sweet Child o' M
Guns N' Roses
It offers two buttons. No more, no less. These are for changing volume (tap) or station (hold).
In short, Rick's NetRadioシ was written to, simply, work.
Before we begin - what's the 'シ'?
It's Japanese katakana (a syllabary) for the sound "shi" (shee). This was added to the end of the application title because the standard LCD character set includes katakana, and it looks a little bit like a smiley.
Things you'll need
It'll cost maybe €15 to €40 depending on what you source and where from (mine cost €30, I explain why below).
Note that you can get all of this cheaper on Amazon, eBay, Alibaba, etc. I specifically chose parts that could be delivered by Amazon Prime so the things would arrive "the day after tomorrow" and not in a month or two from China... It was a little over €25 and I could build it this weekend, so paying a little extra for that was considered an acceptable compromise.
ricksnetradio.zip - the source code! UPDATE AVAILABLE - SEE HERE
The two clunky orange/transparent things on the right join the +5V wires and likewise for the 0V wires - taken from the supply pins of the ESP32 and shared to the LCD and the VS1053 (and also to the buttons in the case of 0V).
The first thing to do
Download the archive containing the sketch program and build it with the Arduino IDE, and upload it to your ESP32. Don't change anything in the program yet. Just build it and upload it.
You'll need to add two libraries to your IDE:
The hard part - wiring it up
Here's a diagram to follow:
The method I used was to just push it into the jumper wire headers, and use a multimeter to check it was connected.
VS1053 ESP32 Notes XDCS D33 Sending data XRST EN Reset SCK D18 SPI MISO D19 SPI XCS D32 Sending command DREQ D35 Data Request MOSI D23 SPI DGND GND From GND split 5V VIN From 5V split
LCD ESP32 Notes GND GND From GND split 5V VIN From 5V split SCL D22 IIC clock SDA D21 IIC data
Hook the second button to that, so both buttons are connected to GND.
+
button.
Hook the other button to D12 on the ESP32. This will be the -
button.
It's now time to go back to the start and check the wiring is correct.
Double check your wiring.
Rick's NetRadioシ
Initialising...
WiFi connecting:
Your_AP_here!
and that happening in a loop.
Restarting...
Setting up your access point
YOU NO LONGER HAVE TO HARDWIRE THE SETTINGS - SEE THE UPDATE.
// Now hardwire the AP's SSID and password.
char ssid[] = "Your_AP_here!";
char pass[] = "your_password_here";
Set "ssid" to be the name of your WiFi access point, and set "pass" to be your WiFi password. Your router's handbook or a sticker on the back should provide you with this information.
If you're using WPA2/AES and your service provider are competent, your password will be a long list of numbers (which includes A-F as it is in base 16). Double-check it.
First radio use
Now that your device knows your WiFi router's name and password, it should be able to connect. Don't panic if it fails the first time and restarts. It does that for me too. It should connect the second time, unless there's a problem with your WiFi signal.
Connect -83dBm
(WiFiAPname)
You will need at least -70dBm for reasonable listening. It will work down to around -86dBm but reception may be interrupted if other devices are using the same WiFi. The WiFi in my room is rubbish, but then it does have to pass through a solid stone wall about a metre thick!
dBm Means... -30 A perfect signal
-30 to -50 Excellent
-50 to -60 Good
-60 to -70 Acceptable
-70 to -80 Poor
-80 to -90 Rubbish
-90... Unlikely to work
Connecting to
Eagle '80s
Then:
Redirecting...
Connecting to
Eagle '80s
before being replaced by the name of the current song and its artist, if the station is currently broadcasting this information (it usually does).
Eagle '80s
128kbit MP3
It goes wrong
Audio glitching? ESP32 crashes and doesn't respond to the buttons?
If the LCD is flickering a lot, then you'll either need to add capacitors, or you'll need bigger capacitors. They must be electrolytic (the can type).
Check the wires to the VS1053 are snug and not at all loose, especially the one between XRST and EN.
Some stations may do this (BBC Radio 4 always does). The radio will wait for a moment and then try again. It might take two or three attempts.
Look at the debug information (serial monitor) when selecting the station. You should see something mentioning "StreamTitle".
If it reads:
StreamTitle=' - ';
this means that there is currently nothing to show (adverts? news program?).
However if it reads:
StreamTitle='';
then this means that the station either isn't broadcasting metadata at the moment (if it's a station that you expect to show infomation), or doesn't broadcast metadata at all.
Radio controls
You have two buttons...
+
will increase the volume. You'll briefly see a report of the current volume, and a set of bars to represent that pictorially.
+
(for more than half a second) will change to the next station in the list.
-
button decreases the volume or changes to the previous station.
+
at the end will wrap around to the beginning (and vice versa).
If it is not being requested, you won't get track information.
The stations
YOU GET TEN STATIONS BY DEFAULT, AND CAN SET UP YOUR OWN AS AND WHEN YOU WANT - SEE THE UPDATE.
Eagle '80s Music from the eigties (metadata, usually) Eagle 96.4FM Local radio for the Guildford (UK) area (metadata, usually) BBC Radio 4 FM The BBC's talky station (no metadata) Alouette Contemporary (FRENCH) (no metadata) JPopsuki Contemporary (JAPANESE) (metadata) Editing the stations
YOU NO LONGER HAVE TO HARDWIRE THE STATIONS - SEE THE UPDATE.
struct stationdef station[5] =
{
// First station - this is the station played at startup
"Eagle '80s",
"streaming.ukrd.com", // may be updated if there's a redirection
"/eagle-80s.mp3", // may be updated if there's a redirection
80,
It's a simple C struct. You will see that it is repeated five times with different information.
The final thing? This is the port. If no port is specified, assume this is 80.
int stationcount = 5; // How many stations we have defined
But, wait, how to find stations?
If you live in the UK and wish to listen to UKRD channels (a lot of local radio stations), there is a list at http://streaming.ukrd.com/.
Pick the mp3 one and place your mouse over the RAW link.
Your browser's status bar should tell you where the link goes. In the case of Eagle '80s, it's:
http://streaming.ukrd.com/eagle-80s.mp3
[playlist]
numberofentries=1
File1=http://213.239.204.252:8000/stream
Title1=JPopsuki Radio
Length1=-1
version=2
The IP address is the same as the domain "jpopsuki.fm", so for this station:
The host is either "jpopsuki.fm
" or "213.239.204.252
". The name is preferable.
The path is /stream
.
And you'll note something else - there's a number in there. That's the port to use (instead of the default 80). The port is, therefore, 8000
.
*ping jpopsuki.fm
PING jpopsuki.fm (213.239.204.252): 56 data bytes
64 bytes from 213.239.204.252: icmp_seq=0 ttl=52 time=41.700 ms
[etc]
You can see it's translated the name to an IP address, and it's the same as the one given.
The finishing touch
Sticking it in a box. You've seen photos of mine. How you do it is entirely up to you.
Source code - definitions and setup
THIS IS RETAINED FOR REFERENCE - YOU SHOULD USE THE UPDATE. It's better. ☺
For those unfamiliar with the EUPL, it is an OSI-approved open source licence, however it is properly "open", not that parody of restrictions that is the GPL, and it has no viral element (indeed, it is quite the opposite). EUPL is open, GPL is not.
/* Rick's little internet radio
For the ESP32. This is a no-bull version. It will connect, play a station
(supporting redirection), and handle displaying metadata on an LCD for
title/artist.
Version 0.04 1st February 2019
http://www.heyrick.co.uk/blog/index.php?diary=20190203
Licenced under the EUPL (v1.1 only).
*/
// Include some standard libraries
#include <WiFi.h> // WiFi support
#include <HTTPClient.h> // Fetch stuff from the internet
#include <esp_wifi.h> // ESP32 specific WiFi functions
#include <Wire.h> // IIC
// Now include libraries to work with the hardware we're using
// VS1053 is from https://github.com/baldram/ESP_VS1053_Library
// download it and install it from zip file
// LiquidCrystal_PCF8574 is available in the library manager
// it's the one by Matthias Hertel
#include <VS1053.h> // VS0153 based MP3 decoder board
#include <LiquidCrystal_PCF8574.h> // IIC connected 1602 LCD
To reiterate, you'll need to install two libraries in order to build the code - one to talk to the audio decoder IC (on GitHub), and one to talk to the LCD (in library manager).
// We're using the standard SPI pins, but we will need to define the
// extra pins for CS, DCS, and DREQ.
#define VS1053_CS 32
#define VS1053_DCS 33
#define VS1053_DREQ 35
// The volume level goes from 0 (off) to 100 (max).
// This volume provides a reasonable level to give me music without
// disturbing others when plugged into speakers beside my monitor.
// (I'll add volume control buttons later on)
#define INITVOLUME 82
// The 1602 LCD is controlled by a PCF8574 with a default base address of &27
#define IICADDR 0x27
// Buttons
// Button Short press Long press
// + (next) Vol+ Next station
// - (prev) Vol- Previous station
#define NEXTBUTTON 13
#define PREVBUTTON 12
#define ACTIONNONE 0
#define ACTIONPRESS 1
#define ACTIONLONGPRESS 2
// Now hardwire the AP's SSID and password.
char ssid[] = "Your_AP_here!";
char pass[] = "your_password_here";
// Project ID (must be <16 chars for LCD)
char appname[] = "Rick's NetRadio\xBC"; // &BC is "shi" in Katakana, like a little smiley :)
char hostname[] = "RicksNetRadio"; // must be UNIQUE if you have more than one radio...
// Define radio stations
typedef struct stationdef
{
char name[64]; // Textual name of station ("Eagle '80s")
char host[128]; // Hostname ("streaming.ukrd.com")
char path[128]; // Path to complete URI ("/eagle-80s.mp3")
int port; // Port number (80)
};
struct stationdef station[5] =
{
// First station - this is the station played at startup
"Eagle '80s",
"streaming.ukrd.com", // may be updated if there's a redirection
"/eagle-80s.mp3", // may be updated if there's a redirection
80,
// Second station
"Eagle 96.4FM",
"streaming.ukrd.com",
"/eagle.mp3",
80,
// Third station
"BBC Radio 4 FM",
"bbcmedia.ic.llnwd.net",
"/stream/bbcmedia_radio4fm_mf_p",
80,
// Fourth station
"Alouette",
"broadcast.infomaniak.net",
"/alouette-high.mp3",
80,
//// Fifth station
//"RTE1", <-- whatever this does on connect, it crashes the ESP!
//"av.rasset.ie",
//"/av/live/radio/adio1.m3u",
//80,
// Fifth station
"JPopsuki Radio!",
"jpopsuki.fm",
"/stream",
8000
// Don't forget to update stationcount below
};
struct buttondef
{
bool state;
unsigned long press;
unsigned long release;
int action;
} button[2];
// The globals
int lcdpresent = 0; // Set to '1' if LCD is detected
int connected = 0; // Set to '1' if connected to a station
int bitrate = 0; // The stream bitrate (in kbit)
int metaint = 0; // The interval of when metadata appears (usually 8192-32768)
int bytecount = 0; // The number of bytes to go until there's a metadata block
char metadata[4080]; // ICY metadata - global for speed
int currentstation = 0; // Currently selected station
int stationcount = 5; // How many stations we have defined
int volume = INITVOLUME; // Current volume
char title[17] = ""; // Current song title
char artist[17] = ""; // Current song artist
int wantmetadata = 1; // Do we want metadata?
unsigned long lcddelay=0; // Delay for temporary messages on LCD (0=none, also is a timeout)
The only thing to note with the above is that stationcount should be changed to how many stations there are.
// Define an instance of the 16x2 LCD
LiquidCrystal_PCF8574 lcd(IICADDR);
// Define an instance of the VS1053, and give it an aligned 32 byte buffer
VS1053 audioplayer(VS1053_CS, VS1053_DCS, VS1053_DREQ);
__attribute__((aligned(4))) uint8_t buffer[32]; // word aligned for speed
// Define an instance of the WiFi client
WiFiClient webclient;
Source code - initialisation
The setup() function gets the system running. You will note that a lot of information is output to the serial port along the way. You could remove this if you wanted (anything beginning "Serial.
"), but it is useful to keep it around for debugging if you wish to fiddle with the code.
// Setup code
void setup ()
{
char serinfo[64];
// Set up serial port for tracing activity (debugging)
Serial.begin(115200);
Serial.print(appname);
Serial.println(" starting up.");
sprintf(serinfo, "Running on CPU %d at %dMHz, with %d memory free.",
xPortGetCoreID(), ESP.getCpuFreqMHz(), ESP.getFreeHeap());
Serial.println(serinfo);
Once the LCD is detected, it is initialised to a known state.
// Is an LCD connected?
Serial.print("Looking for LCD...");
Wire.begin();
Wire.beginTransmission(IICADDR);
if ( Wire.endTransmission() == 0 )
{
Serial.println("found.");
lcdpresent = 1;
// Initialise the LCD (we can't assume anything about its state)
lcd.begin(16, 2);
lcd.setBacklight(127);
lcd.home();
lcd.clear();
lcd.noBlink();
lcd.noCursor();
lcd.setCursor(0, 0);
lcd.print(appname);
lcd.setCursor(0, 1);
lcd.print("initialising...");
}
else
{
Serial.println("not detected (is IICADDR correct?).");
}
That done, GPIO12 (previous) and GPIO13 (next) are set as inputs with pullup (because they are shorted to ground when the buttons are pressed).
// Set up the blue LED (will light when connected to a station)
pinMode(2, OUTPUT);
digitalWrite(2, LOW); // default to off
// Set up the previous/next buttons
pinMode(NEXTBUTTON, INPUT_PULLUP); // pulled up, so ground
pinMode(PREVBUTTON, INPUT_PULLUP); // to activate the button
button[0].state = HIGH;
button[1].state = HIGH;
button[0].action = ACTIONNONE;
button[1].action = ACTIONNONE;
// Set up SPI
SPI.begin();
// Initialise the MP3 decoder - this takes time
mp3_initialise();
// Now connect to the AP
wifi_connect();
// Done, we'll resume in loop...
}
Some people like to break their project into tabs by placing plenty of code into header files, but I don't happen to believe header files are where code should be.
Source code - the main loop
This is called repeatedly, so if anything is to happen, it'll happen here.
void loop()
{
// Round and round we go...
//
// Warning: Adding more to this loop will probably require
// some sort of buffering to be implemented...
uint8_t bytes = 0;
int reply = 0;
// Connect?
if ( !connected )
{
// Keep trying until connected (allows us to handle redirections)
do
{
reply = station_connect();
check_buttons(); // so user can choose another station
} while ( reply == 0 );
Serial.print("Connected to ");
Serial.println(station[currentstation].name);
bytecount = metaint;
}
// If there's data, handle it in 32 byte chunks, catering for metadata
if (webclient.available() > 0)
{
// Read 32 bytes and throw them directly to the MP3 player
// (this actualy works as long as nothing too complex happens in loop()!)
if ( metaint != 0 )
{
// We have metadata, so we need to read the appropriate amounts
if ( bytecount >= 32 )
{
// It's okay, we have data to go until metadata block
bytes = webclient.read(buffer, 32);
audioplayer.playChunk(buffer, bytes);
// Subtract bytes from the bytecount
bytecount -= bytes;
if (bytecount == 0)
{
// If it's reached zero, check to see if there is information to read
read_icy_metadata();
bytecount = metaint; // reset byte count for next bit of metadata
}
}
else
{
// We have less than 32 bytes before metadata, so read what's left
bytes = webclient.read(buffer, bytecount);
audioplayer.playChunk(buffer, bytes);
// Subtract bytes from the bytecount (should equal zero!)
bytecount -= bytes;
if (bytecount == 0)
{
// If it's reached zero, check to see if there is information to read
read_icy_metadata();
bytecount = metaint; // reset byte count for next bit of metadata
}
}
}
else
{
// There is no metadata, so just read and play
bytes = webclient.read(buffer, 32);
audioplayer.playChunk(buffer, bytes);
}
}
// Check the buttons
check_buttons();
// LCD to twiddle?
if ( lcddelay != 0 )
{
// Expired?
if ( lcddelay < millis() )
{
// Yes, so restore what should normally be shown
lcd_title();
lcddelay = 0;
}
}
// Warning: Adding more to this loop will probably require
// some sort of buffering to be implemented...
}
Source code - set up the MP3 player
This initialises the audio chip, checks it is there, sets it to MP3 mode (it starts up in MIDI mode!), and sets the default volume.
That long pause as the radio initialises? It's here...
void mp3_initialise()
{
// Initialise the VS1053 to play in MP3 mode
audioplayer.begin();
if ( audioplayer.isChipConnected() == 0 )
{
lcd_output("VS1053 audio IC", "not responding");
delay(2500); // nothing much we can do here...
}
audioplayer.switchToMp3Mode();
audioplayer.setVolume(volume);
}
Source code - write to the LCD
This is simple, it takes two strings and writes them to the LCD...
void lcd_output(char *lineone, char *linetwo)
{
// Write the two lines given to the LCD
if ( !lcdpresent )
return;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(lineone);
lcd.setCursor(0, 1);
lcd.print(linetwo);
return;
}
Source code - connect to WiFi
This simply starts a connection to the access point, and loops until it is connected.
The only thing of note here is that if it loops for three and a half seconds without any connection, it'll force a reset. This is because my ESP32 board does not connect to WiFi when switched on/powered up, but it will after a soft reset. No idea why, but it's an easy enough fix.
Setting the hostname doesn't work - I think I need a more complex connection routine as it would appear to need to be set at a very specific time...
void wifi_connect()
{
// Try to connect to the AP
int timeout = 0;
long sigstrength = 0;
char sigreport[32] = "";
lcd_output("WiFi connecting:", ssid);
Serial.print("Trying to connect to ");
Serial.println(ssid);
// Kill off any previous behaviour
WiFi.disconnect(true);
// Start the WiFi system
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
WiFi.setHostname(hostname);
// Wait while it connects
while (WiFi.status() != WL_CONNECTED)
{
delay(350);
Serial.print(".");
// If it's taken too long, force a restart
// (this usually works - no idea why)
timeout += 1;
if ( timeout > 10 )
{
Serial.println("Restarting...");
lcd_output("Restarting...", "");
delay(500);
ESP.restart();
}
}
// Report connected, and WiFi signal strength
sigstrength = WiFi.RSSI();
sprintf(sigreport, "Connect %lddBm", sigstrength);
Serial.println(sigreport);
lcd_output(sigreport, ssid);
delay(1500); // delay so message can be seen
}
Source code - connecting to a station
The most complicated function by far...
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 header;
String newhost;
char hdrbuf[128] = "";
int hdrposn = 0;
int twonewlines = 0;
int thisbyte = 0;
int lastbyte = 0;
digitalWrite(2, LOW); // turn off blue LED
lcd_output("Connecting to", station[currentstation].name);
Serial.print("Connecting to ");
Serial.print(station[currentstation].host);
Serial.print(station[currentstation].path);
sprintf(hdrbuf, " on port %d.", station[currentstation].port);
Serial.println(hdrbuf);
title[0] = '\0';
artist[0] = '\0';
metaint = 0;
// Open a connection
if ( webclient.connect(station[currentstation].host, station[currentstation].port) )
{
Serial.println("Port opened, sending request.");
""""""""""""""""""""""
""
>>
""""">"
"">"">""""""""""
""
""
""
>
""""
<>&&<""""""""
""""""""""""""""""キャプテンストライダムバースデー
""""
<½ >½ <&&"""""">"">"">&&<"">&&<"">&&<"">&&<"">&&<""""""><"">><""<<>>
Jeff Doggett, 4th February 2019, 20:24 It shoudn't be necessary to set the quantity of stations manually twice. The 5 in [] isn't needed.
struct stationdef station[] =
will work
Use the following definition:
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
Then
int stationcount = ARRAY_SIZE(station);
>> repeated five times
No, it's repeated four times.David Pilling, 5th February 2019, 23:03 Very good - internet radios are still quite expensive, and I could imagine someone (in China) selling this configuration - none to be found at the moment.alan, 24th June 2019, 18:49 Hi,
Thanks for the work you have put in to publishing all this info.
I have just finished building myself one based on your code and can report that it works really well - thanksMilan Halaj, 15th July 2019, 09:08 Hello
Great work. I would like to ask you for some help. Your sketch having problems to upload cause of the big comment on start between /* ... */. But this is not a big issue.
I have a problem with LCD display, it shows nothing. Serial monitor showing me not detected (is IICADDR correct?)
Radio is playing and showing your serial.print information. Just the display showing nothing. LCD is OK, i tested it with other sketch.
Ahy idea where is the problem?Milan Halaj, 15th July 2019, 13:33 Never mind and please delete my post before. Just noticed that my display I2C output has SCL pin on the bottom. sorryMarek, 1st October 2019, 11:03 I tested it, but the station play about 22 minutes, then it hangs. If I want to listen, I have to switch the station...Rick, 1st October 2019, 15:21 Strange that it hangs. Last Friday it was running most of the evening.
I've not used it since then... For other reasons...Tony Buer, 2nd December 2019, 05:29 Hi Ricks thanks for the clear and concise information on the radio build. It worked first time without any hassle (Wahooo)
Have tried the others you mentioned without any joy.
Your easy to understand sketch walk through was a wonder and enlightenment for this oldie to easy understand.
Again thanks and will follow your blog.Junior , 20th December 2019, 19:15 Hello Rick, i can use the following address: http://astreaming.europafm.ro:8000/europafm_aacp48k ? Work? Sorry for my bad english.Rick, 20th December 2019, 19:47 Hello Junior,
Probably not. My player supports MP3, and it looks like your link is AAC.
Is there an MP3 version?Junior , 20th December 2019, 20:12 Thanks for the reply;is the official address of the radio station, only aac+.Imre, 18th January 2020, 21:56 Hi Rick, Thank you for making available your project. I have built it it works great. This is the first ESP32 radio project which really works. I am a beginner in C++ but I try to find out how to make the song title scroll on the 1602 LCD. Or at least to display one 16 charcters then the next 16 in case the title is longer. May I ask your help here?
Best regards!Yann, 2nd April 2020, 16:47 Hello Rick, I use your Radio Station an it is verry nice.
But i don´t wanted to Control the Radio with the Buttons. I wanted to Control the Radio Station with an Infrared Remote. Now I have an Infrared Control which allows me to Pause the Radio & I have programmed some Buttons of der Remote for my Radiostations Presets. If you want I can send you my Sketch. Now I created my own PCB and i will see in a few weeks if I made everything right with the PCB :).Marek, 5th June 2020, 09:40 I returned to this project...
The radio played for about 20 minutes, then got stuck..
The error was on the board vs1053
some vendors sell 12.000MHz crystal boards
It should be 12.288MHz
this caused jamming problems
thank youJozsef, 25th September 2020, 21:01 Hi Rick, Thank you for your correct and detailed description. My Rick's NetRadio is working well. My main problem is the power supply. The ESP32 is very sensintive. I tried to increase the capacitor value from 470uF to 1000uF, but not enough. I'll play on ...Lotfi, 25th September 2021, 23:56 Hi
I made
https://github.com/blotfi/ESP32-Radio-with-Spectrum-analyzer
is derived from : https://github.com/Edzelf/ESP32-Radio
it adds Spectrum analyzer
I also made a free Android App to control the radio through the local network:
https://github.com/Edzelf/ESP32-Radio
Enjoy and share.Carlos Soto Vargas, 14th July 2022, 04:02 ojala puedas leer en español , genial tu proyecto ..tienes alguna forma de integrarle wifimanager con onDemand?Rick, 14th July 2022, 07:48 Usar wi-fi para la configuración cuando no hay un punto de acceso?
Lo había pensado, pero toma TANTO tiempo construir el firmware...Carlos Soto Vargas, 14th July 2022, 22:55 Hola , logre incluir wifimanger y mejoirar la estabilidad del wifi..si gustas mándame un correo y comparto el código..
Te agradezco mucho el esfuerzo que te tomaste para hacer la versión de radioweb ya que había probado muchas con pobres resultados.Carlos Soto Vargas, 2nd August 2022, 23:33 Hola estimado , muy contento con su proyecto , monte el administrable por hiperterminal y funciona sin problema..
pero hay algunas radios que se ponen intermitente con metadato y otras no. .que pasara?Virginie, 10th August 2022, 15:55 Hi Rick,
Thanks a lot for this project. I'm trying to simplify it (I don't need any button and just want to stream one radio) and I only have an ESP32 with an integrated OLED and a MAX98357A amplifier. I'm however struggling to display the stream's metadata, do I need a VS1053 module to do so? Thank you.Rick, 10th August 2022, 16:49 How are you converting your audio? The MAX98357A only translates PCM to sound. You'll need the VS1053 if you want to handle MP3, as it's the VS1053 that does the decoding.
All the ESP32 does is receive data and throw it to the VS1053.
Decoding the stream metadata is handled by the ESP32. The stream is set up as a series of frames (size given in the header given back by the server, if you specified icy-metadata in the original request). After that many bytes have been received, there will be a metadata block. This begins with a size of the data that follows. After the metadata block, the next data frame will happen. This repeats endlessly.
Unfortunately there are no headers or identification markers in the stream. You must count off each byte in order to remain synchronised.Virginie, 11th August 2022, 15:34 Hi!
I'm streaming this: http://listen.radioking.com/radio/372772/stream/422929 and getting the sound through my MAX98357A.
Here's the full code
#include "Arduino.h"
#include "WiFi.h"
#include "Audio.h"
#define I2S_DOUT 25
#define I2S_BCLK 14
#define I2S_LRC 26
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Audio audio;
String ssid = "...";
String password = "...";
void setup() {
WiFi.disconnect();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
while (WiFi.status() != WL_CONNECTED)
delay(1500);
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(100);
audio.connecttohost("http://listen.radioking.com/radio/372772/st ream/422929");
}
void loop()
{
audio.loop();
}
I've acquired a VS1053 and I'm trying to wire it up to my ESP32 but I have an ESP with less pins (it's a Wemos Lolin32 ESP32 OLED) so I'm trying to see if I can connect the VS1053 to it and then will use your code.Rick, 11th August 2022, 19:02 Aha, this?
https://github.com/schreibfaul1/ESP32-audioI2S
Looks like a useful software MP3 decoder.
For the song information, it looks like you might need to set up certain functions, that if they exist, will be called by the audio library. See the example in the ReadMe.
Interesting little board. You'll need to Google to see how to set up SPI on it.Virginie, 12th August 2022, 13:23 I've tried this library but it looks like I wasn't able to call the functions right (I'm super new at this).
And now trying to wire my ESP32 to the VS1053 following this https://www.youtube.com/watch?v=mFpUNYX6B0U&ab_channel=learnelec tronics and this https://forum.arduino.cc/t/wemos-lolin32-oled-hspi-rfid/528212/2 so
VS1053 ESP32 Notes
XDCS 2 Sending data
XRST ?? Reset
SCK 5 SPI
MISO SO SPI
XCS 8 Sending command
DREQ 3 Data Request
MOSI SI SPI
DGND GND From GND split
5V VIN From 5V split
but it doesn't seem to work. I'm trying a simpler script before using yours and adding the oled : https://github.com/baldram/ESP_VS1053_Library/blob/master/exampl es/WebRadioDemo/WebRadioDemo.ino
And I'm more and more lost. I assume you probably have much more interesting things to do but if you feel like giving me some tips it would be greatly appreciated... CheersRick, 12th August 2022, 15:35 It's not that I have better things to do, it's that I'm not an expert either, however...
Does the OLED display use any of those pins as part of it's IIC bus?
A quick Google suggests that pins 4 and 5 are connected to the OLED so you won't be able to use those.Virginie, 23rd August 2022, 10:38 Hi Nick, it's me again! I gave up using my oled integrated board, I think I'm missing some pins, or anyway I was not able to plug everything in. So I got a regular ESP32, and the code's working! But - of course there's a but - I'm trying to change the led to an oled (I'm using this one because that's all I have atm https://www.az-delivery.de/fr/products/0-91-zoll-i2c-oled-displa y but I'm planning to get a 128 x 64 so I can display on two lines).
I thought I could get it work by simply replacing all mentions of lcd by "display" and declaring my display. It's wired on D21 and D22 like yours but I can't get anything to be printed on (I checked and the display works when I run the code at the bottom of this page https://startingelectronics.org/tutorials/arduino/modules/OLED-1 28x32-I2C-display/)
Would you know how to get it work?
Here's my code:
/* Rick's little internet radio
For the ESP32. This is a no-bull version. It will connect, play a station
(supporting redirection), and handle displaying metadata on an display for
title/artist.
Version 0.04 1st February 2019
http://www.heyrick.co.uk/blog/index.php?diary=20190203
Licenced under the EUPL (v1.1 only).
*/
// Include some standard libraries
#include <WiFi.h> // WiFi support
#include <HTTPClient.h> // Fetch stuff from the internet
#include <esp_wifi.h> // ESP32 specific WiFi functions
#include <Wire.h> // IIC
#include <Adafruit_GFX.h> // bjt
#include <Adafruit_SSD1306.h> // bjt
#include <SPI.h>
#include <splash.h>
// Now include libraries to work with the hardware we're using
// VS1053 is from https://github.com/baldram/ESP_VS1053_Library
// download it and install it from zip file
// LiquidCrystal_PCF8574 is available in the library manager
// it's the one by Matthias Hertel
#include <VS1053.h> // VS0153 based MP3 decoder board
// We're using the standard SPI pins, but we will need to define the
// extra pins for CS, DCS, and DREQ.
#define VS1053_CS 32
#define VS1053_DCS 33
#define VS1053_DREQ 35
// The volume level goes from 0 (off) to 100 (max).
// This volume provides a reasonable level to give me music without
// disturbing others when plugged into speakers beside my monitor.
// (I'll add volume control buttons later on)
#define INITVOLUME 82
#define SCREEN_WIDTH 128 // bjt
#define SCREEN_HEIGHT 32 // bjt
// The 1602 display is controlled by a PCF8574 with a default base address of &27
// #define IICADDR 0x27
// Buttons
// Button Short press Long press
// + (next) Vol+ Next station
// - (prev) Vol- Previous station
#define NEXTBUTTON 13
#define PREVBUTTON 12
#define ACTIONNONE 0
#define ACTIONPRESS 1
#define ACTIONLONGPRESS 2
// Now hardwire the AP's SSID and password.
char ssid[] = "**";
char pass[] = "**";
// Project ID (must be <16 chars for display)
char appname[] = "Rick's NetRadio\xBC"; // &BC is "shi" in Katakana, like a little smiley :)
char hostname[] = "RicksNetRadio"; // must be UNIQUE if you have more than one radio...
// Define radio stations
typedef struct stationdef
{
char name[64]; // Textual name of station ("Eagle '80s")
char host[128]; // Hostname ("streaming.ukrd.com")
char path[128]; // Path to complete URI ("/eagle-80s.mp3")
int port; // Port number (80)
};
struct stationdef station[5] =
{
// First station - this is the station played at startup
"Radio tempete",
"listen.radioking.com", // may be updated if there's a redirection
"/radio/372772/stream/422929", // may be updated if there's a redirection
80,
// Second station
"Eagle 96.4FM",
"streaming.ukrd.com",
"/eagle.mp3",
80,
// Third station
"BBC Radio 4 FM",
"bbcmedia.ic.llnwd.net",
"/stream/bbcmedia_radio4fm_mf_p",
80,
// Fourth station
"Alouette",
"broadcast.infomaniak.net",
"/alouette-high.mp3",
80,
//// Fifth station
//"RTE1", <-- whatever this does on connect, it crashes the ESP!
//"av.rasset.ie",
//"/av/live/radio/adio1.m3u",
//80,
// Fifth station
"JPopsuki Radio!",
"jpopsuki.fm",
"/stream",
8000
// Don't forget to update stationcount below
};
struct buttondef
{
bool state;
unsigned long press;
unsigned long release;
int action;
} button[2];
// The globals
int displaypresent = 0; // Set to '1' if display is detected
int connected = 0; // Set to '1' if connected to a station
int bitrate = 0; // The stream bitrate (in kbit)
int metaint = 0; // The interval of when metadata appears (usually 8192-32768)
int bytecount = 0; // The number of bytes to go until there's a metadata block
char metadata[4080]; // ICY metadata - global for speed
int currentstation = 0; // Currently selected station
int stationcount = 5; // How many stations we have defined
int volume = INITVOLUME; // Current volume
char title[17] = ""; // Current song title
char artist[17] = ""; // Current song artist
int wantmetadata = 1; // Do we want metadata?
unsigned long displaydelay=0; // Delay for temporary messages on display (0=none, also is a timeout)
// Define an instance of the 16x2 display
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Define an instance of the VS1053, and give it an aligned 32 byte buffer
VS1053 audioplayer(VS1053_CS, VS1053_DCS, VS1053_DREQ);
__attribute__((aligned(4))) uint8_t buffer[32]; // word aligned for speed
// Define an instance of the WiFi client
WiFiClient webclient;
// Setup code
void setup ()
{
char serinfo[64];
// Set up serial port for tracing activity (debugging)
Serial.begin(9600);
Wire.begin(21, 22);
Serial.print(appname);
Serial.println(" starting up.");
sprintf(serinfo, "Running on CPU %d at %dMHz, with %d memory free.",
xPortGetCoreID(), ESP.getCpuFreqMHz(), ESP.getFreeHeap());
Serial.println(serinfo);
// Is an display connected?
Serial.print("Looking for display...");
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, false)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
delay(2000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
// Set up the blue LED (will light when connected to a station)
pinMode(2, OUTPUT);
digitalWrite(2, LOW); // default to off
// Set up the previous/next buttons
pinMode(NEXTBUTTON, INPUT_PULLUP); // pulled up, so ground
pinMode(PREVBUTTON, INPUT_PULLUP); // to activate the button
button[0].state = HIGH;
button[1].state = HIGH;
button[0].action = ACTIONNONE;
button[1].action = ACTIONNONE;
// Set up SPI
SPI.begin();
// Initialise the MP3 decoder - this takes time
mp3_initialise();
// Now connect to the AP
wifi_connect();
// Done, we'll resume in loop...
}
void loop()
{
// Round and round we go...
//
// Warning: Adding more to this loop will probably require
// some sort of buffering to be implemented...
uint8_t bytes = 0;
int reply = 0;
// Connect?
if ( !connected )
{
// Keep trying until connected (allows us to handle redirections)
do
{
reply = station_connect();
check_buttons(); // so user can choose another station
} while ( reply == 0 );
Serial.print("Connected to ");
Serial.println(station[currentstation].name);
bytecount = metaint;
}
// If there's data, handle it in 32 byte chunks, catering for metadata
if (webclient.available() > 0)
{
// Read 32 bytes and throw them directly to the MP3 player
// (this actualy works as long as nothing too complex happens in loop()!)
if ( metaint != 0 )
{
// We have metadata, so we need to read the appropriate amounts
if ( bytecount >= 32 )
{
// It's okay, we have data to go until metadata block
bytes = webclient.read(buffer, 32);
audioplayer.playChunk(buffer, bytes);
// Subtract bytes from the bytecount
bytecount -= bytes;
if (bytecount == 0)
{
// If it's reached zero, check to see if there is information to read
read_icy_metadata();
bytecount = metaint; // reset byte count for next bit of metadata
}
}
else
{
// We have less than 32 bytes before metadata, so read what's left
bytes = webclient.read(buffer, bytecount);
audioplayer.playChunk(buffer, bytes);
// Subtract bytes from the bytecount (should equal zero!)
bytecount -= bytes;
if (bytecount == 0)
{
// If it's reached zero, check to see if there is information to read
read_icy_metadata();
bytecount = metaint; // reset byte count for next bit of metadata
}
}
}
else
{
// There is no metadata, so just read and play
bytes = webclient.read(buffer, 32);
audioplayer.playChunk(buffer, bytes);
}
}
// Check the buttons
check_buttons();
// display to twiddle?
if ( displaydelay != 0 )
{
// Expired?
if ( displaydelay < millis() )
{
// Yes, so restore what should normally be shown
display_title();
displaydelay = 0;
}
}
// Warning: Adding more to this loop will probably require
// some sort of buffering to be implemented...
}
void mp3_initialise()
{
// Initialise the VS1053 to play in MP3 mode
audioplayer.begin();
if ( audioplayer.isChipConnected() == 0 )
{
display_output("VS1053 audio IC", "not responding");
delay(2500); // nothing much we can do here...
}
audioplayer.switchToMp3Mode();
audioplayer.setVolume(volume);
}
void display_output(char *lineone, char *linetwo)
{
// Write the two lines given to the display
if ( !displaypresent )
return;
display.setTextSize(2); // Draw 2X-scale text
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print(lineone);
display.display(); // Show initial text
delay(100);
return;
}
void wifi_connect()
{
// Try to connect to the AP
int timeout = 0;
long sigstrength = 0;
char sigreport[32] = "";
display_output("WiFi connecting:", ssid);
Serial.print("Trying to connect to ");
Serial.println(ssid);
// Kill off any previous behaviour
WiFi.disconnect(true);
// Start the WiFi system
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
WiFi.setHostname(hostname);
// Wait while it connects
while (WiFi.status() != WL_CONNECTED)
{
delay(350);
Serial.print(".");
// If it's taken too long, force a restart
// (this usually works - no idea why)
timeout += 1;
if ( timeout > 10 )
{
Serial.println("Restarting...");
display_output("Restarting...", "");
delay(500);
ESP.restart();
}
}
// Report connected, and WiFi signal strength
sigstrength = WiFi.RSSI();
sprintf(sigreport, "Connect %lddBm", sigstrength);
Serial.println(sigreport);
display_output(sigreport, ssid);
delay(1500); // delay so message can be seen
}
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 header;
String newhost;
char hdrbuf[128] = "";
int hdrposn = 0;
int twonewlines = 0;
int thisbyte = 0;
int lastbyte = 0;
digitalWrite(2, LOW); // turn off blue LED
display_output("Connecting to", station[currentstation].name);
Serial.print("Connecting to ");
Serial.print(station[currentstation].host);
Serial.print(station[currentstation].path);
sprintf(hdrbuf, " on port %d.", station[currentstation].port);
Serial.println(hdrbuf);
title[0] = '\0';
artist[0] = '\0';
metaint = 0;
// Open a connection
if ( webclient.connect(station[currentstation].host, station[currentstation].port) )
{
Serial.println("Port opened, sending request.");
if ( wantmetadata )
{
// We want Icy metadata
webclient.print(String("GET ") + station[currentstation].path + " HTTP/1.1\r\n" +
"Host: " + station[currentstation].host + "\r\n" +
"Icy-MetaData:1\r\n" +
"Connection: close\r\n\r\n");
}
else
{
// We do NOT want Icy metadata
webclient.print(String("GET ") + station[currentstation].path + " HTTP/1.1\r\n" +
"Host: " + station[currentstation].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 (webclient.available() == 0)
{
display_output(station[currentstation].name, "No data,retrying");
delay(1500); // 1.5 sec 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 = webclient.read();
if ( thisbyte > 31 )
{
// If a printable, add it to the buffer
hdrbuf[hdrposn++] = thisbyte;
hdrbuf[hdrposn] = '\0';
lastbyte = thisbyte;
// If too long, just loop back. It's probably just
// overlong rubbish like a cookie line or somesuch...
if ( hdrposn > 127 )
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);
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(station[currentstation].host, newhost.c_str(), 128);
strncpy(station[currentstation].path, newpath.c_str(), 128);
station[currentstation].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");
display_output("Redirecting...", "");
delay(500); // half a second so message can be seen (briefly!)
return 0;
}
// Read proper name of station
// look for "icy-name:" and take substring(9)
// Read station bitrate
if ( header.startsWith("icy-br:" ) )
bitrate = header.substring(7).toInt();
// Read ICY info interval
if ( header.startsWith("icy-metaint:" ) )
metaint = header.substring(12).toInt();
}
// If we have a bitrate, write that to the display in line two
if ( bitrate > 0 )
{
display_title(); // will put bitrate into second line as no title/artist info yet
}
else
{
// No bitrate given, so output the hostname in the second line
display_output(station[currentstation].name, station[currentstation].host);
}
}
else
{
Serial.println("Connection failed.");
display_output(station[currentstation].name, "Connect failed!");
delay(1500);
return 0;
}
// Assume by now that we're connected
connected = 1;
digitalWrite(2, HIGH); // turn on blue LED
return 1; // connected!
}
void read_icy_metadata()
{
// Read the metadata, look for a title
int mdatlen = 0;
int recurse = 0;
int corrupt = 0;
char *start = NULL;
char *end = NULL;
// Read length byte
mdatlen = webclient.read();
// No data?
if ( mdatlen == 0 )
return;
// The size is actually multiplied by 16, to allow up to 4080 bytes from a single size byte
// (the payload size does not include the size byte)
mdatlen = mdatlen * 16;
// Read the data
for ( recurse = 0; recurse < mdatlen; recurse++ )
{
metadata[recurse] = webclient.read();
// I don't know if TAB is valid in metadata, but bytes 1-8 aren't, so if
// we see any of those, assume the data has been corrupted and that we
// can no longer look for metadata as we're out of sync.
// (a poor WiFi signal can cause data loss which will trigger this)
if ( (metadata[recurse] > 0) && (metadata[recurse] < 9) )
corrupt = 1;
}
metadata[mdatlen] = '\0';
Serial.print("Metadata \"");
Serial.print(metadata);
Serial.println("\"");
// If corrupt, note this and abort
if ( corrupt )
{
// There's no possible recovery, metadata does not have any distinctive
// header, so just flag us as not connected so we can reconnect.
display_output(station[currentstation].name, "Metadata SyncErr");
connected = 0;
digitalWrite(2, LOW); // turn off blue LED
return;
}
// Okay, now let's look for the title and artist names
// We will see something like:
// StreamTitle='Kirsty MacColl - Days';
// or:
// StreamTitle=' - ';
// The latter being in between songs, during adverts, etc.
// There may be other metadata, such as "StreamUrl='';".
// May also be:
// StreamTitle='';
// if the channel doesn't bother with information
// (but has to support it as the client will be expecting it)
start = strstr(metadata, "StreamTitle");
if ( start != NULL )
{
// We have found the streamtitle
start += 12; // skip over "StreamTitle="
// Now look for the end marker
end = strstr(start, ";");
if ( end == NULL )
{
// No end, so just set it as the string length
end = (char *)start + strlen(start);
}
// Quoted string?
if ( start[0] == '\'')
{
// It seems as if quotes may be optional, so handle them.
start += 1;
end -= 1;
}
// Terminull the part we want
end[0] = '\0';
// Now start should point to a string like:
// "Bananarama - Venus"
// Or maybe just " - ", or even just "".
// Trap empty metadata
if ( strcmp(start, " - ") == 0 )
{
// No metadata right now, so display the station name
title[0] = '\0';
artist[0] = '\0';
display_title();
return;
}
// Okay, sort out what's the title and what's the artist
end = strstr(start, " - ");
if ( end == NULL )
{
// Separator not found, output station title and this string
title[0] = '\0';
strncpy(artist, start, 16);
artist[16] = '\0';
display_title();
}
else
{
// First part is artist, second part is title
end[0] = '\0'; // Terminate artist part
end += 3; // Skip to start of title
strncpy(title, end, 16);
title[16] = '\0';
strncpy(artist, start, 16);
artist[16] = '\0';
display_title();
// TODO:
// As I write this JPopsuki has just given me:
// StreamTitle='キャプテンストライダム - バースデー';
// Which on the display looks like:
// ɛ ɛ シɛ ケɛ ɛ シ
// ɛ ュɛ 」ɛ ɛ ɛ ウɛ
// So I think in the future it might be useful
// to detect UTF-8 and copy across as '?' or
// something for the things that can't be shown.
//
// display character set (Hitachi HD44780):
// !"#$%&'()*+,-./
// 0123456789:;<=>? <-- this is like ASCII
// @ABCDEFGHIJKLMNO
// PQRSTUVWXYZ[¥]^_ <-- but note the '¥' instead of '\'
// `abcdefghijklmno
// pqrstuvwxyz{|}←→ <-- arrows instead of ~ and unprintable
// [ not defined ]
// [ not defined ]
// 。「」、・ヲァィゥェォャュョッ <-- small katakana
// ㆒アイウエオカキクケコサシスセソ <-- katakana
// タチツテトナニヌネノハヒフヘホマ
// ミムメモヤユヨラリルレロワン゛゜
// αäßɛµσρℊ√₁j̽¢Lñö <-- more or less, greek/maths
// ρϕθ∞ΩüΣπ※У千万円÷ ▊ <-- more or less, greek/maths
//
}
}
}
void display_title()
{
// Output something on the display to reflect title, or failing
// that, the station.
char report[32] = "";
if ( strlen(title) == 0 )
{
// We have no title
if ( strlen(artist) == 0 )
{
// We have no artist either, so output station info
sprintf(report, "%dkbit MP3", bitrate);
display_output(station[currentstation].name, report);
}
else
{
// We have an artist but no title (probably omething odd in StreamInfo)
// so display station name and whatever is in artist.
display_output(station[currentstation].name, artist);
}
}
else
{
// We have a title (and thus presumably an artist) so display it
display_output(title, artist);
}
}
void check_buttons()
{
// Check the buttons and perform actions on button release (unless BOTH pressed)
bool level = 0;
unsigned long ticker = 0;
int thisbutton = 0;
char report[32];
char volbar[17];
long sigstrength = 0;
// Deal with the buttons
for ( thisbutton = 0; thisbutton < 2; thisbutton++ )
{
level = (digitalRead( (PREVBUTTON + thisbutton) ) == HIGH );
if ( level != button[thisbutton].state )
{
if ( !level )
{
// HIGH to LOW - button press
button[thisbutton].press = millis();
button[thisbutton].state = level;
// Detect if the other button has been pressed as well
if ( ( button[thisbutton].state == LOW ) &&
( button[(thisbutton ^ 1)].state == LOW ) )
{
// Both buttons pressed - toggle whether or not we want metadata
wantmetadata = wantmetadata ^ 1;
sigstrength = WiFi.RSSI();
sprintf(report, "WiFi %lddBm", sigstrength);
if ( wantmetadata == 1 )
display_output("MetadataEnabled", report);
else
display_output("MetadataDisabled", report);
connected = 0;
digitalWrite(2, LOW); // turn off blue LED
button[0].state = HIGH;
button[1].state = HIGH;
delay(1000);
return;
}
}
else
{
// LOW to HIGH - button release
button[thisbutton].release = millis();
button[thisbutton].state = level;
// Is there an action to perform?
ticker = button[thisbutton].release - button[thisbutton].press;
if ( ticker > 2500 )
{
// Pressed for longer than two and a half seconds, display some info
if ( thisbutton == 1)
{
// +ve button, report WiFi status
sigstrength = WiFi.RSSI();
sprintf(report, "WiFi is %lddBm", sigstrength);
if ( sigstrength > -30 ) strcpy(volbar, "(perfect)");
if ( ( sigstrength >= -50 ) && ( sigstrength < -30) ) strcpy(volbar, "(excellent)");
if ( ( sigstrength >= -60 ) && ( sigstrength < -50) ) strcpy(volbar, "(good)");
if ( ( sigstrength >= -70 ) && ( sigstrength < -60) ) strcpy(volbar, "(acceptable)");
if ( ( sigstrength >= -80 ) && ( sigstrength < -70) ) strcpy(volbar, "(poor)");
if ( ( sigstrength >= -90 ) && ( sigstrength < -80) ) strcpy(volbar, "(very poor)");
display_output(report, volbar);
displaydelay = millis() + 3000; // wait for longer before going away (3 sec)
}
else
{
// -ve button, report system status
// 1234567890123456
// Core x @ 123MHz
// MemFree: 123456
sprintf(report, "Core %d @ %dMHz", xPortGetCoreID(), ESP.getCpuFreqMHz());
sprintf(volbar, "MemFree: %d", ESP.getFreeHeap());
display_output(report, volbar);
displaydelay = millis() + 3000; // wait for longer before going away (3 sec)
}
return;
}
if ( ticker > 500 )
{
// Pressed for longer than half a second, it is a long press
if ( thisbutton == 1 )
{
// Next station
currentstation += 1;
if ( currentstation == stationcount )
currentstation = 0; // wrap around
}
else
{
// Previous station
currentstation -= 1;
if ( currentstation < 0 )
currentstation = (stationcount - 1); // wrap around inverse
}
sprintf(report, "Station change to %d", currentstation);
Serial.println(report);
connected = 0; // to force connecting to new station
digitalWrite(2, LOW); // turn off blue LED
}
else
{
if ( ticker > 50 )
{
// Held for more than 5cs, it is a regular press
if ( thisbutton == 1 )
{
// Increase volume
volume += 3;
if ( volume > 98 )
volume = 98; // clip at 98
}
else
{
// Decrease volume
volume -= 3;
if ( volume < 0 )
volume = 0;
}
audioplayer.setVolume(volume);
// Construct the display report with volume bar
sprintf(report, "Volume %d", volume);
Serial.println(report);
for (int recurse = 0; recurse < 16; recurse++)
volbar[recurse] = ' ';
volbar[16] = '\0';
for (int recurse = 0; recurse < (volume / 6.25); recurse++)
volbar[recurse] = 255;
volbar[16] = '\0';
display_output(report, volbar);
displaydelay = millis() + 1500; // go away after one and a half seconds
} // if ticker > 50 block
} // if ticker > 500 block
} // high to low or low to high block
} // if level different to button block
} // for thisbutton ... loop
}Virginie, 24th August 2022, 17:04 Never mind, I made it work! phew
© 2019 Rick Murray |
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted. RIPA notice: No consent is given for interception of page transmission. |