DIY internet radio

From Helpful
Jump to navigation Jump to search

Why

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 low-bother as possible, that just works, and is low power.


Sure you can use your phone, but needs the phone to stay close, and people want to take those with them.

Sure you can use a bluetooth receiver. We have that too, and it's great but again, it needs to stay close. Also, if someone leaves but doesn't disconnect, you have to figure out how to kick them off. Bother. The "you can sort of connect two at a time" does very little to help that.

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, the interface was never great, and our specific smart TV's app store stopped existing, so it became dumb again. Bother.

Sure you can use Chromecast, which can actually works fairly 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, that just does the music thing, no thoughts like that.

Interface

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

Physically

It's

  • a wooden 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.

Plus the dumber 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. However, a bunch of option 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. It would probably end up being 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 python script, though only ~200 lines long, that mostly just

  • watches the button
(pin is configured with internal pullup - it's one external component fewer)
  • when it sees a (debounced) button press
does a killall ffplay
runs ffplay on the new station URL
  • updates the display
fetching latest-for-a-station via callbacks
determines whether something is playing by using fuser to see if something has the ALSA device (/dev/snd/pcm*) open
renders text into an image to sends it to the display


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 of that 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)
# implied else: not playing, show nothing

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

You can point any standard internet radio player at a shoutcast server, which makes it a moderately 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).

From some tests later, a handful of handful of channels seemed to add 2W of wallpower through CPU use. (I'm happy enough with that, but it's theoretically avoidable -- by not using shoutcast, or writing a bunch of custom code. So I didn't do that).


Software-wise, I have an existing server run:

...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
arguably overkill among simpler choices, but I liked its ability to do replaygain (still figuring out the rough edges around that), a bit of compression, possibly crossfade.


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 should check whether that multiband compressor does what I think it does.

See also https://www.liquidsoap.info/doc-1.4.4/reference.html