Balancing Sinks on Linux
A year or two ago I bought a GameDAC, under the impression that it would
allow me to balance the volume between chat and game channels. It was
not to be, as it has always shown up as a Chat Input + Chat Output
to
both pulseaudio and pipewire (audio servers on linux), a fitting tribute
to the poor linux support many gaming peripherals offer. Fortunately, I
was able to benefit from the flexibility linux offers to solve this
problem in a way I think is far more elegant (not to mention free).
Lets get some terminology out of the way. A sink is a fancy word for an output, while an input is called a source. So when you plug your headphones into your computer, it will show up as a sink, and your mic will show up as a source. But you can also create “virtual” sinks, a sort of fake sink that isn’t actually bound to any physical device. In pipewire you can create virtual sinks that route their audio stream to the default sink, i.e your headphones or speakers. So if we create two such sinks, one for games and the other for chat, we can control the volume of each sink relative to the other before it is forwarded to the default sink. The configuration to create these sinks in pipewire is as follows:
# ~/.config/pipewire/pipewire.conf.d/10-game-sinks.conf
context.modules = [ {
name = libpipewire-module-loopback
args = {
audio.position = [ FL FR FC LFE RL RR ]
capture.props = {
media.class = Audio/Sink
node.name = game_sink
node.description = "Virtual game sink"
}
}
} {
name = libpipewire-module-loopback
args = {
audio.position = [ FL FR FC LFE RL RR ]
capture.props = {
media.class = Audio/Sink
node.name = chat_sink
node.description = "Virtual chat sink"
}
}
} ]
this creates two sinks, called “game_sink” and “chat_sink”, that behave in the way described above. However, it’s still not very convenient to have to go into our audio settings each time we want to adjust the volume! A simple script can make this much easier:
#!/usr/bin/env bash
set -e
GAME_SINK='game_sink'
CHAT_SINK='chat_sink'
increase="$1"
function fail() {
printf '%s\\n' "$\*" >&2
exit 1
}
function get_id() {
local name="$1"
pw-cli list-objects Node \\
| rg -B5 'node.name = "'"$name"'"' \\
| head -n 1 \\
| sd '^\\s+id (?P<id>\\d+),.\*' '$id'
}
function get_volume() {
local id="$1"
wpctl get-volume "$id" | awk '{ print $2 }'
}
function get_new_volumes() {
local left right increase
increase="$3"
case "$increase" in
game)
left="$1"
right="$2"
;;
chat)
right="$1"
left="$2"
order='reverse'
;;
*)
fail "no such sink: $increase"
;;
esac
awk -v left="$left" -v right="$right" -v order="$order" '
BEGIN {
# increase left, or decrease right
increment = 0.05
if (left < 1.0) {
left += increment
if (left > 1.0) {
left = 1.0
}
}
else {
right -= increment
if (right < 0) {
right = 0
}
}
# Print the output
if (order == "reverse") {
print right, left
} else {
print left, right
}
}'
}
game_id="$(get_id "$GAME_SINK")"
chat_id="$(get_id "$CHAT_SINK")"
game_volume="$(get_volume "$game_id")"
chat_volume="$(get_volume "$chat_id")"
volumes="$(get_new_volumes "$game_volume" "$chat_volume" "$increase")"
wpctl set-volume "$game_id" "$(printf '%s' "$volumes" | awk '{ print $1 }')" -l 1.0
wpctl set-volume "$chat_id" "$(printf '%s' "$volumes" | awk '{ print $2 }')" -l 1.0
I will likely improve on this over time, as parts of it are pretty
brittle if anything ever changes in wireplumber (the default pipewire
session manager). You can find the source
on sourcehut
if you want the most recent version. When called with mix-sinks $sink
(when $sink
is “game” or “chat”), it will increase the volume of the
indicated sink relative to the other. It does this by increasing that
sink in increments of 5% until it is at 100%, at which time it will
start decreasing the volume of the other sink.
If your window manager supports it, as mine does, you can bind keys to
run this script for the different sinks, such as super+<
and super+>
as I did. I even configured the rotary encoder on my keyboard to send
these keys, so I can still get the satisfaction of turning a physical
dial to balance the volume as I play. Only now I don’t need to reach
over to some external device, it’s right there on my keyboard!