├── .gitignore ├── BUILDING.md ├── Makefile ├── README.md ├── config.json ├── controller └── multizone-control.py ├── debian └── nginx.override.conf ├── dietpi └── nginx.override.conf ├── doc └── demo-001.md ├── librespot-event.sh ├── mqtt.md ├── multizone-audio.svg ├── players.md ├── render.py ├── requirements.txt └── templates ├── debian └── mopidy@.service.template ├── dev ├── mopidy@.service.template └── snapserver.service.template ├── dietpi └── snapclient@.service.template ├── home-assistant.yaml.template ├── iris.template ├── librespot.template ├── mopidy.template ├── multizone-audio-control.service.template ├── shairport-sync.template ├── snapcast-autoconfig.yaml.template ├── snapclient.template ├── snapclient@.service.template └── snapserver.template /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | systemd/*.service 3 | snapcast-autoconfig.yaml 4 | ./*.conf 5 | home-assistant.*.yaml 6 | server.multizone-audio.json 7 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Build Notes for Components 2 | 3 | ## Debian Buster 4 | 5 | ### snapserver 6 | 7 | Instructions: https://github.com/badaix/snapcast/blob/master/doc/build.md#linux-native 8 | 9 | The Debian-packaged boost headers are too old. 10 | [Download recent boost headers](https://www.boost.org/users/download/) (~90MB) and extract them. 11 | 12 | ```bash 13 | sudo apt-get install libasound2-dev libpulse-dev libvorbisidec-dev libvorbis-dev libopus-dev libflac-dev libsoxr-dev alsa-utils libavahi-client-dev avahi-daemon libexpat1-dev 14 | sudo apt-get cmake ninja-build 15 | 16 | mkdir build && cd build 17 | # Pass the path to extracted boost headers. Don't build snapclient. 18 | cmake -G Ninja -DBUILD_CLIENT=OFF -DBOOST_ROOT=~/src/boost_1_79_0 ../ 19 | cmake --build . 20 | 21 | sudo cmake --build . --target install 22 | ``` 23 | 24 | ### shairport-sync 25 | 26 | Instructions: https://github.com/mikebrady/shairport-sync#building-and-installing 27 | 28 | The Debian-packaged version does not have MQTT support so we must build from source: 29 | 30 | ```bash 31 | sudo apt-get install build-essential git xmltoman autoconf automake libtool libpopt-dev libconfig-dev libasound2-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev libmosquitto-dev 32 | 33 | autoreconf -i -f 34 | # enable mqtt 35 | ./configure --sysconfdir=/etc --with-alsa --with-soxr --with-stdout --with-mqtt-client --with-metadata --with-avahi --with-ssl=openssl --with-pipe --with-systemd 36 | make 37 | 38 | sudo make install 39 | 40 | /usr/local/bin/shairport-sync --version 41 | ``` 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | config ?= config.json 2 | 3 | SHELL := /bin/bash # for curly-brace expansion 4 | HOME_ASSISTANT_CONFIG := ~/network/home-assistant/config 5 | DEV_SYSTEMD_CONFIG_DIR := ~/.config/systemd/user 6 | LIVE_SYSTEMD_CONFIG_DIR := /etc/systemd/system 7 | LIVE_NGINX_CONFIG_DIR := /etc/nginx/sites-available 8 | 9 | VENV := .venv 10 | SYSTEMCTL_USER ?= 11 | 12 | ALL_HOSTS := \ 13 | study \ 14 | kitchen \ 15 | library \ 16 | lounge \ 17 | ballroom \ 18 | ballroom-patio \ 19 | outside \ 20 | bedroom-mark \ 21 | 22 | ALL_LOGICAL := \ 23 | announcer \ 24 | everywhere \ 25 | 26 | ALL_ZONES := \ 27 | $(ALL_HOSTS) \ 28 | $(ALL_LOGICAL) \ 29 | 30 | EXP_SERVICES := {snapclient,mopidy} 31 | 32 | DEV_UNITS := \ 33 | $(DEV_SYSTEMD_CONFIG_DIR)/snapserver.service \ 34 | $(DEV_SYSTEMD_CONFIG_DIR)/snapclient@.service \ 35 | $(DEV_SYSTEMD_CONFIG_DIR)/mopidy@.service \ 36 | $(DEV_SYSTEMD_CONFIG_DIR)/multizone-audio-control.service \ 37 | 38 | DEBIAN_UNITS := \ 39 | $(LIVE_SYSTEMD_CONFIG_DIR)/mopidy@.service \ 40 | $(LIVE_SYSTEMD_CONFIG_DIR)/multizone-audio-control.service \ 41 | 42 | ALL_MOPIDY := $(patsubst %, mopidy.%.conf, $(ALL_ZONES)) 43 | ALL_AIRPLAY := $(patsubst %, shairport-sync.%.conf, $(ALL_HOSTS)) 44 | ALL_SNAPCLIENTS := $(patsubst %, snapclient.%.conf, $(ALL_HOSTS)) 45 | ALL_SPOTIFY := $(patsubst %, librespot.%.toml, $(ALL_ZONES)) 46 | ALL_NGINX := $(patsubst %, iris.%.conf, $(ALL_HOSTS)) 47 | ALL_HOME_ASSISTANT := $(patsubst %, home-assistant.%.yaml, $(ALL_ZONES)) 48 | ALL_HOME_ASSISTANT_INSTALL := \ 49 | $(patsubst %, $(HOME_ASSISTANT_CONFIG)/packages/%/media.yaml, $(ALL_HOSTS)) \ 50 | $(patsubst %, $(HOME_ASSISTANT_CONFIG)/packages/multizone-audio-%.yaml, $(ALL_LOGICAL)) 51 | 52 | ALL_CONFIGS := \ 53 | ../snapserver.conf \ 54 | snapcast-autoconfig.yaml \ 55 | $(ALL_MOPIDY) \ 56 | $(ALL_AIRPLAY) \ 57 | $(ALL_NGINX) \ 58 | $(ALL_HOME_ASSISTANT) \ 59 | $(ALL_SPOTIFY) \ 60 | $(ALL_SNAPCLIENTS) \ 61 | 62 | DEV_CONFIGS := \ 63 | dev/snapserver.service \ 64 | dev/snapclient@.service \ 65 | dev/mopidy@.service \ 66 | dev/multizone-audio-control.service \ 67 | 68 | LIVE_CONFIGS := \ 69 | debian/mopidy@.service \ 70 | debian/multizone-audio-control.service \ 71 | 72 | ALL_SERVICES := \ 73 | $(patsubst %, ${EXP_SERVICES}@%, $(ALL_HOSTS)) \ 74 | $(patsubst %, mopidy@%, $(ALL_LOGICAL)) 75 | 76 | all: $(ALL_CONFIGS) 77 | 78 | # Find chevron or create a venv and install it 79 | SYS_CHEVRON := $(shell which chevron) 80 | ifeq (, $(SYS_CHEVRON)) 81 | CHEVRON := $(VENV)/bin/chevron 82 | $(CHEVRON): venv 83 | else 84 | CHEVRON := $(SYS_CHEVRON) 85 | endif 86 | RENDER := render.py 87 | 88 | $(VENV): 89 | $(info "No chevron in $(PATH). Installing in $(VENV)") 90 | python3 -m venv $(VENV) 91 | $(VENV)/bin/pip install chevron 92 | 93 | venv: $(VENV) 94 | 95 | nginx: $(ALL_NGINX) 96 | 97 | home-assistant: $(ALL_HOME_ASSISTANT) 98 | 99 | $(HOME_ASSISTANT_CONFIG)/packages/%/media.yaml: home-assistant.%.yaml 100 | install -T $^ $@ 101 | 102 | $(HOME_ASSISTANT_CONFIG)/packages/multizone-audio-%.yaml: home-assistant.%.yaml 103 | install -T $^ $@ 104 | 105 | ha-install: $(ALL_HOME_ASSISTANT_INSTALL) 106 | 107 | dietpi/%.service: templates/dietpi/%.service.template $(config) $(RENDER) 108 | python $(RENDER) -d $(config) $< > $@ 109 | 110 | dev/%.service: templates/dev/%.service.template $(config) $(RENDER) 111 | -@mkdir -p dev 112 | python $(RENDER) -d $(config) $< > $@ 113 | 114 | dev/%.service: templates/%.service.template $(config) $(RENDER) 115 | -@mkdir -p dev 116 | python $(RENDER) -d $(config) $< > $@ 117 | 118 | debian/%.service: templates/debian/%.service.template $(config) $(RENDER) 119 | -@mkdir -p debian 120 | python $(RENDER) -d $(config) $< > $@ 121 | 122 | debian/%.service: templates/%.service.template $(config) $(RENDER) 123 | -@mkdir -p debian 124 | python $(RENDER) -d $(config) $< > $@ 125 | 126 | ../snapserver.conf: templates/snapserver.template $(config) $(RENDER) 127 | python $(RENDER) -d $(config) $< > $@ 128 | 129 | snapcast-autoconfig.yaml: templates/snapcast-autoconfig.yaml.template $(config) $(RENDER) 130 | python $(RENDER) -d $(config) $< > $@ 131 | 132 | mopidy.%.conf: $(config) templates/mopidy.template $(RENDER) 133 | python $(RENDER) -z $* -d $< templates/mopidy.template > $@ 134 | 135 | snapclient.%.conf: $(config) templates/snapclient.template $(RENDER) 136 | python $(RENDER) -z $* -d $< templates/snapclient.template > $@ 137 | 138 | shairport-sync.%.conf: $(config) templates/shairport-sync.template $(RENDER) 139 | python $(RENDER) -z $* -d $< templates/shairport-sync.template | grep -v '^//\|^$$' > $@ 140 | 141 | librespot.%.toml: $(config) templates/librespot.template $(RENDER) 142 | python $(RENDER) -z $* -d $< templates/librespot.template > $@ 143 | 144 | iris.%.conf: $(config) templates/iris.template $(RENDER) 145 | python $(RENDER) -z $* -d $< templates/iris.template > $@ 146 | 147 | home-assistant.%.yaml: $(config) templates/home-assistant.yaml.template 148 | python $(RENDER) -z $* -d $< templates/home-assistant.yaml.template > $@ 149 | 150 | 151 | snapserver: ../snapserver.conf 152 | systemctl $(SYSTEMCTL_USER) restart snapserver 153 | 154 | controller: controller/multizone-control.py 155 | systemctl $(SYSTEMCTL_USER) restart multizone-audio-control 156 | 157 | restart: $(ALL_CONFIGS) $(ALL_SNAPCLIENTS) 158 | systemctl $(SYSTEMCTL_USER) restart $(ALL_SERVICES) 159 | 160 | restart-host: 161 | systemctl $(SYSTEMCTL_USER) restart $(EXP_SERVICES)@$(HOST) 162 | 163 | start: restart snapserver controller 164 | 165 | start-host: restart-host 166 | 167 | status: 168 | systemctl $(SYSTEMCTL_USER) status $(ALL_SERVICES) 169 | 170 | status-host: 171 | systemctl $(SYSTEMCTL_USER) status $(EXP_SERVICES)@$(HOST) 172 | 173 | stop: $(ALL_CONFIGS) $(ALL_SNAPCLIENTS) 174 | systemctl $(SYSTEMCTL_USER) stop $(ALL_SERVICES) 175 | 176 | stop-host: $(ALL_CONFIGS) $(ALL_SNAPCLIENTS) 177 | systemctl $(SYSTEMCTL_USER) stop $(EXP_SERVICES)@$(HOST) 178 | 179 | # install the systemd unit files in the appropriate place 180 | 181 | $(DEV_SYSTEMD_CONFIG_DIR)/%.service: dev/%.service 182 | install -t $(DEV_SYSTEMD_CONFIG_DIR) $^ 183 | 184 | $(DEV_SYSTEMD_CONFIG_DIR)/%.service: controller/%.service 185 | install -t $(DEV_SYSTEMD_CONFIG_DIR) $^ 186 | 187 | $(LIVE_SYSTEMD_CONFIG_DIR)/%.service: debian/%.service 188 | install -t $(LIVE_SYSTEMD_CONFIG_DIR) $^ 189 | 190 | $(LIVE_SYSTEMD_CONFIG_DIR)/%.service: controller/%.service 191 | install -t $(LIVE_SYSTEMD_CONFIG_DIR) $^ 192 | 193 | dev: $(ALL_CONFIGS) $(DEV_CONFIGS) 194 | 195 | dev-install: dev $(DEV_UNITS) 196 | systemctl $(SYSTEMCTL_USER) daemon-reload 197 | 198 | debian: $(ALL_CONFIGS) $(LIVE_CONFIGS) 199 | 200 | live-install: debian $(DEBIAN_UNITS) 201 | systemctl $(SYSTEMCTL_USER) daemon-reload 202 | 203 | # Player install 204 | # 205 | debian-%-install: debian iris.%.conf debian/nginx.override.conf 206 | install -t $(LIVE_NGINX_CONFIG_DIR) iris.$*.conf 207 | install -T -D debian/nginx.override.conf $(LIVE_SYSTEMD_CONFIG_DIR)/nginx.service.d/override.conf 208 | 209 | dietpi-%-install: dietpi iris.%.conf dietpi/nginx.override.conf 210 | install -t $(LIVE_NGINX_CONFIG_DIR) iris.$*.conf 211 | install -T -D dietpi/nginx.override.conf $(LIVE_SYSTEMD_CONFIG_DIR)/nginx.service.d/override.conf 212 | 213 | clean: 214 | -rm $(ALL_CONFIGS) 215 | 216 | # Documentation 217 | %.html: %.md Makefile 218 | pandoc -d multizone-audio --self-contained -f gfm+attributes -t html5 -o $@ $< 219 | 220 | doc: doc/demo-001.html 221 | 222 | 223 | .PHONY: clean install status stop controller debian dev 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A **config generator** for a multizone audio system based on snapcast, Mopidy and MQTT. 2 | 3 | # Features 4 | 5 | - [ ] support for common audio streaming protocols 6 | - [x] SpotifyConnect 7 | - [x] track transitions (crossfade and automix) 8 | - [ ] player controls 9 | - [x] Airplay 10 | - [x] Mopidy/MPD 11 | - [x] Kodi 12 | - [ ] Bluetooth 13 | - [x] seamless switching between protocols in each zone 14 | - [x] high-priority announcement streams (alarms, doorbells) 15 | - [ ] invisible, automatic party reconfiguration 16 | - [x] no clashing audio streams 17 | - [ ] no snapcast controls visible to end-users(!) 18 | - [x] eliminate unnecessary software volume controls 19 | - [x] replaygain support 20 | 21 | # Architecture 22 | 23 | ![Multi Zone Architecture](multizone-audio.svg) 24 | 25 | Almost all media services run on a central media server. 26 | 27 | Each media player is its own zone and there are also multiple (logical) "party zones" which group the real zones together. 28 | For more on logical zones see the [snapcast-autoconfig README](https://github.com/ahayworth/snapcast-autoconfig) 29 | 30 | ## Media Server 31 | 32 | The media server runs: 33 | 34 | - one mopidy instance per zone (providing mpd + iris) 35 | - snapserver 36 | - mosquitto mqtt broker 37 | 38 | Snapserver is configured with: 39 | 40 | - _per-zone_ mopidy, shairport-sync and librespot streams 41 | - per-zone master [meta streams](https://github.com/badaix/snapcast/blob/master/doc/configuration.md#meta) 42 | 43 | The snapserver itself runs and manages the `librespot` and `shairport-sync` media services. 44 | 45 | snapcast-autoconfig manages the snapcast groups (including, for the benefit of Iris, naming them!). 46 | 47 | ### Hardware 48 | 49 | - USB audio capture device with S/PDIF input - [Startech ICUSBAUDIO7D ~£28 at Amazon UK](https://www.amazon.co.uk/dp/B002LM0U2S) 50 | 51 | ### Software 52 | 53 | See [BUILDING](BUILDING.md) for notes on building some of these from sources. 54 | 55 | - Debian 10+ "Buster" 56 | - [snapserver v0.27+](https://github.com/badaix/snapcast) 57 | - [librespot-java](https://github.com/librespot-org/librespot-java) 58 | - java 59 | - librespot-api-1.6.3.jar 60 | - default install dir is assumed to be `/opt/librespot` 61 | - [shairport-sync](https://github.com/mikebrady/shairport-sync) 62 | - 3.3.8-OpenSSL-Avahi-ALSA-stdout-pipe-soxr-metadata-mqtt-sysconfdir:/etc 63 | - build with mqtt 64 | - [Mopidy](https://github.com/mopidy/mopidy) 65 | - Mopidy 3.2.0 66 | - Mopidy-Iris 3.59.1 67 | - Mopidy-Local 3.2.1 68 | - Mopidy-MPD 3.2.0 69 | - Mopidy-MQTT-NG 1.0.0 70 | - Mopidy-Spotify 4.1.1 71 | - Mopidy-TuneIn 1.1.0 72 | - [mosquitto MQTT broker](https://mosquitto.org/) 73 | - [snapcast-autoconfig](https://github.com/ahayworth/snapcast-autoconfig) 74 | 75 | ## Media Players 76 | 77 | Each media player runs: 78 | 79 | - snapclient 80 | - nginx, proxying `http:///` to `http://:66xx/iris` 81 | - kodi, if-and-only-if the player has a screen or projector attached 82 | 83 | While snapcast meta streams are neat for auto-switching between audio services it's 84 | still confusing when, say, both a spotify and airplay stream attempt to play to 85 | the same zone. Instead, when one service starts playing we want the others (in the same zone) to pause. 86 | 87 | To accomplish this each media service (mopidy, librespot, ...) is configured to 88 | notify via MQTT when playback starts. 89 | 90 | See: 91 | 92 | - [mopidy-mqtt-ng](https://github.com/odiroot/mopidy-mqtt) 93 | - [shairport-sync MQTT support](https://github.com/mikebrady/shairport-sync/blob/master/MQTT.md) 94 | - `librespot --onevent librespot-event.sh...` 95 | - [kodi2mqtt](https://github.com/void-spark/kodi2mqtt) 96 | 97 | Each media service (except librespot) can also be _controlled_ via MQTT. 98 | 99 | The idea for this came from 100 | [hifiberry](https://github.com/hifiberry/audiocontrol2) 101 | which implements control of concurrent playback streams via 102 | [MPRIS](https://www.freedesktop.org/wiki/Specifications/mpris-spec/). 103 | 104 | ### Hardware 105 | 106 | - RPi1 / RPi2 / RPi3 107 | - Generic USB stereo sound card - [UGREEN USB external sound card](https://www.ugreen.com/products/usb-external-stereo-sound-card) 108 | 109 | ### Software 110 | 111 | - snapclient v0.25+ 112 | - nginx 113 | - dietpi 114 | - OSMC / Kodi v19+ 115 | - [kodi2mqtt](https://github.com/void-spark/kodi2mqtt) 116 | - install by manually extracting the zip in `~/.kodi/addons/` on the client 117 | - Kodi v20 "Nexus" requires [kodi2mqtt v0.22](https://github.com/void-spark/kodi2mqtt/releases/tag/v0.22) 118 | - Kodi v19 "Matrix" requires kodi2mqtt v0.21 (or use the [python2.7 patch by tspspi](https://github.com/tspspi/kodi2mqtt/commit/e7df9fa70284f0e905728c33c4b243bec92073e8)) 119 | 120 | ## Controller 121 | 122 | The pausing of media streams is done by a simple MQTT service - though it could 123 | be implemented as: 124 | 125 | - an extension to [hifiberry's audiocontrol2](https://github.com/hifiberry/audiocontrol2), 126 | - a [HomeAssistant automation](https://www.home-assistant.io/integrations/mqtt/). 127 | 128 | 129 | ## Volume Control 130 | 131 | For end-users only (per-service) soft-volume control is available. 132 | 133 | Hardware volume levels are preset by snapcast-autoconfig. 134 | 135 | Each snapclient is configured to [use an alsa hardware mixer](https://github.com/badaix/snapcast/commit/3ed76e20596b18baa14c04b3ec09c8f232f8e023) (if available). 136 | 137 | ### librespot and shairport-sync controlling a snapserver 138 | 139 | While `librespot` and `shairport-sync` can both be configured to control a 140 | hardware mixer the snapserver can't be, nor does it create a virtual mixer. 141 | 142 | # Usage 143 | 144 | ## Prerequisites 145 | 146 | These requirements are sufficient to customize the configurations for your setup: 147 | 148 | - GNU make 149 | - python3 150 | - [chevron](https://pypi.org/project/chevron/) 151 | 152 | `chevron` is a python implementation of the [mustache templating language](http://mustache.github.io/). 153 | 154 | On first run if `chevron` isn't found, `make` will create a python virtual environment and install it for you. 155 | 156 | The services are configured by generating systemd service files. Many of them are 157 | [template unit files](https://fedoramagazine.org/systemd-template-unit-files/) 158 | so that multiple instances can be started on a single host. (e.g. `systemctl start mopidy@study`) 159 | 160 | ## Customize 161 | 162 | Configurations for each zone (and for all zones) are generated by merging the 163 | `config.json` data into the template files using `chevron`. 164 | 165 | You will need to adapt my existing json config for your purposes. 166 | See also `ALL_HOSTS` in the [`Makefile`](Makefile). 167 | 168 | ## Generate 169 | 170 | | :warning: This will overwrite `../snapserver.conf` | 171 | | -------------------------------------------------- | 172 | 173 | ```bash 174 | make 175 | ``` 176 | 177 | ## Test 178 | 179 | All the services (except for `nginx`) can be run and tested locally as an unprivileged user. 180 | 181 | To install and run mopidy in a python venv you will need: 182 | 183 | - [libspotify-dev](https://mopidy.github.io/libspotify-archive/) 184 | 185 | ```bash 186 | sudo apt-get install libspotify-dev 187 | .venv/bin/pip install -r requirements.txt 188 | ``` 189 | 190 | Generate configs and start all services: 191 | 192 | ```bash 193 | make dev 194 | make dev-install # this will overwrite files in ~/.config/systemd/user! 195 | # set `SYSTEMCTL_USER := --user` in the Makefile 196 | make start 197 | ``` 198 | 199 | To start the services for just one zone: 200 | 201 | ```bash 202 | # study only 203 | make HOST=study start-host 204 | ``` 205 | 206 | ## Deploy 207 | 208 | ### Server 209 | 210 | Requirements: see [#software](#software). 211 | 212 | | :warning: This will overwrite `/etc/snapserver.conf` | 213 | | ---------------------------------------------------- | 214 | 215 | On your production system, as root: 216 | 217 | ```bash 218 | git clone https://github.com/markferry/multizone-audio.git /etc/multizone-audio 219 | cd /etc/multizone-audio 220 | git checkout $your_branch # your customizations 221 | make live-install 222 | ``` 223 | 224 | ### Clients 225 | 226 | For all client hosts: 227 | 228 | ```bash 229 | git clone https://github.com/markferry/multizone-audio.git /etc/multizone-audio 230 | cd /etc/multizone-audio 231 | git checkout $your_branch # your customizations 232 | ``` 233 | 234 | Then run the client-specific-install `make $os-$host-install` 235 | 236 | Where `$os` is one of: `debian`, `dietpi`. 237 | 238 | And `$host` is a host defined in `config.json`. 239 | 240 | e.g.: 241 | ```bash 242 | make dietpi-library-install 243 | ``` 244 | 245 | 246 | # Resources 247 | 248 | ## People to follow 249 | 250 | - [@badaix](https://github.com/badaix) - snapcast maintainer 251 | - [@kingosticks](https://github.com/kingosticks) - pimusicbox developer 252 | - [@frafall](https://github.com/frafall/) - snapcast metadata contributor and kodi snapcast service developer 253 | - [@ahayworth](https://github.com/ahayworth) - snapcast-autoconfig maintainer 254 | 255 | ## Projects 256 | 257 | - [skalavala multi-room audio](https://github.com/skalavala/Multi-Room-Audio-Centralized-Audio-for-Home) - multi-zone but single-stream 258 | - [spocon](https://github.com/spocon/spocon) - librespot-java packaged for Debian 259 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": { 3 | "config_root": "/etc/multizone-audio", 4 | "media_root": "/mnt/md6-media", 5 | "music_metadata": "music/metadata", 6 | "java": "/usr/bin/java" 7 | }, 8 | "mqtt": { 9 | "host": "pixie3" 10 | }, 11 | "python": "python3", 12 | "chromecast": { 13 | "device": "default:CARD=ICUSBAUDIO7D" 14 | }, 15 | "mopidy": { 16 | "host": "media", 17 | "path": "/var/lib/mopidy/bin/mopidy" 18 | }, 19 | "snapcast": { 20 | "host": "media", 21 | "snapweb_root": "/var/www/snapweb" 22 | }, 23 | "spotify": { 24 | "event": "librespot-event.sh", 25 | "jar": "/opt/librespot/librespot-api-1.6.3.jar" 26 | }, 27 | "hosts": [ 28 | { 29 | "name": "study", 30 | "Name": "Study", 31 | "airplay": { 32 | "port": "5106" 33 | }, 34 | "kodi": { 35 | "port": 8080 36 | }, 37 | "mopidy": { 38 | "http_port": "6686", 39 | "mpd_port": "6606" 40 | }, 41 | "spotify": { 42 | "api_port": "24806", 43 | "crossfade_ms": "0", 44 | "zeroconf": "39806" 45 | }, 46 | "snapcast": { 47 | "latency": 0, 48 | "opts": "--mixer=hardware", 49 | "volume": 76 50 | }, 51 | "announcer_streams": ["announcer"], 52 | "streams": ["spotify", "airplay", "iris"] 53 | }, 54 | { 55 | "name": "library", 56 | "Name": "Library", 57 | "airplay": { 58 | "port": "5107" 59 | }, 60 | "mopidy": { 61 | "http_port": "6687", 62 | "mpd_port": "6607" 63 | }, 64 | "snapcast": { 65 | "latency": 0, 66 | "opts": "-s Device --mixer=hardware:Speaker", 67 | "volume": 50 68 | }, 69 | "spotify": { 70 | "api_port": "24807", 71 | "crossfade_ms": "0", 72 | "zeroconf": "39807" 73 | }, 74 | "streams": ["spotify", "airplay", "iris"] 75 | }, 76 | { 77 | "name": "lounge", 78 | "Name": "Lounge", 79 | "airplay": { 80 | "port": "5108" 81 | }, 82 | "kodi": { 83 | "port": 8080 84 | }, 85 | "mopidy": { 86 | "host": "media", 87 | "http_port": "6688", 88 | "mpd_port": "6608" 89 | }, 90 | "snapcast": { 91 | "latency": 0, 92 | "opts": "-s iec958", 93 | "volume": 75 94 | }, 95 | "spotify": { 96 | "api_port": "24808", 97 | "crossfade_ms": "0", 98 | "zeroconf": "39808" 99 | }, 100 | "announcer_streams": ["announcer"], 101 | "streams": ["spotify", "airplay", "iris"] 102 | }, 103 | { 104 | "name": "ballroom", 105 | "Name": "Ballroom", 106 | "airplay": { 107 | "port": "5109" 108 | }, 109 | "kodi": { 110 | "port": 8080 111 | }, 112 | "mopidy": { 113 | "http_port": "6689", 114 | "mpd_port": "6609" 115 | }, 116 | "snapcast": { 117 | "latency": 0, 118 | "opts": "-s ALSA --mixer=hardware", 119 | "volume": 75 120 | }, 121 | "spotify": { 122 | "api_port": "24809", 123 | "crossfade_ms": "2000", 124 | "zeroconf": "39809" 125 | }, 126 | "announcer_streams": ["announcer"], 127 | "streams": ["spotify", "airplay", "iris"] 128 | }, 129 | { 130 | "name": "outside", 131 | "Name": "Outside", 132 | "airplay": { 133 | "port": "5110" 134 | }, 135 | "mopidy": { 136 | "http_port": "6690", 137 | "mpd_port": "6610" 138 | }, 139 | "snapcast": { 140 | "latency": 30, 141 | "opts": "-s b1 --mixer=hardware:HDMI", 142 | "volume": 80 143 | }, 144 | "spotify": { 145 | "api_port": "24810", 146 | "crossfade_ms": "2000", 147 | "zeroconf": "39810" 148 | }, 149 | "announcer_streams": ["announcer"], 150 | "streams": ["spotify", "airplay", "iris"] 151 | }, 152 | { 153 | "name": "kitchen", 154 | "Name": "Kitchen", 155 | "airplay": { 156 | "port": "5111" 157 | }, 158 | "mopidy": { 159 | "http_port": "6691", 160 | "mpd_port": "6611" 161 | }, 162 | "snapcast": { 163 | "latency": 0, 164 | "opts": "-s Device --mixer=hardware:Speaker", 165 | "volume": 65 166 | }, 167 | "spotify": { 168 | "api_port": "24811", 169 | "crossfade_ms": "0", 170 | "zeroconf": "39811" 171 | }, 172 | "streams": ["spotify", "airplay", "iris"], 173 | "other_streams": ["Barn Chromecast"] 174 | }, 175 | { 176 | "name": "bedroom-mark", 177 | "name_": "bedroom_mark", 178 | "Name": "Bedroom Mark", 179 | "airplay": { 180 | "port": "5112" 181 | }, 182 | "mopidy": { 183 | "http_port": "6692", 184 | "mpd_port": "6612" 185 | }, 186 | "snapcast": { 187 | "latency": 0, 188 | "opts": "-s Device --mixer=hardware:Speaker", 189 | "volume": 80 190 | }, 191 | "spotify": { 192 | "api_port": "24812", 193 | "crossfade_ms": "0", 194 | "zeroconf": "39812" 195 | }, 196 | "streams": ["spotify", "airplay", "iris"] 197 | }, 198 | { 199 | "name": "ballroom-patio", 200 | "name_": "ballroom_patio", 201 | "Name": "Ballroom Patio", 202 | "airplay": { 203 | "port": "5113" 204 | }, 205 | "mopidy": { 206 | "http_port": "6693", 207 | "mpd_port": "6613" 208 | }, 209 | "snapcast": { 210 | "latency": -50, 211 | "opts": "-s Device --mixer=hardware:Speaker", 212 | "volume": 90 213 | }, 214 | "spotify": { 215 | "api_port": "24813", 216 | "crossfade_ms": "0", 217 | "zeroconf": "39813" 218 | }, 219 | "announcer_streams": ["announcer"], 220 | "streams": ["spotify", "airplay", "iris"] 221 | } 222 | ], 223 | "announcers": [ 224 | { 225 | "name": "announcer", 226 | "Name": "Announcer", 227 | "mopidy": { 228 | "http_port": "6680", 229 | "mpd_port": "6600", 230 | "mpd_only": true 231 | }, 232 | "snapcast": { 233 | "latency": 0, 234 | "opts": "" 235 | }, 236 | "dev": false 237 | } 238 | ], 239 | "party-zones": [ 240 | { 241 | "name": "everywhere", 242 | "Name": "Everywhere", 243 | "airplay": { 244 | "port": "5101" 245 | }, 246 | "mopidy": { 247 | "http_port": "6681", 248 | "mpd_port": "6601" 249 | }, 250 | "snapcast": { 251 | "latency": 0, 252 | "opts": "" 253 | }, 254 | "spotify": { 255 | "api_port": "24801", 256 | "crossfade_ms": "2000", 257 | "zeroconf": "39801" 258 | }, 259 | "streams": ["spotify", "airplay", "iris"], 260 | "clients": [ 261 | "study", 262 | "kitchen", 263 | "lounge", 264 | "library", 265 | "ballroom", 266 | "ballroom-patio", 267 | "outside", 268 | "bedroom-mark" 269 | ] 270 | } 271 | ] 272 | } 273 | -------------------------------------------------------------------------------- /controller/multizone-control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2021 Mark Ferry 5 | # 6 | # Derived from a paho example. 7 | # Copyright (c) 2014 Roger Light 8 | # 9 | # All rights reserved. This program and the accompanying materials 10 | # are made available under the terms of the Eclipse Distribution License v1.0 11 | # which accompanies this distribution. 12 | # 13 | # The Eclipse Distribution License is available at 14 | # http://www.eclipse.org/org/documents/edl-v10.php. 15 | # 16 | # Contributors: 17 | # Roger Light - initial implementation 18 | # All rights reserved. 19 | 20 | from collections import namedtuple 21 | 22 | import json 23 | 24 | import paho.mqtt.client as mqtt 25 | import paho.mqtt.publish as publish 26 | 27 | Message = namedtuple("Message", "topic payload") 28 | 29 | MQTT_HOST = "pixie3" 30 | TOPIC_ROOT = "media" 31 | 32 | def parse_zone(topic): 33 | return topic.split('/')[1] 34 | 35 | def on_mopidy_status(mosq, obj, msg): 36 | print("mopidy: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) 37 | if msg.payload == b'playing': 38 | zone = parse_zone(msg.topic) 39 | msgs = [ 40 | Message(f"{TOPIC_ROOT}/{zone}/airplay/remote", "pause"), 41 | Message(f"{TOPIC_ROOT}/{zone}/spotify/control", "pause"), 42 | Message(f"{TOPIC_ROOT}/{zone}/kodi/command/playbackstate", "pause"), 43 | ] 44 | for m in msgs: 45 | mosq.publish(m.topic, m.payload) 46 | 47 | 48 | def on_spotify_status(mosq, obj, msg): 49 | print("spotify: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) 50 | zone = parse_zone(msg.topic) 51 | msgs = [ 52 | Message(f"{TOPIC_ROOT}/{zone}/mopidy/c/plb", "pause"), 53 | Message(f"{TOPIC_ROOT}/{zone}/airplay/remote", "pause"), 54 | Message(f"{TOPIC_ROOT}/{zone}/kodi/command/playbackstate", "pause"), 55 | ] 56 | for m in msgs: 57 | mosq.publish(m.topic, m.payload) 58 | 59 | def on_airplay_status(mosq, obj, msg): 60 | print("airplay: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) 61 | if msg.payload == b'playing': 62 | zone = parse_zone(msg.topic) 63 | msgs = [ 64 | Message(f"{TOPIC_ROOT}/{zone}/mopidy/c/plb", "pause"), 65 | Message(f"{TOPIC_ROOT}/{zone}/spotify/control", "pause"), 66 | Message(f"{TOPIC_ROOT}/{zone}/kodi/command/playbackstate", "pause"), 67 | ] 68 | for m in msgs: 69 | mosq.publish(m.topic, m.payload) 70 | 71 | def on_kodi_play(mosq, obj, msg): 72 | print("kodi: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) 73 | zone = parse_zone(msg.topic) 74 | msgs = [ 75 | Message(f"{TOPIC_ROOT}/{zone}/mopidy/c/plb", "pause"), 76 | Message(f"{TOPIC_ROOT}/{zone}/spotify/control", "pause"), 77 | Message(f"{TOPIC_ROOT}/{zone}/airplay/remote", "pause"), 78 | ] 79 | for m in msgs: 80 | mosq.publish(m.topic, m.payload) 81 | 82 | def on_message(mosq, obj, msg): 83 | print(msg.topic) 84 | 85 | 86 | mqttc = mqtt.Client() 87 | 88 | mqttc.message_callback_add(f"{TOPIC_ROOT}/+/mopidy/i/sta", on_mopidy_status) 89 | mqttc.message_callback_add(f"{TOPIC_ROOT}/+/spotify/status", on_spotify_status) 90 | mqttc.message_callback_add(f"{TOPIC_ROOT}/+/airplay/status", on_airplay_status) 91 | mqttc.message_callback_add(f"{TOPIC_ROOT}/+/kodi/status/notification/Player.OnPlay", on_kodi_play) 92 | mqttc.message_callback_add(f"{TOPIC_ROOT}/+/kodi/status/notification/Player.OnResume", on_kodi_play) 93 | mqttc.on_message = on_message 94 | mqttc.connect(MQTT_HOST, 1883, 60) 95 | mqttc.subscribe(f"{TOPIC_ROOT}/#", 0) 96 | 97 | mqttc.loop_forever() 98 | -------------------------------------------------------------------------------- /debian/nginx.override.conf: -------------------------------------------------------------------------------- 1 | # Set nginx to autorestart. Useful if upstream is temporarily unavailable. 2 | [Unit] 3 | After=network-online.target nss-lookup.target 4 | 5 | [Service] 6 | Restart=always 7 | -------------------------------------------------------------------------------- /dietpi/nginx.override.conf: -------------------------------------------------------------------------------- 1 | # Set nginx to autorestart. Useful if upstream is temporarily unavailable. 2 | [Service] 3 | Restart=always 4 | -------------------------------------------------------------------------------- /doc/demo-001.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multizone Audio Demo 3 | subtitle: Some topics to cover when introducing users to the multizone-audio system. 4 | --- 5 | 6 | # Apps 7 | 1. Spotify 8 | 2. Airplay 9 | 3. Web (Iris) 10 | 4. MPD / Mopidy 11 | 5. Show that Iris/MPD/Mopidy are the same but that the others are separate 12 | 13 | # Basic playback 14 | 1. Play a track from Spotify to a mono-zone 15 | 2. Play a track from Spotify to a multi-zone 16 | 3. Do the same for Airplay 17 | 18 | # Stream Priority 19 | spotify > airplay > iris 20 | 21 | 1. Play a track from Iris, show the Spotify track taking priority 22 | 23 | Consider: You may not be the only one playing 24 | 25 | # Multi-zone 26 | 1. Play Spotify to a multi-zone stream, show zones reconfigured 27 | 2. Play a track to mono-zone, show that it takes precedence 28 | 3. Stop the mono-zone track, show that multi-zone takes over again 29 | 30 | # Volume control 31 | 1. Demonstrate the independence of each service's volumes 32 | 33 | Consider: Volume is controlled by your app → full volume on your app means *full volume* 34 | 35 | ## What about Snapcast? 36 | Snapcast is not for end-users, it is only for setting balance between zones. 37 | 38 | This is reinforced by `snapcast-autoconfig` 39 | 40 | 41 | -------------------------------------------------------------------------------- /librespot-event.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mosquitto_pub -h pixie3 -t media/$1/spotify/status -m "$PLAYER_EVENT" 3 | -------------------------------------------------------------------------------- /mqtt.md: -------------------------------------------------------------------------------- 1 | --- 2 | MQTT notes 3 | --- 4 | 5 | # Mopidy 6 | ## State 7 | ``` 8 | media/canard/mopidy/i/sta 9 | ``` 10 | 11 | ## Control 12 | ``` 13 | media/canard/mopidy/c/plb pause 14 | ``` 15 | 16 | ``` 17 | | Kind | Subtopic | Values | 18 | |:----------------:|:--------:|:-----------------------------------------------------------------:| 19 | | Playback control | `/plb` | `play` / `stop` / `pause` / `resume` / `toggle` / `prev` / `next` | 20 | | Volume control | `/vol` | `=` or `-` or `+` | 21 | | Add to queue | `/add` | `` | 22 | | Load playlist | `/loa` | `` | 23 | | Clear queue | `/clr` | ` ` | 24 | | Search tracks | `/src` | `` | 25 | | Request info | `/inf` | `state` / `volume` / `queue` | 26 | 27 | ``` 28 | 29 | # Librespot 30 | ## State 31 | ``` 32 | media/canard/spotify started 33 | media/canard/spotify volume_set 34 | media/canard/spotify playing 35 | media/canard/spotify paused 36 | ``` 37 | 38 | ## Control 39 | 40 | # shairport-sync 41 | 42 | ## State 43 | 44 | ## Control 45 | ``` 46 | media/kitchen/airplay/remote pause 47 | ``` 48 | 49 | commands: 50 | 51 | ``` 52 | command, beginff, beginrew, mutetoggle, nextitem, previtem, pause, 53 | playpause, play, stop, playresume, shuffle_songs, volumedown, volumeup 54 | ``` 55 | 56 | # Kodi 57 | 58 | ## State 59 | ``` 60 | media/study/kodi/status/notification/Player.OnPlay {"val": "{\"item\":{\"id\":14996,\"type\":\"episode\"},\"player\":{\"playerid\":1,\"speed\":1}}"} 61 | media/study/kodi/status/notification/Player.OnStop {"val": "{\"end\":true,\"item\":{\"id\":14996,\"type\":\"episode\"}}"} 62 | 63 | ``` 64 | 65 | ## Control 66 | ``` 67 | media/command/playbackstate "pause" 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /players.md: -------------------------------------------------------------------------------- 1 | --- 2 | Players 3 | --- 4 | * cyclops (5): 6680, 6600, 5100, 39800 5 | * everywhere (1): 6681, 6601, 5101, 39801 6 | * party2 (2): 6682, 6602, 5102, 39802 7 | * study (6): 6686, 6606, 5106, 39806 8 | * library (7): 6687, 6607, 5107, 39807 9 | * lounge (8): 6688, 6608, 5108, 39808 10 | * ballroom (9): 6689, 6609, 5109, 39809 11 | * outside (10): 6690, 6610, 5110, 39810 12 | * kitchen (11): 6691, 6611, 5111, 39811 13 | * bedroom-mark (12): 6692, 6612, 5112, 39812 14 | -------------------------------------------------------------------------------- /render.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | import argparse 3 | import json 4 | import os 5 | import sys 6 | 7 | import chevron 8 | 9 | 10 | def parse_args(): 11 | def is_file_or_pipe(arg): 12 | if not os.path.exists(arg) or os.path.isdir(arg): 13 | parser.error("The file {0} does not exist!".format(arg)) 14 | else: 15 | return arg 16 | 17 | parser = argparse.ArgumentParser(description="Render templates") 18 | parser.add_argument( 19 | "-d", 20 | "--data", 21 | dest="data", 22 | help="The json data file", 23 | type=is_file_or_pipe, 24 | default={}, 25 | ) 26 | parser.add_argument( 27 | "-z", 28 | "--zone", 29 | dest="zone", 30 | required=False, 31 | help="The zone name", 32 | default={}, 33 | ) 34 | parser.add_argument("template", metavar="template", help="The mustache file") 35 | return parser.parse_args() 36 | 37 | 38 | def main(): 39 | args = parse_args() 40 | 41 | with open(args.data, "r") as j: 42 | d = json.load(j) 43 | if args.zone: 44 | all_zones = d["hosts"] + d["announcers"] + d["party-zones"] 45 | d["zone"] = next(z for z in all_zones if z["name"] == args.zone) 46 | with open(args.template, "r") as t: 47 | print(chevron.render(t, d)) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # python requirements 2 | # native requirements (e.g. libspotify-dev) must be installed separately 3 | vext 4 | vext.gi 5 | Mopidy>=3.1.0 6 | Mopidy-Iris>=3.59 7 | Mopidy-Local 8 | Mopidy-MPD 9 | Mopidy-MQTT-NG 10 | Mopidy-Spotify 11 | Mopidy-TuneIn 12 | -------------------------------------------------------------------------------- /templates/debian/mopidy@.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Mopidy music server 3 | After=avahi-daemon.service 4 | After=dbus.service 5 | After=network-online.target 6 | Wants=network-online.target 7 | After=nss-lookup.target 8 | After=pulseaudio.service 9 | After=remote-fs.target 10 | After=sound.target 11 | 12 | [Service] 13 | User=mopidy 14 | PermissionsStartOnly=true 15 | Restart=on-failure 16 | ExecStartPre=/bin/mkdir -p /var/cache/mopidy 17 | ExecStartPre=/bin/chown mopidy:audio /var/cache/mopidy 18 | ExecStartPre=/bin/chown mopidy:audio /tmp/mopidy.%i 19 | ExecStart={{#mopidy.path}}{{mopidy.path}}{{/mopidy.path}}{{^mopidy.path}}/usr/local/bin/mopidy{{/mopidy.path}} --config /usr/share/mopidy/conf.d:{{path.config_root}}/mopidy.%i.conf 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /templates/dev/mopidy@.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Mopidy music server 3 | After=avahi-daemon.service 4 | After=dbus.service 5 | After=network-online.target 6 | Wants=network-online.target 7 | After=nss-lookup.target 8 | After=pulseaudio.service 9 | After=remote-fs.target 10 | After=sound.target 11 | 12 | [Service] 13 | #User=mopidy 14 | ExecStart={{python}} /usr/bin/mopidy --config /usr/share/mopidy/conf.d:{{path.config_root}}/mopidy.%i.conf 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /templates/dev/snapserver.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Snapcast server 3 | Documentation=man:snapserver(1) 4 | Wants=avahi-daemon.service 5 | After=network.target time-sync.target avahi-daemon.service 6 | 7 | [Service] 8 | Environment="SNAPSERVER_CONFIG={{path.config_root}}/snapserver.conf" 9 | EnvironmentFile=-/etc/default/snapserver 10 | ExecStart=/usr/bin/snapserver --config=${SNAPSERVER_CONFIG} --logging.sink=system $SNAPSERVER_OPTS 11 | #User=snapserver 12 | #Group=snapserver 13 | Restart=on-failure 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /templates/dietpi/snapclient@.service.template: -------------------------------------------------------------------------------- 1 | # Multi-instance snapclient service file 2 | [Unit] 3 | Description=Snapcast client 4 | Documentation=man:snapclient(1) 5 | Wants=avahi-daemon.service 6 | After=network-online.target time-sync.target sound.target avahi-daemon.service 7 | 8 | [Service] 9 | EnvironmentFile={{path.config_root}}/snapclient.%i.conf 10 | ExecStart=/usr/bin/snapclient --logsink=system $SNAPCLIENT_OPTS 11 | User=snapclient 12 | Group=snapclient 13 | Restart=on-failure 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /templates/home-assistant.yaml.template: -------------------------------------------------------------------------------- 1 | {{#zone}} 2 | media_player: 3 | - platform: mpd 4 | {{! use slugified name if available}} 5 | name: {{#name_}}{{name_}}{{/name_}}{{^name_}}{{name}}{{/name_}}_mpd 6 | {{! use localhost for dev}} 7 | host: {{#dev}}localhost{{/dev}}{{^dev}}{{mopidy.host}}{{/dev}} 8 | port: {{mopidy.mpd_port}} 9 | {{#kodi}} 10 | 11 | - platform: kodi 12 | name: {{#name_}}{{name_}}{{/name_}}{{^name_}}{{name}}{{/name_}}_kodi 13 | host: {{name}} 14 | port: 8080 15 | {{/kodi}} 16 | 17 | - platform: group 18 | name: {{#name_}}{{name_}}{{/name_}}{{^name_}}{{name}}{{/name_}}_multizone_audio 19 | entities: 20 | - media_player.{{#name_}}{{name_}}{{/name_}}{{^name_}}{{name}}{{/name_}}_mpd 21 | {{#kodi}} 22 | - media_player.{{#name_}}{{name_}}{{/name_}}{{^name_}}{{name}}{{/name_}}_kodi 23 | {{/kodi}} 24 | {{/zone}} 25 | -------------------------------------------------------------------------------- /templates/iris.template: -------------------------------------------------------------------------------- 1 | {{#zone}} 2 | upstream mopidy-{{name}}-host { 3 | # workaround upstream DNS resolution failure 4 | server {{mopidy.host}}:{{mopidy.http_port}}; 5 | keepalive 5; 6 | } 7 | 8 | server { 9 | listen 80; 10 | listen [::]:80; 11 | 12 | server_name {{name}} {{name}}.*; 13 | 14 | proxy_http_version 1.1; 15 | proxy_read_timeout 600s; 16 | location = / { 17 | return 301 /iris/; 18 | } 19 | 20 | location / { 21 | proxy_pass http://mopidy-{{name}}-host; 22 | proxy_set_header Host $host; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection "upgrade"; 25 | } 26 | } 27 | {{/zone}} 28 | # vi: filetype=nginx ts=4 sw=4 29 | -------------------------------------------------------------------------------- /templates/librespot.template: -------------------------------------------------------------------------------- 1 | deviceId = "" ### Device ID (40 chars, leave empty for random) ### 2 | clientToken = "" ### Client Token (168 bytes Base64 encoded) ### 3 | deviceName = "{{zone.Name}}" ### Device name ### 4 | deviceType = "SPEAKER" ### Device type (COMPUTER, TABLET, SMARTPHONE, SPEAKER, TV, AVR, STB, AUDIO_DONGLE, GAME_CONSOLE, CAST_VIDEO, CAST_AUDIO, AUTOMOBILE, WEARABLE, UNKNOWN_SPOTIFY, CAR_THING, UNKNOWN) ### 5 | preferredLocale = "en" ### Preferred locale ### 6 | logLevel = "OFF" ### Log level (OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL) ### 7 | 8 | [auth] ### Authentication ### 9 | strategy = "ZEROCONF" # Strategy (USER_PASS, ZEROCONF, BLOB, FACEBOOK, STORED) 10 | username = "" # Spotify username (BLOB, USER_PASS only) 11 | password = "" # Spotify password (USER_PASS only) 12 | blob = "" # Spotify authentication blob Base64-encoded (BLOB only) 13 | storeCredentials = false # Whether to store reusable credentials on disk (not a plain password) 14 | credentialsFile = "credentials.json" # Credentials file (JSON) 15 | 16 | [zeroconf] ### Zeroconf ### 17 | listenPort = {{zone.spotify.zeroconf}} # Listen on this TCP port (`-1` for random) 18 | listenAll = true # Listen on all interfaces (overrides `zeroconf.interfaces`) 19 | interfaces = "" # Listen on these interfaces (comma separated list of names) 20 | 21 | [cache] ### Cache ### 22 | enabled = true # Cache enabled 23 | dir = "{{path.media_root}}/{{path.music_metadata}}/librespot-java" 24 | doCleanUp = true 25 | 26 | [network] ### Network ### 27 | connectionTimeout = 10 # If ping isn't received within this amount of seconds, reconnect 28 | 29 | [preload] ### Preload ### 30 | enabled = true # Preload enabled 31 | 32 | [time] ### Time correction ### 33 | synchronizationMethod = "NTP" # Time synchronization method (NTP, PING, MELODY, MANUAL) 34 | manualCorrection = 0 # Manual time correction in millis 35 | 36 | [player] ### Player ### 37 | autoplayEnabled = true # Autoplay similar songs when your music ends 38 | preferredAudioQuality = "NORMAL" # Preferred audio quality (NORMAL, HIGH, VERY_HIGH) 39 | enableNormalisation = true # Whether to apply the Spotify loudness normalisation 40 | normalisationPregain = +3.0 # Normalisation pregain in decibels (loud at +6, normal at +3, quiet at -5) 41 | initialVolume = 32767 # Initial volume (0-65536) 42 | volumeSteps = 64 # Number of volume notches 43 | logAvailableMixers = true # Log available mixers 44 | mixerSearchKeywords = "" # Mixer/backend search keywords (semicolon separated) 45 | crossfadeDuration = {{zone.spotify.crossfade_ms}} # Crossfade overlap time (in milliseconds) 46 | output = "STDOUT" # Audio output device (MIXER, PIPE, STDOUT, CUSTOM) 47 | outputClass = "" # Audio output Java class name 48 | releaseLineDelay = 20 # Release mixer line after set delay (in seconds) 49 | pipe = "" # Output raw (signed) PCM to this file (`player.output` must be PIPE) 50 | retryOnChunkError = true # Whether the player should retry fetching a chuck if it fails 51 | metadataPipe = "" # Output metadata in Shairport Sync format (https://github.com/mikebrady/shairport-sync-metadata-reader) 52 | bypassSinkVolume = false # Whether librespot-java should ignore volume events, sink volume is set to the max 53 | localFilesPath = "" # Where librespot-java should search for local files 54 | 55 | [api] ### API ### 56 | port = {{zone.spotify.api_port}} # API port (`api` module only) 57 | host = "0.0.0.0" # API listen interface (`api` module only) 58 | 59 | [proxy] ### Proxy ### 60 | enabled = false # Whether the proxy is enabled 61 | type = "HTTP" # The proxy type (HTTP, SOCKS) 62 | ssl = false # Connect to proxy using SSL (HTTP only) 63 | address = "" # The proxy hostname 64 | port = 0 # The proxy port 65 | auth = false # Whether authentication is enabled on the server 66 | username = "" # Basic auth username 67 | password = "" # Basic auth password 68 | 69 | [shell] ### Shell ### 70 | enabled = false # Shell events enabled 71 | executeWithBash = false # Execute the command with `bash -c` 72 | onContextChanged = "" 73 | onTrackChanged = "" 74 | onPlaybackEnded = "" 75 | onPlaybackPaused = "" 76 | onPlaybackResumed = "" 77 | onPlaybackFailed = "" 78 | onTrackSeeked = "" 79 | onMetadataAvailable = "" 80 | onVolumeChanged = "" 81 | onInactiveSession = "" 82 | onPanicState = "" 83 | onConnectionDropped = "" 84 | onConnectionEstablished = "" 85 | onStartedLoading = "" 86 | onFinishedLoading = "" 87 | -------------------------------------------------------------------------------- /templates/mopidy.template: -------------------------------------------------------------------------------- 1 | [core] 2 | cache_dir = {{path.media_root}}/{{path.music_metadata}}/mopidy/cache 3 | config_dir = /etc/mopidy 4 | data_dir = {{path.media_root}}/{{path.music_metadata}}/mopidy/lib 5 | 6 | [logging] 7 | config_file = /etc/mopidy/logging.conf 8 | debug_file = /var/log/mopidy/mopidy-debug.{{zone.name}}.log 9 | 10 | [audio] 11 | output = audioresample ! audioconvert ! rgvolume ! audio/x-raw,rate=48000,channels=2,format=S16LE ! filesink location=/tmp/mopidy.{{zone.name}} 12 | mixer_volume = 20 13 | 14 | [mqtt] 15 | host = {{mqtt.host}} 16 | port = 1883 17 | topic = media/{{zone.name}}/mopidy 18 | 19 | [mpd] 20 | enabled = true 21 | hostname = :: 22 | port = {{zone.mopidy.mpd_port}} 23 | zeroconf = {{zone.Name}} Mopidy 24 | 25 | {{^zone.mopidy.mpd_only}} 26 | [file] 27 | enabled = true 28 | media_dirs = 29 | {{path.media_root}}/music/library 30 | 31 | [local] 32 | enabled = true 33 | media_dir = {{path.media_root}}/music/library 34 | library = sqlite 35 | scan_flush_threshold = 100 36 | 37 | [m3u] 38 | enabled = true 39 | playlists_dir = {{path.media_root}}/{{path.music_metadata}}/playlists 40 | 41 | [spotify] 42 | enabled = false 43 | # username = 44 | # password = 45 | # client_id = 46 | # client_secret = 47 | timeout = 60 48 | 49 | [dleyna] 50 | enabled = false 51 | # upnp_browse_limit = 500 52 | # upnp_lookup_limit = 20 53 | # upnp_search_limit = 100 54 | # dbus_start_session = dbus-daemon --fork --session --print-address=1 --print-pid=1 55 | 56 | [http] 57 | enabled = true 58 | hostname = :: 59 | port = {{zone.mopidy.http_port}} 60 | zeroconf = {{zone.Name}} Mopidy 61 | 62 | [iris] 63 | country = GB 64 | locale = en_GB 65 | snapcast_enabled = true 66 | snapcast_host = {{snapcast.host}} 67 | {{/zone.mopidy.mpd_only}} 68 | # vi: filetype=dosini 69 | -------------------------------------------------------------------------------- /templates/multizone-audio-control.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Multizone Audio Controller 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart={{python}} {{path.config_root}}/controller/multizone-control.py 7 | User=mopidy 8 | Restart=on-failure 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /templates/shairport-sync.template: -------------------------------------------------------------------------------- 1 | {{#zone}} 2 | // Sample Configuration File for Shairport Sync 3 | // Commented out settings are generally the defaults, except where noted. 4 | // Some sections are operative only if Shairport Sync has been built with the right configuration flags. 5 | // See the individual sections for details. 6 | 7 | // General Settings 8 | general = 9 | { 10 | name = "{{Name}}"; // overridden by snapserver 11 | // The default is "Hostname" -- i.e. the machine's hostname with the first letter capitalised (ASCII only.) 12 | // You can use the following substitutions: 13 | // %h for the hostname, 14 | // %H for the Hostname (i.e. with first letter capitalised (ASCII only)), 15 | // %v for the version number, e.g. 3.0 and 16 | // %V for the full version string, e.g. 3.3-OpenSSL-Avahi-ALSA-soxr-metadata-sysconfdir:/etc 17 | // Overall length can not exceed 50 characters. Example: "Shairport Sync %v on %H". 18 | // password = "secret"; // leave this commented out if you don't want to require a password 19 | // interpolation = "auto"; // aka "stuffing". Default is "auto". Alternatives are "basic" or "soxr". Choose "soxr" only if you have a reasonably fast processor and Shairport Sync has been built with "soxr" support. 20 | // output_backend = "alsa"; // Run "shairport-sync -h" to get a list of all output_backends, e.g. "alsa", "pipe", "stdout". The default is the first one. 21 | // mdns_backend = "avahi"; // Run "shairport-sync -h" to get a list of all mdns_backends. The default is the first one. 22 | // interface = "name"; // Use this advanced setting to specify the interface on which Shairport Sync should provide its service. Leave it commented out to get the default, which is to select the interface(s) automatically. 23 | port = {{airplay.port}}; // overridden by snapserver 24 | // udp_port_base = 6001; // start allocating UDP ports from this port number when needed 25 | // udp_port_range = 10; // look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state. 26 | // regtype = "_raop._tcp"; // Use this advanced setting to set the service type and transport to be advertised by Zeroconf/Bonjour. Default is "_raop._tcp". 27 | 28 | // drift_tolerance_in_seconds = 0.002; // allow a timing error of this number of seconds of drift away from exact synchronisation before attempting to correct it 29 | // resync_threshold_in_seconds = 0.050; // a synchronisation error greater than this number of seconds will cause resynchronisation; 0 disables it 30 | 31 | // playback_mode = "stereo"; // This can be "stereo", "mono", "reverse stereo", "both left" or "both right". Default is "stereo". 32 | // alac_decoder = "hammerton"; // This can be "hammerton" or "apple". This advanced setting allows you to choose 33 | // the original Shairport decoder by David Hammerton or the Apple Lossless Audio Codec (ALAC) decoder written by Apple. 34 | // If you build Shairport Sync with the flag --with-apple-alac, the Apple ALAC decoder will be chosen by default. 35 | 36 | // ignore_volume_control = "no"; // set this to "yes" if you want the volume to be at 100% no matter what the source's volume control is set to. 37 | // volume_range_db = 60 ; // use this advanced setting to set the range, in dB, you want between the maximum volume and the minimum volume. Range is 30 to 150 dB. Leave it commented out to use mixer's native range. 38 | // volume_max_db = 0.0 ; // use this advanced setting, which must have a decimal point in it, to set the maximum volume, in dB, you wish to use. 39 | // The setting is for the hardware mixer, if chosen, or the software mixer otherwise. The value must be in the mixer's range (0.0 to -96.2 for the software mixer). 40 | // Leave it commented out to use mixer's maximum volume. 41 | // volume_control_profile = "standard" ; // use this advanced setting to specify how the airplay volume is transferred to the mixer volume. 42 | // "standard" makes the volume change more quickly at lower volumes and slower at higher volumes. 43 | // "flat" makes the volume change at the same rate at all volumes. 44 | // volume_range_combined_hardware_priority = "no"; // when extending the volume range by combining the built-in software attenuator with the hardware mixer attenuator, set this to "yes" to reduce volume by using the hardware mixer first, then the built-in software attenuator. 45 | // run_this_when_volume_is_set = "/full/path/to/application/and/args"; // Run the specified application whenever the volume control is set or changed. 46 | // The desired AirPlay volume is appended to the end of the command line – leave a space if you want it treated as an extra argument. 47 | // AirPlay volume goes from 0 to -30 and -144 means "mute". 48 | 49 | // audio_backend_latency_offset_in_seconds = 0.0; // This is added to the latency requested by the player to delay or advance the output by a fixed amount. 50 | // Use it, for example, to compensate for a fixed delay in the audio back end. 51 | // E.g. if the output device, e.g. a soundbar, takes 100 ms to process audio, set this to -0.1 to deliver the audio 52 | // to the output device 100 ms early, allowing it time to process the audio and output it perfectly in sync. 53 | // audio_backend_buffer_desired_length_in_seconds = 0.2; // If set too small, buffer underflow occurs on low-powered machines. 54 | // Too long and the response time to volume changes becomes annoying. 55 | // Default is 0.2 seconds in the alsa backend, 0.35 seconds in the pa backend and 1.0 seconds otherwise. 56 | // audio_backend_buffer_interpolation_threshold_in_seconds = 0.075; // Advanced feature. If the buffer size drops below this, stop using time-consuming interpolation like soxr to avoid dropouts due to underrun. 57 | // audio_backend_silent_lead_in_time = "auto"; // This optional advanced setting, either "auto" or a positive number, sets the length of the period of silence that precedes the start of the audio. 58 | // The default is "auto" -- the silent lead-in starts as soon as the player starts sending packets. 59 | // Values greater than the latency are ignored. Values that are too low will affect initial synchronisation. 60 | 61 | // dbus_service_bus = "system"; // The Shairport Sync dbus interface, if selected at compilation, will appear 62 | // as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session". 63 | // mpris_service_bus = "system"; // The Shairport Sync mpris interface, if selected at compilation, will appear 64 | // as "org.gnome.ShairportSync" on the whichever bus you specify here: "system" (default) or "session". 65 | 66 | // resend_control_first_check_time = 0.10; // Use this optional advanced setting to set the wait time in seconds before deciding a packet is missing. 67 | // resend_control_check_interval_time = 0.25; // Use this optional advanced setting to set the time in seconds between requests for a missing packet. 68 | // resend_control_last_check_time = 0.10; // Use this optional advanced setting to set the latest time, in seconds, by which the last check should be done before the estimated time of a missing packet's transfer to the output buffer. 69 | // missing_port_dacp_scan_interval_seconds = 2.0; // Use this optional advanced setting to set the time interval between scans for a DACP port number if no port number has been provided by the player for remote control commands 70 | }; 71 | 72 | // Advanced parameters for controlling how Shairport Sync stays active and how it runs a session 73 | sessioncontrol = 74 | { 75 | // "active" state starts when play begins and ends when the active_state_timeout has elapsed after play ends, unless another play session starts before the timeout has fully elapsed. 76 | // run_this_before_entering_active_state = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line 77 | // run_this_after_exiting_active_state = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line 78 | // active_state_timeout = 10.0; // wait for this number of seconds after play ends before leaving the active state, unless another play session begins. 79 | 80 | // run_this_before_play_begins = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line 81 | // run_this_after_play_ends = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line 82 | 83 | // run_this_if_an_unfixable_error_is_detected = "/full/path/to/application and args"; // if a problem occurs that can't be cleared by Shairport Sync itself, hook a program on here to deal with it. An error code-string is passed as the last argument. 84 | // Many of these "unfixable" problems are caused by malfunctioning output devices, and sometimes it is necessary to restart the whole device to clear the problem. 85 | // You could hook on a program to do this automatically, but beware -- the device may then power off and restart without warning! 86 | // wait_for_completion = "no"; // set to "yes" to get Shairport Sync to wait until the "run_this..." applications have terminated before continuing 87 | 88 | // allow_session_interruption = "no"; // set to "yes" to allow another device to interrupt Shairport Sync while it's playing from an existing audio source 89 | // session_timeout = 120; // wait for this number of seconds after a source disappears before terminating the session and becoming available again. 90 | }; 91 | 92 | // Back End Settings 93 | 94 | // These are parameters for the "alsa" audio back end. 95 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 96 | // --with-alsa 97 | alsa = 98 | { 99 | // output_device = "default"; // the name of the alsa output device. Use "shairport-sync -h" to discover the names of ALSA hardware devices. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc. 100 | // mixer_control_name = "PCM"; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software. 101 | // mixer_device = "default"; // the mixer_device default is whatever the output_device is. Normally you wouldn't have to use this. 102 | 103 | // output_rate = "auto"; // can be "auto", 44100, 88200, 176400 or 352800, but the device must have the capability. 104 | // output_format = "auto"; // can be "auto", "U8", "S8", "S16", "S16_LE", "S16_BE", "S24", "S24_LE", "S24_BE", "S24_3LE", "S24_3BE", "S32", "S32_LE" or "S32_BE" but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor. 105 | 106 | // disable_synchronization = "no"; // Set to "yes" to disable synchronization. Default is "no" This is really meant for troubleshootingG. 107 | 108 | // period_size = ; // Use this optional advanced setting to set the alsa period size near to this value 109 | // buffer_size = ; // Use this optional advanced setting to set the alsa buffer size near to this value 110 | // use_mmap_if_available = "yes"; // Use this optional advanced setting to control whether MMAP-based output is used to communicate with the DAC. Default is "yes" 111 | // use_hardware_mute_if_available = "no"; // Use this optional advanced setting to control whether the hardware in the DAC is used for muting. Default is "no", for compatibility with other audio players. 112 | // maximum_stall_time = 0.200; // Use this optional advanced setting to control how long to wait for data to be consumed by the output device before considering it an error. It should never approach 200 ms. 113 | // use_precision_timing = "auto"; // Use this optional advanced setting to control how Shairport Sync gathers timing information. When set to "auto", if the output device is a real hardware device, precision timing will be used. Choose "no" for more compatible standard timing, choose "yes" to force the use of precision timing, which may cause problems. 114 | 115 | // disable_standby_mode = "never"; // This setting prevents the DAC from entering the standby mode. Some DACs make small "popping" noises when they go in and out of standby mode. Settings can be: "always", "auto" or "never". Default is "never", but only for backwards compatibility. The "auto" setting prevents entry to standby mode while Shairport Sync is in the "active" mode. You can use "yes" instead of "always" and "no" instead of "never". 116 | // disable_standby_mode_silence_threshold = 0.040; // Use this optional advanced setting to control how little audio should remain in the output buffer before the disable_standby code should start sending silence to the output device. 117 | // disable_standby_mode_silence_scan_interval = 0.004; // Use this optional advanced setting to control how often the amount of audio remaining in the output buffer should be checked. 118 | }; 119 | 120 | // Parameters for the "sndio" audio back end. All are optional. 121 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 122 | // --with-sndio 123 | sndio = 124 | { 125 | // device = "snd/0"; // optional setting to set the name of the output device. Default is the sndio system default. 126 | // rate = 44100; // optional setting which can be 44100, 88200, 176400 or 352800, but the device must have the capability. Default is 44100. 127 | // format = "S16"; // optional setting which can be "U8", "S8", "S16", "S24", "S24_3LE", "S24_3BE" or "S32", but the device must have the capability. Except where stated using (*LE or *BE), endianness matches that of the processor. 128 | // round = ; // advanced optional setting to set the period size near to this value 129 | // bufsz = ; // advanced optional setting to set the buffer size near to this value 130 | }; 131 | 132 | // Parameters for the "pa" PulseAudio backend. 133 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 134 | // --with-pa 135 | pa = 136 | { 137 | // server = "host"; // Set this to override the default pulseaudio server that should be used. 138 | // sink = "Sink Name"; // Set this to override the default pulseaudio sink that should be used. (Untested) 139 | // application_name = "Shairport Sync"; //Set this to the name that should appear in the Sounds "Applications" tab when Shairport Sync is active. 140 | }; 141 | 142 | // Parameters for the "jack" JACK Audio Connection Kit backend. 143 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 144 | // --with-jack 145 | jack = 146 | { 147 | // client_name = "shairport-sync"; // Set this to the name of the client that should appear in "Connections" when Shairport Sync is active. 148 | // autoconnect_pattern = ""; // Set this to a POSIX regular expression pattern that describes the ports you would like to connect to 149 | // automatically. Examples: 150 | // "system:playback_[12]" 151 | // "some_app_[0-9]*:in-[LR]" 152 | // "jack_mixer:in_2[78]" 153 | // Beware: if you make a syntax error, libjack might crash. In that case, fix it and start over. 154 | // For a good overview, look here: https://www.ibm.com/support/knowledgecenter/SS8NLW_11.0.1/com.ibm.swg.im.infosphere.dataexpl.engine.doc/c_posix-regex-examples.html 155 | // soxr_resample_quality = "none"; // Enable resampling by setting this to "very high", "high", "medium", "low" or "quick" 156 | // bufsz = ; // advanced optional setting to set the buffer size to this value 157 | }; 158 | 159 | // Parameters for the "pipe" audio back end, a back end that directs raw CD-style audio output to a pipe. No interpolation is done. 160 | // For this section to be operative, Shairport Sync must have been built with the following configuration flag: 161 | // --with-pipe 162 | pipe = 163 | { 164 | // name = "/tmp/shairport-sync-audio.{{name}}"; // set by snapserver 165 | }; 166 | 167 | // There are no configuration file parameters for the "stdout" audio back end. No interpolation is done. 168 | // To include support for the "stdout" backend, Shairport Sync must be built with the following configuration flag: 169 | // --with-stdout 170 | 171 | // There are no configuration file parameters for the "ao" audio back end. No interpolation is done. 172 | // To include support for the "ao" backend, Shairport Sync must be built with the following configuration flag: 173 | // --with-ao 174 | 175 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 176 | // --with-convolution 177 | dsp = 178 | { 179 | 180 | ////////////////////////////////////////// 181 | // This convolution filter can be used to apply almost any correction to the audio signal, like frequency and phase correction. 182 | // For example you could measure (with a good microphone and a sweep-sine) the frequency response of your speakers + room, 183 | // and apply a correction to get a flat response curve. 184 | ////////////////////////////////////////// 185 | // 186 | // convolution = "no"; // Set this to "yes" to activate the convolution filter. 187 | // convolution_ir_file = "impulse.wav"; // Impulse Response file to be convolved to the audio stream 188 | // convolution_gain = -4.0; // Static gain applied to prevent clipping during the convolution process 189 | // convolution_max_length = 44100; // Truncate the input file to this length in order to save CPU. 190 | 191 | 192 | ////////////////////////////////////////// 193 | // This loudness filter is used to compensate for human ear non linearity. 194 | // When the volume decreases, our ears loose more sentisitivity in the low range frequencies than in the mid range ones. 195 | // This filter aims at compensating for this loss, applying a variable gain to low frequencies depending on the volume. 196 | // More info can be found here: https://en.wikipedia.org/wiki/Equal-loudness_contour 197 | // For this filter to work properly, you should disable (or set to a fix value) all other volume control and only let shairport-sync control your volume. 198 | // The setting "loudness_reference_volume_db" should be set at the volume reported by shairport-sync when listening to music at a normal listening volume. 199 | ////////////////////////////////////////// 200 | // 201 | // loudness = "no"; // Set this to "yes" to activate the loudness filter 202 | // loudness_reference_volume_db = -20.0; // Above this level the filter will have no effect anymore. Below this level it will gradually boost the low frequencies. 203 | 204 | }; 205 | 206 | // How to deal with metadata, including artwork 207 | // For this section to be operative, Shairport Sync must be built with at one (or more) of the following configuration flags: 208 | // --with-metadata, --with-dbus-interface, --with-mpris-interface or --with-mqtt-client. 209 | // In those cases, "enabled" and "include_cover_art" will both be "yes" by default 210 | metadata = 211 | { 212 | // enabled = "yes"; // set this to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe 213 | // include_cover_art = "yes"; // set to "yes" to get Shairport Sync to solicit cover art from the source and pass it via the pipe. You must also set "enabled" to "yes". 214 | // cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart"; // artwork will be stored in this directory if the dbus or MPRIS interfaces are enabled or if the MQTT client is in use. Set it to "" to prevent caching, which may be useful on some systems 215 | // pipe_name = "/tmp/shairport-sync-metadata.{{name}}"; // overridden by snapserver 216 | // pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up 217 | // socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes" 218 | // socket_port = 5555; // if socket_address is set, the port to send UDP packets to 219 | // socket_msglength = 65000; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500. 220 | }; 221 | 222 | // How to enable the MQTT-metadata/remote-service 223 | // For this section to be operative, Shairport Sync must be built with the following configuration flag: 224 | // --with-mqtt-client 225 | mqtt = 226 | { 227 | enabled = "yes"; // set this to yes to enable the mqtt-metadata-service 228 | hostname = "{{mqtt.host}}"; 229 | // port = 1883; // Port on the MQTT Broker to connect to 230 | // username = NULL; //set this to a string to your username in order to enable username authentication 231 | // password = NULL; //set this to a string you your password in order to enable username & password authentication 232 | // capath = NULL; //set this to the folder with the CA-Certificates to be accepted for the server certificate. If not set, TLS is not used 233 | // cafile = NULL; //this may be used as an (exclusive) alternative to capath with a single file for all ca-certificates 234 | // certfile = NULL; //set this to a string to a user certificate to enable MQTT Client certificates. keyfile must also be set! 235 | // keyfile = NULL; //private key for MQTT Client authentication 236 | topic = "media/{{name}}/airplay"; 237 | // publish_raw = "no"; //whether to publish all available metadata under the codes given in the 'metadata' docs. 238 | publish_parsed = "yes"; 239 | // Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip, 240 | // Additionally, empty messages at the topics play_start,play_end,play_flush,play_resume are published 241 | // publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker 242 | enable_remote = "yes"; 243 | // Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup" 244 | }; 245 | 246 | // Diagnostic settings. These are for diagnostic and debugging only. Normally you should leave them commented out 247 | diagnostics = 248 | { 249 | // disable_resend_requests = "no"; // set this to yes to stop Shairport Sync from requesting the retransmission of missing packets. Default is "no". 250 | // log_output_to = "syslog"; // set this to "syslog" (default), "stderr" or "stdout" or a file or pipe path to specify were all logs, statistics and diagnostic messages are written to. If there's anything wrong with the file spec, output will be to "stderr". 251 | // statistics = "no"; // set to "yes" to print statistics in the log 252 | // log_verbosity = 0; // "0" means no debug verbosity, "3" is most verbose. 253 | // log_show_file_and_line = "yes"; // set this to yes if you want the file and line number of the message source in the log file 254 | // log_show_time_since_startup = "no"; // set this to yes if you want the time since startup in the debug message -- seconds down to nanoseconds 255 | // log_show_time_since_last_message = "yes"; // set this to yes if you want the time since the last debug message in the debug message -- seconds down to nanoseconds 256 | // drop_this_fraction_of_audio_packets = 0.0; // use this to simulate a noisy network where this fraction of UDP packets are lost in transmission. E.g. a value of 0.001 would mean an average of 0.1% of packets are lost, which is actually quite a high figure. 257 | // retain_cover_art = "no"; // artwork is deleted when its corresponding track has been played. Set this to "yes" to retain all artwork permanently. Warning -- your directory might fill up. 258 | }; 259 | {{/zone}} 260 | -------------------------------------------------------------------------------- /templates/snapcast-autoconfig.yaml.template: -------------------------------------------------------------------------------- 1 | --- 2 | loglevel: info 3 | server: tcp://media:1705 4 | streams: 5 | {{#hosts}} 6 | {{Name}}: 7 | clients: 8 | - {{name}} 9 | volume: 10 | {{name}}: {{snapcast.volume}} 11 | {{/hosts}} 12 | 13 | {{#party-zones}} 14 | {{Name}}: 15 | clients: 16 | {{#clients}} 17 | - {{.}} 18 | {{/clients}} 19 | volume: 20 | {{#clients}} 21 | {{.}}: 100 22 | {{/clients}} 23 | {{/party-zones}} 24 | -------------------------------------------------------------------------------- /templates/snapclient.template: -------------------------------------------------------------------------------- 1 | {{#zone}} 2 | # defaults file for snapclient {{name}} instance 3 | 4 | # start snapclient automatically? 5 | START_SNAPCLIENT=true 6 | 7 | # Allowed options: 8 | # --help produce help message 9 | # -v, --version show version number 10 | # -h, --host arg server hostname or ip address 11 | # -p, --port arg (=1704) server port 12 | # -i, --instance arg (=1) instance id when running multiple instances on the same host 13 | # --hostID arg unique host id, default is MAC address 14 | # -l, --list list PCM devices 15 | # -s, --soundcard arg (=default) index or name of the pcm device 16 | # --latency arg (=0) latency of the PCM device 17 | # --sampleformat arg resample audio stream to :: 18 | # --player arg (=alsa) alsa|file[:|?] 19 | # --mixer arg (=software) software|hardware|script|none|?[:] 20 | # -e, --mstderr send metadata to stderr 21 | # -d, --daemon [=arg(=-3)] daemonize, optional process priority [-20..19] 22 | # --user arg the user[:group] to run snapclient as when daemonized 23 | # --logsink arg log sink [null,system,stdout,stderr,file:] 24 | # --logfilter arg (=*:info) log filter :[,:]* with tag = * or and level = [trace,debug,info,notice,warning,error,fatal] 25 | 26 | USER_OPTS="--user snapclient:audio" 27 | 28 | SNAPCLIENT_OPTS="-h {{snapcast.host}} --hostID={{name}} {{snapcast.opts}}" 29 | {{/zone}} 30 | -------------------------------------------------------------------------------- /templates/snapclient@.service.template: -------------------------------------------------------------------------------- 1 | # Multi-instance snapclient service file 2 | [Unit] 3 | Description=Snapcast client 4 | After=sound.target 5 | Wants=avahi-daemon.service 6 | 7 | [Service] 8 | EnvironmentFile={{path.config_root}}/snapclient.%i.conf 9 | Type=simple 10 | ExecStart=/usr/bin/snapclient --logsink=system $SNAPCLIENT_OPTS 11 | Restart=on-failure 12 | User=snapclient 13 | Group=snapclient 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /templates/snapserver.template: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # ______ # 3 | # / _____) # 4 | # ( (____ ____ _____ ____ ___ _____ ____ _ _ _____ ____ # 5 | # \____ \ | _ \ (____ || _ \ /___)| ___ | / ___)| | | || ___ | / ___) # 6 | # _____) )| | | |/ ___ || |_| ||___ || ____|| | \ V / | ____|| | # 7 | # (______/ |_| |_|\_____|| __/ (___/ |_____)|_| \_/ |_____)|_| # 8 | # |_| # 9 | # # 10 | # Snapserver config file # 11 | # # 12 | ############################################################################### 13 | 14 | # default values are commented 15 | # uncomment and edit to change them 16 | 17 | # Settings can be overwritten on command line with: 18 | # "--
.=", e.g. --server.threads=4 19 | 20 | 21 | # General server settings ##################################################### 22 | # 23 | [server] 24 | # Number of additional worker threads to use 25 | # - For values < 0 the number of threads will be 2 (on single and dual cores) 26 | # or 4 (for quad and more cores) 27 | # - 0 will utilize just the processes main thread and might cause audio drops 28 | # in case there are a couple of longer running tasks, such as encoding 29 | # multiple audio streams 30 | #threads = -1 31 | 32 | # the pid file when running as daemon 33 | #pidfile = /var/run/snapserver/pid 34 | 35 | # the user to run as when daemonized 36 | #user = snapserver 37 | # the group to run as when daemonized 38 | #group = snapserver 39 | 40 | # directory where persistent data is stored (server.json) 41 | # if empty, data dir will be 42 | # - "/var/lib/snapserver/" when running as daemon 43 | # - "$HOME/.config/snapserver/" when not running as daemon 44 | #datadir = 45 | 46 | # 47 | ############################################################################### 48 | 49 | 50 | # HTTP RPC #################################################################### 51 | # 52 | [http] 53 | # enable HTTP Json RPC (HTTP POST and websockets) 54 | enabled = true 55 | 56 | # address to listen on, can be specified multiple times 57 | # use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address 58 | # or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively 59 | # use the address of a specific network interface to just listen to and accept 60 | # connections from that interface 61 | bind_to_address = :: 62 | bind_to_address = 0.0.0.0 63 | 64 | # which port the server should listen to 65 | #port = 1780 66 | 67 | # serve a website from the doc_root location 68 | # disabled if commented or empty 69 | doc_root = {{snapcast.snapweb_root}}{{^snapcast.snapweb_root}}/usr/share/snapserver/snapweb{{/snapcast.snapweb_root}} 70 | 71 | # Hostname or IP under which clients can reach this host 72 | # used to serve cached cover art 73 | # use as placeholder for your actual host name 74 | #host = 75 | 76 | # 77 | ############################################################################### 78 | 79 | 80 | # TCP RPC ##################################################################### 81 | # 82 | [tcp] 83 | # enable TCP Json RPC 84 | #enabled = true 85 | 86 | # address to listen on, can be specified multiple times 87 | # use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address 88 | # or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively 89 | # use the address of a specific network interface to just listen to and accept 90 | # connections from that interface 91 | #bind_to_address = 0.0.0.0 92 | 93 | # which port the server should listen to 94 | #port = 1705 95 | # 96 | ############################################################################### 97 | 98 | 99 | # Stream settings ############################################################# 100 | # 101 | [stream] 102 | # address to listen on, can be specified multiple times 103 | # use "0.0.0.0" to bind to any IPv4 address or :: to bind to any IPv6 address 104 | # or "127.0.0.1" or "::1" to bind to localhost IPv4 or IPv6, respectively 105 | # use the address of a specific network interface to just listen to and accept 106 | # connections from that interface 107 | #bind_to_address = 0.0.0.0 108 | 109 | # which port the server should listen to 110 | #port = 1704 111 | 112 | # source URI of the PCM input stream, can be configured multiple times 113 | # The following notation is used in this paragraph: 114 | # : the whole expression must be replaced with your specific setting 115 | # [square brackets]: the whole expression is optional and can be left out 116 | # [key=value]: if you leave this option out, "value" will be the default for "key" 117 | # 118 | # Format: TYPE://host/path?name=[&codec=][&sampleformat=][&chunk_ms=][&controlscript=[&controlscriptparams=]] 119 | # parameters have the form "key=value", they are concatenated with an "&" character 120 | # parameter "name" is mandatory for all sources, while codec, sampleformat and chunk_ms are optional 121 | # and will override the default codec, sampleformat or chunk_ms settings 122 | # Non blocking sources support the dryout_ms parameter: when no new data is read from the source, send silence to the clients 123 | # Available types are: 124 | # pipe: pipe:///?name=[&mode=create][&dryout_ms=2000], mode can be "create" or "read" 125 | # librespot: librespot:///?name=[&dryout_ms=2000][&username=&password=][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&nomalize=false][&autoplay=false][¶ms=] 126 | # note that you need to have the librespot binary on your machine 127 | # sampleformat will be set to "44100:16:2" 128 | # file: file:///?name= 129 | # process: process:///?name=[&dryout_ms=2000][&wd_timeout=0][&log_stderr=false][¶ms=] 130 | # airplay: airplay:///?name=[&dryout_ms=2000][&port=5000] 131 | # note that you need to have the airplay binary on your machine 132 | # sampleformat will be set to "44100:16:2" 133 | # tcp server: tcp://:?name=[&mode=server] 134 | # tcp client: tcp://:?name=&mode=client 135 | # alsa: alsa:///?name=&device=[&send_silence=false][&idle_threshold=100][&silence_threshold_percent=0.0] 136 | # meta: meta://///.../?name= 137 | #source = pipe:///tmp/snapfifo?name=default 138 | #source = tcp://127.0.0.1?name=mopidy_tcp 139 | 140 | source = alsa://?name=Barn Chromecast{{#chromecast.device}}&device={{.}}{{/chromecast.device}}&idle_threshold=500&silence_threshold_percent=2.0 141 | 142 | {{#announcers}} 143 | source = pipe:///tmp/mopidy.{{name}}?name={{name}}&sampleformat=48000:16:2&codec=flac 144 | {{/announcers}} 145 | 146 | {{#hosts}} 147 | # {{name}} 148 | source = pipe:///tmp/mopidy.{{name}}?name=- {{name}} iris&sampleformat=48000:16:2&codec=flac 149 | source = airplay:///shairport-sync?name=- {{name}} airplay&devicename={{Name}}&port={{airplay.port}}¶ms=--configfile={{path.config_root}}/shairport-sync.{{name}}.conf 150 | source = process://{{path.java}}?name=- {{name}} spotify&log_stderr=true&sampleformat=44100:16:2¶ms=-jar%20{{spotify.jar}}%20--conf-file={{path.config_root}}/librespot.{{name}}.toml 151 | 152 | source = meta:///{{#announcer_streams}}{{.}}/{{/announcer_streams}}{{#streams}}- {{name}} {{.}}/{{/streams}}{{#other_streams}}{{.}}/{{/other_streams}}?name={{Name}} 153 | 154 | {{/hosts}} 155 | 156 | {{#party-zones}} 157 | # {{Name}} 158 | source = pipe:///tmp/mopidy.{{name}}?name=- {{name}} iris&sampleformat=48000:16:2&codec=flac 159 | source = airplay:///shairport-sync?name=- {{name}} airplay&devicename={{Name}}&port={{airplay.port}}¶ms=--configfile={{path.config_root}}/shairport-sync.{{name}}.conf 160 | source = process://{{path.java}}?name=- {{name}} spotify&log_stderr=true&sampleformat=44100:16:2¶ms=-jar%20{{spotify.jar}}%20--conf-file={{path.config_root}}/librespot.{{name}}.toml 161 | 162 | source = meta:///{{#announcer_streams}}{{.}}/{{/announcer_streams}}{{#streams}}- {{name}} {{.}}/{{/streams}}?name={{Name}} 163 | 164 | {{/party-zones}} 165 | 166 | # Default sample format: :: 167 | #sampleformat = 48000:16:2 168 | 169 | # Default transport codec 170 | # (flac|ogg|opus|pcm)[:options] 171 | # Start Snapserver with "--stream:codec=:?" to get codec specific options 172 | #codec = flac 173 | 174 | # Default source stream read chunk size [ms]. 175 | # The server will continously read this number of milliseconds from the source into buffer and pass this buffer to the encoder. 176 | # The encoded buffer is sent to the clients. Some codecs have a higher latency and will need more data, e.g. Flac will need ~26ms chunks 177 | #chunk_ms = 20 178 | 179 | # Buffer [ms] 180 | # The end-to-end latency, from capturing a sample on the server until the sample is played-out on the client 181 | #buffer = 1000 182 | 183 | # Send audio to muted clients 184 | #send_to_muted = false 185 | # 186 | 187 | 188 | # Streaming client options #################################################### 189 | # 190 | [streaming_client] 191 | 192 | # Volume assigned to new snapclients [percent] 193 | # Defaults to 100 if unset 194 | #initial_volume = 100 195 | # 196 | ############################################################################### 197 | 198 | 199 | # Logging options ############################################################# 200 | # 201 | [logging] 202 | 203 | # log sink [null,system,stdout,stderr,file:] 204 | # when left empty: if running as daemon "system" else "stdout" 205 | #sink = 206 | 207 | # log filter :[,:]* 208 | # with tag = * or and level = [trace,debug,info,notice,warning,error,fatal] 209 | #filter = *:info 210 | # 211 | ############################################################################### 212 | --------------------------------------------------------------------------------