# PipeWire Audio Hub: how it's set up (maintenance guide) This describes how a built hub is *wired together* so you can reason about it — not how to install it (that's `RUNBOOK.md`). For day-to-day "is it working / something changed / how do I add a phone", this is the map. Placeholders like ``, ``, ``, `` refer to a specific deployment — see `instances/` for the real values (e.g. `instances/adac.md`). --- ## 0. The one rule: you are `user`, not `root` The whole audio stack runs as the **`user`** account (uid 1000) — PipeWire is a per-user service. Always do audio work logged in as `user`: ```bash ssh user@.local ``` Logged in as `user`, the normal tools just work: `pactl`, `wpctl`, `bluetoothctl`, `systemctl --user`. As **root** they connect to the wrong (empty) session and everything looks dead even when it's healthy — that's the single most common false alarm. Root is only used for system-level pieces (§6). --- ## 1. What this box does (signal flow) It's a small mixer hub. Several sources reach a physical **Universal Audio Volt 4** USB interface, which is patched into an analog mixer; the mixer's main output comes back into the Volt and is re-published on the network for the OBS machines. ``` Phone (Bluetooth) ───────────────────────▶ Volt OUT 1/2 ─┐ Desktop (default sink) ──────────────────▶ Volt OUT 1/2 ─┤ ├─▶ MIXER ─▶ main out Desktop ("Desktop (Volt 3/4)" sink) ─────▶ Volt OUT 3/4 ─┘ │ │ OBS machines ◀── network ◀── Volt IN 3/4 ◀────────────────────┘ ``` - **Wireless:** only the phone (Bluetooth A2DP). Everything else is **wired Ethernet** — Wi-Fi is disabled in firmware (§6). - **The mixer is the heart.** The Pi never mixes in software; it just feeds the Volt's outputs and captures the Volt's inputs. Levels/EQ/mute live on the physical mixer. --- ## 2. The hardware - **Raspberry Pi 4**, hostname ``, wired IP `` (`eth0`). - **Universal Audio Volt 4** over USB, in **Pro Audio** profile so all 4 outputs and 4 inputs appear as plain channels (no surround remap). Its PipeWire card name embeds the unit's serial number: `alsa_card.usb-Universal_Audio_Volt__-00` (see `instances/`). - The Pi's **HDMI and onboard 3.5 mm audio are disabled** (firmware, §6) so the Volt is the *only* sound card. That's why the Volt is always the default. Physical patching you must keep consistent (the software assumes it): | Volt jack | Carries | Patched to | |---|---|---| | **Outputs 1/2** | Phone + desktop default audio | Mixer channel A | | **Outputs 3/4** | Desktop "Desktop (Volt 3/4)" feed | Mixer channel B | | **Inputs 3/4** | Mixer main output (the return) | Mixer main out | > Inputs/outputs **3/4** are used (not 1/2) for the mixer return and the second > desktop feed on purpose: the Volt's inputs 1/2 are the front mic preamps and are > often monitored straight to outputs 1/2, which would create a feedback loop. --- ## 3. The four audio paths (what plays where) Everything the Pi exposes is one of these. Knowing which path you're debugging is half the battle. ### A. Phone → Volt outputs 1/2 *(Bluetooth)* Phone connects over Bluetooth A2DP. WirePlumber routes the phone's audio to the **default sink**, which is the Volt → outputs **1/2** → mixer channel A. No manual routing; the key setting is in `50-bluez.conf` (§4). ### B. Desktop → Volt outputs 1/2 *(network, default)* The desktop selects the published sink **"Volt … Pro"**. Lands on the same outputs **1/2** as the phone (shared mixer channel A). ### C. Desktop → Volt outputs 3/4 *(network, separate channel)* The desktop selects the published sink **"Desktop (Volt 3/4)"** instead. This is a virtual sink (`desktop_34`) that forwards into Volt outputs **3/4** → mixer channel B, so the desktop gets its own fader independent of the phone. This is the path the desktop normally uses. ### D. Mixer main out → OBS machines *(network, the return)* The mixer's main output is patched into Volt **inputs 3/4**. A virtual source `mixer_return` ("Mixer Return (Volt in 3/4)") picks up just those two channels and is published on the network; OBS machines capture it as a **PulseAudio Capture** device. > **"Connected but silent":** if the desktop is connected to a sink but you hear > nothing, the routing is usually fine — check whether the incoming stream is > **muted**. See §7, "Desktop is connected but silent". --- ## 4. The audio config — what each file does All audio behaviour beyond defaults comes from five small drop-in files under `~/.config` (mirrored in this repo's `config/`). These *are* the configuration; there are no scripts. To change behaviour you edit one and restart the relevant service (§5). ### `~/.config/pipewire/pipewire.conf.d/` - **`10-clock-rate.conf`** — locks the whole stack to **48000 Hz** (fixed rate end to end avoids resampling crackle over Bluetooth and the network tunnels). - **`30-mixer-return.conf`** — builds path **D**. A loopback that captures Volt input channels 3/4 (`AUX2`/`AUX3`) and presents them as a clean stereo source named **`mixer_return`**. The Volt's full device name is hardcoded here because it contains the unit's serial number. - **`40-desktop-34.conf`** — builds path **C**. A loopback exposing a virtual sink **`desktop_34`** ("Desktop (Volt 3/4)") that forwards stereo into Volt outputs 3/4 (`AUX2`/`AUX3`). > Both loopbacks use `stream.dont-remix = true` so channels 3/4 are hit literally > instead of being down-mixed. In `30-mixer-return.conf` the published side must be > `media.class = Audio/Source` (a *source* the OBS boxes read); in > `40-desktop-34.conf` the side the desktop plays into is `media.class = Audio/Sink`. ### `~/.config/pipewire/pipewire-pulse.conf.d/` - **`20-network.conf`** — turns on **network audio**. Loads `module-native-protocol-tcp` (lets LAN machines connect, restricted by `auth-ip-acl` to localhost + ``) and `module-zeroconf-publish` (advertises the sinks/sources over mDNS so they auto-appear on other machines). ### `~/.config/wireplumber/wireplumber.conf.d/` - **`50-bluez.conf`** — Bluetooth behaviour for path **A**. Enables SBC-XQ and hardware volume, and the critical line `bluez5.media-source-role = playback`, which makes a connected phone feed the **default sink** (the Volt) instead of showing up as a recording source. Without this, the phone connects but is silent. > A `pipewire -c filter-chain.conf` process also runs (`filter-chain.service`). It > uses the **stock** config in `/usr/share/pipewire/` and does **no custom > processing** — it's a default systemd user unit. Ignore it; there's no EQ/filter > set up here. --- ## 5. Services that keep it running **`user` services** — the audio stack, started at boot via *linger* (so they come up with nobody logged in): ```bash systemctl --user status pipewire pipewire-pulse wireplumber ``` After editing a config file, restart the matching service: | You edited | Restart | |---|---| | `10-clock-rate.conf`, `30-mixer-return.conf`, `40-desktop-34.conf` | `systemctl --user restart pipewire` | | `20-network.conf` | `systemctl --user restart pipewire-pulse` | | `50-bluez.conf` | `systemctl --user restart wireplumber` | When in doubt, restart all three: `systemctl --user restart pipewire pipewire-pulse wireplumber`. **System services** (need `sudo`): - **`bluetooth`** — the BlueZ stack. - **`bt-agent`** — auto-accepts pairing on this headless box (no screen to confirm a PIN). Unit in this repo's `systemd/`. - **`avahi-daemon`** — mDNS; this is what makes `.local` resolve and what carries the network-audio advertisements. --- ## 6. System-level setup (firmware & root pieces) These rarely change but are part of "how it's set up" (full lines in `firmware/config.txt.snippet`): - **`/boot/firmware/config.txt`** holds three deliberate lines: - `dtparam=audio=off` and `dtoverlay=vc4-kms-v3d,noaudio` — kill onboard + HDMI audio so the Volt is the only card. - `dtoverlay=disable-wifi` — wired-only box. **Do not** add `disable-bt` (that kills the phone link). - **Linger** is enabled for `user` (`loginctl show-user user -p Linger` → `Linger=yes`) — that's what starts the audio stack at boot without a login. - Bluetooth tuning lives in **`/etc/bluetooth/main.conf`** (stays discoverable/pairable, auto-reconnect). --- ## 7. Common maintenance tasks All run as **`user`** unless they say `sudo`. **Quick "is it healthy?" check** ```bash systemctl --user is-active pipewire pipewire-pulse wireplumber # all "active" pactl get-default-sink # the Volt pro-output-0 pactl list sinks short # desktop_34 + the Volt pactl list sources short | grep mixer_return # the OBS return exists ``` **Confirm the network feeds are advertised** (what OBS / the desktop discover) ```bash avahi-browse -at | grep -iE 'Volt|Desktop|Mixer Return' ``` You should see the Volt sink ("Volt … Pro"), "Desktop (Volt 3/4)" (sink), and "Mixer Return (Volt in 3/4)" (source). **Desktop is connected but silent** (path B/C) The stream from the desktop can arrive **muted** even though routing is fine. ```bash pactl list sink-inputs | grep -E 'Sink Input #|Mute|media.name' ``` Find the `Tunnel for user@` entry and, if `Mute: yes`, unmute it: ```bash pactl set-sink-input-mute 0 ``` If it keeps coming back muted, the mute is being set **on the desktop** — unmute that output device on the desktop itself. **Pair a new phone** ```bash bluetoothctl power on scan on # find the phone, note its MAC, then: scan off trust # IMPORTANT: trust = it auto-reconnects forever connect quit ``` Then play music on the phone — it should come out mixer channel A (Volt out 1/2). **See which phones are known / connected** ```bash bluetoothctl devices bluetoothctl info | grep -E 'Paired|Trusted|Connected' ``` --- ## 8. Per-instance state Hostnames, IPs, the Volt serial, paired phones, the desktop name, and any deployment-specific history live in **`instances/.md`** (e.g. `instances/adac.md`). Update that file when you pair a phone, change the network, or swap hardware — keep this guide generic.