Fifteen years ago, NetBSD’s Bluetooth audio stack was
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
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.
Last year I
USB device, which presents a standard
device on OpenBSD and handles all of the Bluetooth pairing and audio
communication itself with just one shortcoming: it did not expose any volume
OpenBSD’s sound server,
did have software volume control so it was possible to limit the volume through
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.
The other day I became aware of the updated
which now has a USB-C interface instead of USB-A and finally exposes hardware
mixer control (note the
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
the preferred utility, rather than directly changing hardware mixer settings
mixerctl as in years past.
The new hardware volume control (
outputs.dac) can still be seen or modified
mixerctl and passing it the control device for
/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
# 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 (
If the device is not present, such as when the BT-W3 is not plugged in, it will
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
# 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
sndiod which will see that
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,
Hardware device switching will be seamless and any applications playing audio
won’t have to stop or be restarted.
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
Responding to Headphone Buttons
If your Bluetooth headphones have buttons on them, these can pass through the BT-W3 as USB HID reports. My AirPods Pro have one hardware button (a squeeze on the stem) which can be single, double, or triple pressed to perform a play/pause, next track, and previous track.
The possible actions that the BT-W3 supports can be seen with
on the first HID report of the device, which must be located in
uaudio1 at uhub0 port 1 configuration 1 interface 1 "Creative Technology Ltd Creative BT-W3" rev 2.00/1.00 addr 10 uaudio1: class v1, full-speed, sync, channels: 2 play, 1 rec, 2 ctls audio2 at uaudio1 uhidev4 at uhub0 port 1 configuration 1 interface 3 "Creative Technology Ltd Creative BT-W3" rev 2.00/1.00 addr 10 uhidev4: iclass 3/0, 3 report ids uhid11 at uhidev4 reportid 1: input=2, output=0, feature=0 [...]
In my case, the first HID report on the BT-W3 is
uhid11, so running
/dev/uhid11 can retrieve the full report descriptor:
# usbhidctl -f /dev/uhid11 -r Report descriptor: Collection page=Consumer usage=Consumer_Control Input size=1 count=1 page=Consumer usage=Play/Pause, logical range 0..1 Input size=1 count=1 page=Consumer usage=Scan_Next_Track, logical range 0..1 Input size=1 count=1 page=Consumer usage=Scan_Previous_Track, logical range 0..1 Input size=1 count=1 page=Consumer usage=Stop, logical range 0..1 Input size=1 count=1 page=Consumer usage=Play, logical range 0..1 Input size=1 count=1 page=Consumer usage=Pause, logical range 0..1 Input size=1 count=1 page=Consumer usage=Fast_Forward, logical range 0..1 Input size=1 count=1 page=Consumer usage=Rewind, logical range 0..1 Input size=1 count=1 page=Consumer usage=Volume_Increment, logical range 0..1 Input size=1 count=1 page=Consumer usage=Volume_Decrement, logical range 0..1 Input size=1 count=1 page=Consumer usage=Mute, logical range 0..1 End collection Total input size 2 bytes Total output size 0 bytes Total feature size 0 bytes
By using the
-l option, input reports can be seen when the button on the
AirPod is pressed:
# usbhidctl -f /dev/uhid11 -l Consumer_Control.Play/Pause=1 Consumer_Control.Scan_Next_Track=0 Consumer_Control.Scan_Previous_Track=0 Consumer_Control.Stop=0 Consumer_Control.Play=0 Consumer_Control.Pause=0 Consumer_Control.Fast_Forward=0 Consumer_Control.Rewind=0 Consumer_Control.Volume_Increment=0 Consumer_Control.Volume_Decrement=0 Consumer_Control.Mute=0 Consumer_Control.Play/Pause=0 [...]
One event is generated to report
Consumer_Control.Play/Pause=1, then another
right after it to report
To automate responding to these events,
can be used.
By default, the
/dev/uhid* devices are owned by
root:wheel and are mode
0600, so to make things easier, I’ll
0660 so I can access them
This needed because the program has to run as my own user to access my X11
session and environment variables.
With a simple configuration file, I can make
usbhidaction run my
script to play/pause, skip to the next track, or play the previous track.
$ cat .usbhidaction.conf Consumer:Play/Pause 1 ~/bin/music playpause Consumer:Scan_Next_Track 1 ~/bin/music next Consumer:Scan_Previous_Track 1 ~/bin/music prev $ usbhidaction -dv -c .usbhidaction.conf -f /dev/uhid11 PARSE:1 Consumer:Play/Pause, 1, '~/bin/music playpause' PARSE:2 Consumer:Scan_Next_Track, 1, '~/bin/music next' PARSE:3 Consumer:Scan_Previous_Track, 1, '~/bin/music prev' report size 2 executing '~/bin/music playpause'
usbhidaction is not a very user-friendly program so it must be
started after the BT-W3 is plugged in and you must lookup which
uhid device is
the correct one to operate on each time.
to work around these issues but it would be nice to have something more generic
that listens for input reports from all
uhid devices automatically and outputs
them on some device stream that any program can listen to.
But that is a project for another time.