Ritiek Malhotra Just some of my adventures!

Capturing and piping audio output from a process in Linux

I’ve been looking to intercept audio data from specific processes so I can tinker with this audio data in real time. Vaguely capture audio output from a process, tinker with it, and play this tinkered audio through my hardware, something similar to this pseudocode:

# Resample music from my Spotify desktop app to a higher sample rate (nightcore) in near real-time.
$ capture --sample-rate 48000 spotify | play --buffer-time-in-secs 5 --sample-rate 52000 -

I think I got something pretty close to this figured here.

A little while ago, I moved to PipeWire from PulseAudio. The stuff below will only work if you’re using PipeWire yourself. It might also be possible to adjust this to work with PulseAudio but it isn’t something I’m looking to explore at the moment.

Setup

The plan is to create a new virtual audio output device. We’ll be redirecting the audio output from our target process to this virtual audio output device, so that we’re able to isolate this process’s audio output from all other processes. We’ll then capture all audio data being sent to this virtual output device. Once captured, we can tinker with the audio data and then redirect this tinkered audio output to our actual audio output device(s).

We’ll begin by inserting a kernel module to create a virtual audio loopback device:

$ sudo modprobe snd-aloop

We should now see two new virtual audio devices: Analog Output and Analog Input.

Audio Devices

Launch some application, say Spotify desktop, put some music on, and have Spotify send the audio data to this new virtual audio output Analog Output. In Manjaro, I got it working as in the screenshot below, but it should be similar on other distros (try looking for it under Settings -> Audio), or install and use pavucontrol gui.

Haven’t looked around for a CLI alternative for this purpose yet.

Switching playback device to virtual output device

Once you switch the audio output device for Spotify to the dummy output device, you’ll no longer hear your music from your actual speakers.

Now let’s see the node names given by PipeWire to our currently existing audio devices.

$ pw-cli list-objects | grep node.name
node.name = "Dummy-Driver"
node.name = "Freewheel-Driver"
node.name = "Midi-Bridge"
node.name = "v4l2_input.pci-0000_00_14.0-usb-0_5_1.0"
node.name = "v4l2_input.pci-0000_00_14.0-usb-0_5_1.2"
node.name = "alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp_5__sink"
node.name = "alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp_4__sink"
node.name = "alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp_3__sink"
node.name = "alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink"
node.name = "alsa_input.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__source"
node.name = "alsa_input.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp_6__source"
node.name = "plasmashell"
node.name = "alsa_playback.aplay"
node.name = "alsa_input.platform-snd_aloop.0.analog-stereo"
node.name = "alsa_output.platform-snd_aloop.0.analog-stereo"
node.name = "spotify"
...

(pw-* commands are only available with PipeWire)

After some speculation, I figured out my dummy audio output device is called alsa_output.platform-snd_aloop.0.analog-stereo and my actual audio output device is called alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink.

Confirm yours by running any of the following commands (added all that I figured out in the moment here for documenting purpose) to redirect audio from your dummy audio output device to your actual audio output device. Make sure to replace the --target param with the node names that seemingly fit for your case.

$ pw-record --target alsa_output.platform-snd_aloop.0.analog-stereo - | pw-play --target alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink -

(You may have to also pass in -P stream.capture.sink=true to pw-record, thanks @pkgmvd as reported on 23rd November, 2024!)

$ pw-loopback -C alsa_output.platform-snd_aloop.0.analog-stereo -P alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink
$ pw-link alsa_output.platform-snd_aloop.0.analog-stereo alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink
$ pw-link --disconnect alsa_output.platform-snd_aloop.0.analog-stereo alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink

You should be able to hear your Spotify music if both the dummy audio output device and the actual audio output device you selected are the right ones.

Wohoo! We can now mess with this audio. As an example, we’ll attempt to increase sample rate for whatever’s playing in Spotify to make it sound nightcorish:

$ pw-record --target alsa_output.platform-snd_aloop.0.analog-stereo - | aplay -B 5000000 -r 52000 -f S16_LE -c 2 -

At the time of writing, pw-record captures audio at a sample rate of 44.8KHz. In the above example we resampled it to 52KHz (-r 52000). Since we’ll be playing audio at a higher sample rate than what we’ll be receiving from pw-record, our dummy audio output will have to play catch up with our actual audio output. This means our actual audio output will stutter every now and then.

As a little workaround, we passed -B 5000000 to let Spotify fill up the audio buffer for 5s everytime it starts to play catch up after which aplay would attempt to begin resampling.

We can also do multiple pipes for a bit more complexy stuff:

$ pw-record --target alsa_output.platform-snd_aloop.0.analog-stereo - | ffmpeg -ar 48000 -f s16le -ac 2 -i - -filter:a "asetrate=48000*1.1" -f wav - | mpv --audio-buffer=5 -

Synchronizing audio output from a process to multiple machines

A few years ago, I wrote a post on synchronizing multimedia plaback (and Syncplay). Quite a while after writing it, I discovered Snapcast which is more tailored to audio syncing and seems to work better than Syncplay in that regard. (although, I’ll still stick to Syncplay for video syncing stuff)

Install Snapcast and edit /etc/snapserver.conf to have your source line as:

source = pipe:///tmp/snapfifo?name=default

Launch the Snapcast server:

$ snapserver

In another terminal, we’ll write the audio output from the dummy audio output device to this named pipe which’ll be used by snapcast server to broadcast audio to connected clients:

$ pw-record --target alsa_output.platform-snd_aloop.0.analog-stereo - > /tmp/snapfifo

Have some application writing audio output to this dummy audio output device (Spotify as we talked about in the previous section, anything else works fine too).

Snapcast server by default also provides a little built-in client running on http://0.0.0.0:1780, you can open this in a browser (which’ll be writing audio output to our actual audio output device) and tap the play button. If you’re able to hear music now, then we’re good to go!

Snapcast client app is also available for Android and iOS. We can have our phones be a part of this syncy mesh too.