506 lines
20 KiB
Markdown
506 lines
20 KiB
Markdown
[//]: # (title: Build your multi-room and multi-provider sound server with Platypush, Mopidy and Snapcast)
|
||
[//]: # (description: How to leverage Platypush and other open-source projects to build an extensible and versatile music server.)
|
||
[//]: # (image: /img/mpd-1.png)
|
||
[//]: # (published: 2019-10-22)
|
||
|
||
Today’s abundance of music streaming services has created lots of opportunities to listen to whichever music you like
|
||
wherever you like, but there’s a huge fragmentation problem that hasn’t been tackled seriously by the music tech
|
||
industry yet.
|
||
|
||
Spotify allows you to find and discover a lot of tunes, but not all the music is there.
|
||
|
||
You may want to integrate your collection of mp3s into your Spotify collection, but that is simply not an option.
|
||
|
||
You may have favourite tracks that are only available on SoundCloud or albums that aren’t available on Spotify, but
|
||
you’ve purchased in the past on Google Music or iTunes, but there’s no way to have them all in one place: each of these
|
||
solution comes with its separate app.
|
||
|
||
You may want to integrate your favourite online radios or podcasts into your music app, but that’s, again, not an option
|
||
— TuneIn, Podcast Addict or Google Podcasts are distinct apps.
|
||
|
||
You may want to easily stream your playlists to any speaker or pair of headphones you own, but that’s not as easy as it
|
||
sounds. You may have to replace your existing speakers with expensive solutions (like Sonos or Bose) to enjoy a proper
|
||
multi-room setup. Apps like Spotify come with their solutions (e.g. Spotify Connect), but only a limited number of
|
||
devices is supported and, again, it works as a solution only as long as you stream Spotify content from the Spotify app.
|
||
|
||
There have been commercial solutions that have tried to tackle this fragmentation problem and provide you with the
|
||
possibility to stream music from any app to any speaker without having to replace your audio system, but the situation
|
||
isn’t that bright after Google
|
||
has [discontinued its Chromecast Audio support](https://www.theverge.com/2019/1/11/18178751/google-chromecast-audio-discontinued-sale),
|
||
and AirPlay works relatively well only as long as you’re in an Apple ecosystem.
|
||
|
||
As of today the problem “how do I play whichever piece of music I like, from whichever service I like, on whichever
|
||
device I like, all in one interface, without having to install 10 different apps” is still largely unsolved if you rely
|
||
on commercial solutions.
|
||
|
||
Luckily, we’ve got plenty of open source software around that comes to rescue. It requires a bit more work than just
|
||
downloading an app and logging in, but the rewards are priceless.
|
||
|
||
## One music server to rule them all
|
||
|
||
[Mopidy](https://mopidy.com/) is one of the best open source solutions around when it comes to integrating multiple
|
||
music services under one single interface. It’s entirely written in Python, it’s (almost) 100% compatible
|
||
with [MPD](https://www.musicpd.org/doc/html/protocol.html), a music protocol that has been around since 2003 and comes
|
||
with lots of compatible clients (command-line, web-based, mobile apps etc.), and there are countless plugins that let
|
||
Mopidy integrate with any kind of music service around.
|
||
|
||
It’s relatively easy to install mopidy on a RaspberryPi and turn it into a powerful music centre.
|
||
|
||
- Add the mopidy repository to your apt lists and install the base package:
|
||
|
||
```shell
|
||
wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
|
||
|
||
# Run the following if you're running Raspbian/Debian Buster
|
||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list
|
||
|
||
# Run the following command if you're running Raspbian/Debian Stretch
|
||
sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/stretch.list
|
||
|
||
# Update the repositories and install mopidy
|
||
sudo apt-get update
|
||
sudo apt-get install mopidy mopidy-mpd
|
||
```
|
||
|
||
- Install any additional extension for the music services you like:
|
||
|
||
```shell
|
||
# Spotify support
|
||
sudo apt-get install mopidy-spotify
|
||
|
||
# Local files support
|
||
sudo apt-get install mopidy-local
|
||
|
||
# Dirble support
|
||
sudo apt-get install mopidy-dirble
|
||
|
||
# Podcast support
|
||
sudo apt-get install mopidy-podcast mopidy-podcast-gpodder mopidy-podcast-itunes
|
||
|
||
# Last.FM scrobbling support
|
||
sudo apt-get install mopidy-scrobbler
|
||
|
||
# Soma.FM support
|
||
sudo apt-get install mopidy-somafm
|
||
|
||
# Soundcloud support
|
||
sudo apt-get install mopidy-soundcloud
|
||
|
||
# TuneIn support
|
||
sudo apt-get install mopidy-tunein
|
||
|
||
# YouTube support
|
||
sudo apt-get install mopidy-youtube
|
||
```
|
||
|
||
And there are even more extensions available for Mopidy - you may want to take a look on
|
||
[their website](https://mopidy.com/ext/) to get an overview of the compatible services.
|
||
|
||
Head to the pages of those extensions to find out whether they need some extra dependencies or extra configuration to be
|
||
added to your `~/.config/mopidy/mopidy.conf` file.
|
||
|
||
You may also want to make sure that the HTTP module is enabled in your mopidy configuration, since most of the web
|
||
frontends (including Platypush) rely on it to interact with mopidy over websockets:
|
||
|
||
```yaml
|
||
[http]
|
||
enabled = true
|
||
hostname = 0.0.0.0
|
||
port = 6680
|
||
```
|
||
|
||
There is also a wide range of frontend clients available for Mopidy, from command-line MPD clients to full-blown web
|
||
clients that do a very good job replicating the UI of some of the most popular music apps around. Let's have a quick
|
||
overview of my favourite solutions to interact with Mopidy:
|
||
|
||
- `netcat`/`telnet`. Mopidy is compatible with the MPD protocol, and once started it will listen by default on port 6600
|
||
for MPD commands. It’s relatively straightforward to explore your library or control the playback even without
|
||
installing another client (although it’s probably not the most user-friendly way, but very good if you want to make
|
||
some scripts):
|
||
|
||
```shell
|
||
$ echo status | nc localhost 6600
|
||
OK MPD 0.19.0
|
||
volume: 100
|
||
repeat: 0
|
||
random: 1
|
||
single: 0
|
||
consume: 0
|
||
playlist: 3
|
||
playlistlength: 1489
|
||
xfade: 0
|
||
state: stop
|
||
song: 513
|
||
songid: 560
|
||
nextsong: 173
|
||
nextsongid: 220
|
||
OK
|
||
|
||
$ echo currentsong | nc localhost 6600
|
||
OK MPD 0.19.0
|
||
file: spotify:track:218UgZapIcNRP9f38C5cMp
|
||
Time: 365
|
||
Artist: 3rd Force
|
||
Album: Vital Force
|
||
Title: Echoes Of A Dream
|
||
Date: 1997
|
||
Track: 6
|
||
Pos: 513
|
||
Id: 560
|
||
AlbumArtist: 3rd Force
|
||
X-AlbumUri: spotify:album:3mSCVZabNB0rUmpYgPkDuV
|
||
OK
|
||
|
||
$ echo play | nc localhost 6600
|
||
OK MPD 0.19.0
|
||
OK
|
||
|
||
$ echo stop | nc localhost 6600
|
||
OK MPD 0.19.0
|
||
OK
|
||
```
|
||
|
||
- `mpc` is a tiny command-line utility that makes it a bit easier to interact with MPD/mopidy instances for scripting
|
||
purposes without handling low-level protocol messages:
|
||
|
||
```shell
|
||
sudo apt-get install mpc
|
||
mpc help # To see available commands
|
||
mpc play # Play the music
|
||
```
|
||
|
||
- `ncmpcpp` is a ncurses-based terminal client that I've been using for more than a decade, and it's probably one of
|
||
the lightest yet most versatile music clients I've seen around - and definitely my favourite:
|
||
|
||
```shell
|
||
sudo apt-get install ncmpcpp
|
||
```
|
||
|
||
![ncmpcpp screenshot 1](../img/ncmpcpp-1.png)
|
||
![ncmpcpp screenshot 2](../img/ncmpcpp-2.png)
|
||
![ncmpcpp screenshot 3](../img/ncmpcpp-3.png)
|
||
|
||
- `mopidy-iris` is probably one of the most user-friendly, well-maintained and feature-rich Mopidy clients around, and
|
||
it's compatible with desktop, tablet and mobile and it comes with a UI that successfully mimics that of many popular
|
||
music apps:
|
||
|
||
```shell
|
||
sudo apt-get install mopidy-iris
|
||
```
|
||
|
||
After installing it head to `http://your-raspberry:6680` and select Iris as the web interface.
|
||
|
||
![iris interface](../img/mopidy-iris-1.jpeg)
|
||
|
||
And you have many more compatible clients available (just check the list of extensions), from minimal, to feature-rich
|
||
(such as MusicBox), to a specific client for party mode optimized for multiple users! And, being compatible with MPD,
|
||
all the MPD clients out there should also work out of the box. And the list also includes a *Mopidy Mobile* app and
|
||
several MPD apps for Android and iOS.
|
||
|
||
## Hook Mopidy to Platypush
|
||
|
||
You can connect Platypush to Mopidy. That provides you with one more UI for interacting with your instance (embedded in
|
||
the Platypush web panel), and it opens a world of possibilities when it comes to automating music interactions.
|
||
|
||
- Install Platypush with the HTTP and MPD dependencies:
|
||
|
||
```shell
|
||
[sudo] pip install 'platypush[http,mpd]'
|
||
```
|
||
|
||
- Enable the MPD/mopidy plugin and backend in your platypush configuration file:
|
||
|
||
```yaml
|
||
music.mpd:
|
||
host: localhost
|
||
port: 6600
|
||
|
||
backend.music.mopidy:
|
||
host: localhost
|
||
```
|
||
|
||
A `backend.music.mpd` is also provided, but if you use Mopidy instead of a bare MPD server then it's advised to use
|
||
`backend.music.mopidy` instead - the former checks for updates by polling the server at regular intervals, while the
|
||
Mopidy-specific backend listens for events continuously over the provided websocket interface.
|
||
|
||
- Restart Platypush and head to `http://your-raspberry:8008`. You should see a new tab for Mopidy — yet another web
|
||
interface to interact with the server.
|
||
|
||
![Platypush MPD interface](../img/mpd-1.png)
|
||
|
||
Before proceeding on how to automate the interaction with your new music server, let's see how to turn Mopidy into a
|
||
full multi-room music server with Snapcast.
|
||
|
||
## Multi-room setup
|
||
|
||
The ability to synchronize music across multiple rooms and devices is a great feature of a modern smart home. However,
|
||
most of the commercial solutions available today (like Sonos or Bose) are expensive and require in most of the cases to
|
||
replace your speakers with theirs. Luckily it’s relatively easy to set up a multi-room experience with multiple
|
||
RaspberryPis, without having to change your speakers. Let’s see how.
|
||
|
||
- Install Snapcast by following the instructions on their [Github page](https://github.com/badaix/snapcast)
|
||
|
||
- Create an `/etc/default/snapserver` file on the machine(s) where you’re running your Mopidy instance(s) with the
|
||
following content:
|
||
|
||
```yaml
|
||
USER_OPTS="--user snapserver:snapserver --stream=pipe:///tmp/snapfifo?name=mopidy&codec=pcm --codec=pcm"
|
||
SNAPSERVER_OPTS=""
|
||
```
|
||
|
||
In the example above we’ll use a PCM lossless codec for streaming the music, and we’ll be using `/tmp/snapfifo` as a
|
||
file queue where Mopidy will push its audio stream.
|
||
|
||
- Start snapserver on your Mopidy machine(s) by simply running the executable, and optionally add it to your startup
|
||
configuration.
|
||
|
||
- Configure the [audio] section of your mopidy.conf file to stream to the Snapcast FIFO (note: with this configuration
|
||
Mopidy will only stream to the new file and not to your speakers, you'll need to run `snapclient` to play the audio):
|
||
|
||
```yaml
|
||
[audio]
|
||
mixer = software
|
||
mixer_volume = 100
|
||
output = audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo
|
||
```
|
||
|
||
The `audio.output` setting of Mopidy is actually a very flexible way of building GStreamer pipelines to redirect and
|
||
transform the audio however you like. In this example I'm transforming the audio to stereo WAV at 48 kHz, which may be
|
||
perfect if you're seeking for a true loseless audio experience, but may cause some glitches if your network isn't very
|
||
stable (we're basically passing uncompressed audio around). It's possible to encode and compress the stream by applying
|
||
e.g. an MP3 or OGG encoder to the pipeline, but this causes the GStreamer pipeline to become very unstable for some
|
||
reason (the bug has been opened for a couple of years and Mopidy developers are still scratching their head on why it
|
||
happens), so the loseless stream option may be the only one that works for now.
|
||
|
||
- Create an `/etc/default/snapclient` file on all the machines that will be connecting to your Snapserver, included the
|
||
Mopidy machine itself if you want to directly play music from too (opposed to using it just as a music backend):
|
||
|
||
```yaml
|
||
START_SNAPCLIENT=true
|
||
USER_OPTS="--user snapclient:audio"
|
||
```
|
||
|
||
- Start `snapclient` on the machines that will be connecting to your Mopidy instance. The command will
|
||
be `snapclient -h localhost` on the machine that runs mopidy itself and `snapclient -h remote-host-ip-or-name` on the
|
||
other machines. You can run as many `snapclient` instances on a host as the servers you want to connect it to.
|
||
|
||
- Enable the Snapcast backend and plugin in the Platypush configuration of each of the machines that will be running the
|
||
client or the server:
|
||
|
||
```yaml
|
||
backend.music.snapcast:
|
||
hosts:
|
||
- server1
|
||
- server2
|
||
- server3
|
||
|
||
music.snapcast:
|
||
host: default-server-ip-or-name
|
||
```
|
||
|
||
- Restart Platypush and head to the web panel on port 8008. You should see a new tab for Snapcast, identified by the
|
||
speaker icon. From here you can control which stream will be playing on which host, you can create streaming groups,
|
||
change the volume etc.
|
||
|
||
![Platypush Snapcast interface](../img/snapcast-1.png)
|
||
|
||
- You can also install an [Android app](https://play.google.com/store/apps/details?id=de.badaix.snapcast) to control
|
||
your multi-room setup, even though the app allows you to control one server at the time. The app however will allow
|
||
you to play audio streams also on your smartphone.
|
||
|
||
![Snapcast app interface](../img/snapcast-2.jpeg)
|
||
|
||
If you use Iris as a web interface to Mopidy you can now head to settings and enable the Snapcast plugin. A speaker icon
|
||
will appear in the bottom bar, and you’ll be able to control your music setup from there as well.
|
||
|
||
Time to enjoy your low-cost but powerful multi-room music setup!
|
||
|
||
## Build your remote to control the music
|
||
|
||
All of us have some unused infrared remote collecting dust somewhere in the living room. In this section I’ll show how
|
||
to turn it into a universal remote for controlling your music (and not only) with some Platypush automation. You’ll need
|
||
the following:
|
||
|
||
- An infrared receiver — they’re usually very cheap. Any of them will do, even though I personally
|
||
used [this model](https://www.banggood.in/Universal-IR-Infrared-Receiver-Head-With-Iron-Shell-TL1838-VS1838B-1838-38Khz-p-1204379.html?p=1L111111347088201706).
|
||
|
||
- An Arduino or Arduino-compatible device (or an ESP8266, or a RaspberryPi Pico, or any other microcontroller, although
|
||
the code may be different). Most of the infrared sensors around communicate over an analog interface, but
|
||
the RaspberryPi doesn’t come with an ADC converter. The solution is to plug an Arduino over USB and let it monitor for
|
||
changes on the detected infrared signal.
|
||
|
||
- A breadboard.
|
||
|
||
Once you’ve got all the hardware you can set up your receiver:
|
||
|
||
- Plug the infrared receiver to GND and Vcc, and the data PIN to e.g. the Arduino PIN 2, as shown in the figure below:
|
||
|
||
![Arduino IR sensor connection](../img/arduino-1.gif)
|
||
|
||
- Download and install the Arduino [IRremote library](https://github.com/z3t0/Arduino-IRremote).
|
||
|
||
- Prepare a sketch that reads the data from the infrared receiver PIN and writes it over serial interface as a JSON:
|
||
|
||
```c
|
||
#include <IRremote.h>
|
||
|
||
// When a signal with all bits set to 1 is received it
|
||
// usually means that the previous pressed key is still
|
||
// being pressed, until a signal with all bits set to
|
||
// zero is received.
|
||
#define IR_REPEAT 0xFFFFFFFF
|
||
|
||
const int RECV_PIN = 2;
|
||
|
||
IRrecv irrecv(RECV_PIN);
|
||
decode_results results;
|
||
unsigned int latest_value = 0;
|
||
|
||
void setup(){
|
||
Serial.begin(9600);
|
||
irrecv.enableIRIn();
|
||
irrecv.blink13(true);
|
||
}
|
||
|
||
void send_value(unsigned int value) {
|
||
Serial.print("{\"ir\":");
|
||
Serial.print(value, HEX);
|
||
Serial.println("}");
|
||
}
|
||
|
||
void loop(){
|
||
if (irrecv.decode(&results)){
|
||
if (results.value == IR_REPEAT && latest_value != 0) {
|
||
send_value(latest_value);
|
||
} else if (results.value && results.value != latest_value) {
|
||
send_value(results.value);
|
||
}
|
||
|
||
latest_value = results.value;
|
||
irrecv.resume();
|
||
}
|
||
}
|
||
```
|
||
|
||
- Compile the sketch and upload it to the Arduino.
|
||
|
||
- Open the Arduino serial monitor and verify that you see the JSON string when you press a key on the remote.
|
||
|
||
- Enable the serial plugin and backend in your platypush configuration:
|
||
|
||
```yaml
|
||
serial:
|
||
device: /dev/ttyUSB0
|
||
|
||
backend.sensor.serial:
|
||
enabled: True
|
||
```
|
||
|
||
- Restart platypush, check the output and press a key on your remote. You should see an event in the logs that looks
|
||
like this:
|
||
|
||
```
|
||
INFO|platypush|Received event: {"type": "event", "target": "hostname", "origin": "hostname", "args": {"type": "platypush.message.event.sensor.SensorDataChangeEvent", "data": {"ir": "4b34d827"}}}
|
||
```
|
||
|
||
- Take note of the hexadecimal code reported on the event, that’s the decoded data associated to that specific remote
|
||
button. Then add an event hook to deal with the actions to be run when a certain button is pressed:
|
||
|
||
```python
|
||
from platypush.config import Config
|
||
from platypush.event.hook import hook
|
||
from platypush.utils import run
|
||
|
||
from platypush.message.event.sensor import SensorDataChangeEvent
|
||
|
||
@hook(SensorDataChangeEvent)
|
||
def on_remote_key_press(event, **context):
|
||
ir_code = event.data.get('ir')
|
||
if not ir_code:
|
||
return
|
||
|
||
# Playback control logic
|
||
if ir_code == 'code1':
|
||
run('music.mpd.play')
|
||
elif ir_code == 'code2':
|
||
run('music.mpd.pause')
|
||
elif ir_code == 'code3':
|
||
run('music.mpd.stop')
|
||
elif ir_code == 'code5':
|
||
run('music.mpd.previous')
|
||
elif ir_code == 'code6':
|
||
run('music.mpd.next')
|
||
# ...
|
||
# Multi-room setup logic
|
||
elif ir_code == 'code7':
|
||
# Un-mute the stream to another host
|
||
run('music.snapcast.mute', host=Config.get('device_id'), client='some-client',
|
||
mute=False)
|
||
elif ir_code == 'code8':
|
||
# Mute the stream to another host
|
||
run('music.snapcast.mute', host=Config.get('device_id'), client='some-client',
|
||
mute=True)
|
||
```
|
||
|
||
Congratulations, you’ve just built your own customizable and universal music remote!
|
||
|
||
## Voice assistant integration
|
||
|
||
A smart music setup isn’t really complete without a voice assistant integration. I’ve covered
|
||
[in a previous article](https://blog.platypush.tech/article/Build-your-customizable-voice-assistant-with-Platypush) how
|
||
to set up platypush to turn your device into a full-featured Google Assistant. If you’ve managed to get your
|
||
assistant up and running, you can add some rules to control your music, play specific content, or synchronize your audio
|
||
stream to another room. Let’s see a couple of examples:
|
||
|
||
```python
|
||
from platypush.config import Config
|
||
from platypush.event.hook import hook
|
||
from platypush.utils import run
|
||
|
||
from platypush.message.event.assistant import SpeechRecognizedEvent
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='play (the)? music')
|
||
def on_music_play(*args, **context):
|
||
run('music.mpd.play')
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='stop (the)? music')
|
||
def on_music_pause(*args, **context):
|
||
run('music.mpd.stop')
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='play (the)? radio')
|
||
def on_play_radio(*args, **context):
|
||
run('music.mpd.play', resource='tunein:station:s13606')
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='play playlist ${name}')
|
||
def on_play_playlist(event, name=None, **context):
|
||
run('music.mpd.load', resource=name)
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
||
def search_and_play_song(event, title=None, artist=None, **context):
|
||
results = run('music.mpd.search', artist=artist, title=title)
|
||
if results > 0:
|
||
run('music.mpd.play', resource=results[0]['file'])
|
||
|
||
@hook(SpeechRecognizedEvent, phrase='play (the)? music to (the)? bedroom')
|
||
def sync_music_to_bedroom(event, **context):
|
||
run('music.snapcast.mute', host=Config.get('device_id'), client='bedroom', mute=False)
|
||
run('music.snapcast.volume', host=Config.get('device_id'), client='bedroom', volume=90)
|
||
```
|
||
|
||
## Conclusions
|
||
|
||
The current situation when it comes to music streaming and multi-room setup in a home automation environment is still
|
||
extremely fragmented. Each commercial solution out there seems more interested in building its own walled garden, and a
|
||
proper multi-room setup usually comes with high costs and in most of the cases it won’t be compatible with your existing
|
||
speakers. With the ingredients provided in this article you should be able to walk around most of these limitations and:
|
||
|
||
- Set up your multi-service music player controllable by any interface you like
|
||
|
||
- Set up your multi-room configuration that makes it possible to add a new room by simply adding one more RaspberryPi
|
||
|
||
- Use any existing infrared remote to control the music
|
||
|
||
- Integrate custom music actions into a voice assistant
|
||
|
||
With these foundations in place the only limit to what you can do with your new music set up comes from your own
|
||
imagination!
|