diff --git a/README.md b/README.md new file mode 100644 index 0000000..48c1bd9 --- /dev/null +++ b/README.md @@ -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 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. diff --git a/root-README.md b/root-README.md new file mode 100644 index 0000000..598ff60 --- /dev/null +++ b/root-README.md @@ -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.