DIY internet radio
Why

This was an exercise in practical, not technical.
I wanted to make a music player that is as low-bother as possible, that just works, and is low power.
Sure you can use your phone, but people want to take those with them.
Sure you can use a bluetooth receiver. We have that too, and it's great but temporary - it still means a phone nearby, so is sort of the same problem. And if someone leavs but doesn't disconnect, you have to figure out how to kick them off. Bother.
Sure you can leave a laptop there, but you'd either need to do a bunch of login stuff, or keep it on, or remote control it. Bother, and it's more power than necessary.
Sure you can use a smart TV for internet radio, but the larger the TV, the more that's a visible waste of power. Also, our smart TV's app store stopped existing so it became dumb again. And the interface tends not to be great. Bother.
Sure you can use Chromecast, which can actually works quite well - but even then is extra steps to explain ("No I don't know why it doesn't detect it - works for me, see? I think it's a wifi isolation thing? Wait, are you on iPhone? Maybe the support works differently there, couldn't tell you but it seems more fiddly."), and in my experience everything from bubbleupnp to spotify will, at some time later, either lose its ability to control what's actively playing, or sometimes hang not playing music. It's a minor gripe, but still. Bother. Also, I have a classical rather than audio-only chromecast, so the TV needs to stay on, just to be the DAC. It works, but meh.
So I wanted a little box.
Interface

The most basic I could come up with is
- a pushbutton
- short press goes to next station
- long press turns stops playing
- an SSD1306 style OLED display
- in part because it's small, high-contast, and cheap
- in part because I think it's elegant that it makes no light when off
- showing current station, and whether we're connecting or playing
And if it breaks, it's clear who has to fix it.
...yes, me, but at least that way I know it'll actually happen.

Physically
It's
- a box holding the raspberry
- a piece of nice looking laminate flooring in front.
- mounting a button
- and the display - with a 3D printed bevel
I should probably countersink the screws a little better, but I'm sort of fond of the DIY look.
Platform and software
I considered an ESP8266 or ESP32, for small size, low power use, the sake of learning audio on that platform, and the fact that other people have already made this so it would be little work.
But I figured that its PWM-based audio would probably not sound great, and the point is decent-enough music. It seems the ESP can do DMA'd I2S, which would be good - given an external I2S DAC.
I had some worry about TLS, which ESP8266s don't really have enough memory for; ESP32s would be more comfortable. You can probably usually get away with that because stations often do both HTTPS and HTTP, but it didn't seem very future-proof to assume that.
So I settled on Raspberry Pi.
Without an external DAC (which are relatively easy, there are boards), audio is still PWM-based and not audiophile grade, but almost certainly better than ESP internal PWM. And if needs be, I think have I2S DACs somewhere.
Also, I like the idea that raspberry makes it a little easier to deal with changing configured stations by SSHing in. And maybe dealing with less-usual stations in code.
Also, the dumb reason that its the hardware already has an audio jack (and output buffer, except on the very oldest). Don't make projects harder than they need to be, y'know.
Software
There are various ready-made things to do web radio and more. Many either
- ...leave a lot to be duct taped together (e.g. MPD and a controller interface)
- ...or are so fancy (e.g. volumio is neat) that they need their own web interface that is more complicated interaction than I want
Also, integrating that display and button into fancy software is actually somewhat awkward, and probably half the work regardless of how simple or complex the codebase / interface.
Given that the functionality I want is very basic, I wrote my own.
It's horrible contrivance of a script, though only ~200 lines long, that mostly just
- watches the button
- (pin is configured with internal pullup - it's one external component fewer)
- on a (debounced) button press
- does a killall ffplay
- runs ffplay on the new station URL
- updates the display (see below)
- determines whether something is playing by using fuser to see if something has the ALSA device (/dev/snd/pcm*) open
It relies on CircuitPython for
- the button - an existing interface to Pi's GPIO pins
- display - interface to the Pi's I2C, and a SSD1306 library - see next section
The hardest part was figuring out what arcane path/library entanglement CircuitPython does, and a little version muck around its libraries.
It's a little messy, I'm not sure I'd do the same again, but it works.
I have half a dozen stations in there, like FIP, Shirley and Spinoza, a local station, and some from my own music collection.
OLED display
I use python and the adafruit_ssd1306 library, which among other things lets you send display-sized images.
This certainly isn't a very fast way to change pixels on there (like this is), but this application doesn't need speed. At all.
So we focus on easy instead, using ImageDraw to draw on a PIL image we can send. PIL makes basic drawing and TTF font loading and such fairly simple.
Most of the display code amounts to something like
# setup
from board import SCL, SDA
i2c = busio.I2C(SCL, SDA)
disp = adafruit_ssd1306.SSD1306_I2C( 128,32, i2c )
from PIL import Image, ImageDraw, ImageFont
fontL = ImageFont.truetype('/home/pi/Adore64.ttf', 11)
fontS = ImageFont.truetype('/home/pi/Adore64.ttf', 8)
disp_image = Image.new("1", (disp.width, disp.height)) # image we draw on and send
# in the main loop (in the real code actually on a condition of 'if changed')
display.fill(0) # clear image
if not radio_player.stopped: # (radio_player keeps track of what/whether we're playing)
draw.text((1, 8), radio_player.current_station_name, font=fontL, fill=255)
if radio_player.playing:
draw.text((3, 22), 'playing', font=fontS, fill=255)
else:
draw.text((3, 22), 'connecting...', font=fontS, fill=255)
disp.image( disp_image )
disp.show()
One's own music as web radio
While music streaming is currently finding new (and often proprietary) forms, 'standard' internet radio for a long time was based on SHOUTcast. (a little more on how SHOUTcast and derivatives work here (and on the internet of course))
This makes it a fairly widely supported way to stream music.
One downside to this approach is that the encoder for each channel is continuously running, regardless of whether anyone is listening.
(on internet radio this is a non-issue because there is almost always a bunch of listeners; here we are usually not listening to it)
Each station will use a few percent of one modern CPU core. Not bad at all, but theoretically avoidable (by not using shoutcast, or writing a bunch of custom code. So I didn't do that).
One easy-enough way to put your own music on a standard shoutcast-style stream,
so that you can point any standard internet radio player at, is an existing server that runs:
- ...or similar. This manages the existance of streams, where they're mounted.
- which reads music files and sends it to icecast2
- When all you care about is feeding a playlist, there are various simpler sources that work, with less learning curve
- ...but I like LiquidSoap for flexibility and a little audio processing
After some experimentation, each station amounts to a .liq configuration like:
out = output.icecast( %mp3(bitrate=160, samplerate=44100, stereo=true),
host="localhost", password="hackme", name='piano')
lst = playlist( mode='randomize', '/Music/ice_play/piano.m3u8' )
default = single( "/etc/liquidsoap/Silence.ogg" )
s = fallback( [lst,default] )
skys = sky(s) # multiband compression, makes some sense for quiet listening
out( mount="piano", add( [ skys ] ) )
That silence fallback is because liquidsoap cares about a stream's fallibility
I chose LiquidSoap in part for the ability to use replaygain, but am still figuring out the rough edges around it - I should update that once I figure that out.
And whether I want crossfade.
And whether that multiband compressor does what I think it does.
See also https://www.liquidsoap.info/doc-1.4.4/reference.html