brainbaking/content/post/2023/02/fm-streaming-from-subsonic-...

7.6 KiB

title date tags categories
Streaming Music From Subsonic To Retro Radios 2023-02-10T11:54:00+01:00
radio
music
navidrome
software
retro

Remember those FM transmitters you used in your car in the early 2000s to connect your iPod to your car radio tuner because it didn't have an audio-in port? I built an intricate version for my music setup. This idea is (again) inspired by William Woodruff's excellent Modernizing my 1980s Sound System post, where he combined a Raspberri Pi 3, an USB DAC, and HifiBerryOS to stream to his 1980s speakers from any device on the lAN through AirPlay or DLNA. William wanted to use his old boxes on his new Navidrome streaming music server using any Subsonic-compatible client.

That sounded (ha!) very cool, and I wanted to expand upon that idea to get it running on our own cheap retro radios. The problem is that our 1980s hardware doesn't even come with a (stereo) audio-in jack: the only way to play music is to plug in a cassette/CD, or tune in on a radio channel using FM or AM. The second challenge was also leveraging existing Subsonic clients, such as Sonixd and Substreamer. How to achieve that?

The setup

After a week of fiddling, I came up with an ingenious---but ultimately suboptimal---solution. Observe:

What's going on here? The idea was to be able to select a song on my phone through the Substreamer app I already have installed that interacts with my Navidrome music server on the NAS. To achieve that, I went with a "man in the middle" system: I set up a reverse proxy server on a Radxa Rock Pi 4C+---a Raspberry Pi clone, more on that later. By pointing the app to the Pi (#1 in the photo), it acts as the genuine server, but it's just passing on all HTTP calls, except for the /rest/stream Subsonic API call: that one contains our music data!

Then, we duplicate that data for the response and the local file system, play that song on the Pi itself since it has an audio chip, feed the audio-out into a Adafruit Si4713 FM transmitter breakout board that's connected via I2C/GPIO (#2), which in turn converts it into FM sound waves on a certain channel (#3), that---finally---our old radio is able to pick up! How about that?

The FM transmitter is small and the I2C protocol only requires 2 wires: a clock and an 8-bit (actually 7) data wire, with 2 for 3.3V and ground, and a fifth one to pulse a reset signal. The board works flawlessly with a cheap Arduino UNO, but as I also wanted to host a Go-based HTTP server and required an audio chip, I needed a bit more beef.

Speaking of which, the Rock Pi 4 was a nightmare to install: after the 8th flashed .iso, it finally recognized the audio and video chip, booting the X server. Only the official Debian image seems to work, while I kept trying to install an Armbian one. To complicate matters further, 99% of embedded hardware software libraries are compatible with a Raspberry Pi, but not with a clone. The Rock Pi has a different GPIO mapping that of course was missing in every framework I tried. I eventually fixed it myself in CircuitPython and the pull request is approved, yay! The only reason I bought a Chinese clone was because RaspPi's have been and still are on back-order. In other words, they're unavailable, and it sucks: to the amateur hobbyist such as myself, Linux compatibility is a serious issue.

As for the FM transmitter setup, a bit of soldering is required to connect the pins which I had to retry more than a couple of times as it's very very tiny according to my standards and refused to be recognized by mraa-i2c detect on I2C bus 0 (which is actually I2C7 or pins 3 and 5). Another few days of fiddling later, I discovered sending a high/low/high signal to the reset pin fixed everything.

The proof of concept code is available as the subfmproxy git repository. Did you know that in Go it takes a single line to initialize a reverse proxy: httputil.NewSingleHostReverseProxy(target)?

To summarize, the whole setup involves:

  • Navidrome running on our local NAS;
  • ... which is connected to Substreamer on my phone;
  • ... but not really since it's forwarded by my reverse proxy on the Rock Pi via Wi-Fi;
  • ... which plays the stream locally via the embedded audio chip before passing it on1;
  • ... which gets piped into the Adafruit FM transmitter connected to the Rock Pi;
  • ... which converts it to an FM signal on a certain bandwidth;
  • ... which any old radio is able to pick up;
  • ... which finally plays the music I originally selected on the phone.

The problem

William has been using a commercial FM transmitter for a few weeks as well before coming up with the DAC approach, so I was well aware of its shortcomings: potential interference with commercial senders' stronger signals and occasional drop-outs or cracklings. No problem for me, that's part of the retro radio charm.

But my man-in-the-middle Subsonic API spoofing approach isn't entirely waterproof either:

  1. There is no HTTP call when pressing the pause button. That means the radio keeps on playing.
  2. Some clients, like Sonixd, cache previous songs. Play song 1 (a /stream call), play song 2 (another call), go back to song 1, and suddenly, the Rock Pi server isn't hit: song 1 has already been downloaded and is simply replayed at client level. The Rock Pi will never know.
  3. Some clients, like Sonixd and Navidrome's more streamlined API, stream in data chunks. The Subsonic API doesn't explain this: a GET to /stream should return binary data---the whole re-encoded song---but in reality, depending on the client used, it's not easily playable after scraping off the response. Given enough spare time to reverse-engineer existing clients, I could probably fix this.

I guess this project will never be promoted beyond concept mode. Alternative versions I thought about, but ultimately rejected:

  1. I could simply play random songs we own or songs from a pre-set playlist and broadcast all day long, thereby creating our own FM station. But then, the Subsonic proxy would be pointless: simply scan all songs in a network share. Also, the Rock Pi 4C+ is passively cooled at the moment, but after streaming for a few hours, it gets quite hot. Do I want to have it powered on all day long?
  2. To fix the pause and chunking problem, I could write my own client, but that would require interaction with yet another app, not to mention a lot more effort.

At the moment, most of my music is played while working from home through a Pioneer box connected via the DELL U2421E Hub Monitor (best purchase ever), meaning I simply play music on my Mac that's connected to the screen via USB-C. I could also connect that stereo jack to the Adafruit transmitter and have an Arduino control it. The retro radio lives downstairs and is only occasionally used; I knew this was just an excuse to experiment and learn, but not to create something that would be used regularly.

At least I managed to learn how GPIO and I2C works in Linux, and I contributed to open source software.


  1. That's another problem: Substreamer doesn't know it needs to shut up, meaning the music is played via the Adafruit transmitter and directly on my phone. I "fixed" this by muting the phone... ↩︎