fm streaming from subsonic to retro radios

This commit is contained in:
Wouter Groeneveld 2023-02-10 13:54:47 +01:00
parent 7d298b8428
commit dd9948badd
4 changed files with 94 additions and 0 deletions

View File

@ -0,0 +1,65 @@
---
title: Streaming Music From Subsonic To Retro Radios
date: 2023-02-10T11:54:00+01:00
tags:
- radio
- music
- navidrome
categories:
- 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](https://blog.yossarian.net/2022/11/07/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](/tags/navidrome) 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](https://github.com/jeffvli/sonixd) and [Substreamer](https://substreamerapp.com/). How to achieve that?
## The setup
After a week of fiddling, I came up with an ingenious---but ultimately suboptimal---solution. Observe:
![](../subfmproxy.jpg "The Subfmproxy setup.")
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](http://www.subsonic.org/pages/api.jsp#stream): 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](https://www.adafruit.com/product/1958) 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](https://wiki.radxa.com/Rock4/getting_started) 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](https://wiki.radxa.com/Rock4/hardware/gpio) 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](https://git.brainbaking.com/wgroeneveld/subfmproxy/). 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 on[^cellp];
- ... 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.
[^cellp]: 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...
## 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](/post/2021/02/my-retro-desktop-setup/)), 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](/works/oss-contributions/).

View File

@ -14,6 +14,7 @@ Most of these contributions are small and humble adjustments. For those interest
| Title | Year | Language |
|-------|------|----------|
| [Gobot](https://github.com/hybridgroup/gobot/), a Go framework for robotics, SBCs, and IoT | 2023 | <kbd>Go</kbd> |
| [Adafruit_Blinka](https://github.com/adafruit/Adafruit_Blinka), the CircuitPython API for MicroPython devices | 2023 | <kbd>Python</kbd> |
| [hltb-alfred-workflow](https://github.com/danbush/hltb-alfred-workflow), an Alfred workflow for the HLTB site | 2022 | <kbd>Ruby</kbd> |
| [Yarn Berry](https://github.com/yarnpkg/berry) documentation update | 2021 | <kbd>HTML</kbd> |

View File

@ -1,4 +1,32 @@
[
{
"author": {
"name": "Frank Meeuwsen",
"picture": "/pictures/diggingthedigital.com"
},
"name": "Een rivier met posts of liever een aangeharkt poedelbad?",
"content": "Ha! Nadat ik vanochtend al een bookmark en een favoriet hier postte, vroeg ik het me af tijdens het ontbijt: Zitten die paar lezers die ik hier heb, wel te wachten op die constante stroom van mijn korte brainfarts? Moet mijn blog een stroom zijn zoal...",
"published": "2023-02-10T07:00:11+00:00",
"url": "https://diggingthedigital.com/een-rivier-met-posts-of-liever-een-aangeharkt-poedelbad/",
"type": "mention",
"source": "https://diggingthedigital.com/een-rivier-met-posts-of-liever-een-aangeharkt-poedelbad/",
"target": "https://brainbaking.com/tags/metapost/",
"relativeTarget": "/tags/metapost/"
},
{
"author": {
"name": "Frank Meeuwsen",
"picture": "/pictures/diggingthedigital.com"
},
"name": "Een rivier met posts of liever een aangeharkt poedelbad?",
"content": "Ha! Nadat ik vanochtend al een bookmark en een favoriet hier postte, vroeg ik het me af tijdens het ontbijt: Zitten die paar lezers die ik hier heb, wel te wachten op die constante stroom van mijn korte brainfarts? Moet mijn blog een stroom zijn zoal...",
"published": "2023-02-10T07:00:09+00:00",
"url": "https://diggingthedigital.com/een-rivier-met-posts-of-liever-een-aangeharkt-poedelbad/",
"type": "mention",
"source": "https://diggingthedigital.com/een-rivier-met-posts-of-liever-een-aangeharkt-poedelbad/",
"target": "https://brainbaking.com/post/2023/02/january-2023/",
"relativeTarget": "/post/2023/02/january-2023/"
},
{
"author": {
"name": "Max",

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB