News:

We're live! Join us!

Main Menu

Micropython & ESP32 internet radio

Started by Jeff_T, Feb 28, 2024, 10:13 PM

Previous topic - Next topic

Jeff_T

This is a fun project, I'll start with a few pics of materials used.

https://photos.app.goo.gl/x3wsLtCgvqsKE3Eh9

VS1053 data sheet

https://cdn-shop.adafruit.com/datasheets/vs1053.pdf

Jeff_T

#1
Part 1 Demo sound bite download

This project is an internet radio that uses your local wifi to open a network socket and connect to one of hundreds of radio stations on the internet, the code used is the Micropython language. It should work with various controllers but throughout I will assume you have an Arduino Nano ESP32 which I preferred for various reasons. I use the Thonny IDE for programming but use whatever you are most comfortable with.

I am fairly new to Micropython so my code may not be 100% but I was blown away by the end result and anyone is open to making it better or making modifications to suit their purpose.

To instill confidence and show how easy it is to retrieve a small "sound bite" from one of these radio stations I will begin with an example of connecting to the internet and saving a small mp3 file to flash, the only component needed for this demo is your ESP32 microcontroller.

There are two code files, a module that holds wifi information and a list of radio channels and the second file is the main code file.

The first module is here to contain wifi constants for your ssid and password, also included are constants for any number of radio channels you may want to add. For now I will include just one channel. Copy the following 3 lines to a file named channels.py and save it to the microcontroller.
WIFI_SSID = "myssid"   # Network SSID
WIFI_PASSWORD = "mypassword"   # Network key
chan_1 = 'http://vis.media-ice.musicradio.com/CapitalMP3' #Channel for demo
All of the following code is the second module and needs to be copied into one file called radio.py and saved to the microcontroller.

At the top of the main file we have four imports all of which are included with the micropython firmware also we import the information from the channels.py module we just created.
import time
import network
import socket
import os
from channels import *
Next we have two functions, the first is a way of connecting to wifi.
def wifi_connect():
    global wlan 
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(WIFI_SSID, WIFI_PASSWORD)
    while not wlan.isconnected():
        print("waiting for wifi connection")
        time.sleep_ms(500)
the second is a means to parse the channel text we passed in as an argument and then open a socket to that address.
   
def parse_url(channel):
    global s
   
    s.close()
    s = socket.socket()
    url=channel
    _, _, host, path = url.split('/', 3)
   
    print(path,"         ",host)
   
    addr = socket.getaddrinfo(host, 80)[0][-1]
   
    print(addr)
   
    s.connect(addr)
    s.send(bytes('GET /%s HTTP/1.1\r\nHost: %s\r\n\r\nConnection: close\r\n\r\n' % (path, host), 'utf8'))

    time.sleep(2)
    s.settimeout(1)
Finally the code that writes the stream to a file called tmp.mp3 on the controllers flash. The radio channel is automatically selected and opened using the line parse_url(chan_1). A few prints in there to show progress.
######################################## Main Routine #############################################
   
wifi_connect()
print("wifi connected")

s = socket.socket()
 
parse_url(chan_1)
   
count=0

file = open('/tmp.mp3','wb')
print("Please wait ... reading stream")

while count < 10000:
    file.write(s.recv(32))
    count += 1
   
file.close()
print("file closed")
s.close()
print("socket closed")
wlan.disconnect()
print("wifi disconnected")
print("Finished")   

When you have both the channels.py and the radio.py saved to your microcontroller run radio.py and it should write a mp3 file to flash, if you don't see it straight away try refreshing the IDE file system, copy this tmp.mp3 file to your PC and play it with a PC media player.





Jeff_T

#2
PART 2 Library modules and hardware

Micropython has no means to process the audio stream we captured in our first example, so we have to route the data through a decoder and have a means to listen to it at the other end. For this project we are going to use a vs1053 mp3 decoder chip from VLSI solutions, the one I am using is mounted on an Arduino shield, it has a stereo SPK output , stereo MIC input , an onboard miniature microphone and an SD card holder. This option is reasonably cheap and I have seen similar boards on Ali Express for as low as a dollar, I can't vouch for other boards but the one I have gives excellent results. To complete the hardware for a minimal setup we need a 10K or a 5K pot for volume control and we need wired earbuds or a headset for listening.


Our original radio.py needs modifying to push the radio data through the decoder, before we do that we will add two more code files to the project, the first file we will create ourselves and the second file we will download from github.

The file we will create is a ring buffer, the ring buffer will take the data we push in from the ESP32 which is then pulled from the other end into the vs1053 module. Using a ring buffer we make sure the decoder is never "starved" of data which could lead to clicking sounds or unwanted pauses. Bear in mind that a poor or weak wifi could also effect quality.

