billypom

Valve proton audio fix for Linux audio interface users

2025-03-27

The cross-section in the venn-diagram of music producers, gamers, and linux users is likely small, but this post may be helpful to someone out there.

If you have a ~fancy audio interface and can't hear audio from games running under Valve's proton compatibility layer - this post may help you!

The Problem


  • No audio would play for steam games launched via Proton.
  • I have an audio interface with more than 2 outputs

Observations


  • I opened qpwgraph/coppwr and watched what was happening when launching a proton application; a pipewire audio node was being created and immediately deleted.
  • If I changed my default audio device to something like the built in speakers on my DisplayPort monitor, then launched the app, the proton audio node would attach to the DP audio with no issue.

I figure that the audio process that spawns doesn't know how to handle being given so many outputs to choose from. It just freaks out, and closes itself.

I bet I could make some kind of intermediate node that the application audio could latch onto, then I could have the middle-man node always send audio to my soundcard. 🤔

Application -> Intermediate Node -> Actual Audio Card

That is exactly what this post will be explaining - let's get started.

Environment


Please take note of my environment, as your setup may differ from mine

  • OS: Debian 12
  • Audio Server: PulseAudio 15.0.0 on PipeWire 1.2.7
  • Audio Session Manager: Wireplumber 0.4.13
  • Audio Card: Focusrite Scarlett 18i20
# to find what Audio Server you're running
pactl info

# is what it says
wireplumber --version

The Details


We're going to create 2 scripts.

  • One for creating the intermediate node
  • One for destroying the node.

1. Creating a pipewire node

pw-cli create-node adapter '{ factory.name=support.null-audio-sink node.name=pomeranian-sink media.class=Audio/Sink object.linger=true audio.position=[FL FR] audio.sample_rate=48000 }'
  • pw-cli - The command we're using to interact with pipewire/pulseaudio
  • create-node - Action to create a new audio device/node
  • factory.name=support.null-audio-sink - Creates a null audio sink
  • node.name=pomeranian-sink - Name the sink whatever you would like
  • media.class=Audio/Sink - Specifies the type of media (audio, video) and what type of node (source, sink) are being created
  • object.linger=true - Keeps the node active (otherwise it would delete itself when nothing is attached)
  • audio.position[FL FR] - Specifies positional audio for multichannel usage. We only need Front Left and Front Right
  • audio.sample_rate=48000 - Specifies the sample rate, I use 48khz

2. Set the default audio sink to the node we just created

pactl set-default-sink pomeranian-sink

3. Link the output of the intermediate node to the playback output of our audio card

Before we can do this properly, we will need 3 pieces of information about the target audio card

  1. Name (top level attribute) - which can be found using this command:
pactl list short sinks
  1. api.alsa.pcm.stream (inside Properties)
  2. audio.position (inside Properties) Both of which can be found under the Properties section, when using this command:
pactl list sinks

Alternatively, you could use a GUI application like qpwgraph or coppwr to visually confirm the names of these attributes. The properties are visible in the below screenshot.

api.alsa.pcm.stream = "playback"

audio.position = "AUX0" (Left) & "AUX1" (Right)

Here are the two commands that I would need to run. Adjust them as necessary for your use case

# left
pw-link pomeranian-sink:monitor_FL alsa_output.usb-Focusrite_Scarlett_18i20_USB-00.playback.0.0:playback_AUX0
# right
pw-link pomeranian-sink:monitor_FR alsa_output.usb-Focusrite_Scarlett_18i20_USB-00.playback.0.0:playback_AUX1

Let's put it all together:

Scripts


Creation script

#!/bin/bash
pw-cli create-node adapter '{ factory.name=support.null-audio-sink node.name=pomeranian-sink media.class=Audio/Sink object.linger=true audio.position=[FL FR] audio.sample_rate=48000 }'
pactl set-default-sink pomeranian-sink
scarlett_sink=`pactl list short sinks | grep Focusrite | awk '{print $2}'`
pw-link pomeranian-sink:monitor_FL $scarlett_sink:playback_AUX0
pw-link pomeranian-sink:monitor_FR $scarlett_sink:playback_AUX1
echo "1. New default sink is <pomeranian-sink>"
echo "2. To cleanup, type: 'proton-cleanup'"

