Skip to content

Instantly share code, notes, and snippets.

@lostfictions
Last active April 16, 2023 02:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lostfictions/604e972182dc4fde50f3271341ca5a2f to your computer and use it in GitHub Desktop.
Save lostfictions/604e972182dc4fde50f3271341ca5a2f to your computer and use it in GitHub Desktop.
Script to set up (and tear down) PulseAudio sinks for streaming to Discord or Twitch (including, optionally, via a capture device)

A Somewhat Better Setup for Streaming from Linux

I couldn't find a great way to set up streaming to Discord or Twitch from Linux, so I wrote this.

Intro

The script included below, streaming.sh, is meant to be used in tandem with the PulseAudio Volume Control application (pavucontrol, which is available in repositories for most distributions that use Pulse but might not be installed by default). It adds two PulseAudio "sinks" (destinations) that you can route your applications' audio to: a "null sink" and a "combined sink."

  • Route your audio to the combined sink to hear it through your speakers and stream it to others.
  • Route your audio to the default sink (often called something like "Built-in Audio...") to hear it through your speakers or preferred device but not stream it to others.
  • Route your audio to the null sink to not hear the audio through your speakers but still stream it to others. If you're streaming via browser-based application, you'll need to route microphone audio to this sink to be able to stream game or app audio and your voice simultaneously (details below).

Usage

To get started, grab streaming.sh below, optionally remove the .sh extension (Github requires it for syntax highlighting, but you don't need it), put it somewhere convenient (for example, a directory on your PATH), and mark it as executable. Then run:

  • streaming to set up for streaming to a service like Twitch via an application like OBS. This is the most basic setup, and only adds the two sinks mentioned above.
  • streaming discord to add the two sinks and an additional loopback from a microphone to the null sink. This allows you to combine mic input with game or app audio, since Discord and other browser-based streaming apps (Jitsi, Hangouts, etc.) currently don't seem to capture application audio in Linux (unlike in Windows or macOS).
  • streaming capture to add the two sinks and an additional loopback from an A/V capture device to the combined sink. This allows you to monitor a capture device's audio output on your speakers or headphones while streaming it. (It may be a quirk of my own setup that this is the best way to hear game audio that I'm capturing from a device, but maybe you'll find it useful too.)
  • You can combine both of the options above, ie. streaming discord capture or streaming capture discord.
  • streaming off will tear down and remove all added sinks and loopbacks. Note that this assumes you're not already using any of the above PulseAudio modules (null-sink, combine-sink, or loopback)! If you are, you'll have to adjust this script to teardown by name instead of module.

Once the sinks are added, you'll need to start your streaming application — in the case of Discord or another browser-based app, this means actually joining a voice channel or call! Then open pavucontrol and go to the Recording tab. You should see the application in question. Set its input source to "Monitor of OBS Null Sink":

That should do it. In the Playback tab, you can now route applications' audio to your sinks of choice as described above.

Details

Capturing a monitor of your computer's audio — everything that is sent to your speakers — via PulseAudio is fairly straightforward, but oftentimes you need something a little more complex than that. This script aims to solve the following problems:

  • Currently, neither OBS nor browser-based screensharing (Discord, Jitsi, Hangouts, etc) appear to allow you to select a specific application's audio as a source. Browser-based screenshare applications at the moment simply don't seem to capture application audio at all in Linux, despite doing so in Windows and macOS. You can work around this by routing your desktop audio to the browser (via pavucontrol or a similar application), but while you're doing this the people you're streaming to won't be able to hear you speak via your mic.

  • OBS fares a bit better in that it can capture microphone audio and desktop audio and combine them into a single stream, but there's still no way to exclude desktop audio sources when doing this. (And while streaming from OBS to Discord or other browser-based screenshare apps is possible, it requires building a custom plugin against the OBS sources and installing a kernel module, which if you're like me sounds like way too much work even if this partial solution works for you.)

  • An alternate solution floating around is to use a PulseAudio loopback module to capture and playback your microphone input, so that it's included when you record your desktop audio. This is somewhat less than ideal for streaming, since it means you'll hear yourself in the mix.

  • More generally, if you're streaming to other people via something like Discord or Hangouts, you might want to hear other people talking, but you probably don't want to stream their audio back to them! So capturing all desktop audio isn't a good general solution.