Copy the following code and save it to the microcontroller as ringBuffer.py
class RING_BUFFER:
   
    def __init__(self):
        self._buf = bytearray()

    def put(self, data):
        self._buf.extend(data)

    def get(self, size):
        data = self._buf[:size]
        self._buf[:size] = b''
        return data

    def getvalue(self):
        return self._buf

    def getlen(self):
        return len(self._buf)
From github we need Peter Hinch's vs1053_syn.py which is a very comprehensive driver for the vs1053, download at this link
https://github.com/peterhinch/micropython-vs1053/tree/master/synchronous

For our project we need to modify the __init__ line of vs1053_syn.py to include our ring buffer (rb) so change the init method to look like the following
def __init__(self, spi, sd, reset, dreq, xdcs, xcs, rb, sdcs=None, mp=None, cancb=lambda : False):
        self.rb = rb
additionally we need our own play routine that utilizes the ring buffer so place the following method right after the __init_ method and just before the _wait_ready method inside the vs1053_syn.py file.
def radio_play(self,buf=bytearray(32)):
        dreq=self._dreq
        while dreq() and self.rb.getlen() > 32:
            buf=self.rb.get(32)
            self._xdcs(0) 
            self._spi.write(buf)
            self._xdcs(1)
now we can save vs1053_syn.py to the ESP32, there will now be a total of four files on our controller :- radio.py,channels.py,ringBuffer.py and vs1053_syn.py ( make sure you deleted tmp.mp3 from our first "sound bite" test ). Just a few changes to radio.py and we will be ready to play.

Jeff_T

#3
Part 3 The main code module

All that remains is to modify radio.py so that it interfaces with the vs1053 and it's driver. We begin by importing Pin and SPI objects, a _thread module and our two code files vs1053_syn and ringBuffer. Next we assign pins to our interface, five digital and one analog for volume control followed by instances of the ring buffer, SPI and VS1053.

Moving down we have a new method for volume control, this function will run in a separate thread.

Finally is a while loop that in our first demo wrote a file to flash and has now been modified to write a stream to the decoder plus monitor volume adjustment. During testing if the volume pot is not connected the volume can be a fixed value, the softest volume is a minus value and I use -55 as the softest 0 is the loudest, -30 should be a reasonable listening value.

Here is the full listing
import time
import network
import socket
import os
from channels import *
   
from machine import Pin,SPI,ADC
import _thread
from vs1053_syn import *
from ringBuffer import RING_BUFFER

dreq = Pin(5, Pin.IN)  # Active high data request
xcs = Pin(6, Pin.OUT, value=1)  # Labelled CS on PCB, xcs on chip datasheet
xdcs = Pin(7, Pin.OUT, value=1)  # Data chip select xdcs in datasheet
reset = Pin(8, Pin.OUT, value=1)  # Active low hardware reset
sdcs = Pin(9, Pin.OUT, value=1)  # SD card CS

pot = ADC(Pin(1))
pot.atten(ADC.ATTN_11DB)

rb=RING_BUFFER()

spi = SPI(1)
sd=None

player = VS1053(spi, sd, reset, dreq, xdcs, xcs, rb)

def wifi_connect():
    global wlan 
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(WIFI_SSID, WIFI_PASSWORD)
    while not wlan.isconnected():
        print("waiting for wifi connection")
        time.sleep_ms(500)
   
 
def parse_url(channel):
    global s
   
    s.close()
    s = socket.socket()
    url=channel
    _, _, host, path = url.split('/', 3)
   
    print(path,"         ",host)
   
    addr = socket.getaddrinfo(host, 80)[0][-1]
   
    print(addr)
   
    s.connect(addr)
    s.send(bytes('GET /%s HTTP/1.1\r\nHost: %s\r\n\r\nConnection: close\r\n\r\n' % (path, host), 'utf8'))
    time.sleep(2)
    s.settimeout(0.01)
   
def volume_adjust():
    global vol
   
    while True:
        time.sleep(0.5)
        global pot
        lo_in=0
        hi_in=4095
        lo_out=0
        hi_out=55
        pot_val = pot.read()
        map_value=int((pot_val - lo_in)/(hi_in-lo_in)*(hi_out-lo_out) + lo_out)
        vol=-abs(map_value)
       
######################################## Main Routine #############################################

vol=-50
old_vol=vol

_thread.start_new_thread(volume_adjust, ())

wifi_connect()

s = socket.socket()
 
parse_url(chan_1)


while True:
    buff_count=rb.getlen()
    sckt=0
   
    if vol != old_vol:
        player.volume(vol,vol)
        old_vol=vol
       
    if  buff_count<5000:
        try:
            sckt=s.recv(16)
            player.rb.put(sckt)
        except:
            pass

    player.radio_play()
   
 

