📱 Erkannter Endgerättyp ⛱️ Tag und Nacht. Verbraucht keinen oder einen 🍪. 🖼️ Hintergrund ändern. Verbraucht keinen oder einen 🍪.
🧬 0 Ihre DNS in den Krei.se-DNS-Servern, führt zum Bio-Labor 🍪 0 Anzahl Ihrer gespeicherten Kekse, führt zur Keksdose       

PipeWire-Pulse

Netzwerksound ist etwas Wunderbares, noch schöner ist es, wenn es ohne Neustart von Pulseaudio funktioniert und einfach geht wenn man einen Clienten oder Server neustartet.

Damit auch keine Latenzunterschiede entstehen und quasi alle Geräte von Linux-Servern, Workstations, Android-Clienten und sogar LG-Fernsehern "mitspielen" lohnt es sich den halben Tag zu investieren.

Denn auch Airplay-Clienten und andere Zuspieler sind möglich, mit dem Raspi Pi Pico W bekommt man sogar für 7$ einen Musik-Empfänger und jede RFT-Box aufgebohrt :)

Fundamentales:

Apt:

root@linux:~# apt install pipewire avahi-daemon avahi-utils libspa-0.2-bluetooth
  1. Pipewire/Pulseaudio nutzt in unseren Beispielen viel Netzwerk, daher ist es unerlässlich den Server erst zu starten wenn auch wirklich Netzwerk vorhanden ist. Wir machen das mit einem Service der einen lokalen Router anpingt und dann gestartet alle anderen Dienste als After= dafür erlaubt.

  2. Wir starten Pulseaudio i.d.R. nie als root, auch auf Servern spielt meist ein unpriviligierter Nutzer Musik ab. Für Domänen-Umgebungen ist das gern mal der Hauptnutzer der auch auf anderen Machines / Workstations läuft, daher würde ein Cookie in ~/.config/pulse von >=2 Servern genutzt werden was problematisch wird.

Network-Wait-Service

Die System-targets network-online, etc. stehen für User nicht zur Verfügung und klar, normalerweise hat der User Netzwerk wenn er sich anmeldet, aber auf Servern läuft der User gern mal mit loginctl im Hintergrund und da kann es durchaus das noch keine lokale IP vorhanden ist oder ähnlich.

Für den Nutzer in ~/.config/systemd/user (einfacher) speichern oder für die Machine systemweit in /etc/systemd/user

10.0.0.1 natürlich anpassen, man kann auch einen Host im Internet nutzen.

user-network-wait.service

[Unit]
Description=Wait for Network Connectivity

[Service]
Type=oneshot
ExecStart=/bin/bash -c '[ -f /run/user/$(id -u)/network-online ] || (until ping -c1 10.0.0.1 >/dev/null 2>&1; do sleep 1; done; touch /run/user/$(id -u)/network-online)'

[Install]
WantedBy=default.target

Da die Datei /run/user/123456/network-online beim Neustart gelöscht wird ist hierüber einfach ersichtlich ob schon Netzwerk anliegt und es wird nicht ständig neu gepingt.

user@linux:~# systemctl --user enable user-network-wait.service

Pipewire-Pulse-Service override

Den User-Network-Wait-Service stellen wir jetzt als Abhängigkeit für pipewire-pulse und stellen zudem ein, dass der PULSE_COOKIE direkt in /run/user liegt und nicht in ~/.config/pulse landet - es geht bestimmt noch eleganter als mit meinem ExecStartPre= aber ich war dann auch einfach froh, dass es läuft, denn weder Environment= noch Exec= ließen zu die user-id in eine Env-Variable zu setzen.

Als user die override.conf erstellen geht mit

user@linux:~# systemctl --user edit pipewire-pulse

Dort tragen wir ein, oder speichern als Datei manuell ab:

~/.config/systemd/user/pipewire-pulse.service.d/ override.conf

[Unit]
After=user-network-wait.service avahi-daemon.service

[Service]
ExecStartPre=/bin/bash -c 'systemctl --user set-environment PULSE_COOKIE=/run/user/$(id -u)/pulse/cookie'

Das stellt sicher, dass Pulseaudio immer mit funktionierendem Netzwerk startet und für Zeroconf auch auf Avahi wartet. Zudem ist der Cookie nun für jede Kombination von User und Machine eindeutig.

Pulseaudio für Netzwerk vorbereiten

Na, spüren Sie schon die Vorfreude auf S*nos-like Netzwerksound?

Hier gibt es online 2 Anleitungen die verwirren können, da in beiden Fällen Pulseaudio auf dem Port 4713 lauscht. Allerdings würde eine einfache Angabe des Ports trotzdem eine Authentifizierung per Cookie verlangen.

Wir gehen hier davon aus, dass im LAN alle Maschinen zumindest für Audio vertrauenswürdig sind. Dennoch Vorsicht, denn das erlaubt allen Geräten im LAN Ihre Mikrofone zu nutzen ;d

Server per TCP erreichbar einstellen - erwartet aber Cookie:

/etc/pipewire/pipewire-pulse.conf.d/10-server.conf

pulse.properties = {
    server.address = [
        "unix:native"
        "tcp:4713"
    ]
}

Server per TCP erreichbar und ohne Authentifizierung für bestimmte IP-Bereiche. Achtung, mein /16 hier schaltet 10.0.x.x frei, für 10.0.0.x bitte /24 nutzen oder für 10.x.x.x /8 (auf keinen Fall machen).

/etc/pipewire/pipewire-pulse.conf.d/50-networkparty.conf

context.exec = [
    { path = "pactl" args = "load-module module-native-protocol-tcp auth-anonymous=yes listen=10.0.10.1 auth-ip-acl=127.0.0.1;10.0.0.0/16" }
]

Jetzt können Sie schon Shortcuts erstellen um die Server-Lautstärke, etc. fernzusteuern, z.B.:

user@linux:~# PULSE_SERVER=10.0.10.1 pavucontrol-qt

Zeroconf / Autodiscovery erlauben

/etc/pipewire/pipewire-pulse.conf.d/60-zeroconf.conf

# Discover
context.modules = [
    { name = libpipewire-module-zeroconf-discover
        args = { pulse.latency=400 } # 200 ist standard, für WLAN ausprobieren, 400 funktioniert gut
    }
]

# Das Publish-Modul gibt es nicht als libpipewire-modul, daher manuell mit pactl
context.exec = [
    { path = "pactl"        args = "load-module module-zeroconf-publish" }
]

Avahi

Die Sinks und Sources sollte man nun schon sehen:

user@linux:~# avahi-browse -a
+   eno0 IPv4 user@linux: Remote Sink                  PulseAudio Sound Sink local

Wer keine Lust auf doppelte Sinks/Sources hat - das liegt daran, dass avahi auf IPv4 und v6 läuft.

/etc/avahi/avahi-daemon.conf

[server]
use-ipv6=no

Sammlung praktischer Beispiele

Fixen Tunnel einrichten

/etc/pipewire/pipewire-pulse.conf.d/51-fixedtunnel.conf

sink= legt die remote sink fest, wie die heisst verrät pactl list sinks ("Name: ") oder pactl list sinks short sink_name= legt den Namen für die lokale Sink fest die erstellt wird

# Altes Format, Pulseaudio kompatibel und mit Leerzeichen in der Beschreibung
context.exec = [
    { path = "pactl"        args = "load-module module-tunnel-sink server=10.0.10.1 sink=combine_sink sink_name=combine_sink_on_xyz sink_properties='device.description=\"Fixed Tunnel to XYZ\"'" }
]

2 USB-Sound-Karten zu einer Sink

Hier eine Combosink für 2 digitale USB-Karten.

/etc/pipewire/pipewire-pulse.conf.d/90-combosink.conf

context.modules = [
    { name = libpipewire-module-combine-stream
        args = {
            combine.mode = sink
            node.name = "combine_sink"
            node.description = "Combine Sink"
            combine.latency-compensate = false
            combine.props = {
                audio.position = [ FL FR ]
            }
            stream.props {
                stream.dont-remix = true      # link matching channels without remixing
            }
            stream.rules = [
                {   matches = [
                        {   media.class = "Audio/Sink"
                            node.name = "alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.iec958-stereo"
                        } ]
                    actions = { create-stream = {
                            combine.audio.position = [ FL FL ]
                            audio.position = [ FL FR ]
                    } } }
                {   matches = [
                        {   media.class = "Audio/Sink"
                            node.name = "alsa_output.usb-C-M__C-M__C-M__C-M__C-M__C-M__C-M__C-_USB__USB__USB__USB__USB__USB__USB__US-00.iec958-stereo"

                        } ]
                    actions = { create-stream = {
                            combine.audio.position = [ FR FR ]
                            audio.position = [ FL FR ]
                    } } }
            ]

        }
    }
]

nie genutzte Karten systemweit abschalten

user@linux:~# pactl list cards short

