Introduction
This is the third part of an article describing how to set up whole-home audio using Mopidy and SnapCast. If you haven’t already read them should should probably read Part I and Part II first.
So far, we’ve installed and configured the SnapCast server, which will broadcast the music to the SnapCast clients. We’ve looked at how to set up a RasberryPi Zero (RPi0), install a Digital to Analog Converter (DAC) on it, install the SnapCast client service, and then configure the whole thing so that it can receive a stream from the SnapCast server and play it through the DAC.
What we need now is a way to pull an audio stream from the Internet, and transfer it over to our SnapCast server to broadcast internally. For this we’ll use an application called Mopidy.
Mopidy is a streaming client designed to be used on a “headless” server. It has a variety of plugins to provide a browser interface and to allow it to connect to diffent kinds of streaming services. It also has a REST API that we will be using to provide some simple automation for the whole-home audio.
In my house we are mostly streaming from SomaFM.com, which is an excellent service and has a variety of stations to choose from. Personally, we like the one called “Groove Salad”. That’s what I’ll be showing how to set up. However, if you are interested in streaming from any other service, this guide should get you going on that too.
Mopidy Server
Mopidy is pretty easy to install, but there are a few pain points along the way…
Start by using apt to install Mopidy. This seems to give the latest version:
$ sudo apt update
$ sudo apt install mopidy
You can check the version that you get:
$ mopidy --version
Mopidy 3.4.2
Which, at the time of writing, was the latest version, although from October 2023.
You can use apt to see all of the Mopidy extensions that are available that way:
$ sudo apt search mopidy
We are going to need a couple of extensions, otherwise we won’t be able to do much with Mopidy. The first we will install is the extension for SomaFM. If you are going to be using some other service for a source, you’ll need to install the extension for it. However, I would still recommend that you install the SomaFM add-on because then you’ll have something that you can test before you go trying your own thing.
$ sudo apt install mopidy-somafm
Next, we’ll need a web interface so that we can control everything. I prefer “Muse” because it integrates some of the controls for SnapCast as well.
Many of the extensions, like Muse, cannot be installed via apt. You’ll need to use a Python utility to do it, and the first thing you’ll need to do is install that utility. Here’s how you get Muse installed:
$ sudo apt install python3-pip
$ sudo python3 -m pip install Mopidy-Muse
$ sudo python3 -m pip install --break-system-packages Mopidy-Muse
$ sudo python3 -m pip install --break-system-packages Mopidy-Jellyfin
You can see that I’ve also installed the Jellyfin add-on as well.
That --break-system-packages option is going to generate some ugly warning messages. If you leave it out, you’ll get this:
$ sudo python3 -m pip install Mopidy-Muse
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.
If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.
See /usr/share/doc/python3.13/README.venv for more information.
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
I’m not a Python programmer, and I have absolutely no knowledge of or experience with “Python virtual environments”. When I first encountered this, I tried to figure it out. I really tried. In the end, I gave up and just went with the --break-system-packages, even though it made me feel uneasy. It hasn’t caused any problems, though.
The last bit of installation that you’ll need to do is to install some decoders for the streaming engine. Mopidy uses something called
GStreamer1.0 to do the work. You’ll already have GStreamer1.0 installed, but it will need some additional packages to make it work. There’s three of them; “Good”, “Bad”, and “Ugly”. I don’t know which ones you absolutely need, so I installed all of them:
$ sudo apt install gstreamer1.0-plugins-good
$ sudo apt install gstreamer1.0-plugins-bad
$ sudo apt install gstreamer1.0-plugins-ugly
That’s it for the installation. The last thing you need is the configuration file to make everything work. It’s located at /etc/mopidy/mopidy.conf, and when you are done it should look something like this:
# For information about configuration values that can be set in this file see:
#
# https://docs.mopidy.com/en/latest/config/
#
# Run `sudo mopidyctl config` to see the current effective config, based on
# both defaults and this configuration file.
[core]
restore_state = true
[http]
hostname=10.93.50.208
[audio]
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/var/local/snapserver/snapfifo
mixer_volume = 22
buffer_time = 4000
[somafm]
encoding = aac
quality = highest
[muse]
enabled = true
mopidy_host = 10.93.50.208
mopidy_port = 6680
mopidy_ssl = false
snapcast_host = 10.93.50.208
snapcast_port = 1780
snapcast_ssl = false
[jellyfin]
hostname = 192.168.1.211
username = mopidy
password = PYo78u.p%.MChCTMROE83MͣF
max_bitrate = 100
Obviously, you’ll have to change the IP addresses to match you’re own setup. The restore_state flag just tells Mopidy to resume doing whatever it was doing before it was restarted.
The output tag is every important. Make sure that the “filesink location” matches the fifo pipe that you created for SnapCast. If you followed my instructions, it should be /var/local/snapserver/snapfifo.
If you added any other extensions, you’ll probably have to add a section in this file for that extension. Generally, if you follow a link from the Mopidy extension listing, you’ll get to a page that has a GitHub link listed just above the section called “Installation”. Follow that link and the GitHub page should have a sample configuration block on it.
At this point, you can restart the Mopidy service to get it to load all of the extensions and read the configuration.
You should use systemctl status mopidy -n60 to make sure that it’s working properly. If you’ve muddled up the configuration, you should get some messages in that display that will tell you that it had problems.
Testing It All
I’d suggest opening up two browser tabs: one with the SnapCast page at port 1780, and the other at the Mopidy page at port 6680, then click into the “Muse” page.
On the SnapCast tab, click on the “Play” button. Then go to the Muse page, pick “Browse”, then select “SomaFM” which will expand the station list. Click on one of the stations, then “Play”. If all goes well, you should hear music through your browser.
Automation
In our house, we want the whole home audio streaming SomaFM’s “Groove Salad” from the time we wake up until we go to bed. When our current dog was just a puppy and we had her spending the night in a crate beside the bed, we needed some background noise so that she wouldn’t wake up and start whining whenever someone rolled over or made some small sound. So we started using a Google Home device to play “Forest Sounds” at night. Over time, we became used to this, and found it filters out street noises and other nighttime distractions.
With this new setup, I was looking for a way to have a completely automated, “hands-free”, solution that would turn on Groove Salad in the morning, and turn on some kind of forest sounds at night. Something that just worked, and that I didn’t have to even think about.
Mopidy has a REST API that you can use to control it remotely via HTML POST transactions. It’s not particularly well documented. At least, it looks like documentation was started, but never completed. However, there is enough there to figure out how to do some interesting and useful things.
In order to investigate how to use the API, on my workstation I downloaded an application called, “Bruno”, which is an open source alternative to “Postman”. This application allows you to create REST API calls that you can test and see the results in real time.
That’s what I did with Mopidy. I poked around trying various API calls to see what would happen, and to figure out the correct syntax and structure. Eventually, I had a number of tests written that would control the Mopidy server, and that would start and stop both GrooveSalad and my forest sounds MP3.
One of the nice things about Bruno is that you can export the tests as stand-alone Bash scripts (as well as code in a few programming languages). I copied them out and then put them in /usr/local/bin/mopidy on the Mopidy/Snapcast LXC. Let’s have a look at a couple of them:
$ cat clearTrackList
curl --request POST \
--url http://mopidy.mydomain:6680/mopidy/rpc \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "core.tracklist.clear"
}'
This script clears the tracklist out of Mopidy.
$ cat addGrooveSalad
curl --request POST \
--url http://mopidy.mydomain:6680/mopidy/rpc \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "core.tracklist.add",
"params": {
"uris": [
"somafm:channel:/groovesalad"
]
}
}'
This puts the GrooveSalad channel from SomaFM into the playlist.
$ cat startPlayback
curl --request POST \
--url http://mopidy.mydomain:6680/mopidy/rpc \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "core.playback.play"
}'
This script starts the playback.
$ cat startGrooveSalad
/usr/local/bin/mopidy/clearTrackList
/usr/local/bin/mopidy/addGrooveSalad
/usr/local/bin/mopidy/startPlayback
Finally, this one calls the other three back-to-back to start GrooveSalad playing.
How to run these? Well, cron of course! [Note: On a later project that I used ChatGPT to troubleshoot, it told me that cron is old-fashioned and that there is a more modern approach using systemd. You may want to pursue that, but I find that cron works just fine. What can I say? I’m a dinosaur.]
I’m not going to go into cron in depth, but for those of you who have never heard of it, cron is a service that will run whatever you want on a schedule that you set up. You can specify certain days of the week or of the month, certain minutes of the hour or particular hours that you want your action to run.
I needed two actions, one to start up GrooveSalad in the morning, and one to start up the forest sounds at night:
$ sudo crontab -l
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
30 7 * * * /usr/local/bin/mopidy/startGrooveSalad
0 21 * * * /usr/local/bin/mopidy/startNatureSounds
I’ve left in the default comments so that it will look like what you’ll see if you do this on your own system.
The easiest way to edit /etc/crontab is to just type sudo crontab -e and it’ll get you there. We need this to run as root, so sudo is used here. The command crontab -l just lists the crontab file. These two commands run every day, the first one at 7:30 AM, and the second at 9:00 PM.
Handling Stream Interuptions
The last issue that I had was due to connection issues from SomaFM which caused the stream to stop momentarily. This caused the Mopidy server to throw an error and stop:
Mar 31 15:24:31 snapcast mopidy[25889]: ERROR [MainThread] mopidy.audio.gst GStreamer error: Internal data stream error.
or:
Mar 30 07:30:18 snapcast mopidy[25889]: ERROR [MainThread] mopidy.audio.gst GStreamer error: Server does not support seeking.
I wish that Mopidy or its SomaFM plugin would be more robust to these disruptions, or have some self-recovery feature built in, but it does not. That meant it was up to me to figure out how to detect these stoppages and restart the stream.
The first thing I did was to look for something in the API that would tell me that the stream had stopped. I was hopeful that the “status” command would be the answer. Unfortunately, when I tested this I found that the status was still listed as “playing”, from which I assume that the error that stopped stream wasn’t causing it to go into some elegant shutdown that updated the status.
Luckily, I had decided to connect Mopidy to SnapCast via a named pipe. Perhaps if I checked the last update time of that pipe I could see if the stream had stopped?
This turned out easy to check. The pipe was at /var/local/snapfifo and a listing shows that it’s date and time is right now:
$ ll snapfifo
prw-rw-rw- 1 snapserver snapserver 0 Apr 3 14:54 snapfifo
If I stop the stream, then the date/time stops updating. So it looks like this is going to be an answer. I came up with this script:
$ cat checkPlaybackState
#!/bin/bash
if (((`date +%s` - `date +%s -r /var/local/snapfifo`) < "5"))
then
echo "`date` Still going" >> /var/local/playbackcheck
else
echo "`date` Not running" >> /var/local/playbackcheck
/usr/local/bin/mopidy/startGrooveSalad
fi
Eventually, I removed the echo commands, since they really aren’t needed. In my running version, I have a line that submits a message to my Gotify server in the event that it detects that the stream has stopped.
I need to run it throughout the day, which means more cron to the rescue:
*/3 8-20 * * * /usr/local/bin/mopidy/checkPlaybackState
The */3 means every 3 minutes, and the 8-20 means between 8:00 AM and 8:00 PM. These hours are inclusive, so the last run will be at 20:57, or 8:57 PM. Then, at 9:00 PM the forest sounds start and that is entirely local, so there’s no need to check for stream issues.
Conclusion
Success in these kinds of projects is ususally evaluated by how happy my wife is with how it works. She was getting quite fed up with the Google issues that we were experiencing, and frustrated with the constant drops in the service and times when it simply wouldn’t start up again.
She considers this to be one the biggest successes of my self-hosting project so far! Hurray!
It’s a success because it just works, and it never forces its way into the foreground by making you fiddle with it to get it to work. It starts up the music in the morning and the forest sounds at night and you don’t have to even think about it. It’s just there.
To be sure, there were some hiccoughs to start with…
The Raspberry Pi Zero has a woefully weak WiFi tranceiver in it. This caused grief until I sorted out procurement and placement of WiFi access points around the house. Symptoms of this problem were sporadic pauses in the audio of up to 2 seconds.
The SnapCast system seems to be dependent on stable connectivity between the server and the clients. At first, I had one of my WiFi access points connected to the wired network through a powerline AV connector. These are notoriously quirky, and can suffer interruptions if someone runs a heavy appliance like a clothes washer or dryer in the house. Once again, the symptoms were sporadic pauses in the audio.
When I shifted AP connectivity over to MoCa (Ethernet over co-ax), all of the issues with sporadic audio pauses largely vanished.
Now it just runs. If something goes wrong, it fixes itself. Most of the time, the only reason I’m even aware that it had a problem is because I get a Gotify alert for it. But even this only happens rarely.