Reorganize the repo so it can rebuild the audio hub on any Pi, not just the live `adac` box: - README.md: now a project landing page (overview, repo map, deploy summary) instead of the adac-specific maintenance guide. - MAINTENANCE.md: the "how it's wired / day-to-day upkeep" guide, genericized with <pi-host>/<pi-ip>/<lan-cidr>/<desktop-host> placeholders. - config/: the actual PipeWire/WirePlumber drop-ins as deploy-ready templates (<VOLT_SOURCE>/<VOLT_SINK>/<LAN_CIDR> placeholders, with the commands to resolve them in each file's header comment). - systemd/bt-agent.service, firmware/config.txt.snippet: the remaining deployable artifacts. - instances/adac.md: the live deployment's real values (host, IP, Volt serial, paired phones, history) — the one place machine-specific data lives. - RUNBOOK.md: replace the hardcoded LAN subnet with <lan-cidr>. - root-README.md: genericized; .gitignore keeps local harness settings out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
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 <pi-host>, <pi-ip>, <lan-cidr>, <desktop-host> 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:
ssh user@<pi-host>.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
<pi-host>, wired IP<pi-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_<model>_<serial>-00(seeinstances/). - 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 namedmixer_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 sinkdesktop_34("Desktop (Volt 3/4)") that forwards stereo into Volt outputs 3/4 (AUX2/AUX3).
Both loopbacks use
stream.dont-remix = trueso channels 3/4 are hit literally instead of being down-mixed. In30-mixer-return.confthe published side must bemedia.class = Audio/Source(a source the OBS boxes read); in40-desktop-34.confthe side the desktop plays into ismedia.class = Audio/Sink.
~/.config/pipewire/pipewire-pulse.conf.d/
20-network.conf— turns on network audio. Loadsmodule-native-protocol-tcp(lets LAN machines connect, restricted byauth-ip-aclto localhost +<lan-cidr>) andmodule-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 linebluez5.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.confprocess 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):
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'ssystemd/.avahi-daemon— mDNS; this is what makes<pi-host>.localresolve 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.txtholds three deliberate lines:dtparam=audio=offanddtoverlay=vc4-kms-v3d,noaudio— kill onboard + HDMI audio so the Volt is the only card.dtoverlay=disable-wifi— wired-only box. Do not adddisable-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
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)
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.
pactl list sink-inputs | grep -E 'Sink Input #|Mute|media.name'
Find the Tunnel for user@<desktop-host> entry and, if Mute: yes, unmute it:
pactl set-sink-input-mute <id> 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
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
bluetoothctl devices
bluetoothctl info <MAC> | 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/<host>.md (e.g.
instances/adac.md). Update that file when you pair a phone, change the network,
or swap hardware — keep this guide generic.