Skip to content

Instantly share code, notes, and snippets.

@mie00
Created May 8, 2026 17:02
Show Gist options
  • Select an option

  • Save mie00/ff785d3f5e10d0880e81eff48613c2a6 to your computer and use it in GitHub Desktop.

Select an option

Save mie00/ff785d3f5e10d0880e81eff48613c2a6 to your computer and use it in GitHub Desktop.
Bose QC Ultra 2 Earbuds - Communication Mode on Linux (PipeWire patch for HFP NREC + chime)
DPkg::Post-Invoke { "if dpkg -l libspa-0.2-bluetooth 2>/dev/null | grep -q '^ii'; then /home/mie/go/src/github.com/mie00/workspace/bose-call-mode/rebuild-pipewire-bt.sh >> /var/log/pipewire-bt-patch.log 2>&1 || true; fi"; };
--- a/spa/plugins/bluez5/backend-native.c
+++ b/spa/plugins/bluez5/backend-native.c
@@ -1108,6 +1108,12 @@
} else if (spa_strstartswith(buf, "AT+APLSIRI?")) {
// This command is sent when we activate Apple extensions
rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+NREC=")) {
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BTRH?")) {
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CCWA=")) {
+ rfcomm_send_reply(rfcomm, "OK");
} else if (!mm_is_available(backend->modemmanager)) {
spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf);
rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE);
@@ -1579,10 +1585,18 @@
spa_log_debug(backend->log, "transport %p: enter sco_acquire_cb", t);
- if (optional || t->fd > 0)
+ if (optional || t->fd > 0) {
sock = t->fd;
- else
+ } else {
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (td->rfcomm && td->rfcomm->slc_configured &&
+ t->profile == SPA_BT_PROFILE_HFP_HF) {
+ rfcomm_send_reply(td->rfcomm, "+CIEV: 3,1");
+ rfcomm_send_reply(td->rfcomm, "RING");
+ }
+#endif
sock = sco_do_connect(t);
+ }
if (sock < 0)
goto fail;
@@ -1643,6 +1657,10 @@
#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
rfcomm_hfp_ag_set_cind(td->rfcomm, false);
+ if (td->rfcomm && td->rfcomm->slc_configured &&
+ t->profile == SPA_BT_PROFILE_HFP_HF) {
+ rfcomm_send_reply(td->rfcomm, "+CIEV: 3,0");
+ }
#endif
sco_destroy_cb(t);
#!/bin/bash
# Rebuild PipeWire bluetooth plugin with HFP NREC/comm mode patch.
# Run automatically via apt hook or manually after package updates.
set -e
PATCH_FILE="/home/mie/go/src/github.com/mie00/workspace/bose-call-mode/hfp-nrec-comm-mode.patch"
TARGET="/usr/lib/x86_64-linux-gnu/spa-0.2/bluez5/libspa-bluez5.so"
BUILD_DIR="/tmp/pipewire-patch-build"
echo "[pipewire-bt-patch] Checking if rebuild needed..."
# Get installed version
PKG_VER=$(dpkg-query -W -f='${Version}' libspa-0.2-bluetooth 2>/dev/null)
if [ -z "$PKG_VER" ]; then
echo "[pipewire-bt-patch] libspa-0.2-bluetooth not installed, skipping"
exit 0
fi
# Check if our patch is already applied (look for our marker string)
if strings "$TARGET" 2>/dev/null | grep -q "AT+CCWA="; then
echo "[pipewire-bt-patch] Patch already applied, skipping"
exit 0
fi
echo "[pipewire-bt-patch] Rebuilding with patch for version $PKG_VER..."
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
# Download and extract source
apt source pipewire 2>&1 | tail -3
# Find extracted directory
SRC_DIR=$(find . -maxdepth 1 -type d -name "pipewire-*" | head -1)
if [ -z "$SRC_DIR" ]; then
echo "[pipewire-bt-patch] ERROR: Could not find source directory"
exit 1
fi
cd "$SRC_DIR"
# Apply patch
echo "[pipewire-bt-patch] Applying patch..."
patch -p1 < "$PATCH_FILE"
# Build only the bluetooth plugin
echo "[pipewire-bt-patch] Building..."
meson setup build
ninja -C build spa/plugins/bluez5/libspa-bluez5.so
# Install
echo "[pipewire-bt-patch] Installing..."
cp build/spa/plugins/bluez5/libspa-bluez5.so "$TARGET"
# Restart services
echo "[pipewire-bt-patch] Restarting PipeWire..."
sudo -u mie XDG_RUNTIME_DIR=/run/user/$(id -u mie) systemctl --user restart pipewire wireplumber
echo "[pipewire-bt-patch] Done."
# Cleanup
rm -rf "$BUILD_DIR"

Bose QC Ultra 2 Earbuds - Communication Mode on Linux

