Skip to content

Commit

Permalink
Merge pull request #83 from balenalabs/multi-room-snapcast
Browse files Browse the repository at this point in the history
Multi room support
  • Loading branch information
tmigone authored Feb 12, 2020
2 parents 07b6aa3 + 3e3a65e commit eb3bddb
Show file tree
Hide file tree
Showing 28 changed files with 477 additions and 38 deletions.
17 changes: 17 additions & 0 deletions DeviceSupport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

# balenaSound device type support


| Device Type | Multi-room disabled (`DISABLE_MULTI_ROOM=1`) | Multi-room server | Multi-room client |
| ------------- | ------------- | ------------- | ------------- |
| Raspberry Pi (v1 / Zero / Zero W) ||[1] ||
| Raspberry Pi 2 || [2] | [2] |
| Raspberry Pi 3 [3] ||[4] ||
| Raspberry Pi 4 [3] ||||


**Notes**
- [1] Multi-room master server functionality is disabled by default on Raspberry Pi 1 family devices due to performance constraints.
- [2] Not tested. Feel free to share your results.
- [3] Currently balenaSound can not run on balenaOS 64 bit versions, please use 32 bit alernative. See this [issue](https://github.com/balenalabs/balena-sound/issues/82) for more informaton and an up to date status.
- [4] There is a [known issue](https://github.com/raspberrypi/linux/issues/1444) with all variants of the Raspberry Pi 3 where Bluetooth and WiFi interfere with each other. This will only impact the performance of balenaSound if you use a **Pi 3 as the master server to do multi-room bluetooth streaming**, resulting in stuttering audio (Airplay and Spotify Connect will work fine, as well as all streaming methods with multi-room disabled). In this cases we recommend the use of a Raspberry Pi 4 as the `master` server or a Pi 3 with a bluetooth dongle.
72 changes: 53 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,38 @@

# Bluetooth, Airplay and Spotify audio streaming for any audio device

**Starter project enabling you to add audio streaming via Bluetooth, Airplay or Spotify Connect to any old speakers or Hi-Fi using just a Raspberry Pi.**
**Starter project enabling you to add multi-room audio streaming via Bluetooth, Airplay or Spotify Connect to any old speakers or Hi-Fi using just a Raspberry Pi.**

This project has been tested on Raspberry Pi 3B/3B+ and Raspberry Pi Zero W. If you're using a Raspberry Pi 3 or above you don't need any additional hardware but if you'd like to use a Pi Zero W this will require an additional HAT as this model has no audio output.
**Features**
- **Bluetooth, Airplay and Spotify Connect support**: Stream audio from your favourite music services or directly from your smartphone/computer using bluetooth.
- **Multi-room synchronous playing**: Play perfectly synchronized audio on multiple devices all over your place.

## Hardware required
### Hardware required

* Raspberry Pi 3A+/3B/3B+/Zero W
* Raspberry Pi 3A+/3B/3B+/4B/Zero W
* SD Card (we recommend 8GB Sandisk Extreme Pro)
* Power supply
* 3.5mm audio cable to the input on your speakers/Hi-Fi (usually 3.5mm or RCA)
* 3.5mm audio cable to the input on your speakers/Hi-Fi (usually 3.5mm or RCA). Alternatively you can use the HDMI port to get digital audio out.

**Note:** the Raspberry Pi Zero cannot be used on it's own as it has no audio output. To use the Pi Zero you'll need to add something like the [Pimoroni pHAT DAC](https://shop.pimoroni.com/products/phat-dac) to go with it.
**Notes**
- The Raspberry Pi Zero cannot be used on it's own as it has no audio output. To use the Pi Zero you'll need to add something like the [Pimoroni pHAT DAC](https://shop.pimoroni.com/products/phat-dac) to go with it.
- For an extended list of device types supported please check this [link](DeviceSupport.md).

## Software required
### Software required

* A download of this project (of course)
* Software to flash an SD card ([balenaEtcher](https://balena.io/etcher))
* A free [balenaCloud](https://balena.io/cloud) account
* The [balena CLI tools](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md)

## Setup and use
# Setup and use

To run this project is as simple as deploying it to a balenaCloud application; no additional configuration is required (unless you're using a DAC HAT).
Running this project is as simple as deploying it to a balenaCloud application; no additional configuration is required (unless you're using a DAC HAT).

### Setup the Raspberry Pi

* Sign up for or login to the [balenaCloud dashboard](https://dashboard.balena-cloud.com)
* Create an application, selecting the correct device type for your Raspberry Pi
* Create an application, selecting the correct device type for your Raspberry Pi (we recommend setting the type as Raspberry Pi 1/Zero as your application will then be compatible with the Pi 1/Zero as well as all devices that were released afterward).
* Add a device to the application, enabling you to download the OS
* Flash the downloaded OS to your SD card with [balenaEtcher](https://balena.io/etcher)
* Power up the Pi and check it's online in the dashboard
Expand All @@ -40,14 +44,32 @@ To run this project is as simple as deploying it to a balenaCloud application; n
* Login with `balena login`
* Download this project and from the project directory run `balena push <appName>` where `<appName>` is the name you gave your balenaCloud application in the first step.

### Customize device name
# Usage

By default, your device will be named `balenaSound bluetooth/airplay/spotify xxxx`. This name will show within Airplay device lists, for Spotify Connect and when searching for devices using Bluetooth.
You can change this using `BLUETOOTH_DEVICE_NAME` environment variable that can be set in balena dashboard
(navigate to dashboard -> app -> device -> device variables). This environment variable sets the name for all 3 services.
After the application has pushed and the device has downloaded the latest changes you're ready to go! Before starting, connect the audio output of your Pi to the AUX input on your Hi-Fi or speakers. You can also use the HDMI port for digital audio output.

Connect to your balenaSound device:
* If using Bluetooth: search for your device on your phone or laptop and pair.
* If using Airplay: select the balenaSound device from your audio output options.
* If using Spotify Connect: open Spotify and choose the balenaSound device as an alternate output.
* The `balenaSound xxxx` name is used by default, where `xxxx` will be the first 4 characters of the device ID in the balenaCloud dashboard.

If you are running in multi-room mode, when you start streaming the device you're connected to will configure itself as the `master` and will broadcast a message to all other devices within your balenaCloud application to get them in sync. **Note:** that it can take a few seconds for the system to autoconfigure the first time you stream.

Let the music play!

# Customize your balenaSound experience

You can configure some features of balenaSound by using environment variables. This can be set in the balena dashboard: navigate to dashboard -> your app -> Environment variables. Read more about environment variables [here](https://www.balena.io/docs/learn/manage/serv-vars/#fleet-environment-and-service-variables).

![Setting the device name](images/device-name-config.png)


### Change device name

By default, your device will be named `balenaSound xxxx`. This name will show within Airplay device lists, for Spotify Connect and when searching for devices using Bluetooth.
You can change this using `BLUETOOTH_DEVICE_NAME` environment variable that can be set in balena dashboard.

### Set output volumes

By default, balenaSound will set the output volume of your Raspberry Pi to 75% on the basis you can then control the volume upto the maximum from the connected bluetooth device. If you would like to override this, define the `SYSTEM_OUTPUT_VOLUME` environment variable.
Expand All @@ -56,21 +78,28 @@ Secondly, balenaSound will play connection/disconnection notification sounds at

**Note:** these variables should be defined as integer values without the `%` symbol.

### Set bluetooth PIN code (optional)
### Multi-room

By default, balenaSound will start in multi-room mode. When running multi-room you can stream audio into a fleet of devices and have it play perfectly synchronized. It does not matter wether you have 2 or 100 devices, you only need them to be part of the same local network.

If you don't want to use multi-room or you only have one device, you can disable it by creating the `DISABLE_MULTI_ROOM` variable (with any value, for example: `1`).

**Note:** Multi-room requires a network router that supports IP multicast/broadcast (most modern routers do).

### Set bluetooth PIN code

By default, balenaSound bluetooth will connect using Secure Simple Pairing mode. If you would like to override this and use Legacy Mode with a PIN code you can do it by defining the `BLUETOOTH_PIN_CODE` environment variable. The PIN code must be numeric and up to six digits (1 - 999999).

**Note**: Legacy Mode is no longer allowed on [iOS](https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf) devices.

### Bluetooth Scripts (optional)
### Bluetooth scripts

balenaSound has configurable scripts you can run on connect and disconnect bluetooth events. If you would like to activate this, set the `BLUETOOTH_SCRIPTS` environment variable to `true`.
Sample scripts can be found on the `./bluetooth-audio/bluetooh-scripts/` directory, theses can be edited as needed.

### Spotify login (optional)
### Spotify Connect over the internet

balenaSound Spotify Connect works with only Spotify Premium accounts (due to the use of the [librespot](https://github.com/librespot-org/librespot) library).
To enable Spotify login you can add your username/e-mail and password, which are set with two environment variables: `SPOTIFY_LOGIN` and `SPOTIFY_PASSWORD`. **Note:** this is only required if you want to use Spotify Connect via the internet, the login is not required on local networks.
Spotify Connect only works with Spotify Premium accounts (due to the use of the [librespot](https://github.com/librespot-org/librespot) library).

### DAC Configuration

Expand All @@ -90,5 +119,10 @@ If you are using a DAC board, you will need to make a couple of changes to the d
* If using Airplay: select the balenaSound device from your audio output options.
* If using Spotify Connect: open Spotify and choose the balenaSound device as an alternate output.
* Let the music play!
If you have a Spotify Premium account you can stream locally without any configuration, but if you want to use Spotify Connect over the internet you will need to provide your Spotify credentials.

To enable Spotify login you can add your username/e-mail and password, which are set with two environment variables: `SPOTIFY_LOGIN` and `SPOTIFY_PASSWORD`.

---

This project is in active development so if you have any feature requests or issues please submit them here on GitHub. PRs are welcome, too.
2 changes: 1 addition & 1 deletion airplay/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM balenalib/%%BALENA_MACHINE_NAME%%:buster
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian:buster

ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket
RUN install_packages shairport-sync
Expand Down
10 changes: 9 additions & 1 deletion airplay/start.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#!/usr/bin/env bash

# Set the device broadcast name for AirPlay
if [[ -z "$BLUETOOTH_DEVICE_NAME" ]]; then
BLUETOOTH_DEVICE_NAME=$(printf "balenaSound Airplay %s" $(hostname | cut -c -4))
fi

exec shairport-sync -a "$BLUETOOTH_DEVICE_NAME" | printf "Device is discoverable as \"%s\"\n" "$BLUETOOTH_DEVICE_NAME"
# Use pipe output if multi room is enabled
# Don't pipe for Pi 1 family devices since snapcast-server is disabled by default
if [[ -z $DISABLE_MULTI_ROOM ]] && [[ $BALENA_DEVICE_TYPE != "raspberry-pi" ]]; then
SHAIRPORT_BACKEND="-o pipe -- /var/cache/snapcast/snapfifo"
fi

# Start AirPlay
exec shairport-sync -a "$BLUETOOTH_DEVICE_NAME" $SHAIRPORT_BACKEND | printf "Device is discoverable as \"%s\"\n" "$BLUETOOTH_DEVICE_NAME"
20 changes: 20 additions & 0 deletions bluetooth-audio/.asoundrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pcm.!default {
type plug
slave.pcm rate44100Hz
}

pcm.rate44100Hz {
type rate
slave {
pcm writeFile # Direct to the plugin which will write to a file
format S16_LE
rate 44100
}
}

pcm.writeFile {
type file
slave.pcm null
file "/var/cache/snapcast/snapfifo"
format "raw"
}
11 changes: 6 additions & 5 deletions bluetooth-audio/Dockerfile.aarch64
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
FROM balenalib/raspberrypi3:stretch
FROM balenalib/raspberrypi3-debian:stretch

ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

ENV UDEV=1

RUN install_packages \
alsa-utils \
bluealsa \
bluez \
python-gobject \
mplayer \
python-dbus \
python-gpiozero \
mplayer
python-gobject \
python-gpiozero

# Copy sounds
COPY sounds /usr/src/sounds
Expand All @@ -31,5 +30,7 @@ RUN chmod +x /usr/src/bluetooth-agent
COPY start.sh /usr/src/
RUN chmod +x /usr/src/start.sh

COPY .asoundrc /root/.asoundrc

CMD [ "/bin/bash", "/usr/src/start.sh" ]

11 changes: 6 additions & 5 deletions bluetooth-audio/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
FROM balenalib/%%BALENA_MACHINE_NAME%%:stretch
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian:stretch

ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

ENV UDEV=1

RUN install_packages \
alsa-utils \
bluealsa \
bluez \
python-gobject \
mplayer \
python-dbus \
python-gpiozero \
mplayer
python-gobject \
python-gpiozero

# Copy sounds
COPY sounds /usr/src/sounds
Expand All @@ -31,4 +30,6 @@ RUN chmod +x /usr/src/bluetooth-agent
COPY start.sh /usr/src/
RUN chmod +x /usr/src/start.sh

COPY .asoundrc /root/.asoundrc

CMD [ "/bin/bash", "/usr/src/start.sh" ]
7 changes: 7 additions & 0 deletions bluetooth-audio/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ printf "discoverable on\npairable on\nexit\n" | bluetoothctl > /dev/null
# Start bluetooth and audio agent
/usr/src/bluetooth-agent &

# If multi room is disabled remove audio redirect to fifo pipe
# Also remove if device is from Pi 1 family, since snapcast server is disabled by default
if [[ -n $DISABLE_MULTI_ROOM ]] || [[ $BALENA_DEVICE_TYPE == "raspberry-pi" ]]; then
rm /root/.asoundrc
fi

sleep 2
rm -rf /var/run/bluealsa/
/usr/bin/bluealsa -i hci0 -p a2dp-sink &

hciconfig hci1 down > /dev/null 2>&1 # Disable onboard bluetooth if using a bluetooth dongle (onboard interface gets remapped to hci1)
hciconfig hci0 up
hciconfig hci0 name "$BLUETOOTH_DEVICE_NAME"

Expand Down
39 changes: 37 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,63 @@
version: '2'
services:
bluetooth-audio:
network_mode: host
restart: always
build: ./bluetooth-audio
restart: always
network_mode: host
privileged: true
labels:
io.balena.features.dbus: 1
volumes:
- bluetoothcache:/var/cache/bluetooth
- snapcast:/var/cache/snapcast
airplay:
build: ./airplay
restart: always
network_mode: host
privileged: true
labels:
io.balena.features.dbus: 1
volumes:
- snapcast:/var/cache/snapcast
spotify:
build: ./spotify
restart: always
network_mode: host
privileged: true
volumes:
- spotifycache:/var/cache/raspotify
- snapcast:/var/cache/snapcast
fleet-supervisor:
build: ./fleet-supervisor
restart: on-failure
network_mode: host
privileged: true
ports:
- 3000:3000
labels:
io.balena.features.supervisor-api: 1
snapcast-server:
build:
context: ./snapcast-server
args:
SNAPCAST_VERSION: 0.17.1
restart: on-failure
ports:
- 1704:1704
- 1705:1705
volumes:
- snapcast:/var/cache/snapcast
snapcast-client:
build:
context: ./snapcast-client
args:
SNAPCAST_VERSION: 0.17.1
restart: on-failure
network_mode: host
privileged: true
volumes:
- snapcast:/var/cache/snapcast
volumes:
spotifycache:
bluetoothcache:
snapcast:
1 change: 1 addition & 0 deletions fleet-supervisor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
10 changes: 10 additions & 0 deletions fleet-supervisor/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node
WORKDIR /usr/src

COPY . .
RUN JOBS=MAX npm install --only=production

COPY start.sh /usr/src/
RUN chmod +x /usr/src/start.sh

CMD [ "/bin/bash", "/usr/src/start.sh" ]
14 changes: 14 additions & 0 deletions fleet-supervisor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "fleet-supervisor",
"version": "1.0.0",
"description": "Fleet controller for balenaSound",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "Tomás Migone <[email protected]",
"license": "Apache-2.0",
"dependencies": {
"cote": "^1.0.0"
}
}
Loading

0 comments on commit eb3bddb

Please sign in to comment.