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.