AirPods Pro (A2DP Bluetooth audio) working on FreeBSD 15 with blued + virtual_oss

I managed to get AirPods Pro working with full A2DP audio on FreeBSD 15.0-RELEASE, including PulseAudio integration with volume control. This should work with any A2DP Bluetooth headphones, not just AirPods.

The main challenge is that FreeBSD's built-in Bluetooth daemon hcsecd() doesn't support SSP (Simple Secure Pairing), which modern headphones require. The solution is using comms/blued as a replacement, combined with audio/virtual_oss compiled from source with Bluetooth A2DP support.

I've put together a script and configs to automate the whole process: https://github.com/cl45h/freebsd-airpods

Hardware

- Realtek RTL8761BU USB Bluetooth adapter (USB ID 0bda:8771)
- The Intel onboard adapter (ubt0) didn't work for AirPods — the HCI connection kept failing. The Realtek loads as ubt1 after firmware upload

Audio architecture

Code:
App (Waterfox, mpv, etc.)
  → PulseAudio (null-sink with software volume)
    → module-loopback
      → /dev/dsp (CUSE device created by virtual_oss)
        → virtual_oss (SBC encoder + AVDTP)
          → L2CAP socket
            → Realtek USB adapter (ubt1)
              → AirPods

Step 1: Install packages

pkg install blued rtlbt-firmware pulseaudio

Step 2: Compile virtual_oss from source

The audio/virtual_oss package doesn't include Bluetooth support. You need to compile from source:

Code:
git clone https://github.com/hselasky/virtual_oss.git
cd virtual_oss
make HAVE_BLUETOOTH=YES HAVE_BLUETOOTH_SPEAKER=YES HAVE_COMMAND=YES HAVE_SNDSTAT=YES
sudo make install

This installs to /usr/local/sbin/virtual_oss. Important: the package version at /usr/sbin/virtual_oss does NOT have Bluetooth compiled in — it tries to load voss_bt.so dynamically and fails. Make sure you use the full path /usr/local/sbin/virtual_oss or that /usr/local/sbin comes first in your PATH.

Also note: the Bluetooth backend in backend_bt/backend_bt.c requires device names to start with /dev/bluetooth/, so you need an entry in /etc/bluetooth/hosts (see Step 4).

Step 3: Create firmware symlink

rtlbtfw() looks for firmware in /usr/share/firmware/rtlbt/ but the package installs to /usr/local/share/rtlbt-firmware/:

Code:
mkdir -p /usr/share/firmware
ln -s /usr/local/share/rtlbt-firmware /usr/share/firmware/rtlbt

Step 4: Configure Bluetooth

Add your device to /etc/bluetooth/hosts:
Code:
cc:22:fe:67:67:cf airpods

Configure comms/blued to use the Realtek adapter. Edit /usr/local/etc/blued.conf:
Code:
hci_node = "ubt1hci";

Step 5: Configure rc.conf

Code:
sysrc blued_enable="YES"
sysrc kld_list+=" cuse"

The cuse kernel module is required for virtual_oss to create userspace audio devices.

Step 6: Auto-load Realtek firmware on boot (devd)

Create /usr/local/etc/devd/rtlbt.conf:
Code:
notify 100 {
    match "system"      "USB";
    match "subsystem"   "DEVICE";
    match "type"        "ATTACH";
    match "vendor"      "0x0bda";
    match "product"     "0x8771";
    action "sleep 2 && /usr/sbin/rtlbtfw -d $cdev";
};

Then restart devd:
service devd restart

Step 7: Connect

Load the Realtek firmware (first time or after reboot):
rtlbtfw -d ugen0.3

This creates ubt1. Verify with:
ngctl list | grep ubt1

Put your headphones in pairing mode, then:
Code:
bluecontrol scan
bluecontrol pair cc:22:fe:67:67:cf
bluecontrol connect cc:22:fe:67:67:cf

Start sndiod (must be BEFORE virtual_oss):
sndiod

