My ultimate guide to the Raspberry pi audio server I wanted — PipeWire TCP server
Published: 2026-01-01
Part 10 — Replacing a PulseAudio server with PipeWire
Introduction
In a previous article, I explained how to switch a PulseAudio TCP client to PipeWire. This time, we’ll focus on the server side.
To be fully transparent, I haven’t yet migrated my main Raspberry Pi to PipeWire — I first tested everything on an older Raspberry Pi B with headphones to make sure nothing breaks. The goal here is simple: replace PulseAudio with PipeWire on the audio server without breaking anything in the existing setup.
PipeWire itself is the audio engine. It does not speak the PulseAudio protocol by default. pipewire-pulse runs on top of PipeWire to implement the PulseAudio protocol and replace the PulseAudio daemon.
With pipewire-pulse :
- PulseAudio clients will be able to communicate with PipeWire
- TCP connections work exactly like before
- MPD, DNLA, Snapcast, and even bluetooth keep working as before
1. Replace Pulseaudio server by Pipewire, pipewire-pulse and Wireplumber
Let’s start by stopping PulseAudio, installing Pipewire, before definitely removing PulseAudio
$ systemctl --user disable --now pulseaudio.socket pulseaudio.service
$ sudo apt install pipewire-pulse pipewire wireplumber
$ systemctl --user enable --now pipewire.socket pipewire.service pipewire-pulse.socket pipewire-pulse.service wireplumber.service
$ sudo apt purge --auto-remove pulseaudio
Then edit /etc/pipewire/pipewire-pulse.conf or ~/.config/pipewire/pipewire-pulse.conf.
The important parts in this file are:
- uncommented “tcp:4713” in pulse.properties { server.address [ ] }
- new { cmd = “load-module” args = “module-zeroconf-publish” flags = [ ] } in pulse.cmd = [ ]
# PulseAudio config file for PipeWire version "1.2.4" #
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
# It is also possible to place a file with an updated section in
# /etc/pipewire/pipewire-pulse.conf.d/ for system-wide changes or in
# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes.
context.properties = {
## Configure properties in the system.
log.level = 0
}
context.spa-libs = {
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
{ name = libpipewire-module-rt
args = {
nice.level = -11
}
flags = [ ifexists nofail ]
}
{ name = libpipewire-module-protocol-native }
{ name = libpipewire-module-client-node }
{ name = libpipewire-module-adapter }
{ name = libpipewire-module-metadata }
{ name = libpipewire-module-protocol-pulse
args = {
}
}
]
# Extra scripts can be started here. Setup in default.pa can be moved in
# a script or in pulse.cmd below
context.exec = [
]
# Extra commands can be executed here.
# load-module : loads a module with args and flags
# args = "<module-name> <module-args>"
# ( flags = [ nofail ] )
pulse.cmd = [
{ cmd = "load-module" args = "module-always-sink" flags = [ ] }
{ cmd = "load-module" args = "module-device-manager" flags = [ ] }
{ cmd = "load-module" args = "module-device-restore" flags = [ ] }
{ cmd = "load-module" args = "module-stream-restore" flags = [ ] }
{ cmd = "load-module" args = "module-zeroconf-publish" flags = [ ] }
]
stream.properties = {
}
pulse.properties = {
# the addresses this server listens on
server.address = [
"unix:native"
"tcp:4713" \# IPv4 and IPv6 on all addresses
]
}
# client/stream specific properties
pulse.rules = [
]
Since we only modified PipeWire PulseAudio configuration, restarting pipewire-pulse is sufficient here.
$ systemctl --user restart pipewire-pulse.service
$ systemctl --user status pipewire-pulse.service
● pipewire-pulse.service - PipeWire PulseAudio
Loaded: loaded (/usr/lib/systemd/user/pipewire-pulse.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-30 03:12:00 CET; 40min ago
Invocation: 6342cff7ad284f69a96be05adc4f1a13
TriggeredBy: ● pipewire-pulse.socket
Main PID: 554 (pipewire-pulse)
Tasks: 3 (limit: 379)
CPU: 5.335s
CGroup: /user.slice/user-1000.slice/user@1000.service/session.slice/pipewire-pulse.service
└─554 /usr/bin/pipewire-pulse
déc. 30 03:12:00 rasponkyold systemd[519]: Started pipewire-pulse.service - PipeWire PulseAudio.
déc. 30 03:12:09 rasponkyold pipewire-pulse[554]: mod.zeroconf-publish: error id:24 seq:76 res:-2 (Aucun fichier ou dossier de ce nom): enum params id:15 (Spa:Enum:P>
déc. 30 03:12:09 rasponkyold pipewire-pulse[554]: mod.zeroconf-publish: error id:24 seq:78 res:-2 (Aucun fichier ou dossier de ce nom): enum params id:17 (Spa:Enum:P>
déc. 30 03:13:02 rasponkyold pipewire-pulse[554]: mod.zeroconf-publish: error id:44 seq:189 res:-2 (Aucun fichier ou dossier de ce nom): enum params id:7 (Spa:Enum:P
The mod.zeroconf-publish error you can see might happen at startup, but it does not prevent the module from being loaded eventually
$ pactl list modules | grep -A7 zeroconf
Name: module-zeroconf-publish
Argument:
Usage counter: n/a
Properties:
module.author = "Sanchayan Maity <sanchayan@asymptotic.io"
module.description = "mDNS/DNS-SD Service Publish"
module.version = "1.4.2"
Can we check on the pi if our remote clients managed to connect ? Sure, wpctl is a cli interface for WirePlumber:
$ wpctl status
PipeWire 'pipewire-0' [1.4.2, pi@rasponkyold, cookie:554372954]
└─ Clients:
34. pipewire [1.4.2, pi@rasponkyold, pid:784]
35. pipewire [1.4.2, pi@rasponkyold, pid:784]
38. pipewire [1.4.2, pi@rasponkyold, pid:780]
43. WirePlumber [export] [1.4.2, pi@rasponkyold, pid:972]
65. WirePlumber [1.4.2, pi@rasponkyold, pid:972]
67. wpctl [1.4.2, pi@rasponkyold, pid:2325]
83. PipeWire [1.4.2, bobby@bobby-desktop, pid:3297]
Audio
├─ Devices:
│ 95. Audio interne [alsa]
│ 96. Audio interne [alsa]
│
├─ Sinks:
│ * 94. Audio interne Stéréo [vol: 1.00]
│
├─ Sources:
│
├─ Filters:
│
└─ Streams:
41. PipeWire
97. output_FL > bcm2835 Headphones:playback_FL [init]
99. output_FR > bcm2835 Headphones:playback_FR [init]
Video
...
Settings
└─ Default Configured Devices:
0. Audio/Sink alsa_output.platform-2000b840.mailbox.stereo-fallback
# Inspect the Pipewire stream
$ wpctl inspect 41
id 41, type PipeWire:Interface:Node
adapt.follower.spa-node = ""
application.id = "org.pipewire.PipeWire"
application.language = "fr\_FR.UTF-8"
* application.name = "PipeWire"
application.process.binary = "pipewire"
application.process.host = "bobby-desktop" <---
application.process.id = "3297"
application.process.machine-id = "10b867c6818e48b2a437a18888026b69"
application.process.user = "bobby"
application.version = "1.4.9"
* client.api = "pipewire-pulse" <---
* client.id = "83"
clock.quantum-limit = "8192"
* factory.id = "7"
library.name = "audioconvert/libspa-audioconvert"
* media.class = "Stream/Output/Audio"
media.name = "Tunnel for bobby@bobby-desktop" <---
node.autoconnect = "true"
node.dont-reconnect = "true"
node.latency = "1200/48000"
node.loop.name = "data-loop.0"
* node.name = "PipeWire"
node.rate = "1/48000"
node.want-driver = "true"
object.register = "false"
* object.serial = "210"
pipewire.client.access = "restricted"
port.group = "stream.0"
pulse.attr.maxlength = "4194304"
pulse.attr.minreq = "4800"
pulse.attr.prebuf = "9604"
pulse.attr.tlength = "14400"
pulse.corked = "true"
pulse.server.type = "tcp" <---
stream.is-live = "true"
target.object = "alsa_output.platform-2000b840.mailbox.stereo-fallback"
window.x11.display = ":0"
Looks like the desktop reconnected seemlessly on the new pipewire-pulse sink, great !
2. Check server is available on clients
Let’s see on the desktop if it really works: First, we can see our new PipeWire sink is now exposed on zeroconf
$ avahi-browse -a |grep pulse
...
+ enp2s0 IPv4 pi@rasponkyold: Audio interne St__r__o _pulse-sink._tcp local
...
$ pactl list sinks
...
Destination #203
État : SUSPENDED
Nom : tunnel.rasponkyold.local.alsa_output.platform-2000b840.mailbox.stereo-fallback
Description : Audio interne Stéréo on pi@rasponkyold
Pilote : PipeWire
Spécification de l’échantillon : s16le 2ch 48000Hz
Plan des canaux : front-left,front-right
Module du propriétaire : 4294967295
Sourdine : non
Volume : front-left: 65536 / 100% / 0,00 dB, front-right: 65536 / 100% / 0,00 dB
balance 0,00
Volume de base : 65536 / 100% / 0,00 dB
Source du moniteur : tunnel.rasponkyold.local.alsa_output.platform-2000b840.mailbox.stereo-fallback.monitor
Latence : 0 usec, configuré 0 usec
Marqueurs : NETWORK DECIBEL_VOLUME LATENCY
Propriétés :
audio.format = "S16LE"
audio.rate = "48000"
audio.channels = "2"
audio.position = "[FL,FR]"
node.name = "tunnel.rasponkyold.local.alsa_output.platform-2000b840.mailbox.stereo-fallback"
device.description = "Audio interne Stéréo on pi@rasponkyold"
node.virtual = "true"
node.network = "true"
media.class = "Audio/Sink"
media.name = "pulse"
stream.is-live = "true"
node.want-driver = "true"
node.autoconnect = "true"
port.group = "stream.0"
adapt.follower.spa-node = ""
object.register = "false"
factory.id = "7"
clock.quantum-limit = "8192"
node.loop.name = "data-loop.0"
library.name = "audioconvert/libspa-audioconvert"
client.id = "91"
object.id = "75"
object.serial = "203"
Formats :
pcm

Our new PipeWire-pulse in Gnome Audio quick settings
3. Bluetooth
Good news ! Everything from my Bluetooth article from 2020 still applies and works with pipewire-pulse ! Of course, skip the part where you install pulseaudio. Existing PIN-based connections continue to work, on Android at least.
Now WirePlumber automatically manages routing bluetooth audio streams within PipeWire, as we can see below:
$ wpctl status
PipeWire 'pipewire-0' [1.4.2, pi@rasponkyold, cookie:*********]
└─ Clients:
40. pipewire [1.4.2, pi@rasponkyold, pid:573]
73. WirePlumber [export] [1.4.2, pi@rasponkyold, pid:2094]
81. Snapcast [1.4.2, pi@rasponkyold, pid:1993]
89. WirePlumber [1.4.2, pi@rasponkyold, pid:2094]
95. pipewire [1.4.2, pi@rasponkyold, pid:2188]
97. pipewire [1.4.2, pi@rasponkyold, pid:2188]
110. wpctl [1.4.2, pi@rasponkyold, pid:2198]
Audio
├─ Devices:
│ 63. Audio interne [alsa]
│ 90. Audio interne [alsa]
│ 102. Pixel 6a [bluez5]
│
├─ Sinks:
│ * 118. Audio interne Stéréo [vol: 1.00]
│
├─ Sources:
│
├─ Filters:
│
└─ Streams:
45. bluez_input.C8_2A_DD_A7_D5_0D.2
48. output_FR > bcm2835 Headphones:playback_FR [active]
104. output_FL > bcm2835 Headphones:playback_FL [active]
...
Video
...
Settings
└─ Default Configured Devices:
0. Audio/Sink alsa_output.platform-2000b840.mailbox.stereo-fallback
This shows the phone connected as a bluez5 device, with WirePlumber
automatically routing the Bluetooth audio stream to the default ALSA sink.
Conclusion
Replacing a PulseAudio TCP server with PipeWire turned out to be much easier than expected.
Thanks to pipewire-pulse, all existing clients keep working — TCP reconnections, Bluetooth audio, MPD, and others. No app or client needs any modification.
From the outside, nothing really changes, which is perfect when swapping out a core component like the audio server.
Under the hood, though, you now have a modern, flexible PipeWire stack that provides lower latency, better device management, and full PulseAudio compatibility.
This new base will help push the Raspberry Pi multiroom system even further — and that’s exactly what the next article will explore.