DIY internet radio

From Helpful
Revision as of 12:23, 15 July 2023 by Helpful (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


I wanted simple. And a red button.

This was an exercise in practical, not technical. I wanted to make a music player that is as simple and unobtrusive 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.

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. 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.

Sure you can use Chromecast, which works well when when it works 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. Also, I have a classical rather than audio-only chromecast, so the TV needs to stay on to be the DAC. It works, but meh.

So I wanted a little box.


Made of this (there's a raspberry pi inside), a wooden box, the display holder shown below, some wire, hot snot, and a power supply

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.

Making the hole for the display look less rough



  • 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, and 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 music. It seems the ESP can do DMA'd I2S, which would be good enough - 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 too 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 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.


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
(configures pin with internal pullup - it's one less external component)
  • on a change
does a killall ffplay
runs ffplay on the new station
  • updates the display (see below)
  • determines whether something is playing by using fuser to see if something has an 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, 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, which lets you fiddle with TTF fonts and such in a few lines.

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 ="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)
        draw.text((3, 22), 'connecting...', font=fontS, fill=255)

disp.image( disp_image )

One's own music as web radio

While streaming is currently finding other 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 by many things.

One downside to this approach is that the encoder for each channel is always running regardless of whether anyone is listening, each using maybe ~15% of a CPU core. Not too bad, but theoretically avoidable (by not using shoutcast, and writing a bunch of custom code. So I didn't do that).

One easy-enough way 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, but I like LiquidSoap for flexibility and a little audio processing.
it's so flexible it has a learning curve

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 actually chose LiquidSoap 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