Es gibt diese Momente im Linux-Leben, in denen man denkt:
"Warum kann ich nicht einfach gleichzeitig über Headset und Monitor hören?"
Die Antwort lautet: Es geht.
Man muss PipeWire nur ein bisschen überreden.
Das Problem: Mehrere Ausgänge, ein Sound
Typisches Setup:
- USB-Headset für Meetings und Games
- Lautsprecher eines HDMI-Monitor
- vielleicht noch Bluetooth Lautsprecher
Standardmäßig darf immer nur ein Sink (also ein Audio Ausgabegerät) das Standardgerät sein. Was wir aber wollen: Einen kombinierten virtuellen Sink, damit der Ton gleichzeitig an mehrere echte Ausgänge verteilt wird.
PipeWire kann das mit "combine-stream"
PipeWire bringt ein Modul mit, das genau dafür gemacht wurde: libpipewire-module-combine-stream. Dieses Modul erzeugt einen neuen virtuellen Sink, das intern mehrere echte Sinks ansteuert.
Wichtig dabei:
combine.mode = sink==> Wir erstellen ein neues virtuelles Audio Ausgabegerätcombine.latency-compensate = true==> Latenzunterschiede (USB vs. HDMI vs. Bluetooth) werden ausgeglichen (soweit möglich)audio.position = [ FL FR ]==> Stereo Ausgang
Stereo der einfachheit halber. Die meisten Headsets und HDMI-Ausgänge laufen ohnehin nur in 2.0.
Node-Namen herausfinden
Für die eigentliche Konfiguration werden die internen node.name Bezeichnungen der Sinks, also der Audio Ausgänge, benötigt:
pw-link -o | grep -i output
alsa_output.usb-SteelSeries_Arctis_Nova_4X-00.analog-stereo:monitor_FL
alsa_output.usb-SteelSeries_Arctis_Nova_4X-00.analog-stereo:monitor_FR
alsa_output.usb-Generic_USB_Audio-00.HiFi__SPDIF__sink:monitor_FL
alsa_output.usb-Generic_USB_Audio-00.HiFi__SPDIF__sink:monitor_FR
alsa_output.usb-Generic_USB_Audio-00.HiFi__Speaker__sink:monitor_FL
alsa_output.usb-Generic_USB_Audio-00.HiFi__Speaker__sink:monitor_FR
alsa_output.usb-Generic_USB_Audio-00.HiFi__Headphones__sink:monitor_FL
alsa_output.usb-Generic_USB_Audio-00.HiFi__Headphones__sink:monitor_FR
alsa_output.pci-0000_01_00.1.hdmi-stereo:monitor_FL
alsa_output.pci-0000_01_00.1.hdmi-stereo:monitor_FR
bluez_output.4C_1B_86_70_93_63.1:monitor_FL
bluez_output.4C_1B_86_70_93_63.1:monitor_FRKonfiguration anlegen
Zuerst das Konfigurationsverzeichnis erzeugen:
mkdir -p $XDG_CONFIG_HOME/pipewire/pipewire.conf.d/
nano $XDG_CONFIG_HOME/pipewire/pipewire.conf.d/add-combined-sink.confFalls $XDG_CONFIG_HOME nicht gesetzt ist, ist es üblicherweise ~/.config.
Beispiel: Monitor + USB-Headset
context.modules = [
{ name = libpipewire-module-combine-stream
args = {
combine.mode = sink
node.name = "combined_output_2"
node.description = "Combined Output (Monitor / Headset)"
# gleicht Latenzdifferenzen aus (HDMI vs. USB vs. BT)
# Perfekte Synchronität ist physikalisch aber schwer erreichbar
combine.latency-compensate = true
# Kombi-Sink ist Stereo
combine.props = { audio.position = [ FL FR ] }
stream.rules = [
# USB-Headset (SteelSeries)
{ matches = [
{ media.class = "Audio/Sink"
node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_4X-00.analog-stereo" }
]
actions = { create-stream = {
combine.audio.position = [ FL FR ]
audio.position = [ FL FR ]
} }
},
# HDMI-Monitor (NVidia / Stereo)
{ matches = [
{ media.class = "Audio/Sink"
node.name = "alsa_output.pci-0000_01_00.1.hdmi-stereo" }
]
actions = { create-stream = {
combine.audio.position = [ FL FR ]
audio.position = [ FL FR ]
} }
}
]
}
}
]Neustart nicht vergessen
systemctl --user restart pipewire.serviceDanach erscheint ein neues Ausgabegerät mit Namen "Combined Output (Monitor / Headset)" in den Audioeinstellungen.
Erweiterung: Bluetooth
Im nächsten Beispiel werden die beiden obigen Ausgänge um einen zusätzlichen Bluetooth Ausgang erweitert:
context.modules = [
{ name = libpipewire-module-combine-stream
args = {
combine.mode = sink
node.name = "combined_output_3"
node.description = "Combined Output (Monitor / Headset / Bluetooth)"
# gleicht Latenzdifferenzen aus (HDMI vs. USB vs. BT)
# Perfekte Synchronität ist physikalisch aber schwer erreichbar
combine.latency-compensate = true
# Kombi-Sink ist Stereo
combine.props = { audio.position = [ FL FR ] }
stream.rules = [
# USB-Headset (SteelSeries)
{ matches = [
{ media.class = "Audio/Sink"
node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_4X-00.analog-stereo" }
]
actions = { create-stream = {
combine.audio.position = [ FL FR ]
audio.position = [ FL FR ]
} }
},
# HDMI-Monitor (NVidia / Stereo)
{ matches = [
{ media.class = "Audio/Sink"
node.name = "alsa_output.pci-0000_01_00.1.hdmi-stereo" }
]
actions = { create-stream = {
combine.audio.position = [ FL FR ]
audio.position = [ FL FR ]
} }
},
# Bluetooth-Ausgabe
{ matches = [
{ media.class = "Audio/Sink"
node.name = "bluez_output.4C_1B_86_70_93_63.1" }
]
actions = { create-stream = {
combine.audio.position = [ FL FR ]
audio.position = [ FL FR ]
# optional: etwas größere Blockgröße hilft manchen BT-Stacks
# 10,7ms
# node.latency = "512/48000"
# 1s
node.latency = "48000/48000"
} }
}
]
}
}
]Im node.name lässt sich auch RegEx verwenden.
node.name = ~"bluez_output\\..*"Das bedeutet: Alle BlueZ-Ausgänge werden automatisch angesteuert.
Praktisch, wenn der Kopfhörername wechselt oder man einfach mehrere Bluetooth Geräte hat.
Und jetzt Audio Session Management
Bis hierhin haben wir einen neuen Audio-Sink erzeugt. Aber wer entscheidet eigentlich, welche Anwendung welchen Ausgang nutzt? Hier kommt WirePlumber ins Spiel.
PipeWire selbst ist "nur" der Medien-Server. Er verwaltet Nodes, Streams, Ports und Verbindungen.
Aber er entscheidet nicht aktiv:
- Welche Anwendung bekommt welchen Ausgang?
- Was passiert, wenn ein neues Gerät eingesteckt wird?
- Was passiert, wenn ein Gerät verschwindet?
Diese Logik übernimmt der Session Manager. In modernen Distributionen übernimmt das WirePlumber.
Was macht WirePlumber konkret?
WirePlumber beobachtet permanent:
- Neue Streams (z. B. wenn Firefox Ton abspielt)
- Neue Geräte (z. B. wenn ein Bluetooth-Headset verbunden wird)
- Entfernte Geräte
- Manuelle Routing-Änderungen durch den Benutzer
Er trifft daraufhin Entscheidungen. Beispiel:
- Du startest ein Spiel
- Das Spiel erzeugt einen Audio-Stream
- WirePlumber wählt einen geeigneten Sink
- Du ziehst den Stream manuell auf "combined_output"
- WirePlumber merkt sich diese Entscheidung
Beim nächsten Start desselben Spiels wird wieder dieser Sink genutzt.
Wo speichert WirePlumber das?
Die Zustandsdaten liegen typischerweise unter:
~/.local/state/wireplumber/Dort speichert WirePlumber:
- Zuordnungen von Streams zu Sinks
- Default-Geräte
- manuelle Routing-Änderungen
- Metadaten über Geräte
Löscht man dieses Verzeichnis, "vergisst" es alle bisherigen Zuordnungen.
Wichtige Notiz
Änderungen, die in der Desktopumgebung vorgenommen werden, also:
- Standard-Ausgabegerät wechseln
- Lautstärke einzelner Anwendungen ändern
- Streams manuell auf andere Ausgänge ziehen
werden in der Regel ebenfalls unter ~/.local/state/wireplumber/ gespeichert. Das Desktop-Soundpanel spricht über PipeWire mit WirePlumber, und WirePlumber schreibt den Zustand persistent weg.
Wie erkennt WirePlumber eine Anwendung?
Nicht am Fenstertitel und auch nicht am sichtbaren Programmnamen, sondern an Stream-Properties wie:
application.nameapplication.process.binarymedia.role
Deshalb kann es Unterschiede geben zwischen:
- Firefox (native)
- Firefox (Flatpak)
- Chromium
Aus Sicht von WirePlumber sind das unterschiedliche Identities.
Eigene Routing-Regeln definieren
WirePlumber erlaubt es, eigene Regeln zu definieren.
Beispiele für sinnvolle Policies:
- Alles mit
media.role = Communication==> Headset - Alles mit
media.role = Game==> Combined Sink - Alles mit
application.name = VLC==> HDMI
Neuere Versionen von WirePlumber (0.5+) nutzen eine deklarative Konfiguration in ~/.config/wireplumber/. Damit wird Audio-Routing proaktiv steuerbar.
Zusammenspiel mit dem Combined Sink
Technisch betrachtet ist der Combined Sink für WirePlumber einfach ein weiterer Audio-Knoten. Das bedeutet:
- Er kann Standard-Sink sein
- Er kann Ziel für einzelne Anwendungen sein
- Er kann Teil eigener Routing-Regeln werden
Fazit
Mit "wenigen" Zeilen Konfiguration bekommt man:
- parallele Audioausgabe auf mehreren Geräten
- Latenzkompensation
- persistentes Routing pro Anwendung
- vollständige Kontrolle über Audio-Policies
PipeWire kümmert sich um die Signalverarbeitung. WirePlumber kümmert sich um die Entscheidungen.





