From cce6596780174b6f26ff27625a5d0e882e1370e2 Mon Sep 17 00:00:00 2001 From: Haavar Valeur Date: Fri, 11 Sep 2026 08:53:42 -0700 Subject: [PATCH] docs: add guides for multi-room AirPlay 2 setups Add two ADVANCED TOPICS guides for building a multi-room system: - RunningMultipleInstances.md: running one AirPlay 2 instance per room on a single host, VM or set of containers. Each instance is given a name and a distinct port; the device id and the nqptp shared-memory name are derived from the name, so several instances share one configuration file and differ only in name, port and output device. Covers the one-shared-nqptp model, sharing one IP vs one IP per room, setting the identity explicitly, and a container example. - SplittingASurroundCard.md: carving one multichannel ALSA card into several independent per-room stereo PCMs with dmix/route/plug, including the shared-IPC requirement for doing the split across containers and a note on pinning USB cards by name. Link both from the ADVANCED TOPICS index, and add a pointer from AIRPLAY2.md. --- ADVANCED TOPICS/README.md | 2 + ADVANCED TOPICS/RunningMultipleInstances.md | 117 +++++++++++ ADVANCED TOPICS/SplittingASurroundCard.md | 204 ++++++++++++++++++++ AIRPLAY2.md | 3 +- 4 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 ADVANCED TOPICS/RunningMultipleInstances.md create mode 100644 ADVANCED TOPICS/SplittingASurroundCard.md diff --git a/ADVANCED TOPICS/README.md b/ADVANCED TOPICS/README.md index 4d96cb79e..8ee3b4ae8 100644 --- a/ADVANCED TOPICS/README.md +++ b/ADVANCED TOPICS/README.md @@ -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) diff --git a/ADVANCED TOPICS/RunningMultipleInstances.md b/ADVANCED TOPICS/RunningMultipleInstances.md new file mode 100644 index 000000000..b49ef60c4 --- /dev/null +++ b/ADVANCED TOPICS/RunningMultipleInstances.md @@ -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-`). +4. **An output device** — `alsa.output_device`, or `-- -d `. 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. diff --git a/ADVANCED TOPICS/SplittingASurroundCard.md b/ADVANCED TOPICS/SplittingASurroundCard.md new file mode 100644 index 000000000..2abf4dc79 --- /dev/null +++ b/ADVANCED TOPICS/SplittingASurroundCard.md @@ -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:`. +- In Compose, set `ipc: host` (or `ipc: "service:"`) 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=` 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. diff --git a/AIRPLAY2.md b/AIRPLAY2.md index 52dc45eec..e18201657 100644 --- a/AIRPLAY2.md +++ b/AIRPLAY2.md @@ -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.