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 :

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:

# 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

Image

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.