Skip to content

Commit 0ab38d3

Browse files
authored
Merge pull request #2289 from haavar/docs/multiple-instances [skip ci]
docs: guides for running multiple AirPlay 2 instances and splitting a surround card
2 parents 22d7f2d + cce6596 commit 0ab38d3

4 files changed

Lines changed: 325 additions & 1 deletion

File tree

ADVANCED TOPICS/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Here you will find links to some advanced features and things you can do with Sh
55
* [Metadata](Metadata.md).
66
* [Events](Events.md).
77
* [Statistics](Statistics.md).
8+
* [Running Multiple AirPlay 2 Instances](RunningMultipleInstances.md) on one host – one independent AirPlay 2 zone per room from a single machine, VM, or set of containers.
9+
* [Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md) – carve one multichannel ALSA card into several independent stereo outputs, one per room.
810
* Setting up an [MQTT](../MQTT.md) system.
911
* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync).
1012
* [Car Installation](../CAR%20INSTALL.md)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Running Multiple AirPlay 2 Instances on One Host
2+
3+
Each Shairport Sync process appears on the network as a single AirPlay device. To
4+
offer several independent zones — one per room — from a single machine (bare
5+
metal, a VM, or a set of containers), run **one instance per room**. Each instance
6+
needs a distinct AirPlay identity; Shairport Sync derives that identity from the
7+
instance's **name**, so the setup is minimal.
8+
9+
If you want several rooms driven from a single multichannel sound card, also see
10+
the companion guide [Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).
11+
12+
## The short version
13+
14+
Give each instance its own **name** (`-a` / `general.name`) and its own network
15+
endpoint — a distinct `--port`, a distinct `--address`, or both — and point it at
16+
an output device. Shairport Sync generates a distinct AirPlay 2 identity from the
17+
name, so the instances share one configuration file and differ only on the command
18+
line:
19+
20+
```bash
21+
# one shared configuration file; each room differs only by name, port, device
22+
shairport-sync -c /etc/shairport-sync.conf -a "Kitchen" --port=7000 -- -d room_kitchen
23+
shairport-sync -c /etc/shairport-sync.conf -a "Living Room" --port=7001 -- -d room_livingroom
24+
```
25+
26+
These examples share the host's IP and differ by port; giving each instance its own
27+
`--address` instead (or as well) works the same way. Because the identity comes from
28+
the name, it stays put if a room later moves to a different port or address.
29+
(Everything after `--` is passed to the output backend; `-d` selects its output
30+
device — run `shairport-sync -h` for the options.)
31+
32+
## What makes an instance distinct
33+
34+
1. **A name**`general.name`, or `-a`. The AirPlay menu label, and the source of
35+
the derived identity. Each instance must have a distinct name.
36+
2. **A socket** — a distinct `--port`, a distinct `--address`, or both. Two
37+
instances can't share one address and port.
38+
3. **A timing clock** — each AirPlay 2 instance needs its own clock from the
39+
companion [NQPTP](https://github.com/mikebrady/nqptp) daemon, addressed by a
40+
shared-memory interface name derived from the instance's name (`/nqptp-<name>`).
41+
4. **An output device**`alsa.output_device`, or `-- -d <device>`. Usually its
42+
own, though instances can share a device that permits concurrent playback (their
43+
audio is then mixed). If several rooms come off one multichannel card, see
44+
[Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).
45+
46+
## One NQPTP for the whole host
47+
48+
NQPTP is multi-client. Run **one** NQPTP for the machine — it needs exclusive use
49+
of UDP ports 319 and 320 — and it keeps a separate clock per shared-memory
50+
interface name, demultiplexing its clients by the name each one uses. **Do not run
51+
one NQPTP per instance**; they would contend for ports 319/320. The
52+
automatically-derived names give each instance its own clock with no configuration.
53+
54+
## Running in containers
55+
56+
The container case is the same model — a name and a port (or address) per room —
57+
with three deployment details:
58+
59+
* **Networking.** The instances and NQPTP must reach each other, and NQPTP needs
60+
ports 319/320, so run them with host networking (or place them in one shared
61+
network namespace).
62+
* **Shared memory / IPC (`ipc: host`).** NQPTP hands timing to Shairport Sync
63+
through a POSIX shared-memory object (an entry under `/dev/shm`). By default each
64+
Docker container gets its own private `/dev/shm`, so an instance in one container
65+
cannot see the object NQPTP created in another. `ipc: host` (or a shared `ipc:` in
66+
Compose) puts the containers on the same `/dev/shm`, making the timing interface
67+
visible. This is needed whenever NQPTP runs in a separate container from the
68+
instances, regardless of the audio backend — it is **not** specific to sharing a
69+
sound card. (Splitting a single sound card across containers also needs a shared
70+
IPC namespace, for the separate reason that ALSA's `dmix` coordinates through
71+
System V IPC — see the surround-card guide.)
72+
* **Only one NQPTP.** The official image's launcher starts NQPTP *inside every
73+
container it runs*. That is fine for a single instance, but with several rooms
74+
they would all try to start NQPTP and fight over ports 319/320. So run **one**
75+
dedicated NQPTP (the `nqptp` service below) and set **`ENABLE_NQPTP=0`** on each
76+
room so it starts everything it needs *except* its own NQPTP. (Avahi needs no
77+
equivalent — each room runs its own; see the mDNS note below.)
78+
79+
A Compose sketch — one NQPTP sidecar plus two rooms on the shared host IP:
80+
81+
```yaml
82+
services:
83+
nqptp:
84+
# the shairport-sync image also ships nqptp; run it, and nothing else, here
85+
image: mikebrady/shairport-sync:latest
86+
entrypoint: ["/usr/local/bin/nqptp"]
87+
network_mode: host # NQPTP owns UDP 319/320 for the whole host
88+
89+
kitchen:
90+
image: mikebrady/shairport-sync:latest
91+
network_mode: host
92+
ipc: host # share /dev/shm with nqptp
93+
environment: [ENABLE_NQPTP=0] # use the shared nqptp, don't start another
94+
command: ["-a", "Kitchen", "--port=7000", "--", "-d", "room_kitchen"]
95+
depends_on: [nqptp]
96+
97+
livingroom:
98+
image: mikebrady/shairport-sync:latest
99+
network_mode: host
100+
ipc: host
101+
environment: [ENABLE_NQPTP=0]
102+
command: ["-a", "Living Room", "--port=7001", "--", "-d", "room_livingroom"]
103+
depends_on: [nqptp]
104+
```
105+
106+
(Add the audio-device access your output backend needs — for ALSA, `/dev/snd` and
107+
the `audio` group. If the rooms share one card, the container-side ALSA setup,
108+
including the shared-IPC requirement for `dmix`, is covered in
109+
[Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).)
110+
111+
## A note on mDNS advertisement
112+
113+
Each instance runs its own mDNS responder (Avahi, in the default image). That is
114+
fine: mDNS is a multicast protocol, so several responders coexist on the network,
115+
each advertising its own instance. When the responder can be scoped — for example
116+
by binding Avahi to one interface — each instance advertises on just its own
117+
address, which keeps the AirPlay menu tidy.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Splitting a Surround Card into Per-Room Stereo Outputs
2+
3+
A single surround or multichannel sound card -- a 5.1/7.1 HDMI or analog card, or an 8-channel USB interface -- exposes many output channels on one physical device. Often you want to treat those channels as several independent stereo "rooms": each stereo pair of the card's outputs feeds one room's speakers, with a *different* program driving each pair at the same time.
4+
5+
This is the natural companion to running several Shairport Sync instances on one host (see [`RunningMultipleInstances.md`](RunningMultipleInstances.md)). That guide explains that each instance needs its own output device; this guide shows how to carve one multichannel card into several stereo ALSA PCMs so each instance can own one.
6+
7+
All of the work here is ALSA configuration. Shairport Sync itself needs no special build -- it just points at the named PCM you create.
8+
9+
## The problem
10+
11+
You cannot simply open the raw hardware device (`hw:0`) from several programs at once. A raw `hw` device is *exclusive*: the first program to open it locks it, and every other program gets a "device busy" error. To let several programs share one card you need a software layer that mixes or shares the device between clients, and some way to steer each client's two channels onto a specific pair of the card's output channels.
12+
13+
ALSA provides exactly these pieces:
14+
15+
- **`dshare`** -- lets several clients share one card, each writing to a *different* set of the card's channels. This is what gives each room its own pair.
16+
- **`bindings`** -- inside a `dshare`, map this client's two channels onto chosen channels of the card.
17+
- **`plug`** -- transparently converts rate, format, and channel count so that ordinary stereo sources work.
18+
- **`dmix`** -- the sibling of `dshare` that *mixes* several clients onto the *same* channels. You don't need it to split a card into rooms, but it's the piece to reach for if you want two sources to share one output -- for example a chime or announcement played over whatever is already in a room. Point both sources at a `dmix` on those channels instead of a `dshare`.
19+
20+
You combine these into one named PCM per room.
21+
22+
## General ALSA configuration
23+
24+
Put the following in `/etc/asound.conf` (system-wide) or `~/.asoundrc` (per-user). The two files use identical syntax.
25+
26+
### Define the card once
27+
28+
Define the hardware as a named slave, so every room can refer to it without repeating the device, channel count and rate:
29+
30+
```
31+
pcm_slave.card {
32+
pcm "hw:0,0" # the real multichannel card
33+
channels 6 # the card's output channels (6 for 5.1, 8 for 7.1)
34+
rate 44100
35+
}
36+
```
37+
38+
### One stereo PCM per room
39+
40+
Now define a PCM per room. Each is a `dshare` bound to two of the card's channels -- `dshare` gives each room its own channels and, unlike `dmix`, does not mix them -- wrapped in `plug` so any stereo source is converted automatically. A 5.1 card is three stereo output jacks, so it gives three rooms; name each after the jack it drives:
41+
42+
```
43+
pcm.room_front {
44+
type plug
45+
slave.pcm {
46+
type dshare
47+
ipc_key 4242 # same key for every room on this card
48+
ipc_key_add_uid false
49+
slave card
50+
bindings.0 0 # room left -> channel 0 (front left)
51+
bindings.1 1 # room right -> channel 1 (front right)
52+
}
53+
}
54+
55+
pcm.room_center {
56+
type plug
57+
slave.pcm {
58+
type dshare
59+
ipc_key 4242
60+
ipc_key_add_uid false
61+
slave card
62+
bindings.0 2 # -> channel 2 (centre)
63+
bindings.1 3 # -> channel 3 (LFE / subwoofer)
64+
}
65+
}
66+
67+
pcm.room_rear {
68+
type plug
69+
slave.pcm {
70+
type dshare
71+
ipc_key 4242
72+
ipc_key_add_uid false
73+
slave card
74+
bindings.0 4 # -> channel 4 (rear left)
75+
bindings.1 5 # -> channel 5 (rear right)
76+
}
77+
}
78+
```
79+
80+
`bindings.0 4 / bindings.1 5` put the room's left and right onto the card's channels 4 and 5. Every room on the one card uses the **same `ipc_key`**, so they attach to the same shared device and play at once, each on its own channels -- the rooms do not need a key each. `ipc_key_add_uid false` keeps that key the same for every user, which matters when the rooms run as separate containers (see below). `room_center` just uses the centre/subwoofer jack (channels 2 and 3) as a plain stereo output.
81+
82+
> **Check your channel order.** The mapping above assumes the common layout `FL FR FC LFE RL RR (SL SR)`. Cards can differ, so confirm which physical output each channel drives -- e.g. `speaker-test -D hw:0,0 -c 6 -t wav`, which plays each channel in turn and names its position -- and adjust the `bindings` to match.
83+
84+
### Adding rooms on a 7.1 / 8-channel card
85+
86+
An 8-channel card has a fourth stereo pair -- the side channels. Set `channels 8` in `pcm_slave.card` (above), then add a fourth room alongside `room_front`, `room_center` and `room_rear`:
87+
88+
```
89+
pcm.room_side {
90+
type plug
91+
slave.pcm {
92+
type dshare
93+
ipc_key 4242
94+
ipc_key_add_uid false
95+
slave card
96+
bindings.0 6 # -> channel 6 (side left)
97+
bindings.1 7 # -> channel 7 (side right)
98+
}
99+
}
100+
```
101+
102+
### Pointing Shairport Sync at a room
103+
104+
In the configuration file, name the PCM with the `output_device` setting in the `alsa` section:
105+
106+
```
107+
alsa = {
108+
output_device = "room_front";
109+
};
110+
```
111+
112+
Or on the command line:
113+
114+
```
115+
shairport-sync -o alsa -- -d room_front
116+
```
117+
118+
Give each instance its own room PCM and they will play independently through the one card.
119+
120+
## Doing the split across Docker containers
121+
122+
The scheme above works unchanged on a bare host. In containers -- for example one Shairport Sync instance per container -- three things have to be true inside *each* container:
123+
124+
1. **The ALSA configuration must be visible.** Bind-mount your config read-only, either `/etc/asound.conf` or `~/.asoundrc`.
125+
2. **The device nodes must be reachable.** Pass `--device /dev/snd` (or a device-cgroup rule), and make sure the container's user is in the `audio` group.
126+
3. **A matching `libasound` must be present** in the image so the plugins above can be parsed and loaded.
127+
128+
There is one more, less obvious requirement. A `dshare` device coordinates its clients through **System V IPC** -- the shared-memory segment and semaphore named by `ipc_key`. System V IPC lives in an **IPC namespace**, and by default every container gets its own. So even with the same `ipc_key`, a client in container A and a client in container B land in *different* IPC namespaces, each creates its own private state, and they end up fighting over the card: expect xruns, "device busy", or one container silently winning the device.
129+
130+
The fix is to put the containers in a **shared IPC namespace** so that one `ipc_key` resolves to one IPC object for all of them:
131+
132+
- With `docker run`, give every container `--ipc host`, or start one "owner" and join the rest with `--ipc container:<name>`.
133+
- In Compose, set `ipc: host` (or `ipc: "service:<name>"`) on each service.
134+
135+
Also keep `ipc_key_add_uid false` in each `dshare` definition (as above) and use an explicit `ipc_key`, so every container's client deterministically resolves to the same object regardless of UID.
136+
137+
Finally, `/dev/snd` must refer to the *same* physical card in each container. With host device access (`--device /dev/snd`) it does.
138+
139+
A runnable example combines the ALSA bits above (`/dev/snd`, the `audio` group, the shared `asound.conf`) with the AirPlay 2 essentials from the [companion guide](RunningMultipleInstances.md) -- host networking, a distinct name and port per room, and one shared NQPTP (`ENABLE_NQPTP=0` on the rooms):
140+
141+
```yaml
142+
services:
143+
nqptp: # one shared nqptp for the whole host
144+
image: mikebrady/shairport-sync:latest
145+
entrypoint: ["/usr/local/bin/nqptp"]
146+
network_mode: host
147+
148+
room_front:
149+
image: mikebrady/shairport-sync:latest
150+
network_mode: host # AirPlay discovery, and reaching nqptp
151+
ipc: host # shared /dev/shm for nqptp AND shared IPC for dshare
152+
environment: [ENABLE_NQPTP=0] # use the shared nqptp above
153+
devices:
154+
- "/dev/snd:/dev/snd"
155+
group_add:
156+
- audio
157+
volumes:
158+
- "./asound.conf:/etc/asound.conf:ro"
159+
command: ["-a", "Front Room", "--port=7000", "--", "-d", "room_front"]
160+
depends_on: [nqptp]
161+
162+
room_rear:
163+
image: mikebrady/shairport-sync:latest
164+
network_mode: host
165+
ipc: host
166+
environment: [ENABLE_NQPTP=0]
167+
devices:
168+
- "/dev/snd:/dev/snd"
169+
group_add:
170+
- audio
171+
volumes:
172+
- "./asound.conf:/etc/asound.conf:ro"
173+
command: ["-a", "Rear Room", "--port=7001", "--", "-d", "room_rear"]
174+
depends_on: [nqptp]
175+
```
176+
177+
The equivalent for one room with plain `docker run` (with the shared `nqptp` already running):
178+
179+
```
180+
docker run --network host --ipc host -e ENABLE_NQPTP=0 \
181+
--device /dev/snd --group-add audio \
182+
-v "$PWD/asound.conf:/etc/asound.conf:ro" \
183+
mikebrady/shairport-sync:latest -a "Front Room" --port=7000 -- -d room_front
184+
```
185+
186+
`ipc: host` does double duty here: it shares the host's `/dev/shm` so the rooms read the one nqptp's clock, and it shares the System V IPC namespace so the per-room `dshare` devices resolve to one shared card. The same `asound.conf` (its shared card slave and per-room `dshare` PCMs) goes into every container.
187+
188+
## A note on multiple USB sound cards
189+
190+
If your rooms span several USB cards rather than one multichannel card, do not rely on `hw:0` / `hw:1`: USB devices enumerate in whatever order the kernel probes them, so the indices can change between boots. Pin each card by its *name* and reference it as `hw:CARD=<name>` in its slave -- the name is the one shown by `cat /proc/asound/cards` or `aplay -l`:
191+
192+
```
193+
pcm_slave.card {
194+
pcm "hw:CARD=Device_1,0"
195+
channels 6
196+
rate 44100
197+
}
198+
```
199+
200+
A second card is just a second `pcm_slave` with its own `hw:CARD=...` and its own `ipc_key`; its rooms bind to it exactly the same way. Alternatively, fix each card's index with a `modprobe`/`modules-load.d` option for `snd-usb-audio`. See the [ALSA wiki](https://www.alsa-project.org/wiki/Asoundrc) for the full details.
201+
202+
---
203+
204+
See also [`RunningMultipleInstances.md`](RunningMultipleInstances.md), the companion guide on running several Shairport Sync instances -- one per room -- on a single host.

AIRPLAY2.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ Shairport Sync offers AirPlay 2 support for audio sources on:
5454
- Dolby Atmos is not supported.
5555
- AirPlay 2 for Windows iTunes is not supported.
5656
- AirPlay 2 from macOS prior to 10.15 (Catalina) is not supported.
57-
- Multiple instances of the AirPlay 2 version of Shairport Sync can not be hosted on the same system. It seems that AirPlay 2 clients are confused by having multiple AirPlay 2 players at the same IP addresses.
5857

5958

6059
## General
6160
Shairport Sync uses a companion application called [NQPTP](https://github.com/mikebrady/nqptp) ("Not Quite PTP")
6261
for timing and synchronisation in AirPlay 2. NQPTP must have exclusive access to ports `319` and `320`.
6362

63+
Several AirPlay 2 instances can run on one host — one independent zone per room. See [Running Multiple AirPlay 2 Instances](ADVANCED%20TOPICS/RunningMultipleInstances.md).
64+
6465
## What You Need
6566
For AirPlay 2, a system with the power of a Raspberry Pi B, or better, is recommended.
6667

0 commit comments

Comments
 (0)