[Bluetooth-audio] How to connect and use Bluetooth headphones on FreeBSD

Important notes:
1) Tested and confirmed to work on FreeBSD 13.0-RELEASE.
2) Obviously you need to first confirm whether FreeBSD supports the built-in Bluetooth of your laptop/computer.
3) For this guide, we will focus on Intel Bluetooth as that's my case in particular. Hence the use of comms/iwmbt-firmware.
4) Both "hcsecd" and "sdpd" Bluetooth-related services are intentionally not enabled on /etc/rc.conf because they're not really needed in my setup. Nonetheless, adding them shouldn't do any harm.
5) Remember to replace "BD_ADDR" by the real unique address of your Bluetooth device. How to get this address is explained later on.

# First, become superuser and install the programs we need:
pkg install iwmbt-firmware virtual_oss # If yours isn't Intel, only install audio/virtual_oss.

# Nowadays, drivers are auto-loaded at system start-up, but installing comms/iwmbt-firmware was still needed, as well as issuing the following command afterwards:
service devd restart # This way /etc/devd/iwmbtfw.conf is executed and the required firmware files are downloaded.

# Next load cuse and make it permanent, for it's needed by virtual_oss:
kldload cuse
sysrc kld_list+="cuse"

# Start the Bluetooth stack:
service hcsecd onestart
service bluetooth start ubt0 # If this fails, do it again.

# Now find your Bluetooth headphones (make sure they're on):
hccontrol -n ubt0hci inquiry # Take note of the unique address (BD_ADDR) of your Bluetooth device. Example: "98:52:3d:48:3a:51".

# If unsure, find out the human readable name assigned to your remote device:
hccontrol -n ubt0hci remote_name_request BD_ADDR # Replace BD_ADDR by the real unique address of your Bluetooth device.

# Create a connection to your remote device:
hccontrol -n ubt0hci create_connection BD_ADDR # Replace BD_ADDR by the real unique address of your Bluetooth device.

# Edit /etc/bluetooth/hosts to assign an alias to your device (replace "98:52:3d:48:3a:51" with your actual address). Example:
# $Id: hosts,v 1.1 2003/05/21 17:48:40 max Exp $
# $FreeBSD$
# Bluetooth Host Database
# This file should contain the Bluetooth addresses and aliases for hosts.
# BD_ADDR               Name [ alias0 alias1 ... ]

# 00:11:22:33:44:55    phone
98:52:3d:48:3a:51 headphones

# Now create a virtual sound device so you can use your Bluetooth headphones as a sound sink:
virtual_oss -T /dev/sndstat -S -a o,-4 -C 2 -c 2 -r 44100 -b 16 -s 1024 -R /dev/dsp0 -P /dev/bluetooth/headphones -d dsp -t vdsp.ctl

*Let's explain a little some of the meaning of the above command:
- I wanted this virtual device to be exposed to other apps, so install entry in /dev/sndstat. ( -T /dev/sndstat)
- Enabled automatic resampling to fix bad sound quality when playing audio with a different sample-rate than default. ( -S)
- The default volume was too high so I set a negative output amplification. ( -a o,-4)
- I chose a sample rate of 44100 Hz but you can use 48000 Hz instead if that's better for your hardware. ( -r 44100)
- I wanted to be able to use my laptop's internal mic simultaneously while using my headphones. ( -R /dev/dsp0)
- You can disable the recording device by setting "-R /dev/null" instead.

All this works great for programs that use OSS as sound backend (the vast majority in FreeBSD). However, other backends need certain workarounds or extra steps:

# To fix applications that use sndio as backend (e.g. Chromium), you need to enable the sndiod service:
sysrc sndiod_enable=YES
service sndiod start

# To fix the ones that use a pulseaudio backend (e.g. Firefox and linux Brave), recording the sound output and immediately redirecting it to /dev/dsp acts as a workaround:
pacat --record -d oss_output.dsp0.monitor > /dev/dsp & # It's the only solution I could figure out and it's not perfect, as a very tiny delay can be noticed.
# Also you'd want to mute your computer speakers to avoid hearing double:
mixer pcm 0 # Remember to set it back to 100 after disconnecting your headphones, or you won't hear anything.
# Depending on your particular case, you may need to replace "oss_output.dsp0.monitor" by a different source-output. To list the ones available use:
pacmd list-sources | grep monitor

# Sadly, I haven't found yet a workaround to hear sound from Wine games and programs.

# Finally, I created a little script to automate the audio output device switching between Bluetooth headphones and the built-in speakers:

if [ "$(cat /dev/sndstat | grep 'Virtual OSS')" = "dsp: <Virtual OSS> (play/rec)" ]; then
 killall pacat ; mixer pcm 100 ; sudo /usr/bin/killall virtual_oss ; sudo /sbin/sysctl hw.snd.basename_clone=1 ; notify-send --app-name=Bluetooth --icon=bluetooth -t 5000 'Bluetooth sound device disconnected'
 notify-send --app-name=Bluetooth --icon=bluetooth -t 5000 'Connecting Bluetooth sound device...'
 sudo virtual_oss -T /dev/sndstat -S -a o,-4 -C 2 -c 2 -r 44100 -b 16 -s 1024 -R /dev/dsp0 -P /dev/bluetooth/headphones -d dsp -t vdsp.ctl & sleep 5 && mixer pcm 0 ; pacat --record -d oss_output.dsp0.monitor > /dev/dsp &

*Note: This script is meant to be run as normal user. Remember to make it executable (chmod +x). Also I suggest to add the commands that need "sudo" to /usr/local/etc/sudoers to allow them running without the need of entering your password. At least I did that.
You can then create a keyboard shortcut to run it easily.
You may also want to increase the sleep time to 15 seconds or more if your device takes too long to connect. I say this because it's important to run the pacat command AFTER the device is correctly paired, otherwise the sound will be out of sync. In that case a simple killall pacat ; pacat --record -d oss_output.dsp0.monitor > /dev/dsp & should suffice to set it right anyway.
DISCLAIMER: I'm not a coder, so I assume the script above could be greatly improved. All I can say is it works for me :) Any suggestions and corrections are highly welcome.


UPDATE 1 (2021-11-01) :
- Added the -S option to the virtual_oss command to enable automatic resampling using Project Z. This fixes bad sound quality when playing audio files with a sample rate different than default.
- Mentioned some additional tips regarding the script.
This is awesome thank you. I used a similar virtual-oss line but didn't know about the -S so I must try that. BTW I have had issues when using bluetooth and wifi on my HP Zbook G3. They seem to interfere with one another and either the wifi or the bluetooth will drop from time to time. The laptop has an Intel(R) Dual Band Wireless AC 8260 in it. Anyway what I wanted to add is that for those that use applications that use Alsa you can add your bluetooth audio to the ~/.asoundrc after installing the correct alsa backends for OSS. I think I installed all packages for ALSA in an attempt to get it working. Below is my .asoundrc the mu6 is my bluetooth headset, the headphones device is the headphone jack on the side of the laptop. YMMV.

pcm.hdmi0 {
    type oss
    device /dev/dsp0
pcm.hdmi1 {
    type oss
    device /dev/dsp1
pcm.hdmi2 {
    type oss
    device /dev/dsp2
pcm.hdmi3 {
    type oss
    device /dev/dsp3
pcm.Internal {
    type oss
    device /dev/dsp4
pcm.Headphones {
    type oss
    device /dev/dsp5
pcm.Mu6 {
    type oss
    device /dev/dsp6
ctl.hdmi0 {
    type oss
    device /dev/mixer0
ctl.hdmi1 {
    type oss
    device /dev/mixer1
ctl.hdmi2 {
    type oss
    device /dev/mixer2
ctl.hdmi3 {
    type oss
    device /dev/mixer3
ctl.Internal {
    type oss
    device /dev/mixer4
ctl.Headphones {
    type oss
    device /dev/mixer5
ctl.Mu6 {
    type oss
    device /dev/mixer6
pcm.!default pcm.Headphones
ctl.!default ctl.Headphones

In your virtual_oss command Instead of -d dsp try using -d dsp6 or -d dsp[some number not inuse] then you can tell firefox or Linux Brave to use that dsp device in pulseaudio since it will be listed as a different device in /dev/sndstat so you can make it the default device in pulse if needed. Also you can do a -B in the virtual_oss command and run it in the background. Below is the output after running the virtual_oss with -d dsp6 option notice how it set the virtual_oss pcm/dsp devices as userspace. I couldn't figure out how to force it to be seen as a kernel device it's probably done this way on purpose but I'm not able to do sysctl hw.snd.default_unit=6 to make the userspace device the system default device.

cat /dev/sndstat
Installed devices:
pcm0:  (play)
pcm1:  (play)
pcm2:  (play)
pcm3:  (play)
pcm4:  (play/rec)
pcm5:  (play)
 default Installed devices from userspace:
pcm6:  (play/rec)
dsp6:  (play/rec)

Side Note: Also HDMI audio didn't work until I installed the Nvidia drivers then they all worked.
Thank you very much for the guide. I want to add one thing: OpenAL (Wine programs and games) works with Virtual OSS too, with version at least 1.2.16. Previous versions probably not work. It also requires some changes to the command for starting Virtual OSS. My command looks that:

sudo virtual_oss -T /dev/sndstat -S -C 2 -c 2 -r 44100 -b 16 -s 1024 -P /dev/bluetooth/headphones -R /dev/null -w vdsp.ctl -d dsp -l mixer &

The only problem is, after the change, the line in the script in the guide:

if [ "$(cat /dev/sndstat | grep 'Virtual OSS')" = "dsp: <Virtual OSS> (play/rec)" ]; then

Not detect properly the connected bluetooth headphones. I need to find the solution for the problem. :)
-w vdsp.ctl should probably be -t vdsp.ctl -T /dev/sndstat
Thank you for the suggestion. I just tried it. For me, it works in the same way as -w vdsp.ctl. Both commands properly create a device.

And fix for the shell. I noticed that grep returns two lines instead of one, that is the reason it was not working. The working version:

if [ "$(cat /dev/sndstat | grep 'dsp: <Virtual OSS')" = "dsp: <Virtual OSS> (play/rec)" ]; then
For me -S produces a degradation is quality. So I undo the changes in my /etc/rc.conf to this:

virtual_oss_dsp="-C 2 -c 2 -r 44100 -b 16 -s 1024 -R /dev/null -P /dev/bluetooth/04:fe:a1:4d:fa:7c -T /dev/sndstat -d dsp"

whit that line I am perfectly happy.
In my setup, sometimes in the middle of songs there are some small hiccups. Is it manifesting to you, too?
In my setup, sometimes in the middle of songs there are some small hiccups. Is it manifesting to you, too?
I changed a bit my setup some time ago, now it looks that:

sudo virtual_oss -T /dev/sndstat -C 2 -c 2 -r 48000 -b 16 -s 20ms -P /dev/bluetooth/headphones -R /dev/null -w vdsp.ctl -d dsp -l mixer &

Same as you, removed -S but also raised sample-rate (-r) and changed buffer size (-s).
First of all, thanks to the detailed info! Thanks to this little howto I actually now got bluetooth audio working.

That being said, regarding the comment by zsolt:

In my setup, sometimes in the middle of songs there are some small hiccups. Is it manifesting to you, too?

I am experiencing this too, listenting to music in chromium right now. Did you find the reason why this might be happening?