Full setup guide to enable automatic communication mode (noise suppression, voice focus, chime) on Linux with PipeWire/WirePlumber.

Prerequisites

  • Ubuntu 24.04 (Noble) with PipeWire 1.0.5 and WirePlumber 0.4.17
  • Bose QC Ultra 2 Earbuds paired via Bluetooth
  • BlueZ 5.x

1. Patch PipeWire bluetooth plugin

Get source and build dependencies

# Enable deb-src in /etc/apt/sources.list.d/ubuntu.sources
# Change "Types: deb" to "Types: deb deb-src" on both stanzas
sudo apt update
sudo apt build-dep pipewire
cd /tmp && apt source pipewire

Patch pipewire-1.0.5/spa/plugins/bluez5/backend-native.c

A) AT command handlers

Find the } else if (!mm_is_available(backend->modemmanager)) { line (around line 1111) and add these handlers immediately before it:

} else if (spa_strstartswith(buf, "AT+NREC=")) {
    rfcomm_send_reply(rfcomm, "OK");
} else if (spa_strstartswith(buf, "AT+BTRH?")) {
    rfcomm_send_reply(rfcomm, "OK");
} else if (spa_strstartswith(buf, "AT+CCWA=")) {
    rfcomm_send_reply(rfcomm, "OK");
} else if (!mm_is_available(backend->modemmanager)) {

What this does:

  • AT+NREC=0: Earbuds say "I'll handle noise reduction." Responding OK tells them to keep their DSP active. Without this, the earbuds send raw unprocessed mic audio.
  • AT+BTRH?: Response and Hold status query. OK means "no held calls."
  • AT+CCWA=1: Call Waiting enable. OK acknowledges the request.

All three are part of the HFP Service Level Connection setup. Without OK responses, the handshake is incomplete.

B) RING injection on SCO acquire (triggers chime)

In sco_acquire_cb, replace:

if (optional || t->fd > 0)
    sock = t->fd;
else
    sock = sco_do_connect(t);

With:

if (optional || t->fd > 0) {
    sock = t->fd;
} else {
#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
    if (td->rfcomm && td->rfcomm->slc_configured &&
            t->profile == SPA_BT_PROFILE_HFP_HF) {
        rfcomm_send_reply(td->rfcomm, "+CIEV: 3,1");
        rfcomm_send_reply(td->rfcomm, "RING");
    }
#endif
    sock = sco_do_connect(t);
}

This sends an incoming-call indicator and RING just before the SCO link opens, which makes the earbuds play the "entering call" chime.

C) Call-ended on SCO release

In sco_release_cb, after the existing rfcomm_hfp_ag_set_cind(td->rfcomm, false); line, add:

if (td->rfcomm && td->rfcomm->slc_configured &&
        t->profile == SPA_BT_PROFILE_HFP_HF) {
    rfcomm_send_reply(td->rfcomm, "+CIEV: 3,0");
}

This resets the call setup indicator when switching back to A2DP.

Build and install

cd /tmp/pipewire-1.0.5
meson setup build
ninja -C build spa/plugins/bluez5/libspa-bluez5.so
sudo cp build/spa/plugins/bluez5/libspa-bluez5.so /usr/lib/x86_64-linux-gnu/spa-0.2/bluez5/libspa-bluez5.so
systemctl --user restart pipewire wireplumber

2. Set Bluetooth earbuds as default audio output

In GNOME Settings > Sound, set the Bose earbuds as the default output device. This is required for WirePlumber's built-in auto-switch policy to work.

WirePlumber monitors audio streams with media.role = "Communication" or from known applications (Chrome, Firefox, Zoom, Telegram, etc.). When such a stream opens, it automatically switches from A2DP to HFP. When the stream stops, it restores A2DP after 2 seconds.

How it works

Change Effect
AT+NREC=0 -> OK Earbuds keep their noise reduction DSP active
AT+BTRH? / AT+CCWA=1 -> OK Completes HFP handshake without a real modem
RING on SCO acquire Earbuds play chime when switching to HFP
+CIEV:3,0 on SCO release Clean call-ended state when switching back to A2DP
Default sink = Bluetooth WirePlumber auto-switches profiles for communication apps

Verification

After setup, connect the earbuds and open a voice app (Google Meet, Zoom). You should hear:

  1. The chime when the app starts using the mic (A2DP -> HFP switch)
  2. Good voice quality with noise suppression active
  3. Return to high-quality A2DP audio when the call ends

Notes

  • The patch only builds libspa-bluez5.so (the bluetooth SPA plugin), not all of PipeWire
  • System package updates to libspa-0.2-bluetooth will overwrite the patched library -- you'll need to rebuild after updates
  • No changes to BlueZ config, D-Bus policies, or WirePlumber lua scripts are required
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment