Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ADVANCED TOPICS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Here you will find links to some advanced features and things you can do with Sh
* [Metadata](Metadata.md).
* [Events](Events.md).
* [Statistics](Statistics.md).
* [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.
* [Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md) – carve one multichannel ALSA card into several independent stereo outputs, one per room.
* Setting up an [MQTT](../MQTT.md) system.
* [Digital Signal Processing](https://github.com/mikebrady/shairport-sync/wiki/Digital-Signal-Processing-with-Shairport-Sync).
* [Car Installation](../CAR%20INSTALL.md)
Expand Down
117 changes: 117 additions & 0 deletions ADVANCED TOPICS/RunningMultipleInstances.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Running Multiple AirPlay 2 Instances on One Host

Each Shairport Sync process appears on the network as a single AirPlay device. To
offer several independent zones — one per room — from a single machine (bare
metal, a VM, or a set of containers), run **one instance per room**. Each instance
needs a distinct AirPlay identity; Shairport Sync derives that identity from the
instance's **name**, so the setup is minimal.

If you want several rooms driven from a single multichannel sound card, also see
the companion guide [Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).

## The short version

Give each instance its own **name** (`-a` / `general.name`) and its own network
endpoint — a distinct `--port`, a distinct `--address`, or both — and point it at
an output device. Shairport Sync generates a distinct AirPlay 2 identity from the
name, so the instances share one configuration file and differ only on the command
line:

```bash
# one shared configuration file; each room differs only by name, port, device
shairport-sync -c /etc/shairport-sync.conf -a "Kitchen" --port=7000 -- -d room_kitchen
shairport-sync -c /etc/shairport-sync.conf -a "Living Room" --port=7001 -- -d room_livingroom
```

These examples share the host's IP and differ by port; giving each instance its own
`--address` instead (or as well) works the same way. Because the identity comes from
the name, it stays put if a room later moves to a different port or address.
(Everything after `--` is passed to the output backend; `-d` selects its output
device — run `shairport-sync -h` for the options.)

## What makes an instance distinct

1. **A name** — `general.name`, or `-a`. The AirPlay menu label, and the source of
the derived identity. Each instance must have a distinct name.
2. **A socket** — a distinct `--port`, a distinct `--address`, or both. Two
instances can't share one address and port.
3. **A timing clock** — each AirPlay 2 instance needs its own clock from the
companion [NQPTP](https://github.com/mikebrady/nqptp) daemon, addressed by a
shared-memory interface name derived from the instance's name (`/nqptp-<name>`).
4. **An output device** — `alsa.output_device`, or `-- -d <device>`. Usually its
own, though instances can share a device that permits concurrent playback (their
audio is then mixed). If several rooms come off one multichannel card, see
[Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).

## One NQPTP for the whole host

NQPTP is multi-client. Run **one** NQPTP for the machine — it needs exclusive use
of UDP ports 319 and 320 — and it keeps a separate clock per shared-memory
interface name, demultiplexing its clients by the name each one uses. **Do not run
one NQPTP per instance**; they would contend for ports 319/320. The
automatically-derived names give each instance its own clock with no configuration.

## Running in containers

The container case is the same model — a name and a port (or address) per room —
with three deployment details:

* **Networking.** The instances and NQPTP must reach each other, and NQPTP needs
ports 319/320, so run them with host networking (or place them in one shared
network namespace).
* **Shared memory / IPC (`ipc: host`).** NQPTP hands timing to Shairport Sync
through a POSIX shared-memory object (an entry under `/dev/shm`). By default each
Docker container gets its own private `/dev/shm`, so an instance in one container
cannot see the object NQPTP created in another. `ipc: host` (or a shared `ipc:` in
Compose) puts the containers on the same `/dev/shm`, making the timing interface
visible. This is needed whenever NQPTP runs in a separate container from the
instances, regardless of the audio backend — it is **not** specific to sharing a
sound card. (Splitting a single sound card across containers also needs a shared
IPC namespace, for the separate reason that ALSA's `dmix` coordinates through
System V IPC — see the surround-card guide.)
* **Only one NQPTP.** The official image's launcher starts NQPTP *inside every
container it runs*. That is fine for a single instance, but with several rooms
they would all try to start NQPTP and fight over ports 319/320. So run **one**
dedicated NQPTP (the `nqptp` service below) and set **`ENABLE_NQPTP=0`** on each
room so it starts everything it needs *except* its own NQPTP. (Avahi needs no
equivalent — each room runs its own; see the mDNS note below.)

A Compose sketch — one NQPTP sidecar plus two rooms on the shared host IP:

```yaml
services:
nqptp:
# the shairport-sync image also ships nqptp; run it, and nothing else, here
image: mikebrady/shairport-sync:latest
entrypoint: ["/usr/local/bin/nqptp"]
network_mode: host # NQPTP owns UDP 319/320 for the whole host

kitchen:
image: mikebrady/shairport-sync:latest
network_mode: host
ipc: host # share /dev/shm with nqptp
environment: [ENABLE_NQPTP=0] # use the shared nqptp, don't start another
command: ["-a", "Kitchen", "--port=7000", "--", "-d", "room_kitchen"]
depends_on: [nqptp]

livingroom:
image: mikebrady/shairport-sync:latest
network_mode: host
ipc: host
environment: [ENABLE_NQPTP=0]
command: ["-a", "Living Room", "--port=7001", "--", "-d", "room_livingroom"]
depends_on: [nqptp]
```

(Add the audio-device access your output backend needs — for ALSA, `/dev/snd` and
the `audio` group. If the rooms share one card, the container-side ALSA setup,
including the shared-IPC requirement for `dmix`, is covered in
[Splitting a Surround Card into Per-Room Outputs](SplittingASurroundCard.md).)

## A note on mDNS advertisement

Each instance runs its own mDNS responder (Avahi, in the default image). That is
fine: mDNS is a multicast protocol, so several responders coexist on the network,
each advertising its own instance. When the responder can be scoped — for example
by binding Avahi to one interface — each instance advertises on just its own
address, which keeps the AirPlay menu tidy.
204 changes: 204 additions & 0 deletions ADVANCED TOPICS/SplittingASurroundCard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Splitting a Surround Card into Per-Room Stereo Outputs

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.

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.

All of the work here is ALSA configuration. Shairport Sync itself needs no special build -- it just points at the named PCM you create.

## The problem

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.

ALSA provides exactly these pieces:

- **`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.
- **`bindings`** -- inside a `dshare`, map this client's two channels onto chosen channels of the card.
- **`plug`** -- transparently converts rate, format, and channel count so that ordinary stereo sources work.
- **`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`.

You combine these into one named PCM per room.

## General ALSA configuration

Put the following in `/etc/asound.conf` (system-wide) or `~/.asoundrc` (per-user). The two files use identical syntax.

### Define the card once

Define the hardware as a named slave, so every room can refer to it without repeating the device, channel count and rate:

```
pcm_slave.card {
pcm "hw:0,0" # the real multichannel card
channels 6 # the card's output channels (6 for 5.1, 8 for 7.1)
rate 44100
}
```

### One stereo PCM per room

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:

```
pcm.room_front {
type plug
slave.pcm {
type dshare
ipc_key 4242 # same key for every room on this card
ipc_key_add_uid false
slave card
bindings.0 0 # room left -> channel 0 (front left)
bindings.1 1 # room right -> channel 1 (front right)
}
}

pcm.room_center {
type plug
slave.pcm {
type dshare
ipc_key 4242
ipc_key_add_uid false
slave card
bindings.0 2 # -> channel 2 (centre)
bindings.1 3 # -> channel 3 (LFE / subwoofer)
}
}

pcm.room_rear {
type plug
slave.pcm {
type dshare
ipc_key 4242
ipc_key_add_uid false
slave card
bindings.0 4 # -> channel 4 (rear left)
bindings.1 5 # -> channel 5 (rear right)
}
}
```

`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.

> **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.

### Adding rooms on a 7.1 / 8-channel card

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`:

```
pcm.room_side {
type plug
slave.pcm {
type dshare
ipc_key 4242
ipc_key_add_uid false
slave card
bindings.0 6 # -> channel 6 (side left)
bindings.1 7 # -> channel 7 (side right)
}
}
```

### Pointing Shairport Sync at a room

In the configuration file, name the PCM with the `output_device` setting in the `alsa` section:

```
alsa = {
output_device = "room_front";
};
```

Or on the command line:

```
shairport-sync -o alsa -- -d room_front
```

Give each instance its own room PCM and they will play independently through the one card.

## Doing the split across Docker containers

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:

1. **The ALSA configuration must be visible.** Bind-mount your config read-only, either `/etc/asound.conf` or `~/.asoundrc`.
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.
3. **A matching `libasound` must be present** in the image so the plugins above can be parsed and loaded.

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.

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:

- With `docker run`, give every container `--ipc host`, or start one "owner" and join the rest with `--ipc container:<name>`.
- In Compose, set `ipc: host` (or `ipc: "service:<name>"`) on each service.

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.

Finally, `/dev/snd` must refer to the *same* physical card in each container. With host device access (`--device /dev/snd`) it does.

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):

```yaml
services:
nqptp: # one shared nqptp for the whole host
image: mikebrady/shairport-sync:latest
entrypoint: ["/usr/local/bin/nqptp"]
network_mode: host

room_front:
image: mikebrady/shairport-sync:latest
network_mode: host # AirPlay discovery, and reaching nqptp
ipc: host # shared /dev/shm for nqptp AND shared IPC for dshare
environment: [ENABLE_NQPTP=0] # use the shared nqptp above
devices:
- "/dev/snd:/dev/snd"
group_add:
- audio
volumes:
- "./asound.conf:/etc/asound.conf:ro"
command: ["-a", "Front Room", "--port=7000", "--", "-d", "room_front"]
depends_on: [nqptp]

room_rear:
image: mikebrady/shairport-sync:latest
network_mode: host
ipc: host
environment: [ENABLE_NQPTP=0]
devices:
- "/dev/snd:/dev/snd"
group_add:
- audio
volumes:
- "./asound.conf:/etc/asound.conf:ro"
command: ["-a", "Rear Room", "--port=7001", "--", "-d", "room_rear"]
depends_on: [nqptp]
```

The equivalent for one room with plain `docker run` (with the shared `nqptp` already running):

```
docker run --network host --ipc host -e ENABLE_NQPTP=0 \
--device /dev/snd --group-add audio \
-v "$PWD/asound.conf:/etc/asound.conf:ro" \
mikebrady/shairport-sync:latest -a "Front Room" --port=7000 -- -d room_front
```

`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.

## A note on multiple USB sound cards

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`:

```
pcm_slave.card {
pcm "hw:CARD=Device_1,0"
channels 6
rate 44100
}
```

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.

---

See also [`RunningMultipleInstances.md`](RunningMultipleInstances.md), the companion guide on running several Shairport Sync instances -- one per room -- on a single host.
3 changes: 2 additions & 1 deletion AIRPLAY2.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ Shairport Sync offers AirPlay 2 support for audio sources on:
- Dolby Atmos is not supported.
- AirPlay 2 for Windows iTunes is not supported.
- AirPlay 2 from macOS prior to 10.15 (Catalina) is not supported.
- 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.


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

Several AirPlay 2 instances can run on one host — one independent zone per room. See [Running Multiple AirPlay 2 Instances](ADVANCED%20TOPICS/RunningMultipleInstances.md).

## What You Need
For AirPlay 2, a system with the power of a Raspberry Pi B, or better, is recommended.

Expand Down