Cloning an Audio Stream in Linux
Why Clone an Audio Stream?
One of the reasons that Linux is such a great platform for playing with Amateur Radio is the unequaled flexibility PulseAudio (and PipeWire, more recently, which is drop-in compatible with PulseAudio) give you to send any audio anywhere you want. Since most Amateur Radio software uses audio signals to interface with your radio, this extra flexibility comes in quite handy. However, unless you know what you are looking for, the possibilities are not obvious.
A trick that I find myself using often is to send the audio stream from one application to multiple outputs. That way you can listen on your speakers while the audio goes out to your radio through a different sound card.
Virtual Devices
This technique is made possible by the “null sink” and “loopback” devices in PulseAudio (again, this all works without changes in PipeWire as well). These devices are “virtual”; that is, they are not backed by any hardware. A null sink is a virtual device that does nothing but receive audio. A loopback copies the audio from a source to a sink. You can connect these devices is many different topologies. This is what we are going to set up in this post, using fldigi
as an example application:
Let’s Get Started
We’ll need to create the virtual devices on the command line. After that, you can use pavucontrol
to connect them together if you want, but I’ll show it on the command line. First we’ll create a null sink:
$ pactl load-module module-null-sink sink_name="NS1"
262
pactl
will output a number, which is the module number that you just created. You can see all of the loaded modules with
$ pactl list modules
[...]
Module #262
Name: module-null-sink
Argument: sink_name=NS1
Usage counter: n/a
Properties:
module.author = "Wim Taymans <wim.taymans@gmail.com>"
module.description = "A NULL sink"
module.usage = "sink_name=<name of sink> sink_properties=<properties for the sink> format=<sample format> rate=<sample rate> channels=<number of channels> channel_map=<channel map>"
module.version = "0.3.40"
node.name = "NS1"
audio.channels = "2"
audio.position = "FL,FR"
media.class = "Audio/Sink"
device.description = "NS1 Audio/Sink sink"
factory.name = "support.null-audio-sink"
object.linger = "true"
monitor.channel-volumes = "true"
If you want to remove a module, substitute its module number for 262
in:
$ pactl unload-module 262
Now that we have our virtual sink, we will use the loopbacks to connect it to both of our real outputs. We’ll get a list of all sinks, making note of the node.name
property for the devices in question. I’ve trimmed the output quite a bit for clarity. Note that your sinks will probably be much different than mine depending on your hardware and what is currently running on your system.
$ pactl list sinks
Sink #49
State: IDLE
Name: alsa_output.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00.analog-stereo
Description: CM108 Audio Controller Analog Stereo
Properties:
node.nick = "USB PnP Sound Device"
node.name = "alsa_output.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00.analog-stereo"
device.description = "CM108 Audio Controller Analog Stereo"
Sink #272
State: IDLE
Name: NS1
Description: NS1 Audio/Sink sink
Properties:
node.name = "NS1"
device.description = "NS1 Audio/Sink sink"
Sink #293
State: RUNNING
Name: bluez_output.00_42_79_E6_8A_9A.a2dp-sink
Description: JBL BoomBox
Properties:
device.description = "JBL BoomBox"
node.name = "bluez_output.00_42_79_E6_8A_9A.a2dp-sink"
In my case, the “USB PnP Sound Device” is the output to my radio, “JBL BoomBox” is the device I want to listen on, and “NS1 Audio/Sink sink” is the null sync I created in the last step. Let’s connect them all together.
$ pactl load-module module-loopback source=NS1 sink="alsa_output.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00.analog-stereo"
262145
$ pactl load-module module-loopback source=NS1 sink="bluez_output.00_42_79_E6_8A-9A.a2dp-sink"
262146
Now we have a virtual device that will send any audio we feed it to both our radio and our speakers. The last remaining step is to reroute fldigi
’s output there. You can use pavucontrol
to change the output if you prefer GUI, but where’s the fun in that? We need to find the “sink input” ID corresponding to fldigi
. Once again, I’ve trimmed the output to only the relevant sections.
$ pactl list sink-inputs
Sink Input #233
Client: 181
Sink: 272
Properties:
application.name = "ALSA plug-in [fldigi]"
application.process.id = "972192"
application.process.binary = "fldigi"
node.name = "ALSA plug-in [fldigi]"
And now to move it. Substitute your sink input number for 233.
$ pactl move-sink-input 233 NS1
Summary
Now the audio from our fldigi
process is going into the null sink we created. When it plays some audio, the two loopback devices will copy it from the null sink, which does nothing on its own, to the two different audio devices we specified. As you can see, PulseAudio and PipeWire are extremely versatile tools for taking any audio you have any putting it where you want it to go. I hope you find this technique helpful.
73 DE KZ3A