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:

Audio Flow

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