Start the audio bridge:
nohup /usr/local/sbin/virtual_oss -C 2 -c 2 -r 44100 -b 16 -s 1024 -R /dev/null -P /dev/bluetooth/airpods -d dsp -t vdsp.ctl > /tmp/voss.log 2>&1 &

Verify it's running:
fstat /dev/cuse | grep virtual_oss

Step 8: PulseAudio routing with volume control

If you use PulseAudio (needed for browsers like Firefox/Waterfox), you can't just point it at /dev/dsp directly because module-oss doesn't support software volume on CUSE devices. The fix is a null-sink with a loopback:

Code:
pactl load-module module-null-sink sink_name=airpods_pro sink_properties=device.description="AirPods_Pro"
pactl load-module module-oss device=/dev/dsp sink_name=bt_raw
pactl load-module module-loopback source=airpods_pro.monitor sink=bt_raw latency_msec=50
pactl set-default-sink airpods_pro

Now your volume keys will control the AirPods volume through PulseAudio's software mixer.

Automation script

I wrote a script that handles the entire connection process: cleanup stale CUSE handles, firmware loading, pairing, audio bridge, and PulseAudio routing. Available at:

https://github.com/cl45h/freebsd-airpods

Usage:
Code:
sudo airpods connect      # scan, pair, connect, start audio
sudo airpods disconnect   # stop audio, restore speakers
sudo airpods status       # show connection state

Troubleshooting

"Could not create CUSE DSP device"
A previous virtual_oss process left a stale handle on /dev/cuse. Find and kill it:
Code:
fstat /dev/cuse
kill -9 <PID>
If that doesn't work, reload the module:
kldunload cuse && kldload cuse

"Connection failed" or HCI error 0x0C
The headphones are connected to another device (phone, laptop) or went to sleep. Put them in pairing mode (hold the case button until the light flashes white) and retry.

Audio plays but no sound in browser
Your browser probably uses PulseAudio. Follow Step 8 above, then restart the browser.

"Cannot open voss_bt.so"
You're running the package version of virtual_oss instead of the source-compiled one. Use the full path /usr/local/sbin/virtual_oss.

Update: airpods-ctl — native ANC and feature control

I wrote a small C tool that controls AirPods features from FreeBSD via the AACP (Apple Accessory Communication Protocol). It connects to PSM 0x1001 over L2CAP (separate from the A2DP audio channel) and sends control packets directly.

Build:
Code:
git clone https://github.com/cl45h/freebsd-airpods.git
cd freebsd-airpods
make -f Makefile.ctl
sudo make -f Makefile.ctl install

Usage:

airpods-ctl anc on — Active Noise Cancellation (isolates external sound)
airpods-ctl anc off — No noise control
airpods-ctl anc transparency — Hear your surroundings
airpods-ctl anc adaptive — Adaptive mode
airpods-ctl ca on — Conversational Awareness on
airpods-ctl ca off — Conversational Awareness off
airpods-ctl battery — Battery levels for each earbud and case
airpods-ctl ear — Ear detection state
airpods-ctl info — Device name, model, serial, firmware

Example output:
Code:
$ airpods-ctl battery
Connecting to airpods (PSM 0x1001)...
Connected.
Handshake OK.
Waiting for battery status...
Battery:
  Right : 89% (discharging)
  Left  : 87% (discharging)
  Case  : 0% (disconnected)

$ airpods-ctl info
Connecting to airpods (PSM 0x1001)...
Connected.
Handshake OK.
Waiting for device info...
Device Info:
  Name          : AirPods Pro de cl45h
  Model         : A3063
  Manufacturer  : Apple Inc.
  Serial        : JK4C213QYW
  Firmware      : 81.2675000075000000.6814

The protocol is based on the reverse engineering work done by LibrePods. All commands are simple hex byte sequences over L2CAP — no encryption needed for basic features. The AirPods need to be connected via bluecontrol connect first (same as for audio).

Code is in the same repo: https://github.com/cl45h/freebsd-airpods


Credits

- JRG Systems blog post — original blued guide that got me started
- virtual_oss by Hans Petter Selasky — virtual audio device with Bluetooth A2DP backend
- LibrePods - reverse-engineered AirPods protocol documentation
 
Back
Top