[Pipewire Guide] audio crackling/popping and latency

Some people have issues with audio crackling or latency on Linux and that motivated me to write this guide as I’ve been doing some latency tests and came to very clear conclusions recently.

Pipewire docs: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire
Pipewire-pulse docs: https://docs.pipewire.org/page_module_protocol_pulse.html
Arch wiki: https://wiki.archlinux.org/title/PipeWire

Pipewire and Pulseaudio have countless settings, some of which directly affect your audio output latency as well as other issues such as audio crackling and CPU usage. This guide covers Pipewire specifically since the tendency is for more people to use it than Pulseaudio over time. This guide also covers pipewire-pulse a bit.

Audio latency formula and settings

In /etc/pipewire/pipewire.conf lies the primary configuration file for Pipewire, inside it you have multiple settings. Look for the ones inside context.properties, specifically the settings default.clock.rate, default.clock.quantum, default.clock.min-quantum and default.clock.max-quantum.

Your Pipewire system’s latency is defined by the quantum size and the sample rate. The quantum size serves as a buffer while the sample rate serves as a temporal resolution.

Sound latency (in seconds) can be calculated with this formula:
quantum_size / sample_rate

To measure it in milliseconds, multiply the result by 1000:
quantum_size / sample_rate * 1000

Note that your real sound latency is a bit higher than the calculated value because of extra software overhead.

Min, default and max quantum

The quantum size does not vary between min, default and max over time, instead each application chooses one of the 3 latency settings provided. Web browsers seem to use the maximum quantum size, while WINE seems to use the minimum quantum size.

Setting audio latency and fixing audio crackling

Based on the formula written above, your audio latency is defined by both the quantum size and the sample rate. Most of us use 48KHz or 44.1KHz sample rates and have no real usecase for higher sample rates than that, and so I will assume that you are using 48KHz.

Adjusting your audio latency is as simple as changing the quantum size. Lower values result in lower audio latency but also higher CPU usage and eventually audio crackling if the system cannot keep up. Audio crackling is very common if you are running an application that uses the lowest quantum size while also using a lot of CPU. It can also happen if you are multitasking or
just have slow and old hardware.

Setting a decently low latency that does not result in audio crackling requires that you do some tests, but I have some values I recommend. My general recommendation for audio latency is for it to be equal or lower to 60Hz, which is 16ms. Latency below 20ms is likely to be imperceptible, but just to play safe a latency below 16ms is very decent.

The default min, default and max quantum sizes are exaggerated, as the minimum value is way too low, the default value is a bit too high and the maximum value is also too high.

This is the configuration I currently use:

  default.clock.rate          = 48000
  default.clock.allowed-rates = [ 48000 ]
  default.clock.quantum       = 800
  default.clock.min-quantum   = 512
  default.clock.max-quantum   = 1024

I don’t have a big usecase for really low latency, so I use values below 20ms as reference. I use 512 quantum size for the minimum latency, and 800 for its default latency.

The maximum quantum value is usually used by applications that don’t require audio latency to be used properly, such as web browsers. Because of this, you can choose whatever high quantum size you want.

Change pipewire-pulse too!

Quantum size settings are also available in /etc/pipewire/pipewire-pulse.conf. Scroll down to pulse.properties and find the place where pulse.min.req, pulse.default.req and pulse.min.quantum are, and change the values to your liking. In this case, you provide not only the quantum size but also the sample rate. For example if you want a quantum size of 256 for pulse.min.quantum at 48KHz, set the value to 256/48000.

Thank you !
I had a lot of problems with my bluetooth headset under bazzite, and it seems finaly ok with your configuration !

cd ~/.config/pipewire/pipewire.conf.d$ 

nano 99-bluetooth.conf

context.properties = {
default.clock.rate = 48000
default.clock.allowed-rates = [48000]
default.clock.quantum = 800
default.clock.min-quantum = 512
default.clock.max-quantum = 1024
}

systemctl --user restart pipewire pipewire-pulse wireplumber

thank you!

to get it perfect for me i needed to drop a little lower.

default.clock.quantum = 256
default.clock.min-quantum = 128

Unfortunately, I’m also having this problem (recently started, some time ago no problem) with Endeavour Titan KDE installed on an HP Z1 G8 machine, Intel i7, Nvidia RTX 3060. The audio and video files I produced with Guvcview and Ocenaudio (webcam with integrated microphone) have a background crackling. I followed these instructions, but it didn’t work for me. Does anyone have any other suggestions, please?

I wanted to attach a copy of the modified file but the upload device doesn’t accept it, hoping it’s not against the rules I’ll copy it here.

Thanks for your attention.

Regards

Daemon config file for PipeWire version “1.6.8”

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.conf.d/ for system-wide changes or in

~/.config/pipewire/pipewire.conf.d/ for local changes.

context.properties = {

Configure properties in the system.

#library.name.system = support/libspa-support
#context.data-loop.library.name.system = support/libspa-support
#support.dbus = true
#link.max-buffers = 64
link.max-buffers = 16 # version < 3 clients can’t handle more
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
#clock.power-of-two-quantum = true
#log.level = 2
#cpu.zero.denormals = false
#rlimit.nofile = -1

#loop.rt-prio = -1            # -1 = use module-rt prio, 0 disable rt
#loop.class = data.rt
#thread.affinity = [ 0 1 ]    # optional array of CPUs
#context.num-data-loops = 1   # -1 = num-cpus, 0 = no data loops
#
#context.data-loops = [
#    {   loop.rt-prio = -1
#        loop.class = [ data.rt audio.rt ]
#        #library.name.system = support/libspa-support
#        thread.name = data-loop.0
#        #thread.affinity = [ 0 1 ]    # optional array of CPUs
#    }
#]

core.daemon = true              # listening for socket connections
core.name   = pipewire-0        # core name and socket name

## Properties for the DSP configuration.
#default.clock.rate          = 48000
#default.clock.allowed-rates = [ 48000 ]
#default.clock.quantum       = 256
#default.clock.min-quantum   = 128
#default.clock.max-quantum   = 1024
#default.clock.quantum-limit = 8192
#default.clock.quantum-floor = 4
#default.video.width         = 640
#default.video.height        = 480
#default.video.rate.num      = 25
#default.video.rate.denom    = 1
#
#settings.check-quantum      = false
#settings.check-rate         = false

}

context.properties.rules = [
{ matches = [ { cpu.vm.name = !null } ]
actions = {
update-props = {

These overrides are only applied when running in a vm.

default.clock.min-quantum = 1024
}
}
}
]

context.spa-libs = {

=

Used to find spa factory names. It maps an spa factory name

regular expression to a library name that should contain

that factory.

audio.convert.* = audioconvert/libspa-audioconvert
avb.* = avb/libspa-avb
api.alsa.* = alsa/libspa-alsa
api.v4l2.* = v4l2/libspa-v4l2
api.libcamera.* = libcamera/libspa-libcamera
api.bluez5.* = bluez5/libspa-bluez5
api.vulkan.* = vulkan/libspa-vulkan
api.jack.* = jack/libspa-jack
support.* = support/libspa-support
video.convert.* = videoconvert/libspa-videoconvert
#filter.graph = filter-graph/libspa-filter-graph
#videotestsrc = videotestsrc/libspa-videotestsrc
#audiotestsrc = audiotestsrc/libspa-audiotestsrc
}

context.modules = [
#{ name =

( args = { = … } )

( flags = [ ( ifexists ) ( nofail ) ] )

( condition = [ { = … } … ] )

#}

Loads a module with the given parameters.

If ifexists is given, the module is ignored when it is not found.

If nofail is given, module initialization failures are ignored.

If condition is given, the module is loaded only when the context

properties all match the match rules.

# Uses realtime scheduling to boost the audio thread priorities. This uses
# RTKit if the user doesn't have permission to use regular realtime
# scheduling. You can also clamp utilisation values to improve scheduling
# on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices.
# use module.rt.args = { ... } to override the arguments.
{ name = libpipewire-module-rt
    args = {
        nice.level    = -11
        rt.prio       = 88
        #rt.time.soft = -1
        #rt.time.hard = -1
        #rlimits.enabled = true
        rtportal.enabled = false
        #rtkit.enabled = true
        #uclamp.min = 0
        #uclamp.max = 1024
    }
    flags = [ ifexists nofail ]
    condition = [ { module.rt = !false } ]
}

# The native communication protocol.
{ name = libpipewire-module-protocol-native
    args = {
        # List of server Unix sockets, and optionally permissions
        #sockets = [ { name = "pipewire-0" }, { name = "pipewire-0-manager" } ]
    }
}

# The profile module. Allows application to access profiler
# and performance data. It provides an interface that is used
# by pw-top and pw-profiler.
# use module.profiler.args = { ... } to override the arguments.
{ name = libpipewire-module-profiler
    args = {
        #profile.interval.ms = 0
    }
    condition = [ { module.profiler = !false } ]
}

# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata
    condition = [ { module.metadata = !false } ]
}

# Creates a factory for making devices that run in the
# context of the PipeWire server.
{ name = libpipewire-module-spa-device-factory
    condition = [ { module.spa-device-factory = !false } ]
}

# Creates a factory for making nodes that run in the
# context of the PipeWire server.
{ name = libpipewire-module-spa-node-factory
    condition = [ { module.spa-node-factory = !false } ]
}

# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node
    condition = [ { module.client-node = !false } ]
}

# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device
    condition = [ { module.client-device = !false } ]
}

# The portal module monitors the PID of the portal process
# and tags connections with the same PID as portal
# connections.
{ name = libpipewire-module-portal
    flags = [ ifexists nofail ]
    condition = [ { module.portal = !false } ]
}

# The access module can perform access checks and block
# new clients.
{ name = libpipewire-module-access
    args = {
        # Socket-specific access permissions
        #access.socket = { pipewire-0 = "default", pipewire-0-manager = "unrestricted" }

        # Deprecated legacy mode (not socket-based),
        # for now enabled by default if access.socket is not specified
        #access.legacy = true
    }
    condition = [ { module.access = !false } ]
}

# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter
    condition = [ { module.adapter = !false } ]
}

# Makes a factory for creating links between ports.
# use module.link-factory.args = { ... } to override the arguments.
{ name = libpipewire-module-link-factory
    args = {
        #allow.link.passive = false
}
    condition = [ { module.link-factory = !false } ]
}

# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager
    condition = [ { module.session-manager = !false } ]
}

# Use libcanberra to play X11 Bell
{ name = libpipewire-module-x11-bell
    args = {
        #sink.name = "@DEFAULT_SINK@"
        #sample.name = "bell-window-system"
        #x11.display = null
        #x11.xauthority = null
    }
    flags = [ ifexists nofail ]
    condition = [ { module.x11.bell = !false } ]
}
# The JACK DBus detection module. When jackdbus is started, this
# will automatically make PipeWire become a JACK client.
# use module.jackdbus-detect.args = { ... } to override the arguments.
{ name = libpipewire-module-jackdbus-detect
    args = {
        #jack.library     = libjack.so.0
        #jack.server      = null
        #jack.client-name = PipeWire
        #jack.connect     = true
        #tunnel.mode      = duplex  # source|sink|duplex
        source.props = {
            #audio.channels = 2
	#midi.ports = 1
            #audio.position = [ FL FR ]
            # extra sink properties
        }
        sink.props = {
            #audio.channels = 2
	#midi.ports = 1
            #audio.position = [ FL FR ]
            # extra sink properties
        }
    }
    flags = [ ifexists nofail ]
    condition = [ { module.jackdbus-detect = !false } ]
}

]

context.objects = [
#{ factory =

( args = { = … } )

( flags = [ ( nofail ) ] )

( condition = [ { = … } … ] )

#}

Creates an object from a PipeWire factory with the given parameters.

If nofail is given, errors are ignored (and no object is created).

If condition is given, the object is created only when the context properties

all match the match rules.

#{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } }
#{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
#{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
#{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
#{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false }} }
#{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }

# A default dummy driver. This handles nodes marked with the "node.always-process"
# property when no other driver is currently active. JACK clients need this.
{ factory = spa-node-factory
    args = {
        factory.name    = support.node.driver
        node.name       = Dummy-Driver
        node.group      = pipewire.dummy
        node.sync-group  = sync.dummy
        priority.driver = 200000
        #clock.id       = monotonic # realtime | tai | monotonic-raw | boottime
        #clock.name     = "clock.system.monotonic"
    }
    condition = [ { factory.dummy-driver = !false } ]
}
{ factory = spa-node-factory
    args = {
        factory.name    = support.node.driver
        node.name       = Freewheel-Driver
        priority.driver = 190000
        node.group      = pipewire.freewheel
        node.sync-group  = sync.dummy
        node.freewheel  = true
        #freewheel.wait = 10
    }
    condition = [ { factory.freewheel-driver = !false } ]
}

# This creates a new Source node. It will have input ports
# that you can link, to provide audio for this source.
#{ factory = adapter
#    args = {
#        factory.name     = support.null-audio-sink
#        node.name        = "my-mic"
#        node.description = "Microphone"
#        media.class      = "Audio/Source/Virtual"
#        audio.position   = "FL,FR"
#        monitor.passthrough = true
#    }
#}

# This creates a single PCM source device for the given
# alsa device path hw:0. You can change source to sink
# to make a sink in the same way.
#{ factory = adapter
#    args = {
#        factory.name           = api.alsa.pcm.source
#        node.name              = "alsa-source"
#        node.description       = "PCM Source"
#        media.class            = "Audio/Source"
#        api.alsa.path          = "hw:0"
#        api.alsa.period-size   = 1024
#        api.alsa.headroom      = 0
#        api.alsa.disable-mmap  = false
#        api.alsa.disable-batch = false
#        audio.format           = "S16LE"
#        audio.rate             = 48000
#        audio.channels         = 2
#        audio.position         = "FL,FR"
#    }
#}

# Use the metadata factory to create metadata and some default values.
#{ factory = metadata
#    args = {
#        metadata.name = my-metadata
#        metadata.values = [
#            { key = default.audio.sink   value = { name = somesink } }
#            { key = default.audio.source value = { name = somesource } }
#        ]
#    }
#}

]

context.exec = [
#{ path =

( args = “” | [ … ] )

( condition = [ { = … } … ] )

#}

Execute the given program with arguments.

If condition is given, the program is executed only when the context

properties all match the match rules.

You can optionally start the session manager here,

but it is better to start it as a systemd service.

Run the session manager with -h for options.

#{ path = “/usr/bin/pipewire-media-session” args = “”

condition = [ { exec.session-manager = !false } ] }

You can optionally start the pulseaudio-server here as well

but it is better to start it as a systemd service.

It can be interesting to start another daemon here that listens

on another address with the -a option (eg. -a tcp:4713).

#{ path = “/usr/bin/pipewire” args = [ “-c” “pipewire-pulse.conf” ]

condition = [ { exec.pipewire-pulse = !false } ] }

]