Jeff_T

Part 4 Final thoughts

There is a lot of room for enhancements for example a means to change channels or a way of disconnecting the socket and wifi on exit also the vs1053 has other features such as bass and treble control. I leave that for you I just wanted to give something that gave the basic function.

The vs1053 that I used was designed for the Arduino uno which is a 5v controller, because the vs1053 chip and the SPI on this shield operate at 3.3v it is safe to use with the ESP32 if you use a different breakout board verify the voltage.

For listening I bought bluetooth dongle and plugged it in the SPK jack, it has the drawback of being an extra step because I have to pair devices but I gain the option to connect any bluetooth device I have and the sound quality from my ear buds is outstanding.

Change the name of radio.py to main.py and the program will auto run on power up.

 

Jeff_T

#5
Part 5 BONUS mp3 player

This is a nice mp3 player for music, voice or sound effects and builds on what we have already done, with the following code it will be simple to quickly switch between a radio and a mp3 player that uses the onboard SD card slot

Remove everything from the microcontroller except ringBuffer.py and the vs1053_syn.py that we modified. We need to download a module for the SD card, here is the link and it is another module written by Peter Hinch and is contained in his vs1053 repository.

https://github.com/peterhinch/micropython-vs1053

The filename you need is sdcard.py, load it onto the microcontroller with the other two files.

The final module is a modification of our original radio.py, the top of the file is very similar but in this case we add an object for the sd card. The first function we come to is play_track() which reads a mp3 file from the sdcard and streams it to the vs1053, following this we retain our volume function.

The main routine mounts the sd card and lists all the files, for this example make sure all the files on the card are valid mp3 files, the while loop plays each file in turn and starts over when it reaches the end.

This is just an example and is open to much more enhancement, there are times I prefer this project over the radio.

The full listing

from machine import Pin,SPI,ADC
import time
import os
import _thread
from vs1053_syn import *
from ringBuffer import RING_BUFFER
import sdcard
   
dreq = Pin(5, Pin.IN)  # Active high data request_GN
xcs = Pin(6, Pin.OUT, value=1)  # Labelled CS on PCB, xcs on chip datasheet_BE
xdcs = Pin(7, Pin.OUT, value=1)  # Data chip select xdcs in datasheet_V
reset = Pin(8, Pin.OUT, value=1)  # Active low hardware reset_GY
sdcs = Pin(9, Pin.OUT, value=1)  # SD card CS_W

rb=RING_BUFFER()
spi = SPI(1)
sd = sdcard.SDCard(SPI(1), cs=sdcs)
player = VS1053(spi, sd, reset, dreq, xdcs, xcs, rb)

vol=-50

mp3 = [0  , False ]

pot = ADC(Pin(1))
pot.atten(ADC.ATTN_11DB)

def play_track(mp3):
    global vol
    old_vol=-50
    with open(mp3[0],'rb') as f:
        data=f.read(1000)
        while data :
           
            if vol != old_vol:
                player.volume(vol,vol)
                old_vol=vol
           
            if  player.rb.getlen()<1000:
                data=f.read(32)
                player.rb.put(data)
           
            player.radio_play()
           
        mp3[1] = False
       
       
def volume_adjust():
    global vol
   
    while True:
        time.sleep(0.5)
        global pot
        lo_in=0
        hi_in=4095
        lo_out=30
        hi_out=62
        pot_val = pot.read()
        map_value=int((pot_val - lo_in)/(hi_in-lo_in)*(hi_out-lo_out) + lo_out)
        vol=-abs(map_value)
       
######################################## Main Routine #############################################
   
vfs=os.VfsFat(sd)

os.mount(sd,'/sd')

print("MP3 Files on SD card")
path='/sd/'
dir_list=[]
dir_list=os.listdir(path)
 
print(len(dir_list) - 1,'\n')
for i in range(1,len(dir_list)):
    print(i,' ',dir_list[i])
   
mp3_count = len(dir_list) - 1
count = 1
mp3[0] = path + dir_list[count]

_thread.start_new_thread(volume_adjust, ())

while True:
    if not mp3[1]:
        mp3[0]=path + dir_list[count]
        _thread.start_new_thread(play_track, (mp3,))
        mp3[1]=True
        print('\nPlaying ',dir_list[count])
        count+=1
        if count > mp3_count:
            count = 1
    else:
        time.sleep(3)
   


Chris Savage

While I still have a small mountain of projects in front of this one, I did get things started by purchasing the ESP32 module below from Amazon.com . This will lead to me creating a stub for this project which I can then post the progress on Savage///Circuits.

