Recording pulse audio sound while being able to change the volume

Nikita Skobov - July 05, 2020

Recently I took on a fun side project where I would like to record the audio of some program while listening to it at the same time. I am going to show you two ways of accomplishing this:

  1. by just capturing the output of a program via its sink input.
  2. by redirecting the audio to a null sink

Capture output of a program via its sink input

We can accomplish this using parec. I am currently on an arch-based distribution, and on my system parec comes from the libpulse library, which is a dependency of pulseaudio, as well as many other programs. However, I believe parec should be available on most linux distributions by default.

With parec, we have several ways of recording audio. Probably the easiest, and most powerful, is to record the audio of a single sink input, which is typically just a program. To find out what sink inputs are available, start running some software that plays audio (don't use a browser because I believe some browsers bypass pulseaudio and play their sound directly through ALSA), and then do:

pacmd list-sink-inputs

Here is some sample output:

1 sink input(s) available.
    index: 289
        driver: <protocol-native.c>
        flags: START_CORKED 
        state: CORKED
        sink: 0 <alsa_output.pci-0000_00_1b.0.analog-stereo>
        volume: front-left: 0 /   0% / -inf dB,   front-right: 0 /   0% / -inf dB
                balance 0.00
        muted: no
        current latency: 0.00 ms
        requested latency: 75.00 ms
        sample spec: s16le 2ch 48000Hz
        channel map: front-left,front-right
                    Stereo
        resample method: speex-float-1
        module: 9
        client: 527 <VirtualBox>
        properties:
            media.name = "VirtualBox Front"
            application.name = "VirtualBox"
            native-protocol.peer = "UNIX socket client"
            native-protocol.version = "33"
            application.process.id = "231011"
            application.process.user = "admin"
            application.process.host = "admin-az42ce6"
            application.process.binary = "VirtualBoxVM"
            application.language = "en_US.UTF-8"
            window.x11.display = ":0"
            application.process.machine_id = "13dab296a66b2f35aa308c4ad730d9f2"
            application.process.session_id = "2"
            module-stream-restore.id = "sink-input-by-application-name:VirtualBox"

The important thing we want here is the index at the top, in this case it is 289.

We can then record the audio that is coming from this program, without any other sounds from any other program using:

parec --monitor-stream=289 --file-format=wav sample1.wav
# then hit ctrl+c to stop the recording

This by itself is pretty powerful!

This lets us record the audio of a program, while being able to listen to it, and while being able to listen to other sounds simultaneously, and without the extra sounds showing up in our recording.

Capture audio and change the volume while listening

The above method works fine, but it has one issue that was bothering me:

If you change the volume of that program, the volume change shows up in the recording

This is inconvenient, and made me look into alternative ways of recording program output. After some searching around, I found that you can tell pulseaudio to redirect sink inputs to different sinks (by default you probably just have one sink which is your master volume). You can then redirect a sink into a series of "null" sinks, and finally out into your default sink where you will actually be able to hear the output. The magic of this method is that we will be able to record our audio from one of the first sinks, and then adjust volume on the sink that it feeds into. Any volume adjustment on the downstream sink will not have any effect on the recording. Consider this diagram:

            we can record here          and adjust the volume here
                    |                        |
                    v                        v
+------------+   +------------------+   +------------------+   +--------------+
| sink input |-->| null_sink_record |-->| null_sink_listen |-->| default_sink |
+------------+   +------------------+   +------------------+   +--------------+

because the null_sink_listen comes after the null_sink_record, the volume adjustments will not be reflected in the recording.

We will first find the name of our default sink:

pacmd list-sinks

The output looks something like this:

1 sink(s) available.
  * index: 0
	name: <alsa_output.pci-0000_00_1b.0.analog-stereo>
	driver: <module-alsa-card.c>
	flags: HARDWARE HW_MUTE_CTRL HW_VOLUME_CTRL DECIBEL_VOLUME LATENCY DYNAMIC_LATENCY
	state: RUNNING
    # more stuff below omitted for brevity

You will probably just have one sink, but if you have more, the default one should be the one with an asterisk (*) by it.

We remember its name (everything between the angle brackets) and proceed to make two null sinks:

pacmd load-module module-null-sink sink_name=null_sink_record sink_properties=device.description=nullsinkrecord
pacmd load-module module-null-sink sink_name=null_sink_listen sink_properties=device.description=nullsinklisten

The sink_properties=device.description is optional, but I think it's good to have because some applications will use this when displaying the sink. You can then look at this property to tell which sink is which.

When making null sinks, pacmd will also create a source, and name it <null_sink_name>.monitor, you can verify this by running

pacmd list-sources

You should see at least 2 sources there, one being named null_sink_record.monitor and the other null_sink_listen.monitor. We will be using these named sources for our recording soon, as well as for redirecting our output.

We start by pointing the listen sink into our default sink:

pacmd load-module module-loopback source="null_sink_listen.monitor" sink="alsa_output.pci-0000_00_1b.0.analog-stereo"

Then we do the same, but we point the record sink into the listen sink:

pacmd load-module module-loopback source="null_sink_record.monitor" sink="null_sink_listen"

Now, we can move our actual sink input to use the record null sink instead of the default sink:

# remember 289 is the index of the sink input we wish
# to redirect
pacmd move-sink-input 289 null_sink_record

And finally, let's record our audio. parec has different ways of recording the audio. Previously, we used the --monitor-stream= option which lets us choose a sink input to record via its index. But numbers are hard to remember, and inconvenient to script, so now since we have named sinks and similarly named sources, lets use the -d or --device option to record from the source:

# remember to use the NAME of the null sink source, not the description
parec -d null_sink_record.monitor --file-format=wav sample2.wav

And once again, if you hit ctrl+c you can stop the recording, and test the output. But wait, we haven't done the interesting part yet!

As of right now, everything should work the same as before, but let's try adjusting the volume and seeing if the volume change shows up in the recording.

Unfortunately, you cannot use the program's volume slider since that will affect the volume of the sink input. Instead, you will need to modify the volume of the null_sink_listen null sink. You can do that with pacmd, but that is verbose and nobody (at least I hope!) changes their volume by calling pacmd set-sink-volume. Instead, let's use a user interface with a slider to adjust the volume.

Unfortunately there are few programs that let you adjust sink volume instead of sink-input volume, but I found a pretty good one that does allow that: pulsemixer.

just follow their download/install instructions, or find it on your distributions package manager (on arch it is here).

Then simply run it with pulsemixer

And here you will see something like this if you followed the directions:

pulsemixer1

Under nullsinkrecord mine says 440 Hz Sine because thats the 'program' that I was testing with, but yours will be different.

You can now adjust the volume of the nullsinklisten sink, and your recording volume should not change.

You can change either the volume of the null sink: nullsinklisten or its source monitor that appears under it. Both should work, but Ive noticed when adjusting the monitor volume I get a scratchy sound, but I don't get that sound when adjusting the sink volume.

Also, you might be thinking: why even have a record sink, and not just record from the sink input like we did in the first example?

Well, you're right. We don't need it. But I included it in this example to give you a slightly better understanding of the things you can do with pulseaudio. Also I liked making that diagram above

:)

About me:

I am Nikita Skobov.
Contact me via email: skobo002@umn.edu.
Check out my projects: https://github.com/nikita-skobov.
Check out my other blog posts: https://blog.nikitas.link.