Ultimately, what we need is some way to selectively select playback sources for inclusion or exclusion in a stream. It might be possible to do this with JACK, but for most people that would require installing and running an extra sound daemon alongside what they already have.

For folks running a system with PulseAudio, pavucontrol is almost a good enough solution to this problem: it's a GUI that gives you a list of applications playing audio, applications recording audio, input devices, and output devices, and lets you route them to each other. The only thing missing is some extra sinks — extra destinations to point output towards. This script provides the latter, and sets them up so that they're opt-in: by default no audio is output to the streaming sink, avoiding mishaps like feeding back Discord voice participants' back to them.

This approach is inspired by this guide (and associated discussion) from the OBS forum from 2014, as well as this blog post. (The latter might also have some troubleshooting tips if you're having trouble getting this method to work.)

#!/bin/bash
set -Eeuo pipefail
null_sink_name='OBSNullSink'
null_sink_human_name='OBS Null Sink'
combined_sink_name='OBSCombinedSink'
combined_sink_human_name='OBS Combined Sink'
while test $# -gt 0
do
case "$1" in
off)
mod_num=$(pactl list short modules | grep "sink_name=$combined_sink_name" | awk '{print $1}' || true)
if [[ -n $mod_num ]]; then
did_unload=true
pactl unload-module $mod_num
fi
mod_num=$(pactl list short modules | grep "sink_name=$null_sink_name" | awk '{print $1}' || true)
if [[ -n $mod_num ]]; then
did_unload=true
pactl unload-module $mod_num
fi
# TODO: doesn't look like we can assign a name to loopback modules, so it's a bit tricker to remove them by index.
# instead, we unload the entire module, which might be a problem if it's already in use for another reason.
if [ "$(pactl list short modules | grep module-loopback)" ]; then
did_unload=true
pactl unload-module module-loopback
fi
if [ -v did_unload ]; then
echo "Unloaded modules."
exit 0
else
echo "Didn't unload any modules! Are you sure they were loaded?"
exit 1
fi
;;
discord)
discord=true
;;
capture)
capture=true
;;
*)
echo "Unknown option $1";
;;
esac
shift
done
if [ "$(pactl list short sinks | grep $null_sink_name)" ]; then
echo "Sink already exists!"
exit 1;
fi
echo "Adding null sink (named \"$null_sink_human_name\")! Route your audio to it to stream but not hear it. (For example, your mic output.)"
pactl load-module module-null-sink sink_name=$null_sink_name > /dev/null
pacmd "update-sink-proplist $null_sink_name device.description=\"$null_sink_human_name\""
pacmd "update-source-proplist $null_sink_name.monitor device.description=\"Monitor of $null_sink_human_name\""
output_name=$(pactl list short sinks | grep alsa_output.pci | awk '{print $2}')
echo "Adding combined sink (named \"$combined_sink_human_name\")! Route your audio to it to stream AND hear it. (For example, your game's audio.)"
pactl load-module module-combine-sink slaves="$null_sink_name,$output_name" sink_name=$combined_sink_name > /dev/null
pacmd "update-sink-proplist $combined_sink_name device.description=\"$combined_sink_human_name\""
pacmd "update-source-proplist $combined_sink_name.monitor device.description=\"Monitor of $combined_sink_human_name\""
if [ -v discord ]; then
echo "Streaming to Discord! Adding loopback from mic to null sink. You can tweak the exact input device in pavucontrol."
pactl load-module module-loopback latency_msec=1 sink=$null_sink_name > /dev/null
fi
if [ -v capture ]; then
echo "Capturing from device! Adding loopback from device audio to combined sink. You can tweak the exact input device in pavucontrol."
pactl load-module module-loopback latency_msec=1 sink=$combined_sink_name > /dev/null
fi
echo "Done! You're all set."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment