Audio breaking up over DisplayPort

I have an issue since I switched to a new GPU (RX 7900 XTX, had a 1070 Nvidia before). Whenever GPU’s load is at 100%, the sound starts breaking up. It’s not a typical stutter, but 5+ second breaks every half a minute or so.

I assumed it’s the same error as the one I had over HDMI, so I applied the same solution (commenting wireplumber code suspending audio after 5+ seconds of idle), and it stopped audio from not playing immediatelly after hitting “play”, but it revealed this audio-breaking-up issue. It’s 100% related to GPU and audio over DisplayPort, because it doesn’t happen when using a headset (I won’t be able to check if it happens over HDMI for at least a month - don’t ask, please - but I’ll check if sound cable from sound card to screen speakers input helps sometime over next week).

I understand the GPU’s purpose is to render frames, and if it struggles with that, audio isn’t a priority. Is there a setting I could change to make it a priority? I’d rather have 5 fps less than loosing 20% of a conversation when having a voice call while gaming. It’d be also helpful to get a log letting me pinpoint the cause.

The solution I mentioned:

--- /usr/share/wireplumber/scripts/node/suspend-node.lua	2024-12-03 03:21:41.000000000 +0100
+++ suspend-node.lua	2025-02-02 17:11:17.403779417 +0100
@@ -34,32 +34,32 @@
       sources[id] = nil
     end
 
-    -- Add a timeout source if idle for at least 5 seconds
-    if new_state == "idle" or new_state == "error" then
-      -- honor "session.suspend-timeout-seconds" if specified
-      local timeout =
-          tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
-
-      if timeout == 0 then
-        return
-      end
-
-      -- add idle timeout; multiply by 1000, timeout_add() expects ms
-      sources[id] = Core.timeout_add(timeout * 1000, function()
-        -- Suspend the node
-        -- but check first if the node still exists
-        if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
-          log:info(node, "was idle for a while; suspending ...")
-          node:send_command("Suspend")
-        end
-
-        -- Unref the source
-        sources[id] = nil
-
-        -- false (== G_SOURCE_REMOVE) destroys the source so that this
-        -- function does not get fired again after 5 seconds
-        return false
-      end)
-    end
+    -- -- Add a timeout source if idle for at least 5 seconds
+    -- if new_state == "idle" or new_state == "error" then
+    --   -- honor "session.suspend-timeout-seconds" if specified
+    --   local timeout =
+    --       tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
+    --
+    --   if timeout == 0 then
+    --     return
+    --   end
+    --
+    --   -- add idle timeout; multiply by 1000, timeout_add() expects ms
+    --   sources[id] = Core.timeout_add(timeout * 1000, function()
+    --     -- Suspend the node
+    --     -- but check first if the node still exists
+    --     if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
+    --       log:info(node, "was idle for a while; suspending ...")
+    --       node:send_command("Suspend")
+    --     end
+    --
+    --     -- Unref the source
+    --     sources[id] = nil
+    --
+    --     -- false (== G_SOURCE_REMOVE) destroys the source so that this
+    --     -- function does not get fired again after 5 seconds
+    --     return false
+    --   end)
+    -- end
   end
 }:register ()

Anyone who was looking for the above fix, you need to restart services after:

systemctl --user restart pipewire pipewire-pulse wireplumber

My hardware (inxi -Fazy):

System:
  Kernel: 6.12.11-1-MANJARO arch: x86_64 bits: 64 compiler: gcc v: 14.2.1
    clocksource: tsc avail: hpet,acpi_pm
    parameters: BOOT_IMAGE=/boot/vmlinuz-6.12-x86_64
    root=UUID=f54aa98f-0686-45c4-8bc3-6d95ca77e027 rw quiet
    udev.log_priority=3 nospec_store_bypass_disable tsx=on tsx_async_abort=off
    mitigations=off zswap.enabled=0 amdgpu.ppfeaturemask=0xffffffff
  Desktop: i3 v: 4.24 with: i3bar tools: avail: i3lock,xautolock vt: 7
    dm: LightDM v: 1.32.0 Distro: Manjaro base: Arch Linux
Machine:
  Type: Desktop System: Gigabyte product: A620M GAMING X v: -CF
    serial: <superuser required>
  Mobo: Gigabyte model: A620M GAMING X v: x.x serial: <superuser required>
    uuid: <superuser required> UEFI: American Megatrends LLC. v: F2
    date: 03/10/2023
CPU:
  Info: model: AMD Ryzen 7 7800X3D bits: 64 type: MT MCP arch: Zen 4 gen: 4
    level: v4 note: check built: 2022+ process: TSMC n5 (5nm) family: 0x19 (25)
    model-id: 0x61 (97) stepping: 2 microcode: 0xA601203
  Topology: cpus: 1x dies: 1 clusters: 1 cores: 8 threads: 16 tpc: 2
    smt: enabled cache: L1: 512 KiB desc: d-8x32 KiB; i-8x32 KiB L2: 8 MiB
    desc: 8x1024 KiB L3: 96 MiB desc: 1x96 MiB
  Speed (MHz): avg: 4388 min/max: 400/5050 boost: enabled scaling:
    driver: amd-pstate-epp governor: powersave cores: 1: 4388 2: 4388 3: 4388
    4: 4388 5: 4388 6: 4388 7: 4388 8: 4388 9: 4388 10: 4388 11: 4388 12: 4388
    13: 4388 14: 4388 15: 4388 16: 4388 bogomips: 134205
  Flags: avx avx2 ht lm nx pae sse sse2 sse3 sse4_1 sse4_2 sse4a ssse3 svm
  Vulnerabilities:
  Type: gather_data_sampling status: Not affected
  Type: itlb_multihit status: Not affected
  Type: l1tf status: Not affected
  Type: mds status: Not affected
  Type: meltdown status: Not affected
  Type: mmio_stale_data status: Not affected
  Type: reg_file_data_sampling status: Not affected
  Type: retbleed status: Not affected
  Type: spec_rstack_overflow status: Vulnerable
  Type: spec_store_bypass status: Vulnerable
  Type: spectre_v1 status: Vulnerable: __user pointer sanitization and
    usercopy barriers only; no swapgs barriers
  Type: spectre_v2 status: Vulnerable; IBPB: disabled; STIBP: disabled;
    PBRSB-eIBRS: Not affected; BHI: Not affected
  Type: srbds status: Not affected
  Type: tsx_async_abort status: Not affected
Graphics:
  Device-1: Advanced Micro Devices [AMD/ATI] Navi 31 [Radeon RX 7900 XT/7900
    XTX/7900 GRE/7900M] vendor: ASUSTeK TUF Gaming driver: amdgpu v: kernel
    arch: RDNA-3 code: Navi-3x process: TSMC n5 (5nm) built: 2022+ pcie: gen: 4
    speed: 16 GT/s lanes: 16 ports: active: DP-1 empty: DP-2, DP-3, HDMI-A-1,
    Writeback-1 bus-ID: 03:00.0 chip-ID: 1002:744c class-ID: 0300
  Device-2: SunplusIT USB 2.0 Camera driver: snd-usb-audio,uvcvideo type: USB
    rev: 2.0 speed: 480 Mb/s lanes: 1 mode: 2.0 bus-ID: 5-1.4:8
    chip-ID: 0806:0806 class-ID: 0102 serial: <filter>
  Display: x11 server: X.org v: 1.21.1.15 driver: X: loaded: amdgpu
    unloaded: modesetting,radeon alternate: fbdev,vesa dri: radeonsi gpu: amdgpu
    display-ID: :0 screens: 1
  Screen-1: 0 s-res: 3840x2160 s-size: <missing: xdpyinfo>
  Monitor-1: DP-1 mapped: DisplayPort-0 model: Philips PHL BDM4065
    serial: <filter> built: 2015 res: mode: 3840x2160 hz: 60 scale: 100% (1)
    dpi: 111 gamma: 1.2 size: 878x485mm (34.57x19.09") diag: 1003mm (39.5")
    ratio: 16:9 modes: max: 3840x2160 min: 720x400
  API: Vulkan v: 1.4.303 layers: 14 device: 0 type: discrete-gpu name: AMD
    Radeon RX 7900 XTX (RADV NAVI31) driver: N/A device-ID: 1002:744c
    surfaces: xcb,xlib
  API: OpenGL Message: Unable to show GL data. glxinfo is missing.
  Info: Tools: api: clinfo,vulkaninfo gpu: corectrl wl: swaymsg
    x11: xprop,xrandr
Audio:
  Device-1: Advanced Micro Devices [AMD/ATI] Navi 31 HDMI/DP Audio
    driver: snd_hda_intel v: kernel pcie: gen: 4 speed: 16 GT/s lanes: 16
    bus-ID: 03:00.1 chip-ID: 1002:ab30 class-ID: 0403
  Device-2: Advanced Micro Devices [AMD] Family 17h/19h/1ah HD Audio
    vendor: Gigabyte driver: snd_hda_intel v: kernel pcie: gen: 4 speed: 16 GT/s
    lanes: 16 bus-ID: 10:00.6 chip-ID: 1022:15e3 class-ID: 0403
  Device-3: SunplusIT USB 2.0 Camera driver: snd-usb-audio,uvcvideo type: USB
    rev: 2.0 speed: 480 Mb/s lanes: 1 mode: 2.0 bus-ID: 5-1.4:8
    chip-ID: 0806:0806 class-ID: 0102 serial: <filter>
  Device-4: YZ 2.4G Wireless headset driver: hid-generic,snd-usb-audio,usbhid
    type: USB rev: 1.1 speed: 12 Mb/s lanes: 1 mode: 1.1 bus-ID: 5-2.2:5
    chip-ID: 7545:0897 class-ID: 0300
  API: ALSA v: k6.12.11-1-MANJARO status: kernel-api with: aoss
    type: oss-emulator tools: alsactl,alsamixer,amixer
  Server-1: sndiod v: N/A status: off tools: aucat,midicat,sndioctl
  Server-2: PipeWire v: 1.2.7 status: active with: 1: pipewire-pulse
    status: active 2: wireplumber status: active 3: pipewire-alsa type: plugin
    4: pw-jack type: plugin tools: pactl,pw-cat,pw-cli,wpctl
Network:
  Device-1: Realtek RTL8111/8168/8211/8411 PCI Express Gigabit Ethernet
    vendor: Gigabyte driver: r8169 v: kernel pcie: gen: 1 speed: 2.5 GT/s
    lanes: 1 port: e000 bus-ID: 07:00.0 chip-ID: 10ec:8168 class-ID: 0200
  IF: enp7s0 state: up speed: 1000 Mbps duplex: full mac: <filter>
  Info: services: NetworkManager, systemd-networkd, systemd-timesyncd
Drives:
  Local Storage: total: 2.38 TiB used: 1.26 TiB (52.9%)
  ID-1: /dev/sda maj-min: 8:0 vendor: Samsung model: SSD 840 EVO 120GB
    size: 111.79 GiB block-size: physical: 512 B logical: 512 B speed: 6.0 Gb/s
    tech: SSD serial: <filter> fw-rev: BB6Q scheme: GPT
  SMART Message: Unknown smartctl error. Unable to generate data.
  ID-2: /dev/sdb maj-min: 8:16 vendor: Western Digital
    model: WD20EFRX-68AX9N0 size: 1.82 TiB block-size: physical: 4096 B
    logical: 512 B speed: 6.0 Gb/s tech: N/A serial: <filter> fw-rev: 0A80
    scheme: GPT
  SMART Message: Unknown smartctl error. Unable to generate data.
  ID-3: /dev/sdc maj-min: 8:32 vendor: Crucial model: CT500MX500SSD1
    size: 465.76 GiB block-size: physical: 4096 B logical: 512 B speed: 6.0 Gb/s
    tech: SSD serial: <filter> fw-rev: 023 scheme: MBR
  SMART Message: Unknown smartctl error. Unable to generate data.
Partition:
  ID-1: / raw-size: 103.79 GiB size: 103.74 GiB (99.95%)
    used: 54.67 GiB (52.7%) fs: xfs block-size: 512 B dev: /dev/sda3 maj-min: 8:3
  ID-2: /boot/efi raw-size: 199 MiB size: 198.8 MiB (99.89%)
    used: 152 KiB (0.1%) fs: vfat block-size: 512 B dev: /dev/sda2 maj-min: 8:2
  ID-3: /home raw-size: 1.82 TiB size: 1.79 TiB (98.42%)
    used: 1.07 TiB (59.9%) fs: ext4 block-size: 4096 B dev: /dev/sdb1
    maj-min: 8:17
Swap:
  Kernel: swappiness: 180 (default 60) cache-pressure: 100 (default) zswap: no
  ID-1: swap-1 type: zram size: 31.08 GiB used: 371 MiB (1.2%) priority: 100
    comp: lzo avail: lzo-rle,lz4,lz4hc,zstd,deflate,842 max-streams: 16
    dev: /dev/zram0
Sensors:
  System Temperatures: cpu: 48.1 C mobo: 30.0 C gpu: amdgpu temp: 56.0 C
    mem: 68.0 C
  Fan Speeds (rpm): N/A gpu: amdgpu fan: 0
Info:
  Memory: total: 32 GiB note: est. available: 31.08 GiB used: 8.55 GiB (27.5%)
  Processes: 361 Power: uptime: 2d 21h 12m states: freeze,mem,disk
    suspend: deep avail: s2idle wakeups: 3 hibernate: platform avail: shutdown,
    reboot, suspend, test_resume image: 12.41 GiB services: upowerd
    Init: systemd v: 257 default: graphical tool: systemctl
  Packages: pm: pacman pkgs: 1941 libs: 415 tools: pamac,yay Compilers:
    clang: 19.1.7 gcc: 14.2.1 alt: 12 Shell: Zsh v: 5.9 running-in: tmux:
    inxi: 3.3.37

WirePlumber - ArchWiki

Configuration file layout

WirePlumber’s configuration comprises global PipeWire-flavored JSON objects such as context and alsa_monitor that are modified to change its behavior. The configuration files are read from ~/.config/wireplumber/ (user configuration), /etc/wireplumber/ (global configuration), and then /usr/share/wireplumber/ (stock configuration).

Modifying the configuration

The recommended way to configure WirePlumber is to add a SPA-JSON file to the appropriate wireplumber.conf.d/ directory within /etc/wireplumber/ or ~/.config/wireplumber/

PipeWire - Noticeable audio delay or audible pop/crack when starting playback - ArchWiki

This is caused by node suspension when inactive.

With wireplumber, create a new file to overwrite the default configuration:

/etc/wireplumber/wireplumber.conf.d/51-disable-suspension.conf
( or ~/.config/wireplumber/wireplumber.conf.d/51-disable-suspension.conf)

monitor.alsa.rules = [
  {
    matches = [
      {
        # Matches all sources
        node.name = "~alsa_input.*"
      },
      {
        # Matches all sinks
        node.name = "~alsa_output.*"
      }
    ]
    actions = {
      update-props = {
        session.suspend-timeout-seconds = 0
      }
    }
  }
]
# bluetooth devices
monitor.bluez.rules = [
  {
    matches = [
      {
        # Matches all sources
        node.name = "~bluez_input.*"
      },
      {
        # Matches all sinks
        node.name = "~bluez_output.*"
      }
    ]
    actions = {
      update-props = {
        session.suspend-timeout-seconds = 0
      }
    }
  }
]

Some devices implement their own detection of silence and suspension. For them disabling node suspension alone won’t work. It’s possible to work around them by adding a small amount of noise, making it so the output never goes fully silent:

.../51-disable-suspension.conf

...
    session.suspend-timeout-seconds = 0,  # 0 disables suspend
    dither.method = "wannamaker3", # add dither of desired shape
    dither.noise = 2, # add additional bits of noise
...

It may be necessary to play with dither.noise and dither.method parameters to make it so the noise is sufficiently silent and simultaneously loud enough to prevent detection of silence. See PipeWire documentation

I went through Archwiki before posting, and the only related issue is this one: PipeWire - ArchWiki. But even though journalctl -u rtkit-daemon.service has one entry mentioning a “canary” thread:

