DIY internet radio

From Helpful
Revision as of 09:59, 29 July 2021 by Helpful (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Why

Music in one's living room is nice, as is a choice of what music.

I wanted to make something as basic as possible, unobtrusive, low power, that just works.

And if it breaks, it's clear who to whine at to fix it. Yes, me, but at least that way I know it'll actually happen.


I wanted simple. And a red button.

Sure you can use a smart TV for internet radio -- but only until that TV's app store and support stop existing and it becomes a dumb TV again, as ours did. Also, the larger the TV, the more that's a visible waste of power.

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 again, more power than necessary.

Sure you can leave your phone there, on a wire or bluetooth receiver. We have both, and this works well, yet both mean you have to physically leave your phone, and people actually tend to to use their phones for other things.

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? 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 happening, or sometimes hang not playing music. It's a small 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.

Interface

Made of this, 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 and cheap
in part because I think it's more elegant that it makes no light when off
showing current station, and whether we're connecting or playing
Making the hole for the display look less rough

Physically

It's

  • a box holding the raspberry
  • a piece of nice looking laminate flooring in front.
    • holding the 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, which should be easy enough since other people have already made this.

But I figured that its PWM-based audio would probably not sound great, and the point is nice music. ESP can do I2S (DMA'd, which it would need to), which would basically fix that.

I had some worry about TLS, which ESP8266s don't really have enough memory for (ESP32s would be more comfortable). Yes, if stations do HTTPS, they mostly seem to also still also do HTTP, but it didn't seem too future-proof to assume that.


So I settled on Raspberry Pi.

Without an external DAC, audio is still PWM-based and not audiophile grade, but almost certainly better than ESP PWM. And if needs be I have I2S DACs, somewhere.

Also, that platform makes it a little easier to deal with changing configured stations by SSHing in, and dealing with less-usual stations in code.

Also, it 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 too fancy, e.g. giving an interface much more complicated than I need (e.g. volumio)

I had some practical worries. Say, some radio stations change their stream URL over time. That's easy enough to work around with a few lines of screen scraping code, not so much if all you get is "add shoutcast URL here".


But more importantly, integrating that display and button into pretty much any option would be somewhat awkward, and much of the work anyway.


That, and given that the functionality is very basic, meant 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, one less external component)
  • runs ffplay on the new station
and does a
killall ffplay
before switching to a 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.


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, 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 = 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 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. For audio most are much the same.
When all you care about is feeding a playlist, there are various other 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 https://www.liquidsoap.info/doc-1.4.4/reference.html