PulseAudio loopback guide

Share your own howto's etc. Not for support questions!

PulseAudio loopback guide

Postby dk75 » 2014-01-04 15:33

I wanted to stream some game video to twitch.tv. It's easy on Windows, they have some officially supported programs on their site, but with linux, you need to use scripts with either FFMPEG or other grabbing tools (aconv) to stream X11 desktop.

And I found one particular nice script, which give option to stream webcam as PIP, and it had on line inside:
echo "Please setup the Audio Output (something like 'pavucontrol')"

What?
Then what is that script for, if I need to manually change sound preference in order to grab sound.
And then I've reached top of my frustration when I couldn't find any control to do that i my volume control program.
Until... I've found that I've used GNOME Volume Control where I should use pavucontrol.

After that I've set on quest to discover how to do it from script - and nowhere I could find it.
Everywhere, after making sinks and loopbacks there was a line
go to pavucontrol and connects sinks and streams


That's it... I forgot to eat and used whole day for it.

What do you need.
Particularly whole PulseAudio package (execute as root or root privileged user):
Code: Select all
aptitude install pulseaudio pulseaudio-utils pavumeter pavucontrol paman paprefs pasystray

Absolute minimum is "pulseaudio" dummy, and "pulseaudio-utils" and "pavucontrol", but it's nice package, especially with "pasystray" - tray icon to access all of the PulseAudio tools. (run from ~/.config/autostart)

You also need MAWK, as it's lighter than GAWK and serves it's purpose, but you can use AWK everywhere I use MAWK - no problem whatsoever.


So,lets start.

What do we want to achieve?
Grab a sound from one program (be it game, or media player - I've tested it on MPD), plus, grab mic sound (if mic is not muted in volume control - don't do it with speakers on because of powerloop between them and mic, better to use headphones).
I've used FFMPEG for that.

First, we must run source of our sound, then grabbing tool, to create recording stream that we could manipulate with PulseAudio.
So I've started playback with MPD, and then run FFMPEG:
Code: Select all
ffmpeg -y -f alsa -i pulse -ss 1 -strict -2 -c:a aac -b:a 64k /tmp/ffgrabaudio.mp4

then we can make PulseAudio evil. :twisted:

Make new null sinks:
Code: Select all
SINK_GRAB_ID=$(pactl load-module module-null-sink sink_name="grab" sink_properties=device.description="INgrabOUT")
SINK_DUPLEX_ID=$(pactl load-module module-null-sink sink_name="duplex_out" sink_properties=device.description="duplexOUT")

it returns with SINK ID, which is needed to unload it after work done, so we need to store it in variable.
Also, don't forget a "sink_properties=device.description". It can't be exact as "sink_name", but if you ever want to look into pavucontrol and tell what is what, you'll need them, because without, every null sink will be shown in pavucontrol as... "null sink" - very descriptive and informing.

Now, we need to make some loopbacks, so output of some sink would go to input another one.
When made with no parameters, it reroute both your sound output and input to input together - that's simplest thing to grab whole system sound for screencasting.
No need for null sinks at all, just one loopback module to load and all works... you will record game sound, system sound, notifications... everything.

It's as simple as
Code: Select all
pactl load-module module-loopback

and that's it... but not for me, and not for this guide.

So, let's back on track.
First, make loopback that put grab sink sound back to default sound output, so you can hear it - you want to play a game without a sound?
Code: Select all
MODULE1=$(pactl load-module module-loopback source="grab.monitor")

yeah... every sink has output and input.
They "play" what comes to input on output.
Input is simple sink_name, output is sink_name.monitor
So we made loopback from grab sink to default output sink (PulseAudio is routing it to default output sink, but you might chose your own).
Now, interesting part, make route from grab sink to duplex sink, so sound could be mixed with mic and recorded from duplex sink:
Code: Select all
MODULE2=$(pactl load-module module-loopback source="grab.monitor" sink="duplex_out")

So, we have two routes for grab sink, one to default sink and another one to duplex sink.

And then, we need reroute mic to duplex sink, so it will not be heard at default output (speakers/headphones - it actually prevents powerloop that I wrote earlier).
For that, we need default source name as well:
Code: Select all
DEFAULT_SOURCE=`pacmd dump | mawk '/set-default-source/ {print $2}'`
MODULE3=$(pactl load-module module-loopback source=${DEFAULT_SOURCE} sink="duplex_out")