Destruction script

#!/bin/bash
scarlett_sink=$(pactl list short sinks | grep Focusrite | awk '{print $2}')
pw-cli destroy pomeranian-sink
echo "Destroyed <pomeranian-sink>"
pactl set-default-sink $scarlett_sink
echo "Set default sink to $scarlett_sink"

I dynamically retrieve the name of my card and place the name in $scarlett_sink, but this is not necessary. You can hard-code in the card name.

awk '{print $2}'

The awk bit returns the item in the 2nd "column" - which is the name of my card, only.

Scripts 2


There is 1 additional issue that I have run into: the names playback_AUX0 and playback_AUX1 will randomly change to playback_1 and playback_2. I don't know if it's something in PipeWire or the kernel that's causing this. Either way, I've made some extra modifications to my script to account for these seemingly random changes.

I've also included some nice-ness by coloring text in the terminal with ASCII escape codes.

Creation script

#!/bin/bash
function colored_text() {
  local color_code=$1
  local text=$2
  printf "\033[%s%s\033[0m" "$color_code" "$text"
}
echo "Creating node adapter"
pw-cli create-node adapter '{ factory.name=support.null-audio-sink node.name=pomeranian-sink media.class=Audio/Sink object.linger=true audio.position=[FL FR] audio.sample_rate=48000 }'
echo "Setting the default sink"
pactl set-default-sink pomeranian-sink
echo "Finding Focusrite sink"
scarlett_sink=`pactl list short sinks | grep Focusrite | awk '{print $2}'`
echo "Found sink: ${scarlett_sink}"
echo "Finding appropriate audio input name..."

# See if pw Focusrite input includes AUX, it's probably playback_AUX0 and AUX1
is_aux=`pw-link -i | grep Focusrite | grep AUX`
# If the grep is empty, its playback_1 and 2
if [[ -z "$is_aux" ]]; then
  pw-link pomeranian-sink:monitor_FL $scarlett_sink:playback_1
  pw-link pomeranian-sink:monitor_FR $scarlett_sink:playback_2
  echo "Linked pomeranian-sink:monitor_FL -> ${scarlett_sink}:playback_1"
  echo "Linked pomeranian-sink:monitor_FR -> ${scarlett_sink}:playback_2"
else
  pw-link pomeranian-sink:monitor_FL $scarlett_sink:playback_AUX0
  pw-link pomeranian-sink:monitor_FR $scarlett_sink:playback_AUX1
  echo "Linked pomeranian-sink:monitor_FL -> ${scarlett_sink}:playback_AUX0"
  echo "Linked pomeranian-sink:monitor_FR -> ${scarlett_sink}:playback_AUX1"
fi

echo "----------- Finished ------------"
echo "1. New default sink is: $(colored_text '94m' 'pomeranian-sink')"
echo "2. To cleanup, type: $(colored_text '94m' 'proton-cleanup')"

Destruction script

#!/bin/bash
function colored_text() {
  local color_code=$1
  local text=$2
  printf "\033[%s%s\033[0m" "$color_code" "$text"
}
scarlett_sink=$(pactl list short sinks | grep Focusrite | awk '{print $2}')
pw-cli destroy pomeranian-sink
echo "Destroyed <pomeranian-sink>"
pactl set-default-sink $scarlett_sink
echo "Set default sink to $(colored_text "94m" "$scarlett_sink")"

Conclusion

I like to put these scripts files in ~/utils and create aliases for them in ~/.bash_aliases for quick & easy use.

mkdir -p ~/utils
touch ~/utils/proton-audio.sh
touch ~/utils/proton-cleanup.sh
echo "alias proton-audio='bash ~/utils/proton-audio.sh'" >> ~/.bash_aliases
echo "alias proton-cleanup='bash ~/utils/proton-cleanup.sh'" >> ~/.bash_aliases

Thanks for reading - hope this helps someone!

#proton
#audio
#linux
#focusrite
#pro audio