Jan 03 13:28:07 ■■■■■■■■■ rtkit-daemon[58156]: The canary thread is apparently starving. Taking action.
Jan 03 13:28:07 ■■■■■■■■■ rtkit-daemon[58156]: Demoting known real-time threads.
Jan 03 13:28:07 ■■■■■■■■■ rtkit-daemon[58156]: Successfully demoted thread 61016 of process 60927.
Jan 03 13:28:07 ■■■■■■■■■ rtkit-daemon[58156]: Successfully demoted thread 61015 of process 60927.
Jan 03 13:28:07 ■■■■■■■■■ rtkit-daemon[58156]: Demoted 2 threads.

…it’s a month old, and the last entry is:

Jan 07 20:41:10 ■■■■■■■■■ systemd[1]: rtkit-daemon.service: Deactivated successfully.

So I assumed it can’t be root of my problem - can it still be the cause, even though sound-breaking-up problem happened e.g. today?

I’ll gladly try the fix, but there’s no directory /etc/systemd/system/rtkit-daemon.service.d on my Manjaro install, and AFAICR I didn’t remove it - do I just create the directory and the override.conf file? I tried editing the unit, but it says:

❯ systemctl edit rtkit-daemon.service
No files found for rtkit-daemon.service.
Run 'systemctl edit --force --full rtkit-daemon.service' to create a new unit.

The directory itself should be there:

ls /etc/systemd/system/

If it were not, I’d expect much more serious issues with your system. :wink:

… That command shown in the output you posted, which you are suggested to run, probably needs sudo (try without first) and will (hopefully) create that file for you.

Yeah, the system directory is there - I was talking about rtkit-daemon.service.d subdirectory inside it.

…but it says it’ll create a new unit, and the fix only mentions override on “Exec” value (adding the --no-canary flag). I have no idea how to create a whole new Systemd unit. :thinking: I’m quite lost, TBH.

Modifying Wireplumber default configuration in /usr/share/wireplumber/ is likely to be overwritten when wireplumber is updated

Archwiki recommendation is to add modifications to wireplumber configuration in /etc/wireplumber/wireplumber.conf.d/ or ~/.config/wireplumber/wireplumber.conf.d/

To create directory

sudo mkdir /etc/systemd/system/rtkit-daemon.service.d/
1 Like

I did that fix, IDK if it took or not - there’s no difference. Also, after reading about the bug a bit more, I finally understood it happens only after wake-up from sleep, so I just hard-rebooted and checked. Same issue. Audio breaking-up when through DisplayPort and GPU under load.

Also, I knew how to make a directory. My question was if I should just create it, or if it should already be there. The answer for anyone else who has this struggle: no, it shouldn’t be there, unless you did some other changes to rtkit-daemon.service in the past. Systemd’s way of changing settings is to create a file in a *.d directory only with lines that require changing - the file can be named whatever.conf, and these files are read in alphabetical order.

systemd - Editing provided units - ArchWiki

Use systemctl cat unit to view the content of a unit file and all associated drop-in snippets.

systemctl cat rtkit-daemon.service

systemd - Drop-in files - ArchWiki

To create drop-in files for the unit file /usr/lib/systemd/system/unit, create the directory /etc/systemd/system/unit.d/ and place .conf files there to override or add new options. systemd will parse and apply these files on top of the original unit.

User should create directory /etc/systemd/system/rtkit-daemon.service.d/ before using sudo systemctl edit rtkit-daemon.service to create /etc/systemd/system/rtkit-daemon.service.d/override.conf

1 Like

Well, at least this confirms I didn’t had the rtkit-daemon.service in the 1st place.

I removed the manually added “fix”, and after creating that directory sudo systemctl edit --force rtkit-daemon.service indeed works, and the override.conf file gets created where it should - thank you. But restarting or cat-ing the service doesn’t work, because:

❯ systemctl --user restart rtkit-daemon.service pipewire.service wireplumber.service
Failed to restart rtkit-daemon.service: Unit rtkit-daemon.service not found.
❯ systemctl cat rtkit-daemon.service
No files found for rtkit-daemon.service.

So as suspected, my issue is lying elsewhere, I guess.