Last active
September 5, 2024 11:03
-
-
Save Kodehawa/edff6856470ad660a0d69416834b09da to your computer and use it in GitHub Desktop.
Low-latency osu pipewire configuration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is the lowest I could get audio latency to work on my system (Ryzen 5 5700X, 32GB 3600MHz RAM, RX 6700). | |
Scavenged as much of the man pages as possible to get a configuration close to what https://blog.thepoon.fr/osuLinuxAudioLatency/ gave. | |
This has given me no crackling nor lag, but it'll depend on your computer. | |
Use https://github.com/NelloKudo/osu-winello, export STAGING_AUDIO_DURATION=24000 and STAGING_AUDIO_PERIOD=12000 to start. | |
Try lower, I've been able to do as low as STAGING_AUDIO_DURATION=3500 and STAGING_AUDIO_PERIOD=7000. | |
To adjust both STAGING_AUDIO_DURATION and STAGING_AUDIO_DURATION, make sure the PERIOD is a multiply of DURATION | |
(3500 * 2 = 7000 duration, 4000 * 2 = 8000). This gives the highest chance of it to work. | |
Expect around -30~-35ms of offset, but that's not latency, that's just a difference on how osu handles audio on windows vs | |
what it ends up doing on linux. | |
The real measurement of latency will be whether your hits and your taps feel "connected". | |
Give yourself the realtime group and install realtime utilities to get realtime priority working. | |
This is IMPORTANT for the audio to not stutter into oblivion. | |
The files below are /etc/pipewire/pipewire-pulse.conf and /etc/pipewire/pipewire.conf | |
Run systemctl --user restart pipewire pipewire-pulse after replacing them. Please remember to give yourself realtime permissions | |
prior to this (see "Adjusting Latency" on https://blog.thepoon.fr/osuLinuxAudioLatency/). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PulseAudio config file for PipeWire version "0.3.77" # | |
# | |
# 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-pulse.conf.d/ for system-wide changes or in | |
# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes. | |
# | |
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-limit = 1024 | |
} | |
context.spa-libs = { | |
audio.convert.* = audioconvert/libspa-audioconvert | |
support.* = support/libspa-support | |
} | |
context.modules = [ | |
{ name = libpipewire-module-rt | |
args = { | |
nice.level = -20 | |
rt.prio = 80 | |
#rt.time.soft = -1 | |
#rt.time.hard = -1 | |
} | |
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-switch-on-connect" } | |
#{ cmd = "load-module" args = "module-gsettings" flags = [ nofail ] } | |
] | |
stream.properties = { | |
node.latency = 96/48000 | |
#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.min.req = 48/48000 # 0.5ms | |
pulse.default.req = 64/48000 # 1.3ms | |
pulse.min.frag = 64/48000 # 1.3ms | |
pulse.default.frag = 6000/48000 # 125ms | |
pulse.default.tlength = 6000/48000 # 125ms | |
pulse.min.quantum = 48/48000 # 1ms | |
pulse.idle.timeout = 0 # don't pause after underruns | |
#pulse.default.format = F32 | |
#pulse.default.position = [ FL FR ] | |
# These overrides are only applied when running in a vm. | |
vm.overrides = { | |
pulse.min.quantum = 1024/48000 # 22ms | |
} | |
} | |
# client/stream specific properties | |
pulse.rules = [ | |
{ | |
matches = [ | |
{ | |
# all keys must match the value. ~ starts regex. | |
client.name = "Firefox" | |
client.name = "plasmashell" | |
#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 = 512/48000 # 10.6ms | |
pulse.min.quantum = 512/48000 # 10.6ms | |
pulse.idle.timeout = 5 # pause after 5 seconds of underrun | |
} | |
} | |
} | |
{ | |
matches = [ { application.process.binary = "Discord" } ] | |
actions = { | |
update-props = { | |
pulse.min.quantum = 1024/48000 # 21ms | |
} | |
} | |
} | |
{ | |
matches = [ { application.process.binary = "libcanberra" } ] | |
actions = { | |
update-props = { | |
pulse.min.quantum = 1024/48000 # 21ms | |
} | |
} | |
} | |
{ | |
matches = [ { application.process.binary = "electron" } ] | |
actions = { | |
update-props = { | |
pulse.min.quantum = 1024/48000 # 21ms | |
} | |
} | |
} | |
#{ | |
# matches = [ { application.process.binary = "Discord" } ] | |
# actions = { quirks = [ block-source-volume ] } | |
#} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Daemon config file for PipeWire version "0.3.77" # | |
# | |
# 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 | |
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 = 64 | |
default.clock.min-quantum = 16 | |
default.clock.max-quantum = 512 | |
default.clock.quantum-limit = 512 | |
#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 | |
# | |
# These overrides are only applied when running in a vm. | |
vm.overrides = { | |
default.clock.min-quantum = 1024 | |
} | |
# 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 | |
} | |
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 | |
#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. | |
{ name = libpipewire-module-rt | |
args = { | |
nice.level = -20 | |
rt.prio = 70 | |
#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 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 = { | |
# 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 | |
} | |
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 = "" | |
#sample.name = "bell-window-system" | |
#x11.display = null | |
#x11.xauthority = null | |
} | |
flags = [ ifexists nofail ] | |
condition = [ { module.x11.bell = 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 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 } } | |
#{ 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-driver" | |
# 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 | |
#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 = 19000 | |
node.group = pipewire.freewheel | |
node.freewheel = true | |
} | |
} | |
# 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" | |
# } | |
#} | |
# 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>" ) | |
# ( 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 } ] } | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment