# adac — PipeWire Audio Hub: how it's set up This is the **maintenance guide**: it describes how this machine is *currently wired together* so you can reason about it, not a list of install commands. If you ever need to rebuild from a blank SD card, that's a separate install runbook (`RUNBOOK.md`, kept on the workstation). For day-to-day "is it working / something changed / how do I add a phone", this file is the map. --- ## 0. The one rule: you are `user`, not `root` The whole audio stack runs as **your account, `user` (uid 1000)** — PipeWire is a per-user service. Always do audio work logged in as `user`: ```bash ssh user@adac.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 "animal" (default sink) ──▶ Volt OUT 1/2 ─┤ ├─▶ MIXER ─▶ main out Desktop "animal" ("Desktop 3/4") ─▶ 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 **`adac`**, wired IP **10.12.10.110** (`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 is `alsa_card.usb-Universal_Audio_Volt_476P_22432056005060-00`. - 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)* `animal` selects the published sink **"Volt 476P Pro"**. Lands on the same outputs **1/2** as the phone (shared mixer channel A). ### C. Desktop → Volt outputs 3/4 *(network, separate channel)* `animal` 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" (the June 2026 fault):** 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, task "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`. These *are* the configuration; there are no scripts. To change behaviour you edit one of these 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 + `10.12.10.0/24`) 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 **Your (`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`, managed from root or with `sudo`): - **`bluetooth`** — the BlueZ stack. - **`bt-agent`** — auto-accepts pairing on this headless box (no screen to confirm a PIN). - **`avahi-daemon`** — mDNS; this is what makes `adac.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": - **`/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` — this box is wired-only. **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 "Volt 476P Pro" (sink), "Desktop (Volt 3/4)" (sink), and "Mixer Return (Volt in 3/4)" (source). **Desktop is connected but silent** (the June 2026 fault — 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@animal` 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 `animal`** — 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. Current state & known notes (June 2026) - **Paired phones:** `Pixel 8 Pro` (B8:DB:38:79:90:CC) and `atomic` (A4:42:3B:9A:5F:82). - **Both phones are trusted** (set 2026-06-15), so they auto-reconnect when they return to range with no SSH needed. If you pair a *new* phone, remember to `bluetoothctl trust ` it too — trust is what makes reconnect automatic. - **Remote desktop:** `animal` — appears on this Pi as `Tunnel for user@animal` streams. - Wi-Fi is disabled; if the wired link ever dies the box is unreachable until the cable/switch is fixed.