From db7c74a042b89feb917313bd4347b6888a754a0c Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Fri, 16 Jan 2026 16:16:22 +0800 Subject: [PATCH 1/7] bluez5/backend-native: Add HFP/HSP hardware offload datapath configuration Add support for configuring the SCO hardware offload data path for HFP/HSP profiles using the Bluetooth SIG-specified procedure. This enables vendor-specific SCO offload integrations. Changes: - Add `bluez5.hw-offload-datapath` configuration property (default: 0) - Implement `sco_offload_btcodec()` to set BT_CODEC socket option - Add `SPA_BT_FEATURE_HW_OFFLOAD` quirk feature flag - Apply offload configuration when creating SCO sockets if quirk enabled - Document new property in pipewire-props.7.md The datapath ID is configurable via device parameters and only applied when the hardware offload feature flag is set in quirks, allowing platform-specific SCO offload implementations. --- doc/dox/config/pipewire-props.7.md | 9 ++++++ spa/plugins/bluez5/backend-native.c | 46 +++++++++++++++++++++++++++++ spa/plugins/bluez5/defs.h | 4 ++- spa/plugins/bluez5/quirks.c | 6 ++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index ed82b2f41..d7e75e33d 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1171,6 +1171,15 @@ in a platform-specific way. See `tests/examples/bt-pinephone.lua` in WirePlumber Do not enable this setting if you don't know what all this means, as it won't work. \endparblock +@PAR@ device-param bluez5.hw-offload-datapath # integer +\parblock +HFP/HSP hardware offload data path ID (default: 0). + +This feature configures the SCO hardware‑offload data path for HFP/HSP using the Bluetooth +SIG–specified procedure. It is intended for advanced setups and vendor integrations. Do not +edit this unless required; incorrect values can disable SCO offload. +\endparblock + @PAR@ monitor-prop bluez5.a2dp.opus.pro.channels = 3 # integer PipeWire Opus Pro audio profile channel count. diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 5c4a2b785..5dde972fc 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -112,6 +112,7 @@ struct impl { int hfp_default_speaker_volume; struct spa_source sco; + unsigned int hfphsp_sco_datapath; const struct spa_bt_quirks *quirks; @@ -297,6 +298,32 @@ static const struct media_codec *codec_list_best(struct impl *backend, struct sp return NULL; } +static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) +{ + int err; + char buffer[255]; + struct bt_codecs *codecs; + + spa_log_info(backend->log, "%s: sock(%d) msbc(%d)", __func__, sock, msbc); + + memset(buffer, 0, sizeof(buffer)); + codecs = (void *)buffer; + if (msbc) + codecs->codecs[0].id = 0x05; + else + codecs->codecs[0].id = 0x02; + codecs->num_codecs = 1; + codecs->codecs[0].data_path_id = backend->hfphsp_sco_datapath; + codecs->codecs[0].num_caps = 0x00; + + err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); + if (err < 0) + spa_log_error(backend->log, "%s: ERROR: %s (%d)", __func__, strerror(errno), errno); + else + spa_log_info(backend->log, "%s: set offload codec succeeded", __func__); + return err; +} + static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) { if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) @@ -2564,6 +2591,7 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte struct sockaddr_sco addr; socklen_t len; bdaddr_t src; + uint32_t bt_features; spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); if (sock < 0) { @@ -2595,6 +2623,11 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte } } + if (backend->quirks && + (spa_bt_quirks_get_features(backend->quirks, NULL, NULL, &bt_features) == 0) && + ((bt_features & (SPA_BT_FEATURE_HW_OFFLOAD)) != 0)) + sco_offload_btcodec(backend, sock, transparent); + return spa_steal_fd(sock); } @@ -4101,6 +4134,18 @@ static void parse_hfp_default_volumes(struct impl *backend, const struct spa_dic backend->hfp_default_speaker_volume = SPA_BT_VOLUME_HS_MAX; } +static void parse_sco_datapath(struct impl *backend, const struct spa_dict *info) +{ + uint32_t tmp; + const char *str; + + backend->hfphsp_sco_datapath = HFP_SCO_DEFAULT_DATAPATH; + + if ((str = spa_dict_lookup(info, "bluez5.hw-offload-datapath")) != NULL && + (tmp = atoi(str)) > 0) + backend->hfphsp_sco_datapath = tmp; +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_native_free, @@ -4163,6 +4208,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, parse_hfp_disable_nrec(backend, info); parse_hfp_default_volumes(backend, info); parse_hfp_pts(backend, info); + parse_sco_datapath(backend, info); #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE if (!dbus_connection_register_object_path(backend->conn, diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 3efec465a..4bc4fa3bb 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -135,7 +135,8 @@ extern "C" { #define PROFILE_HFP_AG "/Profile/HFPAG" #define PROFILE_HFP_HF "/Profile/HFPHF" -#define HSP_HS_DEFAULT_CHANNEL 3 +#define HSP_HS_DEFAULT_CHANNEL 3 +#define HFP_SCO_DEFAULT_DATAPATH 0 #define SOURCE_ID_BLUETOOTH 0x1 /* Bluetooth SIG */ #define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */ @@ -809,6 +810,7 @@ enum spa_bt_feature { SPA_BT_FEATURE_SBC_XQ = (1 << 5), SPA_BT_FEATURE_FASTSTREAM = (1 << 6), SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), + SPA_BT_FEATURE_HW_OFFLOAD = (1 << 8), }; struct spa_bt_quirks; diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c index c4b293e68..2a1c5e860 100644 --- a/spa/plugins/bluez5/quirks.c +++ b/spa/plugins/bluez5/quirks.c @@ -52,6 +52,7 @@ struct spa_bt_quirks { int force_sbc_xq; int force_faststream; int force_a2dp_duplex; + int force_hw_offload; char *device_rules; char *adapter_rules; @@ -69,6 +70,7 @@ static enum spa_bt_feature parse_feature(const char *str) { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, { "faststream", SPA_BT_FEATURE_FASTSTREAM }, { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, + { "hw-offload", SPA_BT_FEATURE_HW_OFFLOAD }, }; SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { if (spa_streq(str, f->key)) @@ -228,6 +230,7 @@ struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct s this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume"); this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); + this->force_hw_offload = parse_force_flag(info, "bluez5.hw-offload-sco"); if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) { spa_log_debug(this->log, "loading session manager provided data"); @@ -385,6 +388,9 @@ static int get_features(const struct spa_bt_quirks *this, if (this->force_a2dp_duplex != -1) SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); + if (this->force_hw_offload != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_OFFLOAD, this->force_hw_offload); + return 0; } From 2d6a7d2186e1ae383eb4a88eed33c55cb51fbe0e Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Sat, 17 Jan 2026 19:44:56 +0800 Subject: [PATCH 2/7] bluez5: fix format string in sco_offload_btcodec log message --- spa/plugins/bluez5/backend-native.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 5dde972fc..cba0fd4f3 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -304,7 +304,7 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) char buffer[255]; struct bt_codecs *codecs; - spa_log_info(backend->log, "%s: sock(%d) msbc(%d)", __func__, sock, msbc); + spa_log_info(backend->log, "sock(%d) msbc(%d)", sock, msbc); memset(buffer, 0, sizeof(buffer)); codecs = (void *)buffer; @@ -318,9 +318,9 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); if (err < 0) - spa_log_error(backend->log, "%s: ERROR: %s (%d)", __func__, strerror(errno), errno); + spa_log_error(backend->log, "ERROR: %s (%d)", strerror(errno), errno); else - spa_log_info(backend->log, "%s: set offload codec succeeded", __func__); + spa_log_info(backend->log, "set offload codec succeeded"); return err; } From 2b5d21da5b9c928f363ca2d6bb78c9278bd5bb0e Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:04:46 +0800 Subject: [PATCH 3/7] bluez5: simplify SCO datapath parsing with spa_atou32 --- spa/plugins/bluez5/backend-native.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index cba0fd4f3..312618081 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -4136,14 +4136,10 @@ static void parse_hfp_default_volumes(struct impl *backend, const struct spa_dic static void parse_sco_datapath(struct impl *backend, const struct spa_dict *info) { - uint32_t tmp; - const char *str; - backend->hfphsp_sco_datapath = HFP_SCO_DEFAULT_DATAPATH; - if ((str = spa_dict_lookup(info, "bluez5.hw-offload-datapath")) != NULL && - (tmp = atoi(str)) > 0) - backend->hfphsp_sco_datapath = tmp; + spa_atou32(spa_dict_lookup(info, "bluez5.hw-offload-datapath"), + &backend->hfphsp_sco_datapath, 10); } static const struct spa_bt_backend_implementation backend_impl = { From 78f16bc04b9444a88e69ac52d2c918d9b2e08761 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:15:26 +0800 Subject: [PATCH 4/7] bluez5: Remove hw-offload feature flag check and associated quirks The sco_offload_btcodec() function now returns void and only skips offload setup when using the default datapath, simplifying the logic and removing the need for explicit feature flag checks. --- spa/plugins/bluez5/backend-native.c | 11 +++++------ spa/plugins/bluez5/defs.h | 1 - spa/plugins/bluez5/quirks.c | 6 ------ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 312618081..e5bd5eb1c 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -298,12 +298,15 @@ static const struct media_codec *codec_list_best(struct impl *backend, struct sp return NULL; } -static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) +static void sco_offload_btcodec(struct impl *backend, int sock, bool msbc) { int err; char buffer[255]; struct bt_codecs *codecs; + if (backend->hfphsp_sco_datapath == HFP_SCO_DEFAULT_DATAPATH) + return; + spa_log_info(backend->log, "sock(%d) msbc(%d)", sock, msbc); memset(buffer, 0, sizeof(buffer)); @@ -321,7 +324,6 @@ static int sco_offload_btcodec(struct impl *backend, int sock, bool msbc) spa_log_error(backend->log, "ERROR: %s (%d)", strerror(errno), errno); else spa_log_info(backend->log, "set offload codec succeeded"); - return err; } static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) @@ -2623,10 +2625,7 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte } } - if (backend->quirks && - (spa_bt_quirks_get_features(backend->quirks, NULL, NULL, &bt_features) == 0) && - ((bt_features & (SPA_BT_FEATURE_HW_OFFLOAD)) != 0)) - sco_offload_btcodec(backend, sock, transparent); + sco_offload_btcodec(backend, sock, transparent); return spa_steal_fd(sock); } diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 4bc4fa3bb..afc56c920 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -810,7 +810,6 @@ enum spa_bt_feature { SPA_BT_FEATURE_SBC_XQ = (1 << 5), SPA_BT_FEATURE_FASTSTREAM = (1 << 6), SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), - SPA_BT_FEATURE_HW_OFFLOAD = (1 << 8), }; struct spa_bt_quirks; diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c index 2a1c5e860..c4b293e68 100644 --- a/spa/plugins/bluez5/quirks.c +++ b/spa/plugins/bluez5/quirks.c @@ -52,7 +52,6 @@ struct spa_bt_quirks { int force_sbc_xq; int force_faststream; int force_a2dp_duplex; - int force_hw_offload; char *device_rules; char *adapter_rules; @@ -70,7 +69,6 @@ static enum spa_bt_feature parse_feature(const char *str) { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, { "faststream", SPA_BT_FEATURE_FASTSTREAM }, { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, - { "hw-offload", SPA_BT_FEATURE_HW_OFFLOAD }, }; SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { if (spa_streq(str, f->key)) @@ -230,7 +228,6 @@ struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct s this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume"); this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); - this->force_hw_offload = parse_force_flag(info, "bluez5.hw-offload-sco"); if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) { spa_log_debug(this->log, "loading session manager provided data"); @@ -388,9 +385,6 @@ static int get_features(const struct spa_bt_quirks *this, if (this->force_a2dp_duplex != -1) SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); - if (this->force_hw_offload != -1) - SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_OFFLOAD, this->force_hw_offload); - return 0; } From 254620676f929a38bade32fb4293760b9bffeb97 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:17:11 +0800 Subject: [PATCH 5/7] bluez5: Use named constants for Bluetooth codec IDs Replace magic numbers (0x02, 0x05) with named constants BT_CODEC_CVSD and BT_CODEC_MSBC for better code readability. Also remove redundant zero initialization of num_caps field since the buffer is already memset to zero. --- spa/plugins/bluez5/backend-native.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index e5bd5eb1c..a791dec4a 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -63,6 +63,9 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native"); #define RFCOMM_MESSAGE_MAX_LENGTH 256 +#define BT_CODEC_CVSD 0x02 +#define BT_CODEC_MSBC 0x05 + enum { HFP_AG_INITIAL_CODEC_SETUP_NONE = 0, HFP_AG_INITIAL_CODEC_SETUP_SEND, @@ -312,12 +315,11 @@ static void sco_offload_btcodec(struct impl *backend, int sock, bool msbc) memset(buffer, 0, sizeof(buffer)); codecs = (void *)buffer; if (msbc) - codecs->codecs[0].id = 0x05; + codecs->codecs[0].id = BT_CODEC_MSBC; else - codecs->codecs[0].id = 0x02; + codecs->codecs[0].id = BT_CODEC_CVSD; codecs->num_codecs = 1; codecs->codecs[0].data_path_id = backend->hfphsp_sco_datapath; - codecs->codecs[0].num_caps = 0x00; err = setsockopt(sock, SOL_BLUETOOTH, BT_CODEC, codecs, sizeof(buffer)); if (err < 0) From 332e35039d93780bfbc21fcb0f42cc29b7d42bbd Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Mon, 19 Jan 2026 15:46:55 +0800 Subject: [PATCH 6/7] doc: Fix bluez5.hw-offload-datapath property type in documentation --- doc/dox/config/pipewire-props.7.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index d7e75e33d..52ac3e369 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -1171,7 +1171,7 @@ in a platform-specific way. See `tests/examples/bt-pinephone.lua` in WirePlumber Do not enable this setting if you don't know what all this means, as it won't work. \endparblock -@PAR@ device-param bluez5.hw-offload-datapath # integer +@PAR@ monitor-prop bluez5.hw-offload-datapath # integer \parblock HFP/HSP hardware offload data path ID (default: 0). From bc53b6b34331435d395e17f405af588a85d2aed1 Mon Sep 17 00:00:00 2001 From: Mengshi Wu Date: Tue, 20 Jan 2026 09:58:29 +0800 Subject: [PATCH 7/7] bluez5: Remove unused bt_features variable in sco_create_socket. --- spa/plugins/bluez5/backend-native.c | 1 - 1 file changed, 1 deletion(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index a791dec4a..4d14183e7 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -2595,7 +2595,6 @@ static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapte struct sockaddr_sco addr; socklen_t len; bdaddr_t src; - uint32_t bt_features; spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); if (sock < 0) {