/etc/pipewire/pipewire-pulse.conf.d/99-disableunused.conf

context.exec = [
    # P4000
    { path = "pactl"        args = "set-card-profile alsa_card.pci-0000_05_00.1 off" }
    # OnBoard
    { path = "pactl"        args = "set-card-profile alsa_card.pci-0000_00_1b.0 off" }

]

RTP

--- erhält noch einen Multicast-Blog-Post ---

🚿 Multicast

Wer nun merkt - mmmh, eigentlich soll doch nur ein Server Musik netzwerkweit abspielen landet irgendwann bei RTP und bei einem völlig überlasteten WLAN.

Fundamentales

Lesen Sie bitte den Artikel über Multicast-Traffic im Heimnetzwerk, wir einigen uns hier allein auf die Multicast-Gruppe.

Wir nehmen hier als Beispiel 239.0.1.1 weil unser Server der hauptsächlich sendet 10.0.1.1 ist.

RTP Sender Sink

Damit wir eine Sink haben die überhaupt über RTP gesendet wird erstellen wir eine auf dem Server:

/etc/pipewire/pipewire-pulse.conf.d/70-rtp-sender-sink.conf

context.exec = [
    { path = "pactl"        args = "load-module module-null-sink sink_name=rtp_sender_sink format=s32le channels=2 rate=48000 sink_properties='device.description=\"RTP Sender Sink\"'" }
]

Die brauchen Sie sowieso ständig, daher gleich als Standard einstellen. Wir senden hier immer mit S32LE und 48kHz - das hat bei mir bislang am zuverlässigsten funktioniert. Der Empfänger hat i.d.R. eh S32LE/48kHz überall (zumindest bei mir hat USB-Audio S16LE und Onboard S32LE) und damit bekommt man die Latenz auf etwa 6 millisekunden runter.

Diese Sink senden wir jetzt standardmässig über RTP raus:

/etc/pipewire/pipewire-pulse.conf.d/70-rtp-sender-239011.conf

context.exec = [
    { path = "pactl" args = "load-module module-rtp-send source=rtp_sender_sink.monitor destination_ip=239.0.1.1 port=5004" }
]

Das hat den Vorteil, dass wir MPD oder andere Abspieler nur auf diese Sink zeigen lassen müssen und uns jegliche Neustarts von Pulseaudio sparen können. Auch die Log-Meldungen von Zeroconf was irgendwas nicht findet und die 20 Sekunden Denkpause verschwinden.

Zudem ist dieser dumme MPD-Bug mit Pulse Zeroconf weg, RTP läuft immer, selbst wenn man Pulseaudio mal neustartet geht es automatisch spätestens beim nächsten Track.

RTP Receiver

/etc/pipewire/pipewire-pulse.conf.d/71-rtp-receiver-239011.conf

context.exec = [
    { path = "pactl"        args = "load-module module-rtp-recv sink=combine_sink sap_address=239.0.1.1 latency_msec=12.875000" }
]

Damit empfängt man dann im Client den Stream. Die sink kann sicherlich auch eine andere sein, mit pactl list sinks short schauen wo es ausgegeben werden soll.

Nun zur Latenz: Der Server sendet in meinem Fall Frames mit 6.437500 ptime (packetization time), also ~7ms pro Frame. Diesen Wert bekommt man raus, wenn man mit latency_msec=0 startet - dann wird die ptime genutzt und in journalctl angezeigt:

user@rtp-client:~# journalctl --user -r -u pipewire-pulse
Mar 07 01:02:03 rtp-client pipewire-pulse[6666]: mod.rtp-source: sess.latency.msec 0.000000 cannot be lower than rtp.ptime 6.437500

oder auch

Mar 07 01:02:03 rtp-client pipewire-pulse[6666]: mod.rtp-source: sess.latency.msec 8.000000 should be an integer multiple of rtp.ptime 6.437500

Damit wir es auf allen Clients synchron bekommen kann man einfach latency_msec=0 einstellen und die Daumen drücken. Oder das doppelte von dem Wert eintragen, das ergibt zwar einen Frame Verzögerung (etwa 120hz) also beim Zocken könnte das schon auffallen, aber dann laufen auch Konfigurationen mit komplexeren Ausgaben stabiler.

So oder so ist das weitaus fixer als tunnel-pulse, man hat das Maximum aus dem Protokoll rausgeholt - ich würde mal behaupten mit gut aufgesetztem Netzwerk wird es über Rechnergrenzen hinweg nicht zeitnaher als mit 6,4375 ms.

Optimizing