Now, what's left, is to connect MPD to grab sink, and FFMPEG to duplex_out sink.
Easy peasy lemon squeezy. Or I thought at first. :D

So, lets connect FFMPEG to duplex_out sink as first (no particular reason for that).
We need, FFMPEG stream ID and duplex_out input, or a "source" as PulseAudio call them. And we need FFMPEG client ID for finding stream of it:
Code: Select all
FFMPEG_ID=$(pactl list short clients | mawk 'tolower($3) ~ "ffmpeg" {print $1}')
FFMPEG_STREAM_ID=$(pactl list short source-outputs | mawk -v check=${FFMPEG_ID} '$3 == check {print $1}')
SOURCE_DUPLEX_ID=$(pactl list short sources | mawk '$2 ~ "duplex_out" {print $1}')

with that, we can route FFMPEG stream to have to record from dulpex_out:
Code: Select all
pactl move-source-output ${FFMPEG_STREAM_ID} ${SOURCE_DUPLEX_ID}


Now, it's time to route MPD to grab sink (hm... maybe I should do that as first thing...).

Again, we need client stream ID, for that we need client ID and finally we need grab sink ID:
Code: Select all
CLIENT_ID=$(pactl list short clients | mawk 'tolower($3) ~ "mpd" {print $1}')
CLIENT_STREAM_ID=$(pactl list short sink-inputs | mawk -v clientID=${CLIENT_ID} '$3 == clientID {print $1}')
OUTPUT_SINK_ID=$(pactl list short sinks | mawk '$2 ~ "duplex_out" {print $1}')

with all information gathered, we can connect MPD to grab sink:
Code: Select all
pactl move-sink-input ${CLIENT_STREAM_ID} ${OUTPUT_SINK_ID}


Done.
MPD is routed to grab sink and default sink, mic is routed to duplex sink and grab sink is also routed to duplex sink.

Why not route MPD to FFMPEG then?
Well... only sinks can get more than one route... so routing MPD to FFMPEG would cut off MPD sound from speakers and no mic recording.


After recording ,we should clean.

Simplest way is to run
Code: Select all
pulseaudio -k

but if you have some other PulseAudio routes that you want to preserve, then
Code: Select all
pactl unload-module ${MODULE3}
pactl unload-module ${MODULE2}
pactl unload-module ${MODULE1}
pactl unload-module ${SINK_DUPLEX_ID}
pactl unload-module ${SINK_GRAB_ID}




Time to make this into a script.
This is my test script to play with, enjoy:
Code: Select all
#! /bin/bash

# FFMPEG starts first, since it's streams are needed for operations
function ffmpeg_grab(){
   ffmpeg -y -f alsa -i pulse -ss 1 -t 30 -strict -2 -c:a aac -b:a 64k /tmp/ffgrabaudio.mp4 &>/dev/null
}

# Confiuguration of PulseAudio for simmultaneos recording of desired application + mic (if not muted)
function PAset(){
# set name of grab sink (I've cosen to be name of grabed application)
SINK_GRAB=$(echo $1 |mawk '{gsub(/ +/,"_"); print}')
# set name of duplex sink from which all recordings will happen
SINK_DUPLEX="duplex_out"
DEFAULT_SINK=`pacmd dump | mawk '/set-default-sink/ {print $2}'`
DEFAULT_SOURCE=`pacmd dump | mawk '/set-default-source/ {print $2}'`


# make "grab" sink - to this sink application will be redirected
SINK_GRAB_ID=$(pactl load-module module-null-sink sink_name=${SINK_GRAB} sink_properties=device.description="INgrabOUT")
#   echo "Grab: ${SINK_GRAB}: ${SINK_GRAB_ID}"


# make "duplex" sink - from this sink FFMPEG will record
SINK_DUPLEX_ID=$(pactl load-module module-null-sink sink_name=${SINK_DUPLEX} sink_properties=device.description="duplexOUT")
# get ID of duplex source sink - need it as source for FFMPEG
SOURCE_DUPLEX_ID=$(pactl list short sources |mawk -v check=${SINK_DUPLEX} '$2 ~ check {print $1}')
#   echo "${SINK_DUPLEX}: ${SINK_DUPLEX_ID}: ${SOURCE_DUPLEX_ID}"


# connect source of "grab" sink by loopback to default output - it will allow to hear recorded application
MODULE1=$(pactl load-module module-loopback source="${SINK_GRAB}.monitor")
# connect source of "grab" sink by loopback to "duplex" sink - this will allow to record from "grab"
MODULE2=$(pactl load-module module-loopback source="${SINK_GRAB}.monitor" sink=${SINK_DUPLEX})
# connect default sources by loopback to duplex - this will redirect mic to duplex
#   echo "Default source"
MODULE3=$(pactl load-module module-loopback source=${DEFAULT_SOURCE} sink=${SINK_DUPLEX})
#   echo "Moved to duplex"



# get client ID of application to record from
CLIENT_ID=$(pactl list short clients |mawk -v clientName=$1 'tolower($3) ~ clientName {print $1}')
# get stream ID of application to record from
CLIENT_STREAM_ID=$(pactl list short sink-inputs |mawk -v clientID=${CLIENT_ID} '$3 == clientID {print $1}')
# get output ID of "grab" sink
OUTPUT_SINK_ID=$(pactl list short sinks |mawk -v check=${SINK_GRAB} '$2 ~ check {print $1}')
#   echo "$1: ${CLIENT_ID}: ${CLIENT_STREAM_ID}: ${OUTPUT_SINK_ID}"
# move output stream of client as input to sink of "grab" - application will send sound data to "grab" sink
pactl move-sink-input ${CLIENT_STREAM_ID} ${OUTPUT_SINK_ID}

# get FFMPEG client ID
FFMPEG_ID=$(pactl list short clients |mawk -v clientName="ffmpeg" 'tolower($3) ~ clientName {print $1}')
# get FFMPEG recording stream ID
FFMPEG_STREAM_ID=$(pactl list short source-outputs |mawk -v check=${FFMPEG_ID} '$3 == check {print $1}')
#   echo "FFMPEG: ${FFMPEG_ID}: ${FFMPEG_STREAM_ID}"
# move output of "duplex" sink to FFMPEG stream recording
pactl move-source-output ${FFMPEG_STREAM_ID} ${SOURCE_DUPLEX_ID}
}


function PAsystem(){
SOURCE_ID=$(pactl list short sources |mawk '/combine/ {print $1}')
[ ${SOURCE_ID} ] || SOURCE_ID=$(pactl list short sources |mawk '/alsa_output/ {print $1}')
FFMPEG_ID=$(pactl list short clients |mawk -v clientName="ffmpeg" 'tolower($3) ~ clientName {print $1}')
FFMPEG_STREAM_ID=$(pactl list short source-outputs |mawk -v check=${FFMPEG_ID} '$3 == check {print $1}')

echo "FFMPEG: ${FFMPEG_ID}: ${FFMPEG_STREAM_ID}"
pactl move-source-output ${FFMPEG_STREAM_ID} ${SOURCE_ID}
}



case $1 in
   -g)   ffmpeg_grab &
      [[ "$2" != "system" ]] && PAset $2
      ;;
   -c)   pulseaudio -k
      #pactl unload-module ${MODULE3}
      #pactl unload-module ${MODULE2}
      #pactl unload-module ${MODULE1}
      #pactl unload-module ${SINK_DUPLEX_ID}
      #pactl unload-module ${SINK_GRAB_ID}
      ;;
   -l)   if [ "$2" ]; then pactl list short clients |mawk -v clientID=$2 'tolower($3) ~ clientID {print "ID:",$1,"\t\tName:",$3}'
      else pactl list short clients |mawk '{print $3}'; fi
      ;;
   *)    echo "use 'ffgrabaudio.sh -g <clientname>' to record sound from specified client"
      echo "use 'ffgrabaudio.sh -g system' to record everything"
      echo "use 'ffgrabaudio.sh -l' to list available clients"
      echo "use 'ffgrabaudio.sh -c' to clear mess in PulseAudio"
      echo "use 'ffgrabaudio.sh -h' or 'ffgrabaudio.sh' for this help"
      ;;
esac

exit
dk75
 
Posts: 2
Joined: 2014-01-04 12:50

Re: PulseAudio loopback guide

Postby dk75 » 2014-01-05 13:15

I woke up this morning and one though sticked to me out of blue - why do I move stream of FFMPEG from default source? Isn't that every new client to PulseAudio connects to what's defaut at the moment?
Whoa! It would kill two birds with one stone.
No more running FFMPEG first to make stream and then manipulate means that FFMPEG can run in a same thread as rest of the script.
Could be easier controlled then.

So, as before, I'm making two null sinks, grab and duplex. As before I make three loopback streams, but this time I make them all with specific source and destination, no more PulseAudio automata.
Why?
Because with PulseAudio automata it make as many loopbacks as possible, and that means, that if you use bluetooth headset, then even if you route all sound to it disabling speakers, PulseAudio loopback automata makes stream to speakers and suddenly, sound appears in speakers (it's shown in my script with "-g system" option).

And because now FFMPEG runs in same thread as whole script, it's possible to clean up after, with anything else than server restart. That means no playback stop, and any other previous PulseAudio routes/sinks/sources survive.

So, there is a my new testing script, much improved:
Code: Select all
#! /bin/bash

function ffmpeg_status(){
while [[ ! ${FFPID} ]]
do
   FFPID=$(ps aux |mawk '/ffmpeg/ && /-i pulse/ && !/mawk/ {print $2}')
   sleep 1
done

echo "FFMPEG is running"
echo "Press <q><ENTER> to quit"
echo
echo

while [ -e /proc/${FFPID} ]
do
   tail -n 1 /tmp/ffmpeg.log |mawk '{print "\033[0G\033[1A\033[K"$0}'
   sleep 1
done

echo
tail -n 1 /tmp/ffmpeg.log

echo -e "\n\n\
################\n\
#     done!    #\n\
################\n\
\n"
}



function ffmpeg_grab(){
# set name of grab sink (I've cosen to be name of grabed application)
SINK_GRAB=$(echo $1 |mawk '{gsub(/ +/,"_"); print}')

# set name of duplex sink from which all recordings will happen
SINK_DUPLEX="duplex_out"

# get name of default sink
DEFAULT_SINK=`pacmd dump | mawk '/set-default-sink/ {print $2}'`

# get name of default source
DEFAULT_SOURCE=`pacmd dump | mawk '/set-default-source/ {print $2}'`

# make "grab" sink - to this sink application will be redirected
#   echo "load grab sink"
SINK_GRAB_ID=$(pactl load-module module-null-sink sink_name=${SINK_GRAB} sink_properties=device.description="INgrabOUT")
#   echo "Grab: ${SINK_GRAB}: ${SINK_GRAB_ID}"

# make "duplex" sink - from this sink FFMPEG will record
#   echo "load duplex sink"
SINK_DUPLEX_ID=$(pactl load-module module-null-sink sink_name=${SINK_DUPLEX} sink_properties=device.description="duplexOUT")

# get ID of duplex source sink - need it as source for FFMPEG
SOURCE_DUPLEX_ID=$(pactl list short sources |mawk -v check=${SINK_DUPLEX} '$2 ~ check {print $1}')
#   echo "${SINK_DUPLEX}: ${SINK_DUPLEX_ID}: ${SOURCE_DUPLEX_ID}"

# connect source of "grab" sink by loopback to default output - it will allow to hear recorded application
#   echo "load loopback module from grab source to default"
MODULE1=$(pactl load-module module-loopback source="${SINK_GRAB}.monitor") sink=${DEFAULT_SINK}

# connect source of "grab" sink by loopback to "duplex" sink - this will allow to record from "grab"
#   echo "load loopback module from grab to duplex"
MODULE2=$(pactl load-module module-loopback source="${SINK_GRAB}.monitor" sink=${SINK_DUPLEX})

# connect default sources by loopback to duplex - this will redirect mic to duplex
#   echo "load loopback module from default to duplex"
MODULE3=$(pactl load-module module-loopback source=${DEFAULT_SOURCE} sink=${SINK_DUPLEX})

# get client ID of application to record from
CLIENT_ID=$(pactl list short clients |mawk -v clientName=$1 'tolower($3) ~ clientName {print $1}')

# get stream ID of application to record from
CLIENT_STREAM_ID=$(pactl list short sink-inputs |mawk -v clientID=${CLIENT_ID} '$3 == clientID {print $1}')

# get output ID of "grab" sink
OUTPUT_SINK_ID=$(pactl list short sinks |mawk -v check=${SINK_GRAB} '$2 ~ check {print $1}')
#   echo "$1: ${CLIENT_ID}: ${CLIENT_STREAM_ID}: ${OUTPUT_SINK_ID}"

# move output stream of client as input to sink of "grab" - application will send sound data to "grab" sink
pactl move-sink-input ${CLIENT_STREAM_ID} ${OUTPUT_SINK_ID}

# set duplex sink output as default source
pactl set-default-source "${SINK_DUPLEX}.monitor"

# actual command to grab audio - FFMPEG connects to default source, which is "dulpex_out.monitor" at the time
ffmpeg -y -f alsa -i pulse -strict -2 -c:a aac -b:a 64k /tmp/ffgrabaudio.mp4 &>/tmp/ffmpeg.log

# cleaning without restarting PulseAudio server and without destroing other virtual devices/streams
pactl set-default-source ${DEFAULT_SOURCE}
pactl set-default-sink ${DEFAULT_SINK}
pactl unload-module ${MODULE3}
pactl unload-module ${MODULE2}
pactl unload-module ${MODULE1}
pactl unload-module ${SINK_DUPLEX_ID}
pactl unload-module ${SINK_GRAB_ID}
}



function ffmpeg_grab_system(){
# get name of default sink
DEFAULT_SINK=`pacmd dump | mawk '/set-default-sink/ {print $2}'`

# get name of default source
DEFAULT_SOURCE=`pacmd dump | mawk '/set-default-source/ {print $2}'`

# load loopback module
MODULE0=$(pactl load-module module-loopback)

# actual command to grab audio - FFMPEG connects to default source, which is loopback at the time
ffmpeg -y -f alsa -i pulse -strict -2 -c:a aac -b:a 64k /tmp/ffgrabaudio.mp4 &>/tmp/ffmpeg.log

# cleaning without restarting PulseAudio server and without destroing other virtual devices/streams
pactl set-default-source ${DEFAULT_SOURCE}
pactl set-default-sink ${DEFAULT_SINK}
pactl unload-module ${MODULE0}
}



case $1 in
   -g)   ffmpeg_status & STATPID=$!
      [[ "$2" != "system" ]] && ffmpeg_grab $2 || ffmpeg_grab_system
      #   echo "Stat PID: ${STATPID}"
      sleep 1
      kill -9 ${STATPID} &>/dev/null
      rm -f /tmp/ffmpeg.log
      ;;
   -c)   pulseaudio -k
      ;;
   -l)   if [ "$2" ]; then pactl list short clients |mawk -v clientID=$2 'tolower($3) ~ clientID {print "ID:",$1,"\t\tName:",$3}'
      else pactl list short clients |mawk '{print $3}'; fi
      ;;
   -d)   pacmd dump |mawk '/set-default-sink/ {print "Default output:",$2} /set-default-source/ {print "Default input:",$2}'
      ;;
   *)    echo "use 'ffgrabaudio.sh -g <clientname>' to record sound from specified client"
      echo "use 'ffgrabaudio.sh -g system' to record everything"
      echo "use 'ffgrabaudio.sh -l' to list available clients"
      echo "use 'ffgrabaudio.sh -c' to clear mess in PulseAudio by server restart"
      echo "use 'ffgrabaudio.sh -d' to dump default devices"
      echo "use 'ffgrabaudio.sh -h' or 'ffgrabaudio.sh' for this help"
      ;;
esac

exit
dk75
 
Posts: 2
Joined: 2014-01-04 12:50

Re: PulseAudio loopback guide

Postby TCK78 » 2015-03-14 18:40

I just wanted to thank you for posting this guide. Your posts have really helped me understand the concept of null sinks and loopback streams. Fantastic work!
TCK78
 
Posts: 1
Joined: 2015-03-14 18:37

Re: PulseAudio loopback guide

Postby @zephyr » 2015-08-08 01:33

@dk75: This is awesome, have had a lot of misunderstandings about pulseaudio, been tinkering with this script and have gotten results. Thanks-zephyr
ZephyrLinux
User avatar
@zephyr
 
Posts: 18
Joined: 2014-09-17 21:34
Location: Apache, Oklahoma USA


Return to Docs, Howtos, Tips & Tricks

Who is online

Users browsing this forum: No registered users and 2 guests

fashionable