Learning out Loud in Milwaukee, WI

Radio Player: Alexa without all the skills

“Hey Siri, play Radio Milwaukee.”

“Hey Alexa, play WUWM.”

It may be hard for some folks to remember what it was like to play a web stream before these home assistants existed. For people who own such a device they close the cumbersome gaps involved with finding and playing your favorite radio station on decent (if not good) speakers. But if you already have a stereo and speakers you like, and don’t have the money or desire to upgrade to something internet connected, you’re stuck with plugging your phone into the aux in, finding the station, never disconnecting it until you are done listening, and hoping your browser’s connection doesn’t reset for some reason. These new thingies aren’t interoperable with each other or what you already have.

These devices are limited radio listeners. Like many speakers, they work best in the room they’re in and have to be turned up loud to hear them outside that limited space. Then, the people in that room have to deal with too-loud radio else everybody else deals with too-quiet radio. It’s especially frustrating when you have some good audio hardware sitting right there, it’s just not connected to the internet.

They’re also multi-purpose. The HomePod, Amazon Echo, and Google Home can all play radio streams, but that’s just one skill out of many it is capable of. It’s also a hub for your “smart” home devices. Some of these devices do have headphone jacks you could use to plug them into a standard stereo, but then you have to have your stereo on for all the smart home things you’re doing.

Some device makers have solved this problem by allowing the same feed to play on multiple devices. This is Sono’s whole shtick: Lots of small speaker devices scattered around your house and you get to choose which plays what. That’s nice if you have the cash for new hardware.

The plan: Make an unskilled Alexa

We just moved into a new house that has speakers wired into the walls in our bedroom and living room. These speakers are good quality and positioned so that you can hear what’s playing clearly in adjacent rooms without having the volume up wildly high. We plugged our stereo into them to test the record player and they all work. The only problem, the receiver is in the basement where it can’t get an FM signal. To allow this to work, I decided to take a Raspberry Pi we already have on our network and use it to play radio stations we listen to regularly.

The parts

Hardware:

  • Raspberry Pi 3b (512MB)
  • 3.5mm TRS to RCA audio cable
  • Stereo receiver
  • Speakers

Software:

  • Flask
  • VLC
  • VLC python library

Web API

I choose Flask for one reason alone: I needed something more robust than hand-made web pages. I wanted a RESTful API that would trigger or read information from some server side processing, and Flask is a capable tool for that kind of job. I was learning enough making a Raspberry Pi play a webstream without a GUI, I didn’t need to overcomplicate the basics.

I took an API oriented approach to designing this thing. Each discrete thing I wanted the Pi to do would have its own API endpoint, with possible query parameters to change state on that route. In other words, if you wanted information about the network conditions, you could request /network/. For the radio player, request /radio/<ID>/ to get all the relevant information about that station including how to play or stop the station and what the current playback status is.

{"last-played":"","play":"http://192.168.0.73:8000/radio/wuwm?play","station":"wuwm","status":"stopped","stop":"http://192.168.0.73:8000/radio/wuwm?stop","url":{"info":false,"logo":"logo-wuwm.jpg","nicename":"WUWM Milwaukee's NPR","stream":"http://wuwm.streamguys1.com/live.m3u"}}

Instead of a database, available station are defined in YAML format. This keeps the project simple and reduces the security boundary. In a file called config.yml in the root directory, stations are specified as:

radio:
  stations:
    "889": 
      "stream": "https://wyms.streamguys1.com/live.m3u"
      "logo": "logo-889.jpg"
      "info": True
      "nicename": "88Nine Radio Milwaukee"

There’s also a section called xspf for a link to the stations feed showing what’s currently playing. I haven’t fully implemented it yet because it turns out these streams aren’t as standardized as you might expect.

I’ll admit I’m not a great API designer, so it doesn’t all come together perfectly well. Routes you might expect don’t exist, or don’t exist at an expected end point. I’ve also been pretty poor at documentation. It’s working for the one user group I have so far, and that’s me and my family.

Templates

I really only have one template for this app and that’s for / all other end points return JSON data so far. The home page lists all available stations and at the top of the page indicates which, if any, of them are playing right now. For the one stations where I have data for it, 889 Radio Milwaukee, it also shows the song currently playing and sorta updates when you refresh the page.

Almost all of the information seen on the page is pulled from the station’s record in the YAML file. The logo is expected to live at a certain location with the station’s ID name. The playback status is derived from an API call to the station’s info page. Finally, there’s a stop all button at the bottom of the page that burns through all the players and stops them which is a good fail safe.

