Audio Lag on Manjaro (Pipewire)

Recently I noticed that the audio was lagging pretty drastically in my Manjaro installation - while playing Arkham Origins, I noticed that the sound of the fighting wasn’t lining up with the images on screen, and the cutscenes were horribly out of sync as well. I did some other testing (system sounds on Steam, YouTube video, etc) and discovered that in every instance, the audio was significantly behind the action or video.

Figured it gave me an excuse to do a reinstall of my system. Turns out, that did not solve the problem

  • I’m using pipewire
  • I’ve taken the Arch Wiki’s suggestion of a wireplumber file to solve the problem, with no luck
  • I did some tinkering with TLP to prevent audio devices from being put into a powersave mode.

The one place it doesn’t seem to happen is Kodi - I had to play with the audio latency a bit for my bluray’s to be properly synced, however I’ve always had to do that, it doesn’t appear to be as off as everything else.
I’m honestly not sure where to go from here.
Any help would be appreciated.

Try these commands in Terminal:

pw-metadata -n settings 0 default.clock.quantum 2048
pw-metadata -n settings 0 default.clock.min-quantum 2048
pw-metadata -n settings 0 default.clock.max-quantum 2048
pw-metadata -n settings 0 clock.force-quantum 2048

Significant improvement! Everything seems to be much better synced up (of course now I’m paranoid and REALLY paying attention).
are these commands a one time thing, or do I need to do anymore editing to get them to persist through reboots?

are these commands a one time thing

Yes, so:

Made and set these files:

/etc/pipewire/pipewire.conf
Content
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                      = 16
    #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

    #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          = 44100
    clock.force-rate            = 44100
    #default.clock.allowed-rates = [ 48000 ]
    default.clock.quantum       = 2048
    clock.force-quantum         = 2048
    default.clock.min-quantum   = 2048
    default.clock.max-quantum   = 2048
    #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
    node.suspend-on-idle = false

    # keys checked below to disable module loading
    module.x11.bell = true
    # enables autoloading of access module, when disabled an alternative
    # access module needs to be loaded.
    module.access = true
    # enables autoloading of module-jackdbus-detect
    module.jackdbus-detect = true
}

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 = 2048
	    }
        }
    }
]

context.spa-libs = {
    #<factory-name regex> = <library-name>
    #
    # 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
    #videotestsrc   = videotestsrc/libspa-videotestsrc
    #audiotestsrc   = audiotestsrc/libspa-audiotestsrc
}

context.modules = [
    #{ name = <module-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( ifexists ) ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # 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.
    { name = libpipewire-module-rt
        args = {
            nice.level   = -15
            rt.prio     = 88
            #rt.time.soft = -1
            #rt.time.hard = -1
            #uclamp.min = 0
            #uclamp.max = 1024
        }
        flags = [ ifexists nofail ]
    }

    # 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.
    { name = libpipewire-module-profiler }

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

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

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

    # 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 }

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

    # 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 ]
    }

    # 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 = true } ]
    }

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

    # Makes a factory for creating links between ports.
    { name = libpipewire-module-link-factory }

    # Provides factories to make session manager objects.
    { name = libpipewire-module-session-manager }

    # 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 = true } ]
    }
    { 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 = true } ]
    }
]

context.objects = [
    #{ factory = <factory-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # 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 "Spa:Pod:Object: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 } }
    #{ 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"
        }
    }
    { 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
        }
    }

    # 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 = <program-name>
    #    ( args = "<arguments>" | [ <arg1> <arg2> ... ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # 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 = null } { exec.session-manager = true } ] }
    #
    # 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 = null } { exec.pipewire-pulse = true } ] }
]

node.suspend-on-idle = false

/etc/pipewire/pipewire-pulse.conf
Content
context.properties = {
    ## Configure properties in the system.
    #mem.warn-mlock  = false
    mem.allow-mlock = true
    #mem.mlock-all   = false
    #log.level       = 2
    default.clock.quantum = 2048
    default.clock.quantum-limit = 2048
    default.clock.max-quantum = 2048
    default.clock.min-quantum = 2048

}

context.spa-libs = {
    audio.convert.* = audioconvert/libspa-audioconvert
    support.*       = support/libspa-support
}