https://a.co/d/3EUCTs7

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Jeff_T

Hi Chris, I don't think you will regret the purchase, I actually bought another one yesterday also from Amazon. My wife bought some draw pulls for a dresser we are making over and I tacked the mcu on the order lol.

The Nano ESP32 can be used for Micro-python or C\C++ (not at the same time), it has 2 cores that can run 2 separate programs in parallel if you use C\C++ . The development board also has a huge amount of PSRAM, I think it's 16 MB, available for the user .

TFT Displays using Micro-python drivers and framebuffers is my current interest and might be something for me to post sometime soon .




Chris Savage

#8
Quote from: Jeff_T on Mar 30, 2024, 07:40 PMHi Chris, I don't think you will regret the purchase, I actually bought another one yesterday also from Amazon. The Nano ESP32 can be used for Micro-python or C\C++ (not at the same time), it has 2 cores that can run 2 separate programs in parallel if you use C\C++ . The development board also has a huge amount of PSRAM, I think it's 16 MB, available for the user.

Jeff,

I actually purchased two (I always get a spare of things I don't normally have, in case I make a mistake). That said, what's a good platform that uses Python or would you simply say to use the R-Pi? I've been considering learning Python for several years now and didn't really have a reason.

I do have a question on the VS1053...what do you think of this option?

https://www.adafruit.com/product/1381



OR THIS:

https://www.adafruit.com/product/1788


        I'm only responsible for what I say, not what you understand.

Jeff_T

Hi Chris, the Adafruit links look like good options, I like their products.

The module that I went with for this project I chose because of price, using a bluetooth transmitter was primarily because I didn't want to mess with speakers and amplifier but in the end it turned out to be a great decision, the sound quality I get is awesome.

With regard to Micro-python the Nano ESP32 that you have is a good choice, I would say put the Micro-python firmware on one of the two so that you can try it out.

If you want to start a conversation on Micro-python I would certainly be interested, I am not a guru but I could help you or anyone else that was interested to get started. Python is so easy to get into and the results are pretty good.

Here is the link that describes loading the MP firmware to the Nano ESP32   https://docs.arduino.cc/micropython/basics/board-installation/ 

For an IDE I would go with Thonny, its lightweight, cross platform and good to use  https://thonny.org/

I'll post a "Getting started" tutorial in the next few days.

Chris Savage

They arrived today. Yes, on Easter Sunday. Who knew? In any event, I can at least experiment until I can get to the project.

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Chris Savage

#11
Just a follow-up to this thread...I also have the following item on its way for the project in this thread.

VS1053 VS1053B Stereo Audio MP3 Player Shield Record Decode Development Board Module with TF Card Slot for R3

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Chris Savage

Amazon.com really needs to rethink their packaging for circuit boards. With the exception of authentic Arduinos, which come in their own box, every accessory I have purchased is a PCB in a foil bag, which Amazon then sticks inside of a bubble wrap envelope for shipping.

My 1.8" TFT Display arrived yesterday as described...and as usual, crushed. I honestly was about to fill out the return form. I am so used to that. But I decided to stick the display back on the PCB, bend the pins back and try it anyway.

I wrote a quick test demo which generates random RGB values and a random "y" coordinate and prints, "Hello, World!" on the display in the random color and y location, before erasing that message and looping. I recorded the following video.


One thing I didn't notice while recording was some artifacts around the bottom and right borders. Not sure if that is related to the driver or if the display does have some damage. More testing needs to be done. In the mean time, I bought a second display that looks identical, but came from a different seller.

You cannot view this attachment.

        I'm only responsible for what I say, not what you understand.

Jeff_T

That is really good Chris, I hate the display was damaged but looking at it you made it look good as new.

The pixilation around the edge is most likely settings in the driver file, what I have done in the past is place four pixels one in each corner to make sure the coordinates are right. 0,0 -160,0 - 0,128 - 160,128. There is probably some adjustment in the driver, you could ask in the Arduino forum. I'm afraid I can't help much with the c/c++ file but if you try it with micropython I'm pretty sure we could get it lined up.

I have a couple of utilities you might be interested in, one converts a bmp or png image to binary data for writing to a display, it's not micropython but it is Python but you can still run the program in Thonny.

The same bin file can be used no matter which language you are using, I'll try and post a video later.

I have code that does the same in a running program but extracting the image data on the fly takes much more time

Chris Savage

Jeff,

I am hoping to get the ESP32 hooked up to this display tomorrow evening. I grabbed the Arduino because it was sitting on this desk (Gaming PC) and I had some wires nearby. It was quick & dirty. But yeah, we'll try your demo tomorrow evening as I start sorting and organizing the various source code you have posted.

        I'm only responsible for what I say, not what you understand.