There’s a little bit of really primitive JavaScript at work, too. When you press a play button, the site sends a request to the appropriate end point and (usually) starts playing that station and updates the button to become a stop button. A trick I didn’t anticipate is that VLC player will actually let you play multiple streams at once, so you can inadvertently have two or more streams of the same station overlapping each other if you aren’t careful about the play/stop operations. If your feeling cheeky you can alsoplay every station listed at the same time and have a cacophony on par with Lou Reed’s Metal Machine Music.

I’m also really not a web designer, though the 23 year old version of myself definitely thought I was, so the styles are really basic and I haven’t really had much care to do anything more with it.

Behind the scenes

The python package for VLC bindings is a bit low on documentation. It’s also massively more useful for manipulating audio and video streams. If you had to serve videos at specific dimensions but couldn’t rely on the incoming streams to provide that dimension, you could use this package to crop, blur, or modify the video before it’s presented. It’s probably too complicated for what I need, but it’s the solution I knew, and one I could get working on the Raspberry Pi.

One of the complicated things about VLC is the many different kinds of players it can create and which one is the right one to use for a given problem. After a lot of StackOverflowing and ChatGPT interrogation, I eventually settled on the ListPlayer as the most appropriate since I had m3u files as my input source. A limitation of the ListPlayer is that it’s confusing whether you can get the metadata for the current audio file playing within the list. The answers I’ve found all involve making a second player for the individual item in the player, then extracting the metadata out of that. It all feels a little overwrought to me.

The upshot of VLC is that it will play basically any format thrown at it from the URL. Other options for programmatic playback involve identifying the file type in advance or are limited in what they can play. This way I don’t have to deal with lamelib or anything like that. So while there’s an opportunity for refactoring, I’m basically happy with it.

What’s next?

  1. I should probably stop all currently playing stations before starting a new stream. Nobody wants the Metal Machine Music effect.
  2. Make it a true Progressive Web App: I need to do some more research into progressive web apps in order to make this an app that can add to a phone’s Home Screen and behave as a native app: Isolated in it’s own window, and not a tab in the user’s browser.
  3. The code is a bit of a mess: There are no tests, which is fine because the stakes are so low, but I think I’d take a more test-driven approach to refactoring. One area I want to give particular attention to is making the includes directory better reflect the API structure: All the player logic in one file, or a set of files, all the system files in another, etc.
  4. Bring metadata forward: Right now I can only pull metadata from one of the stations in the config.yml and only on the first page load. There is a published spec for XSPF files, but individual stations seem really inconsistent about how much they care about following it. They’re also hard to discover, and some stations don’t use them at all. It’d be better to grab the metadata from the audio file currently playing, if possible, and rely on XSPF as a backup.
  5. It’d also be nice to have an interface for this that isn’t tied to a phone. What if I had a web-connected touch screen mounted on the wall and a user could tap on the station they wanted to play? A nice thing about FM radio is you can do it without pulling out your phone and fiddling with an app. It’d be great for web radio to be the same. I could also model it after this dementia friendly music box.

Web radio in general

A disappointing thing about this whole venture is how much of the work of web streaming is closed-source and opaque. Some stations publish standard Icecast pages. (Here’s Radio Milwaukee’s.) This eases the burden of finding the URLs to use for a given stream. It seems like the bigger the budget the more stations obscure their streams. For stations like The Current and Brewers Radio Network (WTMJ), I had to pop open the web inspector on my browser and reverse engineer the streaming widget on their webpages to figure out what URL to use. Kind of a painful process to find what is meant to be a “universal resource location.”

I get that there are copyright considerations at play, and that those are real liabilities that scale with audience size, but it would sure be nice if there were more open standards to count on when building radio clients. Icecast has a built-in open discoverability standard, but it isn’t very reliable, at least not with search. It seems broadcast radio is streets ahead of podcasts when it comes to walling off otherwise open technology.

It’s fancier to tell your audience to download your app or tell Alexa to play your station, but it comes at the cost of interoperability. The more entrenched we get in these proprietary and walled garden devices, the more freedom we lose. To get a local broadcast over the airwaves, your job is as simple as a $6 radio receiver dial into the right place. It’s a shame new technology has made this so much harder instead of making it increasingly easy.

Want to help?

If this sounds like something you’d like, feel free to grab the code from GitHub, fork it, remix it, or contribute back with pull requests or bugs you find. It’s open source software released under the GPL so enjoy your four freedoms.