Add audio hub maintenance guide and root redirect README

README.md: maintenance guide describing how the adac PipeWire audio
hub is wired together — the four audio paths, the five config drop-ins,
services, firmware, and common maintenance tasks. Written as a mental
model for upkeep rather than install/troubleshooting commands.

root-README.md: short redirect telling anyone on the root account to
log in as the `user` account (PipeWire is a per-user service), pointing
to the maintenance guide.

These mirror the files deployed at /home/user/README.md and
/root/README.md on the Pi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-15 07:23:55 -05:00
parent e0a2b8df5b
commit 9507f259d9
2 changed files with 285 additions and 0 deletions

257
README.md Normal file
View file

@ -0,0 +1,257 @@
# 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 <id> 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 <MAC> # IMPORTANT: trust = it auto-reconnects forever
connect <MAC>
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 <MAC> | 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 <MAC>` 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.

28
root-README.md Normal file
View file

@ -0,0 +1,28 @@
# Don't manage audio from the root account
This Pi (`adac`) is a PipeWire audio hub, but **the audio system does not run as
root.** It runs as the **`user`** account (uid 1000). PipeWire is a per-user
service, so `pactl`, `wpctl`, `bluetoothctl`, and `systemctl --user` only work
when you are logged in as `user` — run as root they talk to an empty session and
everything looks broken even when it's fine.
## Log in as `user` instead
```bash
ssh user@adac.local # or: ssh user@10.12.10.110
```
Then read **`/home/user/README.md`** — that's the maintenance guide explaining how
the whole system is wired together and how to keep it running.
## If you are already stuck at a root shell
You can reach the audio session without logging out by prefixing commands:
```bash
sudo -u user XDG_RUNTIME_DIR=/run/user/1000 pactl info
```
But prefer just logging in as `user`. Root is only needed for the handful of
system-level pieces (Bluetooth daemon, Avahi, firmware config in
`/boot/firmware/config.txt`); those are documented in `/home/user/README.md` too.