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.
This commit is contained in:
Mengshi Wu 2026-01-16 16:16:22 +08:00 committed by Wim Taymans
parent 35817c0d85
commit db7c74a042
4 changed files with 64 additions and 1 deletions

View file

@ -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. Do not enable this setting if you don't know what all this means, as it won't work.
\endparblock \endparblock
@PAR@ device-param bluez5.hw-offload-datapath # integer
\parblock
HFP/HSP hardware offload data path ID (default: 0).
This feature configures the SCO hardwareoffload data path for HFP/HSP using the Bluetooth
SIGspecified 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 @PAR@ monitor-prop bluez5.a2dp.opus.pro.channels = 3 # integer
PipeWire Opus Pro audio profile channel count. PipeWire Opus Pro audio profile channel count.

View file

@ -112,6 +112,7 @@ struct impl {
int hfp_default_speaker_volume; int hfp_default_speaker_volume;
struct spa_source sco; struct spa_source sco;
unsigned int hfphsp_sco_datapath;
const struct spa_bt_quirks *quirks; 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; 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) static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata)
{ {
if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) 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; struct sockaddr_sco addr;
socklen_t len; socklen_t len;
bdaddr_t src; bdaddr_t src;
uint32_t bt_features;
spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO);
if (sock < 0) { 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); 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; 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 = { static const struct spa_bt_backend_implementation backend_impl = {
SPA_VERSION_BT_BACKEND_IMPLEMENTATION, SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
.free = backend_native_free, .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_disable_nrec(backend, info);
parse_hfp_default_volumes(backend, info); parse_hfp_default_volumes(backend, info);
parse_hfp_pts(backend, info); parse_hfp_pts(backend, info);
parse_sco_datapath(backend, info);
#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
if (!dbus_connection_register_object_path(backend->conn, if (!dbus_connection_register_object_path(backend->conn,

View file

@ -135,7 +135,8 @@ extern "C" {
#define PROFILE_HFP_AG "/Profile/HFPAG" #define PROFILE_HFP_AG "/Profile/HFPAG"
#define PROFILE_HFP_HF "/Profile/HFPHF" #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_BLUETOOTH 0x1 /* Bluetooth SIG */
#define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */ #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_SBC_XQ = (1 << 5),
SPA_BT_FEATURE_FASTSTREAM = (1 << 6), SPA_BT_FEATURE_FASTSTREAM = (1 << 6),
SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7),
SPA_BT_FEATURE_HW_OFFLOAD = (1 << 8),
}; };
struct spa_bt_quirks; struct spa_bt_quirks;

View file

@ -52,6 +52,7 @@ struct spa_bt_quirks {
int force_sbc_xq; int force_sbc_xq;
int force_faststream; int force_faststream;
int force_a2dp_duplex; int force_a2dp_duplex;
int force_hw_offload;
char *device_rules; char *device_rules;
char *adapter_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 }, { "sbc-xq", SPA_BT_FEATURE_SBC_XQ },
{ "faststream", SPA_BT_FEATURE_FASTSTREAM }, { "faststream", SPA_BT_FEATURE_FASTSTREAM },
{ "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX },
{ "hw-offload", SPA_BT_FEATURE_HW_OFFLOAD },
}; };
SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) {
if (spa_streq(str, f->key)) 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_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume");
this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream");
this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); 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) { if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) {
spa_log_debug(this->log, "loading session manager provided data"); 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) if (this->force_a2dp_duplex != -1)
SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); 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; return 0;
} }