add audio-rt module to fight xruns under cpu load
EasyEffects DSP plugins (rnnoise, deepfilternet, crystalizer) drop audio when the system is under load. Layered workaround with per-optimization toggles for bisecting impact: - cgroup-based CPU partitioning: dedicated audio.slice with AllowedCPUs pinning, restricted app/session/background slices for everything else - rlimits so PipeWire's module-rt sets SCHED_FIFO 88 directly instead of going through RTKit's priority-10 ceiling - persistent watcher that re-applies performance governor on audio cores (gamemoded/PPD keep resetting it) - ananicy rule pinning easyeffects to nice -12 for non-RT DSP threads - znver4-tuned rebuilds of easyeffects and its DSP deps Master switch + per-feature toggles via lumpiasty.audioRt.*; all enabled by default on acer host. None of this fully eliminates dropouts on this thermally constrained laptop but each layer is independently testable.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
desktop/plasma.nix
|
||||
desktop/touchpad.nix
|
||||
desktop/pulseaudio.nix
|
||||
desktop/audio-rt.nix
|
||||
desktop/tailscale.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
# Workarounds for audio xruns under CPU load.
|
||||
#
|
||||
# Each optimization is independently toggleable so behavior can be bisected.
|
||||
# `lumpiasty.audioRt.enable` is the master switch; individual sub-flags default
|
||||
# to `true` when the master is on and can be flipped per-host to test impact.
|
||||
|
||||
let
|
||||
cfg = config.lumpiasty.audioRt;
|
||||
|
||||
marchFlags = " -march=znver4 -O3";
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-build-system helpers (see commit history for rationale on LTO choices).
|
||||
# ---------------------------------------------------------------------------
|
||||
withMarch = pkg: pkg.overrideAttrs (old: {
|
||||
env = (old.env or {}) // {
|
||||
NIX_CFLAGS_COMPILE =
|
||||
((old.env or {}).NIX_CFLAGS_COMPILE or old.NIX_CFLAGS_COMPILE or "")
|
||||
+ marchFlags;
|
||||
};
|
||||
});
|
||||
|
||||
cmakePkg = pkg: pkg.overrideAttrs (old: {
|
||||
env = (old.env or {}) // {
|
||||
NIX_CFLAGS_COMPILE =
|
||||
((old.env or {}).NIX_CFLAGS_COMPILE or old.NIX_CFLAGS_COMPILE or "")
|
||||
+ marchFlags;
|
||||
};
|
||||
cmakeFlags = (old.cmakeFlags or []) ++ [ "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON" ];
|
||||
preConfigure = (old.preConfigure or "") + "\nexport AR=gcc-ar\n";
|
||||
});
|
||||
|
||||
rustPkg = pkg: pkg.overrideAttrs (old: {
|
||||
RUSTFLAGS = (old.RUSTFLAGS or "") + " -C target-cpu=znver4";
|
||||
});
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.lumpiasty.audioRt = {
|
||||
enable = lib.mkEnableOption "Audio RT scheduling and CPU isolation";
|
||||
|
||||
audioCpus = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "12-15";
|
||||
description = "CPU list reserved for audio services (systemd cpuset syntax).";
|
||||
};
|
||||
|
||||
nonAudioCpus = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0-11";
|
||||
description = "CPU list for everything else.";
|
||||
};
|
||||
|
||||
# ------ Individual optimization toggles ------
|
||||
|
||||
cpuPartitioning = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.enable;
|
||||
description = ''
|
||||
Cgroup-based CPU partitioning via dedicated audio.slice and
|
||||
restricted app/session/background slices.
|
||||
'';
|
||||
};
|
||||
|
||||
rtLimits = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.enable;
|
||||
description = ''
|
||||
Raise rlimits (RTPRIO=95, MEMLOCK=infinity) for the audio group
|
||||
so PipeWire's module-rt can set SCHED_FIFO 88 directly instead
|
||||
of going through RTKit's priority-10 ceiling.
|
||||
'';
|
||||
};
|
||||
|
||||
performanceGovernor = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.enable;
|
||||
description = ''
|
||||
Keep cpufreq governor `performance` on the audio cores so they
|
||||
stay boosted regardless of measured utilization.
|
||||
'';
|
||||
};
|
||||
|
||||
ananicy = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.enable;
|
||||
description = ''
|
||||
Run ananicy-cpp with a rule that pins easyeffects to nice -12 so
|
||||
its non-RT DSP threads get scheduler preference under load.
|
||||
'';
|
||||
};
|
||||
|
||||
optimisedBinaries = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.enable;
|
||||
description = ''
|
||||
Rebuild easyeffects and its DSP dependencies with -march=znver4 -O3
|
||||
(and LTO for cmake builds, target-cpu for rust builds).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
|
||||
# --- Optimised binary builds ---------------------------------------------
|
||||
(lib.mkIf (cfg.enable && cfg.optimisedBinaries) {
|
||||
nixpkgs.overlays = [
|
||||
(final: prev: {
|
||||
easyeffects = cmakePkg (prev.easyeffects.override {
|
||||
fftw = withMarch prev.fftw;
|
||||
fftwFloat = withMarch prev.fftwFloat;
|
||||
speexdsp = withMarch prev.speexdsp;
|
||||
rubberband = withMarch prev.rubberband;
|
||||
soundtouch = withMarch prev.soundtouch;
|
||||
zita-convolver = withMarch prev.zita-convolver;
|
||||
webrtc-audio-processing = withMarch prev.webrtc-audio-processing;
|
||||
rnnoise = withMarch prev.rnnoise;
|
||||
libebur128 = cmakePkg prev.libebur128;
|
||||
libbs2b = withMarch prev.libbs2b;
|
||||
lilv = withMarch prev.lilv;
|
||||
onetbb = cmakePkg prev.onetbb;
|
||||
calf = cmakePkg prev.calf;
|
||||
lsp-plugins = withMarch prev.lsp-plugins;
|
||||
zam-plugins = withMarch prev.zam-plugins;
|
||||
mda_lv2 = withMarch prev.mda_lv2;
|
||||
deepfilternet = rustPkg prev.deepfilternet;
|
||||
});
|
||||
})
|
||||
];
|
||||
})
|
||||
|
||||
# --- RT scheduling rlimits ----------------------------------------------
|
||||
(lib.mkIf (cfg.enable && cfg.rtLimits) {
|
||||
security.pam.loginLimits = [
|
||||
{ domain = "@audio"; type = "-"; item = "rtprio"; value = "95"; }
|
||||
{ domain = "@audio"; type = "-"; item = "memlock"; value = "unlimited"; }
|
||||
{ domain = "@audio"; type = "-"; item = "nice"; value = "-20"; }
|
||||
];
|
||||
systemd.user.extraConfig = ''
|
||||
DefaultLimitRTPRIO=95
|
||||
DefaultLimitMEMLOCK=infinity
|
||||
'';
|
||||
})
|
||||
|
||||
# --- CPU partitioning (cgroup-based) ------------------------------------
|
||||
#
|
||||
# Cgroup hierarchy under user@.service:
|
||||
# ├── app.slice AllowedCPUs=<nonAudioCpus> (Steam-launched apps)
|
||||
# ├── session.slice AllowedCPUs=<nonAudioCpus> (kwin, plasmashell, kded)
|
||||
# ├── background.slice AllowedCPUs=<nonAudioCpus> (akonadi, polkit)
|
||||
# └── audio.slice AllowedCPUs=<audioCpus> (pipewire, easyeffects)
|
||||
#
|
||||
# Reasoning:
|
||||
# - No isolcpus= : breaks scheduler load balancing on the rest of the system.
|
||||
# - No nohz_full= : amd-pstate can't sample utilization in tickless mode
|
||||
# so cores get clamped at minimum frequency.
|
||||
# - No rcu_nocbs= : microsecond-scale jitter is irrelevant at 21ms quantum.
|
||||
(lib.mkIf (cfg.enable && cfg.cpuPartitioning) {
|
||||
systemd.user.extraConfig = ''
|
||||
CPUAffinity=${cfg.nonAudioCpus}
|
||||
'';
|
||||
systemd.settings.Manager.CPUAffinity = cfg.nonAudioCpus;
|
||||
|
||||
# Delegate the cpuset controller to user managers so user-level slices
|
||||
# can use AllowedCPUs=.
|
||||
systemd.services."user@".serviceConfig.Delegate = "cpu cpuset io memory pids";
|
||||
|
||||
systemd.user.slices = {
|
||||
app.sliceConfig.AllowedCPUs = cfg.nonAudioCpus;
|
||||
session.sliceConfig.AllowedCPUs = cfg.nonAudioCpus;
|
||||
background.sliceConfig.AllowedCPUs = cfg.nonAudioCpus;
|
||||
audio = {
|
||||
description = "Audio services pinned to reserved CPU cores";
|
||||
sliceConfig.AllowedCPUs = cfg.audioCpus;
|
||||
};
|
||||
};
|
||||
|
||||
# easyeffects.service Slice= is set in home-modules/pc.nix.
|
||||
systemd.user.services.pipewire.serviceConfig.Slice = "audio.slice";
|
||||
systemd.user.services.pipewire-pulse.serviceConfig.Slice = "audio.slice";
|
||||
systemd.user.services.wireplumber.serviceConfig.Slice = "audio.slice";
|
||||
})
|
||||
|
||||
# --- Performance governor on audio cores --------------------------------
|
||||
(lib.mkIf (cfg.enable && cfg.performanceGovernor) {
|
||||
systemd.services.audio-cores-performance = {
|
||||
description = "Keep performance governor on audio cores";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
# Expand systemd CPU list ("12-15" / "12,13,14,15") into a flat list.
|
||||
ExecStart = pkgs.writeShellScript "audio-cores-performance" ''
|
||||
cpus=$(echo "${cfg.audioCpus}" | ${pkgs.coreutils}/bin/tr ',' ' ' | \
|
||||
${pkgs.gawk}/bin/awk '{
|
||||
for (i=1; i<=NF; i++) {
|
||||
if (match($i, /^([0-9]+)-([0-9]+)$/, m))
|
||||
for (j=m[1]; j<=m[2]; j++) print j
|
||||
else print $i
|
||||
}
|
||||
}')
|
||||
while true; do
|
||||
for cpu in $cpus; do
|
||||
cur=$(cat /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor)
|
||||
if [ "$cur" != "performance" ]; then
|
||||
echo performance > /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor
|
||||
fi
|
||||
done
|
||||
sleep 2
|
||||
done
|
||||
'';
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
# --- Ananicy rule for easyeffects ---------------------------------------
|
||||
(lib.mkIf (cfg.enable && cfg.ananicy) {
|
||||
services.ananicy = {
|
||||
enable = true;
|
||||
package = pkgs.ananicy-cpp;
|
||||
extraRules = [
|
||||
{ name = "easyeffects"; type = "Audio"; nice = -12; }
|
||||
];
|
||||
};
|
||||
})
|
||||
|
||||
];
|
||||
}
|
||||
@@ -20,13 +20,21 @@
|
||||
# no need to redefine it in your config for now)
|
||||
#media-session.enable = true;
|
||||
|
||||
extraConfig.pipewire."99-quantum" = {
|
||||
"context.properties" = {
|
||||
"default.clock.quantum" = 1024;
|
||||
"default.clock.min-quantum" = 1024;
|
||||
"default.clock.max-quantum" = 8192;
|
||||
};
|
||||
};
|
||||
|
||||
wireplumber.configPackages = [
|
||||
(pkgs.writeTextDir "share/wireplumber/wireplumber.conf.d/99-alsa-nova-3.conf" ''
|
||||
monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_3-00.analog-stereo"
|
||||
node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_7-00.analog-stereo"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
@@ -34,7 +42,7 @@
|
||||
audio.format = "S24LE"
|
||||
audio.rate = 96000
|
||||
api.alsa.period-size = 1024
|
||||
api.alsa.period-num = 4
|
||||
api.alsa.period-num = 8
|
||||
api.alsa.disable-batch = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user