DIY internet radio
This was an exercise in practical, not technical. I wanted to make a music player that is as basic as possible, unobtrusive, low power, that just works.
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. But that usually still means phone, and nearby.
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 use a smart TV for internet radio, but the larger the TV, the more that's a visible waste of power. Also, our TV's app store stopped existing and it becomes a dumb TV 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 happening, 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.
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.
- 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
But I figured that its PWM-based audio would probably not sound great, and the point is nice music. Though note that ESP can do I2S (DMA'd, which it would need to), which would basically fix that, 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). 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 (which are relatively easy, there are boards), audio is still PWM-based and not audiophile grade, but almost certainly better than ESP PWM. And if needs be, I think 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.
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 they need their own web interface that is more complicated than I need (e.g. volumio)
Also, integrating that display and button into pretty much any option would be somewhat awkward, and probably half the work regardless of how simple or compelx the codebase / interface.
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 before switching to a new station
- updates the display (see below)
- determines whether something is playing by using 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 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 = 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. 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.