Austin Shafer

home contact

Using USB Headphones with virtual_oss

If you're like me and run a FreeBSD desktop you may have become inticed into buying some wireless headphones. The kind I prefer have ditched bluetooth and use a USB dongle with a 2.4Ghz radio to communicate with the headset. This is nice from FreeBSD's perspective as the headset just looks like a normal USB audio device. This is a writeup of my configuration for headsets like these, partly so others can copy it and partly so I don't forget it myself (I've already had to rediscover this once).

Unfortunately using a headset like this doesn't always work on FreeBSD out-of-the-box. Sure you can point the default sound unit at your USB's /dev/dsp* number but then what about dynamic switching? There is a way to configure sysctls to indicate a fallback sound device but that requires a application or system restart to take effect, what about fully dynamic hotswapping? Additionally there is the longstanding issue of the FreeBSD kernel panicking or waiting indefinitely if a dsp device is still opened by an application while trying to suspend.

We can actually solve all of the above issues relatively easily and without too much configuration: enter virtual_oss, a audio mixing application written by hselasky. Virtual_oss will create a virtual sound device that our applications will use, allowing us to easily switch between devices. We can also use rc and devfs to notify virtual_oss when suspend/resume and device attach/detach events take place and allow it to switch to whatever devices we want.

virtual_oss setup

Installing virtual_oss is as easy as:

$ pkg install virtual_oss
# activate the virtual_oss service
$ sysrc virtual_oss=YES
$ service virtual_oss start

Now you should be able to cat /dev/sndstat and see a virtual dsp device created by virtual_oss.

$ cat /dev/sndstat                                                   
Installed devices:
...
pcm7: <USB audio> (play/rec)
pcm8: <USB audio> (play)
Installed devices from userspace:
dsp: <Virtual OSS> (play/rec)

You then tell virtual_oss to use a particular device for playback/recording with:

$ virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp7

At which point you should have functional audio output. We still need a little more to solve the problems we originally outlined. You can also use /dev/null as the device to send the sound to the abyss.

About OSS device nodes and sound servers

Sound devices are devfs entries which applications can open and use to perform Open Sound System operations on to perform audio output/input. These are the /dev/dsp* devices. There are also the /dev/mixer* devices which allow for integration in the kernel's mixer framework. OSS has a confusing history and FreeBSD has it's own implementation of it, I'm not going to pretend to understand the full story or be able to recount it here.

Interacting with sound devices is a little bit of a pain though, in that your app is only interacting with one device at a time and must implement things like device switching themselves. If we want the OS/desktop to handle device switching then we need a "sound server". Popular sound servers are Pulseaudio and Pipewire, virtual_oss is the sound server we are using here due to its simplicity. The app will write to /dev/dsp (our virtual sound device) and virtual_oss can pipe its data to whatever sound device we selected.

Other system applications will open every sound device advertised by devfs to monitor them. Pulseaudio may do this, but other examples are the volume menu tray widget in your favorite desktop environment.

Suspend/Resume

At this point we should be able to switch to whatever device we want on the fly with virtual_oss_cmd. The next problem we run into is that we have to ensure that no applications are holding the dsp device for our USB headset open when we suspend.

Failure to do this can cause panics or indefinite waits, as the USB device will try to detach during the suspend process but there are still things using it. This was supposed to be changed to simply kill whatever app holds the sound device open, but as of the time of writing I'm still seeing panics.

To be clear this is not good behavior from the kernel. It is lunacy to deadlock the entire operating system because one app is misbehaving, even if they aren't properly using the OSS API like we want them to. We should fix the intended behavior, which is to kill any apps that are holding onto references to the sound device and complete the suspend successfully.

Luckily since we already have a dynamic swapping working we can just tell virtual_oss to stop using the device before we are suspending. You could do this manually, but luckily there is an rc script which will get called during suspend that can trigger it for us:

/etc/rc.suspend:
...
virtual_oss_cmd /dev/dsp.ctl -f /dev/null
sleep 1
...

NOTE: you must put the virtual_oss_cmd line before the acpiconf -k line at the end of the file. The reason for this is that rc.suspend will get triggered when a suspend is requested and the kernel must wait for it to complete before continuing with the suspend. This acpiconf -k line tells the kernel to go ahead, so placing this after will not have any effect.

I like to include a brief sleep just to ensure the device has time to be switched.

You should also re-enable the device after resume since it has re-attached

/etc/rc.resume:
...
virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp7
...

Hiding sound devices from misbehaving apps

Now that we have virtual_oss behaving and releasing its grip on the sound device, we might still have problems with applications who have opened every sound device not releasing them properly. This takes place with programs like the MATE volume service. Ideally we would like them to stop doing this, but since that's out of our control we can instead hide our USB headsets devfs nodes so they can't open them.

This can be done by adding the following to /etc/devfs.rules:

add path 'dsp[789]' hide 
add path 'mixer[789]' hide 

These commands can also be done manually through devfs.

The above hides the 7-9 devices, but your devices may be different. You can add any regex necessary to disable the devices you'd like. Don't hide the /dev/dsp virtual_oss device or you won't have any sound. I hide both dsp and mixer versions just to be sure.

At this point you should be able to suspend and resume without any panics and without having to reset the device in use.

Device hotswap (optional)

You can also use devd.conf to trigger virtual_oss_cmd commands for when a USB audio device has been plugged/unplugged:

ashafer@mick:git/wlroots % cat /usr/local/etc/devd/steelseries.conf                

# connect steelseries headphones to virtual_oss
notify 100 {
        match "system"         "USB";
        match "subsystem"      "INTERFACE";
        match "type"           "ATTACH";
        match "vendor"         "0x1038";  << Use your appropriate vendor
        action "/usr/local/sbin/virtual_oss_cmd /dev/dsp.ctl -f /dev/dsp7";
};

notify 100 {
        match "system"         "USB";
        match "subsystem"      "INTERFACE";
        match "type"           "DETACH";
        match "vendor"         "0x1038";
        action "/usr/local/sbin/virtual_oss_cmd /dev/dsp.ctl -f /dev/null";
};

Before doing this you'll have to grab your USB vendor id, which can be done like so:

$ usbconfig
...
ugen0.6: <SteelSeries SteelSeries Arctis 7> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)
$ usbconfig  -d ugen0.6 dump_device_desc
ugen0.6: <SteelSeries SteelSeries Arctis 7> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)

  bLength = 0x0012 
  bDescriptorType = 0x0001 
  bcdUSB = 0x0110 
  bDeviceClass = 0x0000  <Probed by interface class>
  bDeviceSubClass = 0x0000 
  bDeviceProtocol = 0x0000 
  bMaxPacketSize0 = 0x0040 
  idVendor = 0x1038 
  idProduct = 0x12ad 
  bcdDevice = 0x0119 
  iManufacturer = 0x0004  <SteelSeries >
  iProduct = 0x0005  <SteelSeries Arctis 7>
  iSerialNumber = 0x0000  <no string>
  bNumConfigurations = 0x0001 

This is useful if you are switching physical devices and want hotplug to work, but if you leave your headset plugged in 24/7 you probably don't need this.

Concluding thoughts

virtual_oss makes for a nice sound server on FreeBSD. Once you have it set up it doesn't ever need poking and it just works. I find the audio quality of USB devices to be far superior to the wired AUX ports (at least for mic input) on all of my computers, and I use this setup on my daily driver where it handles everything from basic sound output to video calls.