context.modules = [
    { name = libpipewire-module-rt
        args = {
            nice.level   = -15
            #rt.prio      = 83
            #rt.time.soft = -1
            #rt.time.hard = -1
            #uclamp.min = 0
            #uclamp.max = 1024
        }
        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 = {
	    # contents of pulse.properties can also be placed here
	    # to have config per server.
        }
    }
]

# Extra scripts can be started here. Setup in default.pa can be moved in
# a script or in pulse.cmd below
context.exec = [
    #{ path = "pactl"        args = "load-module module-always-sink" }
    #{ path = "pactl"        args = "upload-sample my-sample.wav my-sample" }
    #{ path = "/usr/bin/sh"  args = "~/.config/pipewire/default.pw" }
]

# 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-switch-on-connect" }
    #{ cmd = "load-module" args = "module-gsettings" flags = [ nofail ] }
]

stream.properties = {
    node.latency          = 2048/44100
    #node.autoconnect      = true
    #resample.quality      = 4
    #channelmix.normalize  = false
    #channelmix.mix-lfe    = true
    #channelmix.upmix      = true
    #channelmix.upmix-method = psd  # none, simple
    #channelmix.lfe-cutoff = 150
    #channelmix.fc-cutoff  = 12000
    #channelmix.rear-delay = 12.0
    #channelmix.stereo-widen = 0.0
    #channelmix.hilbert-taps = 0
    #dither.noise = 0
}

pulse.properties = {
    # the addresses this server listens on
    server.address = [
        "unix:native"
        #"unix:/tmp/something"              # absolute paths may be used
        #"tcp:4713"                         # IPv4 and IPv6 on all addresses
        #"tcp:[::]:9999"                    # IPv6 on all addresses
        #"tcp:127.0.0.1:8888"               # IPv4 on a single address
        #
        #{ address = "tcp:4713"             # address
        #  max-clients = 64                 # maximum number of clients
        #  listen-backlog = 32              # backlog in the server listen queue
        #  client.access = "restricted"     # permissions for clients
        #}
    ]
    #server.dbus-name       = "org.pulseaudio.Server"
    #pulse.allow-module-loading = true
    pulse.min.req          = 2048/44100    # 2.7ms
    pulse.default.req      = 2048/44100     # 20 milliseconds
    pulse.min.frag         = 2048/44100     # 2.7ms
    #pulse.default.frag     = 96000/48000   # 2 seconds
    #pulse.default.tlength  = 96000/48000   # 2 seconds
    pulse.min.quantum      = 2048/44100    # 2.7ms
    pulse.idle.timeout     = 0             # don't pause after underruns
    #pulse.default.format   = F32
    #pulse.default.position = [ FL FR ]
}

pulse.properties.rules = [
    {   matches = [ { cpu.vm.name = !null } ]
        actions = {
            update-props = {
            # These overrides are only applied when running in a vm.
                pulse.min.quantum = 2048/44100      # 22ms
	    }
        }
    }
]

# client/stream specific properties
pulse.rules = [
    {
        matches = [
            {
                # all keys must match the value. ! negates. ~ starts regex.
                #client.name                = "Firefox"
                #application.process.binary = "teams"
                #application.name           = "~speech-dispatcher.*"
            }
        ]
        actions = {
            update-props = {
                #node.latency = 512/48000
            }
            # Possible quirks:"
            #    force-s16-info                 forces sink and source info as S16 format
            #    remove-capture-dont-move       removes the capture DONT_MOVE flag
            #    block-source-volume            blocks updates to source volume
            #    block-sink-volume              blocks updates to sink volume
            #quirks = [ ]
        }
    }
    {
        # skype does not want to use devices that don't have an S16 sample format.
        matches = [
             { application.process.binary = "teams" }
             { application.process.binary = "teams-insiders" }
             { application.process.binary = "skypeforlinux" }
        ]
        actions = { quirks = [ force-s16-info ] }
    }
    {
        # firefox marks the capture streams as don't move and then they
        # can't be moved with pavucontrol or other tools.
        matches = [ { application.process.binary = "firefox" } ]
        actions = { quirks = [ remove-capture-dont-move ] }
    }
    {
        # speech dispatcher asks for too small latency and then underruns.
        matches = [ { application.name = "~speech-dispatcher.*" } ]
        actions = {
            update-props = {
                pulse.min.req          = 2048/44100      # 10.6ms
                pulse.min.quantum      = 2048/44100      # 10.6ms
                pulse.idle.timeout     = 0              # pause after 5 seconds of underrun

        #matches = [ { application.name = "WirePlumber" } ]
        #actions = {
        #    update-props = {
        #        pulse.min.req          = 2048/48000      # 10.6ms
        #        pulse.min.quantum      = 2048/48000      # 10.6ms
        #        pulse.idle.timeout     = 0              # pause after 5 seconds of underrun
        #        default.clock.quantum = 2048

        #matches = [ { application.name = "easyeffects" } ]
        #actions = {
        #    update-props = {
        #        pulse.min.req          = 2048/48000      # 10.6ms
        #        pulse.min.quantum      = 2048/48000      # 10.6ms
        #        pulse.idle.timeout     = 0              # pause after 5 seconds of underrun
        #        default.clock.quantum = 2048
            }
        }
    }
    #{
    #    matches = [ { application.process.binary = "Discord" } ]
    #    actions = { quirks = [ block-source-volume ] }
    #}
]



/etc/pipewire/minimal.conf
Content
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

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

    ## Properties for the DSP configuration.
    default.clock.rate          = 44100
    #default.clock.allowed-rates = [ 48000 ]
    default.clock.quantum       = 2048
    default.clock.min-quantum   = 2048
    default.clock.max-quantum   = 2048
    #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      = true
    #settings.check-rate         = true

    # This config can use udev or hardcoded ALSA devices. Make sure to
    # change the alsa device below when disabling udev
    minimal.use-udev = true

    # Load the pulseaudio emulation daemon
    minimal.use-pulse = true
}

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 = 2048
	    }
        }
    }
]

context.spa-libs = {
    #<factory-name regex> = <library-name>
    #
    # 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
    audio.adapt     = audioconvert/libspa-audioconvert
    api.alsa.*      = alsa/libspa-alsa
    support.*       = support/libspa-support
}

context.modules = [
    #{ name = <module-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( ifexists ) ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # 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.
    #

    # Uses realtime scheduling to boost the audio thread priorities. This uses
    # RTKit if the user doesn't have permission to use regular realtime
    # scheduling.
    { name = libpipewire-module-rt
        args = {
            nice.level   = -15
            rt.prio      = 88
            #rt.time.soft = -1
            #rt.time.hard = -1
        }
        flags = [ ifexists nofail ]
    }

    # The native communication protocol.
    { name = libpipewire-module-protocol-native }

    # The profile module. Allows application to access profiler
    # and performance data. It provides an interface that is used
    # by pw-top and pw-profiler.
    { name = libpipewire-module-profiler }

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

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

    { name = libpipewire-module-spa-device-factory }

    # 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 }

    # The access module can perform access checks and block
    # new clients.
    { name = libpipewire-module-access
        args = {
            # access.allowed to list an array of paths of allowed
            # apps.
            #access.allowed = [
            #    /usr/bin/pipewire-media-session
            #]

            # An array of rejected paths.
            #access.rejected = [ ]

            # An array of paths with restricted access.
            #access.restricted = [ ]

            # Anything not in the above lists gets assigned the
            # access.force permission.
            #access.force = flatpak
        }
    }

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

    # Makes a factory for creating links between ports.
    { name = libpipewire-module-link-factory }

    { name = libpipewire-module-protocol-pulse
      condition = [ { minimal.use-pulse = true } ]
    }
]

pulse.properties = {
    # the addresses this server listens on
    server.address = [
        "unix:native"
    ]
}

stream.properties = {
    adapter.auto-port-config = { mode = dsp }
}

context.objects = [
    #{ factory = <factory-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # Creates an object from a PipeWire factory with the given parameters.
    # If nofail is given, errors are ignored (and no object is created).
    #
    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc "Spa:Pod:Object: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 } }
    #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }

    # Make a default metadata store
    { factory = metadata
        args = {
            metadata.name = default
    #       metadata.values = [
    #            { key = default.audio.sink   value = { name = somesink } }
    #            { key = default.audio.source value = { name = somesource } }
    #        ]
        }
    }

    # 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
            priority.driver = 20000
        }
    }
    { factory = spa-node-factory
        args = {
            factory.name    = support.node.driver
            node.name       = Freewheel-Driver
            priority.driver = 19000
            node.group      = pipewire.freewheel
            node.freewheel  = true
            #freewheel.wait = 10
        }
    }

    # This creates a ALSA udev device that will enumerate all
    # ALSA devices. Because it is using ACP and has the auto-profile
    # property set, this will enable a profile and create associated
    # nodes, which will be automatically configured to their best
    # configuration with the auto-port-config settings.
    # Extra node and device params can be given with node.param and
    # device.param prefixes.
    { factory = spa-device-factory
        args = {
            factory.name           = api.alsa.enum.udev
            alsa.use-acp = true
            device.object.properties = {
                api.acp.auto-profile = true
                api.acp.auto-port = true
                device.object.properties = {
                    node.adapter             = audio.adapt
                    resample.disable         = false
                    adapter.auto-port-config = {
                        mode = dsp
                        monitor = false
                        control = false
                        position = preserve   # unknown, aux
                    }
                    #node.param.Props    = {
                    #    channelVolumes = [ 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.6 ]
                    #}
                }
                #device.param.Profile    = {
                #    #idx = 0
                #    name = pro-audio
                #}
            }
        }
        condition = [ { minimal.use-udev = 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              = "system"
            node.description       = "system"
            media.class            = "Audio/Source"
            api.alsa.path          = "hw:4"
            #api.alsa.period-size   = 0
            #api.alsa.period-num    = 0
            #api.alsa.headroom      = 0
            #api.alsa.start-delay   = 0
            #api.alsa.disable-mmap  = false
            #api.alsa.disable-batch = false
	    #api.alsa.use-chmap     = false
	    #api.alsa.multirate     = true
	    #latency.internal.rate  = 0
	    #latency.internal.ns    = 0
	    #clock.name             = api.alsa.0
            node.suspend-on-idle   = false
            #audio.format           = "S32"
            audio.rate             = 44100
            #audio.allowed-rates    = [ ]
            #audio.channels         = 4
            #audio.position         = [ FL FR RL RR ]
            #resample.quality       = 4
            resample.disable       = true
            #monitor.channel-volumes = false
            #channelmix.normalize   = false
            #channelmix.mix-lfe     = true
            #channelmix.upmix       = true
            #channelmix.upmix-method = psd  # none, simple
            #channelmix.lfe-cutoff  = 150
            #channelmix.fc-cutoff   = 12000
            #channelmix.rear-delay  = 12.0
            #channelmix.stereo-widen = 0.0
            #channelmix.hilbert-taps = 0
            #channelmix.disable     = false
            #dither.noise = 0
            #node.param.Props      = {
            #    params = [
            #        audio.channels 6
            #    ]
            #}
            adapter.auto-port-config = {
                mode = dsp
                monitor = false
                control = false
                position = unknown   # aux, preserve
            }
            #node.param.PortConfig    = {
            #    direction = Output
            #    mode = dsp
            #    format = {
            #        mediaType = audio
            #        mediaSubtype = raw
            #        format = F32
            #        rate = 48000
            #        channels = 4
            #        position = [ FL FR RL RR ]
            #    }
            #}
            #node.param.Props    = {
            #    channelVolumes = [ 0.5 0.4 0.3 0.5 ]
            #}
        }
        condition = [ { minimal.use-udev = false } ]
    }
    { factory = adapter
        args = {
            factory.name           = api.alsa.pcm.sink
            node.name              = "system"
            node.description       = "system"
            media.class            = "Audio/Sink"
            api.alsa.path          = "hw:4"
            #api.alsa.period-size   = 0
            #api.alsa.period-num    = 0
            #api.alsa.headroom      = 0
            #api.alsa.start-delay   = 0
            #api.alsa.disable-mmap  = false
            #api.alsa.disable-batch = false
            #api.alsa.use-chmap     = false
            #api.alsa.multirate     = true
            #latency.internal.rate  = 0
            #latency.internal.ns    = 0
            #clock.name             = api.alsa.0
            node.suspend-on-idle     = false
            #audio.format           = "S32"
            audio.rate             = 44100
            #audio.allowed-rates    = [ ]
            #audio.channels         = 2
            #audio.position         = "FL,FR"
            #resample.quality      = 4
            resample.disable      = true
            #channelmix.normalize  = false
            #channelmix.mix-lfe    = true
            #channelmix.upmix      = true
            #channelmix.upmix-method = psd  # none, simple
            #channelmix.lfe-cutoff = 150
            #channelmix.fc-cutoff  = 12000
            #channelmix.rear-delay = 12.0
            #channelmix.stereo-widen = 0.0
            #channelmix.hilbert-taps = 0
            #channelmix.disable     = false
            #dither.noise = 0
            #node.param.Props      = {
            #    params = [
            #        audio.format  S16
            #    ]
            #}
            adapter.auto-port-config = {
                mode = dsp
                monitor = false
                control = false
                position = unknown   # aux, preserve
            }
            #node.param.PortConfig    = {
            #    direction = Input
            #    mode = dsp
            #    monitor = true
            #    format = {
            #        mediaType = audio
            #        mediaSubtype = raw
            #        format = F32
            #        rate = 48000
            #        channels = 4
            #    }
            #}
            #node.param.Props    = {
            #    channelVolumes = [ 0.5 0.4 0.3 0.5 ]
            #}
        }
        condition = [ { minimal.use-udev = 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
    #        adapter.auto-port-config = {
    #            mode = dsp
    #            monitor = true
    #            position = preserve   # unknown, aux, preserve
    #        }
    #    }
    #}
    # This creates a new link between the source and the virtual
    # source ports.
    #{ factory = link-factory
    #    args = {
    #        link.output.node = system
    #        link.output.port = capture_1
    #        link.input.node  = my-mic
    #        link.input.port  = input_FL
    #    }
    #}
    #{ factory = link-factory
    #    args = {
    #        link.output.node = system
    #        link.output.port = capture_2
    #        link.input.node  = my-mic
    #        link.input.port  = input_FR
    #    }
    #}
]

context.exec = [
    #{   path = <program-name>
    #    ( args = "<arguments>" | [ <arg1> <arg2> ... ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # Execute the given program with arguments.
    #
    # 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" }
]

node.rules = [
    {   matches = [
            {
                # all keys must match the value. ! negates. ~ starts regex.
                #alsa.card_name = "ICUSBAUDIO7D"
                #api.alsa.pcm.stream = "playback"
            }
        ]
        actions = {
            update-props = {
                #node.name = "alsa_playback.ICUSBAUDIO7D"
            }
        }
    }
]
device.rules = [
    {   matches = [
            {
                #alsa.card_name = "ICUSBAUDIO7D"
            }
        ]
        actions = {
            update-props = {
                #device.name = "alsa_card.ICUSBAUDIO7D"
                #api.acp.auto-profile = false
                #api.acp.auto-port = false
                #device.param.Profile    = {
                #    #idx = 0
                #    name = off
                #}
            }
        }
    }
]


/etc/pipewire/client.conf
Content
context.properties = {
    ## Configure properties in the system.
    #mem.warn-mlock  = false
    mem.allow-mlock = true
    #mem.mlock-all   = false
    log.level        = 0

    #default.clock.quantum-limit = 8192
    default.clock.quantum       = 2048
    default.clock.min-quantum   = 2048
    default.clock.max-quantum   = 2048
    settings.check-quantum      = true
}

context.spa-libs = {
    #<factory-name regex> = <library-name>
    #
    # 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
    support.*       = support/libspa-support
}

context.modules = [
    #{ name = <module-name>
    #    ( args  = { <key> = <value> ... } )
    #    ( flags = [ ( ifexists ) ( nofail ) ] )
    #    ( condition = [ { <key> = <value> ... } ... ] )
    #}
    #
    # 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.
    #

    # The native communication protocol.
    { name = libpipewire-module-protocol-native }

    # 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 }

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

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

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

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

filter.properties = {
    node.latency = 2038/44100
}

stream.properties = {
    node.latency          = 2048/44100
    #node.autoconnect      = true
    #resample.quality      = 4
    #channelmix.normalize  = false
    #channelmix.mix-lfe    = true
    #channelmix.upmix      = true
    #channelmix.upmix-method = psd  # none, simple
    #channelmix.lfe-cutoff = 150
    #channelmix.fc-cutoff  = 12000
    #channelmix.rear-delay = 12.0
    #channelmix.stereo-widen = 0.0
    #channelmix.hilbert-taps = 0
    #dither.noise = 0
}

Then in Terminal:

systemctl --user restart pipewire.service pipewire-pulse.service

Should be enough, otherwise, reboot.

2 Likes