This page looks best with JavaScript enabled

Hot audio device swap under FreeBSD

 ·  🎃 kr0m

FreeBSD allows us to easily switch between audio devices using the command “sysctl hw.snd.default_unit”. We just need to check the available devices:

Installed devices:
pcm0: <Intel Haswell (HDMI/DP 8ch)> (play)
pcm1: <Realtek ALC292 (Analog 2.0+HP/2.0)> (play/rec) default
pcm2: <Realtek ALC292 (Internal Analog Mic)> (rec)
pcm3: (play/rec)
Installed devices from userspace:

And perform the appropriate assignment:

sysctl hw.snd.default_unit=3

The issue with this method is that each time we switch devices, we have to restart the open applications so they can recognize the new audio device.

To avoid this inconvenience, we can use virtual_oss, a service that multiplexes and demultiplexes audio devices. The point is that applications will always open the device created by virtual_oss, which then routes the audio to the final device.

We install virtual_oss:

pkg install virtual_oss

We enable the service:

sysrc virtual_oss_enable=YES

We configure the parameters for virtual_oss. In my case, I’m using a laptop where the speakers are represented by the device pcm1, which corresponds to dsp1:

sysrc virtual_oss_dsp="-T /dev/sndstat -C 2 -c 2 -S -i 0 -r 48000 -b 24 -s 8.0ms -f /dev/dsp1 -d dsp -t dsp.ctl"

-T devname      Install entry in /dev/sndstat
-C num          Set the maximum number of mix channels to num.
-c num          Set mix channels for the subsequent commands.
-S              Enable automatic DSP rate resampling.
-i priority     Set real-time priority to priority.  Refer to rtprio(1) for more information.
                Priority is an integer between 0 and RTP_PRIO_MAX (usually 31). 0 is the highest priority 31 the lowest.
-r rate         Set default sample-rate for the subsequent commands.
-b bits         Set sample depth to bits for the subsequent commands. Valid values are 8, 16, 24 and 32.
-s value        Set default buffer size to value. If the argument is suffixed by "ms" it is interpreted as milliseconds.
                Else the argument gives number of samples. The buffer size specified is per channel. If there are multiple
                channels, the total buffer size will be larger.
-f devname      Set both playback and recording DSP device
-d name         Create an OSS device by given name.
-t devname      Set control device name.

Both input and output audio devices can be defined using a single parameter:

-f devname      Set both playback and recording DSP device

Or independently:

-P devname      Set playback DSP device only.  Specifying /dev/null is magic and
                means no playback device.  Specifying a sndio(7) device
                descriptor prefixed by "/dev/sndio/" is also magic, and will use
                a sndio backend rather than an OSS device.

-R devname      Set recording DSP device only.  Specifying /dev/null is magic and
                means no recording device.

We start the service:

service virtual_oss start

We check the available devices and see that there is one more device under userspace section:

cat /dev/sndstat

Installed devices:
pcm0: <Intel Haswell (HDMI/DP 8ch)> (play)
pcm1: <Realtek ALC292 (Analog 2.0+HP/2.0)> (play/rec) default
pcm2: <Realtek ALC292 (Internal Analog Mic)> (rec)
pcm3: <USB audio> (play/rec)
Installed devices from userspace:
dsp: <Virtual OSS> (play/rec)

At this point, it would be ideal to close X graphical session to ensure that all applications use the device created by virtual_oss.

Now, if we want to make a hot swap to USB headphones, for example, we just need to specify the correct DSP:

virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp3

To revert back to the speakers:

virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp1

A useful script can be this:

#!/usr/bin/env bash

grep 'Virtual OSS' /dev/sndstat 1>/dev/null
if [ $? -eq 0 ]; then
    VIRTUAL_OSS_DETECTED=1
else
    VIRTUAL_OSS_DETECTED=0
fi

LAST_DEVICE=$(grep pcm /dev/sndstat | awk -F ":" '{print$1}' | awk -F "pcm" '{print$2}' | tail -n1)
#echo "LAST_DEVICE: $LAST_DEVICE"
for NEXT_DEVICE in $(grep pcm /dev/sndstat | awk -F ":" '{print$1}' | awk -F "pcm" '{print$2}'); do
    #echo "NEXT_DEVICE: $NEXT_DEVICE"

    if [ $VIRTUAL_OSS_DETECTED -eq 1 ]; then
        CURRENT_DEVICE=$(virtual_oss_cmd /dev/dsp.ctl|grep 'Output device name' | awk -F "/dev/dsp" '{print$2}')
    else
        CURRENT_DEVICE=$(grep default /dev/sndstat | awk -F ":" '{print$1}' | awk -F "pcm" '{print$2}')
    fi
    #echo "CURRENT_DEVICE: $CURRENT_DEVICE"

    if [ $NEXT_DEVICE -le $CURRENT_DEVICE ] && [ $NEXT_DEVICE -ne $LAST_DEVICE ]; then
        #echo "Continue"
        continue
    else
        if [ $CURRENT_DEVICE -eq $LAST_DEVICE ] && [ $NEXT_DEVICE -eq $LAST_DEVICE ]; then
            NEXT_DEVICE=0
            #echo "NEXT_DEVICE: $NEXT_DEVICE"
        fi
        DEVICE_NAME=$(grep pcm$NEXT_DEVICE /dev/sndstat | awk -F "<" '{print$2}' | awk -F ">" '{print$1}')
        if [ $VIRTUAL_OSS_DETECTED -eq 1 ]; then
            virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp$NEXT_DEVICE
        else
            sysctl hw.snd.default_unit=$NEXT_DEVICE
        fi

        if [ $? -eq 0 ]; then
            notify-send "Audio device changed: PCM$NEXT_DEVICE: $DEVICE_NAME"
            break
        else
            notify-send "Error changing audio device, reverting"
            if [ $VIRTUAL_OSS_DETECTED -eq 1 ]; then
                virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp$CURRENT_DEVICE
            else
                sysctl hw.snd.default_unit=$CURRENT_DEVICE
            fi
        fi
    fi
done
If you liked the article, you can treat me to a RedBull here