Bluetooth Audio on OpenBSD with the Creative BT-W3

posted on nov 18th, 2020 with tags openbsd

Fifteen years ago, NetBSD’s Bluetooth audio stack was imported into OpenBSD. From what I remember using it back then, it worked sufficiently well but its configuration was cumbersome. It supported Bluetooth HID keyboards and mice, audio, and serial devices. Six years ago, however, it was tedu'd due to conflicts with how it integrated into our kernel.

While we still have no Bluetooth support today, it is possible to play audio on Bluetooth headphones using a small hardware dongle.

Creative BT-W2

Last year I came across the Creative BT-W2 USB device, which presents a standard uaudio(4) device on OpenBSD and handles all of the Bluetooth pairing and audio communication itself with just one shortcoming: it did not expose any volume control mechanism. OpenBSD’s sound server, sndiod, did have software volume control so it was possible to limit the volume through aucatctl (now sndioctl).

I’ve been using the BT-W2 frequently since then to send audio from my OpenBSD laptop to my Apple AirPods Pro, but unfortunately Apple released a firmware at some point that limited the volume output when paired with such devices, including Android phones. Presumably this was a safety measure because unless the sending side was doing software volume control (which the AirPods wouldn’t know about), the AirPods would play at maximum volume.

Unfortunately, even at the loudest volume from sndiod, the volume to the AirPods was still quite low, sometimes even too low to understand YouTube videos with poor audio like conference talks. Otherwise though, the BT-W2 worked well and I didn’t notice any latency or video sync issues on OpenBSD.

Creative BT-W3

The other day I became aware of the updated Creative BT-W3, which now has a USB-C interface instead of USB-A and finally exposes hardware mixer control (note the 2 ctls):

uaudio0 at uhub0 port 3 configuration 1 interface 1 "Creative Technology Ltd Creative BT-W3" rev 2.00/1.00 addr 2
uaudio0: class v1, full-speed, sync, channels: 2 play, 1 rec, 2 ctls
audio1 at uaudio0

Since Tweeting about the BT-W2 last year, OpenBSD’s audio system has changed quite a bit and now sndiod controls output volume itself with sndioctl being the preferred utility, rather than directly changing hardware mixer settings with mixerctl as in years past. The new hardware volume control (outputs.dac) can still be seen or modified directly with mixerctl and passing it the control device for audio1 (as the default /dev/audioctl0 is for the built-in audio device of my laptop):

# mixerctl -f /dev/audioctl1
outputs.dac=161
outputs.dac_mute=off
record.enable=sysctl

Whatever mechanism the BT-W3 uses to handle this hardware volume control (whether just doing software volume limiting itself, or passing it through to the AirPods through some fancy audio protocol), the benefit is that now the AirPods can be used at full volume from OpenBSD.

Automatically Switching to Bluetooth

My laptop’s Dolby Atmos speaker setup is pretty good, so normally I just listen to music or play YouTube videos through the speakers. When my son is napping and I need to use my AirPods, I want to just plug in the BT-W3 dongle and have it automatically start sending audio to my AirPods, and have the volume controls on my keyboard control the AirPods.

To accomplish this, set an alternate device name with sndiod:

# rcctl set sndiod flags -f rsnd/0 -F rsnd/1
# rcctl restart sndiod

In this mode, sndiod will play through rsnd/1 if it exists, which maps to the second audio device (audio1). If the device is not present, such as when the BT-W3 is not plugged in, it will play through rsnd/0 which maps to audio0, the laptop’s built-in speakers.

This works fine if the device is present when sndiod starts, but otherwise it will need a SIGHUP to re-scan the audio devices once the BT-W3 is plugged in, and start sending audio through it. This can be done automatically with hotplugd:

# cat > /etc/hotplug/attach
case $2 in
uaudio*)
	pkill -HUP sndiod
	;;
esac
^D
# chmod +x /etc/hotplug/attach
# rcctl enable hotplugd
# rcctl start hotplugd

Now when a new uaudio device is plugged in and detected by the kernel, hotplugd will send a SIGHUP to sndiod which will see that rsnd/1 is available and start sending audio to it. When the BT-W3 is unplugged, sndiod will automatically detect that the device is no longer usable and send audio to its fallback, rsnd/0. Hardware device switching will be seamless and any applications playing audio won’t have to stop or be restarted.

My window manager is configured to respond to the hardware volume keys on my laptop (F4 for mute, F5 for volume down, and F6 for volume up) by executing sndioctl, so the commands will work the same regardless of which device sndiod is talking to.

definekey top F4 exec sndioctl -q output.mute=!; pkill -USR1 i3status; true
definekey top F5 exec sndioctl -q output.mute=0; sndioctl -q output.level=-0.05; pkill -USR1 i3status; true
definekey top F6 exec sndioctl -q output.mute=0; sndioctl -q output.level=+0.05; pkill -USR1 i3status; true