“HiFi” Online Radio: Internet Streaming With ESP32 and VS1053

How to make an Internet Radio Streamer using the great ESP32 and the MP3 decoder “VS1053”. There are many similar instructables for a gadget like this on the web (many from where I gathered lots of info and inspiration), but I wanted to share this because I had to come up with different solutions for the “needs” my project had in order to finish it. So I want to share those solutions compiled in only one project. Many of them probably are “noob” things and most certainly my code can be optimized, but for the noob like me that is reading this, I hope I can solve many of the doubts regarding the programming of some things.

HiFi Online Radio Internet Streaming With ESP32 and VS1053

The “HiFi” part of my title, comes from the final use I wanted to give this Streamer: for a long time I wanted to add an internet streamer to my stereo setup, but the prices for streamers of big brands are very very high, specially for a solution I always thought “was easy to make”. Besides, I always could connect a xCast device to stream from my smartphone or connect a laptop directly. But none of those solutions were the final and stable product I wanted on my “desk” setup.

“Thanks” to COVID, I finally had the time to start experimenting with Arduino and later, with ESP to make different DIY projects…and then all of the sudden I made the ‘click’ about making my dreamed streamer!

The solutions I found on the web were too basic or had variants that I did not want: like using an amplification stage, speakers, using batteries, etc. What I wanted was a product that could blend on my audio setup and that I could use with my already existing amplifier+speakers, connected to them all the time. Well…here it is!

Summarizing, with this project you’ll have:

  • An internet radio streamer that uses WiFi, which can be connected to any amplifier, active speaker or even headphones using any 5V power source.
  • A way to preset more than one WiFi, so you can connect this device in different places without having to compile again
  • A streamer that shows complete info about the source and what is playing, using text scrolling
  • Buttons with different functions depending of the length of the press
  • A way to save radios on internal memory
  • A way to shut down the device (deep sleep)
  • Using a remote control through IR
  • Debugging through Serial Monitor

Supplies

Optionals (but really useful):

  • Proto PCBs (to mount ESP and buttons)
  • JST plugs (to avoid some soldering and ease connection/interchange of components)
  • Glue gun
  • Some skills (wood, metal, plastic) for the case

Step 1: Plan Ahead (the Hardware)

Plan Ahead (the Hardware)

For me this was the most difficult and easiest part of all this process. Difficult because with my “expertise” I used pins that didn’t serve the purpose I was looking for. At first it was trial and error, and later I had to read the documentation for the ESP32 and learn about the use of each pin to understand better what I was doing. After that, it was so easy to plan, solder and mount everything in place!!

First of all, I recommend you enfatically to plan ahead all the things you’ll use and connections you want to make. I assume you’ll make a variant of this model, so I “enforce” you to read the compatibility of the different pins of the ESP32 so you don’t have troubles (like I did at some points, not every pin is useful for everything).

Before all, I made a PCB board map on a spreadsheet, ‘put’ the ESP32 above it and marked each square of one color for each component, and later, a color of font for each cable color I would use.

I used a Proto PCB to mount the ESP32 and ease the soldering for each small pin, very useful in those cases where pins are shared (5V or ground).

For the TFT, the MP3 decoder and the IR receiver instead of soldering, I used JST plugs for each one of them; the other end of the cables were ‘free’ to solder into the Proto PCB.

The antenna connects directly to the ESP32 board through its PCI U.FL IPX plug.

Finally for the buttons: I mounted them on another Proto PCB board not only to ease soldering, but also, to let them in a fixed position so I could put them on a case later on.

After all this, and looking at my “map” for pins, components and cable colors, the soldering was really easy.

At this point I recommend connecting the ESP32 and use the example libraries for each component (TFT, IR Receiver, MP3 Decoder, Buttons) to check if everything is working correctly. If so, it means that the soldering is fine. If something fails, check again the soldering before changing components (I had to desolder and solder again several times until all worked properly). When everything is fine, I also recommend to use a glue gun on the soldering’s, this way when you move all the components and cables, you’ll reduce the chance of disconnecting something.

A final warning: be very careful that no pins are making contact with each other before powering it up. I fried 2 ESP for not soldering properly and making a short-circuit with some pins.

Step 2: The Code

Here I’ll explain some parts of the code that were my special requirements. I won’t get into details of everything (since most of it is basic coding or is commented in the file), but I’ll focus on the things that I wanted to add and didn’t found in other projects of this type (or at least not in the way I wanted).

SETUP:

In the setup(), this code besides the usual initialization of pins and utilities:

– Enables the “ON” button, which in reality is using a button as a ‘Wake Up’ caller from the deep sleep mode (in this case, pin 26, my ‘forward’ button). You can set anything you want here. I first tried to use a remote control (using the pin for the IR receptor), but the problem was that this streamer waked up with ANY IR input, even from remote controls that weren’t used in this. So if I changed the channel of my TV, the streamer turned on. I had to compromise and let a “manual” button to turn this on.

Also, I prevent a black screen when coming back from deep sleep, maintaining the previous state of the LED backlight (pin 17). Without this, even setting the pin to HIGH, the screen stayed OFF everytime I woke up the streamer.

esp_sleep_enable_ext0_wakeup(GPIO_NUM_26,0);
gpio_hold_dis(GPIO_NUM_17);

– Uses the Preferences library and start the “storage space” for saving data to the EEPROM. The “false” argument serves to indicate that we will use this for read and write.

preferences.begin("Save/Load", false);

– Calls the function setScreen() which “draws” everything that will be fixed all the time to the screen. If you check the code, you’ll see that it sets the orientation of the screen, fills the background color, sets some texts, etc.

Also, I wanted to use a “sober” font since this streamer would be used around my HiFi system. The TFT library allows the use of custom fonts, so I included mine but you certainly can change it to the one you want. Doing so, you’ll have to change some parameters here in the code (such as heights, positions, etc. for the texts) and in the library (point to the created font).

Finally, in this function I initialize the two sprites used for the scrolling texts. Here’s an example for the text that will show the tuned station:

    scrolltxt_station.setFreeFont(OWNFONT1);
    scrolltxt_station.setColorDepth(8);
    scrolltxt_station.createSprite(750, 11);
    scrolltxt_station.fillSprite(TFT_BLACK);
    scrolltxt_station.setTextColor(tft.color565(0,255,0),TFT_BLACK);

– Calls the function connectToWiFinStreamer() which first connects to a WiFi. Here I added some extra functions to the standard initialization of WiFi which are very useful when needed.

The first one allows to connect to different WiFi networks already preset in an array at the beginning of the code. So, if the streamer cannot connect to one, it will try with the next one.

while (WiFi.status() != WL_CONNECTED)
    {
        Serial.print(".");
        delay(500);
        currentMillisWiFi2 = millis();
        if (currentMillisWiFi2 - previousMillisWiFi2 >= 5000)  // If 5 secconds pass, connect to next network
        {
            previousMillisWiFi2 = currentMillisWiFi2;
            wifi_index += 1;
            if (wifi_index >= key_qty)
            {
                wifi_index = 0;
                break;
            }
            WiFi.begin(ssid[wifi_index], password[wifi_index]);
            Serial.println();
            Serial.print("Connecting to new network: ");
            Serial.println(ssid[wifi_index]);
        }
    }

If it can’t connect to ANY of the preset SSIDs, it’ll show a little icon made by me on screen and will stay as “Not connected”, then the loop() will take charge (details on this later).

If it can’t connect after 4 attempts, the Streamer will shut down.

If it can connect, it draws another icon on screen to show the connected status, and starts the Radio. Also, it sets the wifi_index to the previous position. This will be useful if internet goes down and it tries to reconnect. This way, it’ll connect to the same working SSID.

But, why I don’t leave the same index? For some reason I still don’t know, whenever I connect to a SSID from the array, the first one is discarded, always. That’s why I leave the first SSID and Password empty on the arrays.

The Radio starts by calling another function: connectiontoRadio() which contains the standard calling from the Radio library.

With all these explained, let’s move to the next part of the code:

LOOP

– The first thing the loop does is play constantly through the Radio player.

player.loop();

– Although they are outside of the loop per se, I use the functions of the MP3 decoder library I wanted for my streamer (it has more than this) which names are very self explainatory: SHOWSTATION, SHOWSTREAMTITLE and BITRATE. They are constantly updating their status as soon as they change.

Here is where I wasted a lot of time: the worst case scenario were long strings of texts, where I had many troubles that have been worked around: text scrolling on top of the older text making everything unreadable, text that didn’t reset when the string changed, etc. So I had to put some code inside the functions of the Radio library, and others inside the loop. The final result is a clean scrolling, that changes when it has to change and stay fixed when it has to.

So, inside the functions from the Radio library, I added some code to:

  • Get the size of the info to show
  • Prepare the “Scrolling spaces” with the required info
  • If no data from the stream, show a message and don’t scroll
  • If data is short (considering the size of the screen and the size of the font) show the text and don’t scroll

Here’s an example inside the Radio functions for the text that shows the songs playing on the station:

stream_text = String(info);
    length_stream = stream_text.length();
    scrolltxt_stream.fillSprite(TFT_BLACK);
    tft.fillRect(0,100,160,11, TFT_BLACK);
    tft.setCursor(10,108);
    tcount2=0;
    if        (length_stream < 2)                            {tft.print("No data");}    // If no text, show "No data"
    else if    (length_stream >=2 && length_stream <= 23)    {tft.print(stream_text);}    // If there's text and is short, don't scroll

In the loop is where the scrolling happens if data from the stream is longer than a preset length. Here, to avoid differences in speed (since the loop “speed” is not always constant), I set the scrolling time to 30ms. Then, it puts the text in the “Sprite space” and each time a 30ms passes, it changes the position of the characters. Here’s an example for the same text but now inside the loop:

if(length_stream > 23)
   {
      currentMillis2 = millis();
      if (currentMillis2 - previousMillis2 >= 30)
      {
         previousMillis2 = currentMillis2;
         scrolltxt_stream.pushSprite(10,100); // Sets Sprite on screen (x,y)
         scrolltxt_stream.scroll(-1); // Scroll direction
         tcount2--;
         if (tcount2 <= 0)
         {
            tcount2 = length_stream*7.5; // 7.5 factor was the best for this size of screen in terms of spacing between sprites
            scrolltxt_stream.drawString(stream_text, 160, 0); // Draws word in position (x,y)
         }
      }
   }

Regarding the 7.5 factor, this is the number that allows spacing between the end of the stream text and the start of the same (or a new) text again. I tried with short texts and very very long text and for this screen and font, is was the sweet spot to not overlap texts, or have too much spacing between them. If you have problems of this sort, change this number.

– Then, the loop manages the buttons (for my project I used 6 in total: 1 as “backward”, 1 as “forward”, and 4 for radio presets).

The first thing that the loop does on each iteration is read the current status of a button, then calls a function that checks “what to do” with that status:

checkXXButton(current status, previous status)

comparing the status of the current loop vs the previous loop. After that saves the current status of that button as the last status for the next iteration of the loop.

For this project I’m using 3 types of buttons:

Backward

  • Short press (less than 0.5s): Goes back 1 radio station
  • Semilong press (between 0.5s and 2s): Goes back 5 radio stations
  • Long press (more than 2s): Calls Off Routine to turn off the Streamer

Forward

  • Short press (less than 0.5s): Goes forward 1 radio station
  • Long press (more than 0.5s): Goes forward 5 radio stations
  • If the streamer is turned off, one short press of this button turns it on

Preset n° X

  • Short press (less than 0.5s): Goes to saved radio on preset X
  • Long press (more than 0.5s): Saves current radio to preset X

– Then the loop checks for IR inputs. This is pretty straightforward. Using the IR example library, I was able to check the HEX codes of each one of my remote control buttons (I wanted to use the remote control from my amplifier). With that data, I use my remote to “push” the buttons ‘backward’, ‘forward’; and if I change the “source” on it, to turn off the Streamer. Any other button that I press and didn’t include it here, does nothing.

– After the IR, the loop checks for Serial inputs. Besides using the Serial Monitor for debugging, I added some extra options:

  • I can put a direct link of a radio and will play it
  • I can put a radio number and it will go right to it

– Finally, the loop checks for the status of WiFi every 1 minute. Here calls a function checkWiFi() that in case WiFi is disconnected, calls again the function that connects to WiFi (the same used in setup).


About The Author

Muhammad Bilal

I am a highly skilled and motivated individual with a Master's degree in Computer Science. I have extensive experience in technical writing and a deep understanding of SEO practices.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top