pi_audio/MAINTENANCE.md
Me Here f0a07c269d Restructure into a reusable, genericized deploy kit
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>
2026-06-15 08:06:03 -05:00

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 (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 + <lan-cidr>) 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):

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 <pi-host>.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 LingerLinger=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.