From cc71d011e0a3e4b4b8a4cfe30b42495d39b6d2d5 Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Sun, 5 Apr 2026 19:52:53 +0300 Subject: [PATCH 1/8] bluez5: aac: use maximum possible peak bitrate according to MTU Android 11 and newer, in both CBR and VBR modes, * Sets bitrate (AACENC_BITRATE) to the max_bitrate value of A2DP * Sets peak bitrate (AACENC_PEAK_BITRATE) according to the maximum data which could fit into single audio packet based on MTU AACENC_BITRATE is used only in CBR mode. For VBR mode, the only limiting factor is AACENC_PEAK_BITRATE. Do the same in Pipewire. Link: https://gitlab.freedesktop.org/pipewire/pipewire/-/work_items/1482#note_2949680 Link: https://cs.android.com/android/platform/superproject/+/android16-qpr2-release:packages/modules/Bluetooth/system/stack/a2dp/a2dp_aac_encoder.cc;drc=37d7b4549f7b8740df1a290f04c20c591a2d3391;l=269 (cherry picked from commit 49d5f4f2363564cd05236a22d1a3e1d13bf57b39) --- spa/plugins/bluez5/a2dp-codec-aac.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index a82efe983..689e26ba6 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -380,8 +380,12 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, // Fragmentation is not implemented yet, // so make sure every encoded AAC frame fits in (mtu - header) this->max_bitrate = ((this->mtu - sizeof(struct rtp_header)) * 8 * this->rate) / 1024; - this->max_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf)); - this->cur_bitrate = this->max_bitrate; + this->cur_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf)); + spa_log_debug(log, "AAC: max (peak) bitrate: %d, cur bitrate: %d, mode: %d (vbr: %d)", + this->max_bitrate, + this->cur_bitrate, + bitratemode, + conf->vbr); res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate); if (res != AACENC_OK) From cb1d19e4338e489ef9fd261452628d71811b7992 Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Sun, 5 Apr 2026 19:58:00 +0300 Subject: [PATCH 2/8] bluez5: aac: use higher band limit for CBR mode FDK-AAC encoder uses band pass filter, which is automatically applied at all bitrates. For CBR encoding mode, its values are as follows (for stereo): * 0-12 kb/s: 5 kHz * 12-20 kb/s: 6.4 kHz * 20-28 kb/s: 9.6 kHz * 40-56 kb/s: 13 kHz * 56-72 kb/s: 16 kHz * 72-576 kb/s: 17 kHz VBR uses the following table (stereo): * Mode 1: 13 kHz * Mode 2: 13 kHz * Mode 3: 15.7 kHz * Mode 4: 16.5 kHz * Mode 5: 19.3 kHz 17 kHz for CBR is a limiting value for high bitrate. Assume >110 kbit/s as a "high bitrate" CBR and increase the band pass cutout up to 19.3 kHz (as in mode 5 VBR). Link: https://github.com/mstorsjo/fdk-aac/blob/d8e6b1a3aa606c450241632b64b703f21ea31ce3/libAACenc/src/bandwidth.cpp#L114-L160 (cherry picked from commit a35b6b0c4bcf93c7f261c6317a1a8e8eb141f33c) --- spa/plugins/bluez5/a2dp-codec-aac.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index 689e26ba6..c426dc1db 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -395,6 +395,15 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, if (res != AACENC_OK) goto error; + // Assume >110 kbit/s as a "high bitrate" CBR and increase the + // band pass cutout up to 19.3 kHz (as in mode 5 VBR). + if (!conf->vbr && this->cur_bitrate > 110000) { + res = aacEncoder_SetParam(this->aacenc, AACENC_BANDWIDTH, + 19293); + if (res != AACENC_OK) + goto error; + } + res = aacEncoder_SetParam(this->aacenc, AACENC_TRANSMUX, TT_MP4_LATM_MCP1); if (res != AACENC_OK) goto error; From 5918e6f05de71dbc57e98ceb0f3c7a59f0180718 Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Sun, 5 Apr 2026 20:14:23 +0300 Subject: [PATCH 3/8] bluez5: aac: Use VBR encoding with Mode 5 by default (cherry picked from commit ee1b42944121c20d6ae64e64a70e8fc2db8cd4f1) --- spa/plugins/bluez5/a2dp-codec-aac.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index c426dc1db..f4cbecd9b 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -286,7 +286,7 @@ static void *codec_init_props(const struct media_codec *codec, uint32_t flags, c return NULL; if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.aac.bitratemode")) == NULL) - str = "0"; + str = "5"; p->bitratemode = SPA_CLAMP(atoi(str), 0, 5); return p; From 78888a78c3eb800a4292ff1bec8432cd563b3004 Mon Sep 17 00:00:00 2001 From: Alexander Sarmanow Date: Tue, 31 Mar 2026 14:30:46 +0200 Subject: [PATCH 4/8] bluez5: bap: add support for manual BIS config (cherry picked from commit 54a4515b09bbed17ed493179a839099389fc72cf) --- spa/plugins/bluez5/bap-codec-lc3.c | 18 +++++++++-- spa/plugins/bluez5/bluez5-dbus.c | 49 +++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index b1b12cb78..881af4e14 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -1503,6 +1503,10 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, struct ltv_writer writer = LTV_WRITER(caps, *caps_size); const struct bap_qos *preset = NULL; + uint32_t retransmissions = 0; + uint8_t rtn_manual_set = 0; + uint32_t max_transport_latency = 0; + uint32_t presentation_delay = 0; *caps_size = 0; if (settings) { @@ -1511,6 +1515,14 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, sscanf(settings->items[i].value, "%"PRIu32, &channel_allocation); if (spa_streq(settings->items[i].key, "preset")) preset_name = settings->items[i].value; + if (spa_streq(settings->items[i].key, "max_transport_latency")) + spa_atou32(settings->items[i].value, &max_transport_latency, 0); + if (spa_streq(settings->items[i].key, "presentation_delay")) + spa_atou32(settings->items[i].value, &presentation_delay, 0); + if (spa_streq(settings->items[i].key, "retransmissions")) { + spa_atou32(settings->items[i].value, &retransmissions, 0); + rtn_manual_set = 1; + } } } @@ -1537,9 +1549,9 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, else qos->framing = 0; qos->sdu = preset->framelen * get_channel_count(channel_allocation); - qos->retransmission = preset->retransmission; - qos->latency = preset->latency; - qos->delay = preset->delay; + qos->retransmission = rtn_manual_set ? retransmissions : preset->retransmission; + qos->latency = max_transport_latency ? max_transport_latency : preset->latency; + qos->delay = presentation_delay ? presentation_delay : preset->delay; qos->phy = 2; qos->interval = (preset->frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index f4b384547..22d971c37 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -188,9 +188,16 @@ struct spa_bt_metadata { uint8_t value[METADATA_MAX_LEN - 1]; }; +#define RTN_MAX 0x1E +#define MAX_TRANSPORT_LATENCY_MIN 0x5 +#define MAX_TRANSPORT_LATENCY_MAX 0x0FA0 + struct spa_bt_bis { struct spa_list link; char qos_preset[255]; + int retransmissions; + int rtn_manual_set; + int max_transport_latency; int channel_allocation; struct spa_list metadata_list; }; @@ -6192,8 +6199,11 @@ static void configure_bis(struct spa_bt_monitor *monitor, struct bap_codec_qos qos; struct spa_bt_metadata *metadata_entry; struct spa_dict settings; - struct spa_dict_item setting_items[2]; + struct spa_dict_item setting_items[4]; + uint32_t n_items = 0; char channel_allocation[64] = {0}; + char retransmissions[3] = {0}; + char max_transport_latency[5] = {0}; int mse = 0; int options = 0; @@ -6218,12 +6228,27 @@ static void configure_bis(struct spa_bt_monitor *monitor, metadata_size += metadata_entry->length - 1; } + spa_log_debug(monitor->log, "bis->channel_allocation %d", bis->channel_allocation); - if (bis->channel_allocation) + if (bis->channel_allocation) { spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, bis->channel_allocation); - setting_items[0] = SPA_DICT_ITEM_INIT("channel_allocation", channel_allocation); - setting_items[1] = SPA_DICT_ITEM_INIT("preset", bis->qos_preset); - settings = SPA_DICT_INIT(setting_items, 2); + } + spa_log_debug(monitor->log, "bis->rtn_manual_set %d", bis->rtn_manual_set); + spa_log_debug(monitor->log, "bis->retransmissions %d", bis->retransmissions); + if (bis->rtn_manual_set) { + spa_scnprintf(retransmissions, sizeof(retransmissions), "%"PRIu8, bis->retransmissions); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("retransmissions", retransmissions); + } + spa_log_debug(monitor->log, "bis->max_transport_latency %d", bis->max_transport_latency); + if (bis->max_transport_latency) { + spa_scnprintf(max_transport_latency, sizeof(max_transport_latency), "%"PRIu32, bis->max_transport_latency); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("max_transport_latency", max_transport_latency); + } + + setting_items[n_items++] = SPA_DICT_ITEM_INIT("preset", bis->qos_preset); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("channel_allocation", channel_allocation); + + settings = SPA_DICT_INIT(setting_items, n_items); caps_size = sizeof(caps); ret = codec->get_bis_config(codec, caps, &caps_size, &settings, &qos); @@ -7126,6 +7151,20 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const if (spa_json_get_string(&it[1], bis_entry->qos_preset, sizeof(bis_entry->qos_preset)) <= 0) goto parse_failed; spa_log_debug(monitor->log, "bis_entry->qos_preset %s", bis_entry->qos_preset); + } else if (spa_streq(bis_key, "retransmissions")) { + if (spa_json_get_int(&it[2], &bis_entry->retransmissions) <= 0) + goto parse_failed; + if (bis_entry->retransmissions > RTN_MAX) + goto parse_failed; + bis_entry->rtn_manual_set = 1; + spa_log_debug(monitor->log, "bis_entry->retransmissions %d", bis_entry->retransmissions); + } else if (spa_streq(bis_key, "max_transport_latency")) { + if (spa_json_get_int(&it[2], &bis_entry->max_transport_latency) <= 0) + goto parse_failed; + if (bis_entry->max_transport_latency < MAX_TRANSPORT_LATENCY_MIN && + bis_entry->max_transport_latency > MAX_TRANSPORT_LATENCY_MAX) + goto parse_failed; + spa_log_debug(monitor->log, "bis_entry->max_transport_latency %d", bis_entry->max_transport_latency); } else if (spa_streq(bis_key, "audio_channel_allocation")) { if (spa_json_get_int(&it[1], &bis_entry->channel_allocation) <= 0) goto parse_failed; From c148028f5af20129484dd24e82ca7e9d46a4850e Mon Sep 17 00:00:00 2001 From: Martin Geier Date: Wed, 8 Apr 2026 16:59:29 +0200 Subject: [PATCH 5/8] bluez5: iso-io: don't use streams without tx_latency enabled for fill level calculation When there is a stream without tx_latency enabled, the fill_count ends with MIN_FILL value. This causes one buffer of silence to be written to every stream before the actual data in each iteration. Consequently, more data is written than consumed in each iteration. After several iterations, spa_bt_send fails, triggering a group_latency_check failure in few next iterations and leading to dropped data. Skip streams without tx_latency enabled in fill level calculations to prevent these audio glitches. (cherry picked from commit 42415eadd9dbdfd7f043518c92155fa29ef8243d) --- spa/plugins/bluez5/iso-io.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/iso-io.c b/spa/plugins/bluez5/iso-io.c index 2cc65a2bf..ce1fd7d0c 100644 --- a/spa/plugins/bluez5/iso-io.c +++ b/spa/plugins/bluez5/iso-io.c @@ -411,7 +411,7 @@ static void group_on_timeout(struct spa_source *source) /* Ensure controller fill level */ fill_count = UINT_MAX; spa_list_for_each(stream, &group->streams, link) { - if (!stream->sink || !group->started) + if (!stream->sink || !group->started || !stream->tx_latency.enabled) continue; if (stream->tx_latency.queue < MIN_FILL) fill_count = SPA_MIN(fill_count, MIN_FILL - stream->tx_latency.queue); From b2028a03f0f45acf79a28cfeef4a7c978d2d51e4 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Mon, 6 Apr 2026 02:20:38 +0300 Subject: [PATCH 6/8] bluez5: fix disabling HFP codecs via bluez5.codecs backend-native should not advertise disabled HFP codecs as available. (cherry picked from commit 6e8e234e61ede949b4ea28a39cdc0d6e07db790e) --- spa/plugins/bluez5/backend-native.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 64a4c25c1..f4bb37995 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -986,8 +986,11 @@ static void make_available_codec_list(struct impl *backend, struct spa_bt_device for (i = 0; backend->codecs[i]; ++i) { const struct media_codec *codec = backend->codecs[i]; + if (codec->kind != MEDIA_CODEC_HFP) continue; + if (!spa_bt_get_hfp_codec(backend->monitor, codec->codec_id)) + continue; if (device_supports_codec(backend, device, codec->id)) codec_list_add(codec_list, codec); } From bb6199214df7556205f85eaf8eaba2c56626d0ff Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Thu, 16 Apr 2026 22:51:51 +0300 Subject: [PATCH 7/8] bluez5: add quirk for LC3-24kHz for HFP MT7925 fails to setup a SCO connection that results to working LC3-24kHz audio. Other controllers (Intel etc) appear to work OK. Add quirk for disabling this codec, and disable it for this Mediatek controller. (cherry picked from commit 84e6845aa68e3f992c3a624f27003e814ef17a21) --- spa/plugins/bluez5/backend-native.c | 7 ++++++- spa/plugins/bluez5/bluez-hardware.conf | 4 ++++ spa/plugins/bluez5/defs.h | 1 + spa/plugins/bluez5/quirks.c | 6 ++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index f4bb37995..1cb2235ff 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -904,7 +904,7 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de { int res; bool alt6_ok = true, alt1_ok = true; - bool msbc_alt6_ok = true, msbc_alt1_ok = true; + bool msbc_alt6_ok = true, msbc_alt1_ok = true, lc3_a127_ok = true; uint32_t bt_features; if (device->adapter == NULL) @@ -913,6 +913,7 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de if (backend->quirks && spa_bt_quirks_get_features(backend->quirks, device->adapter, device, &bt_features) == 0) { msbc_alt1_ok = (bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL)); msbc_alt6_ok = (bt_features & SPA_BT_FEATURE_MSBC); + lc3_a127_ok = (bt_features & SPA_BT_FEATURE_LC3_A127); } switch (codec) { @@ -922,6 +923,10 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de alt1_ok = msbc_alt1_ok; alt6_ok = msbc_alt6_ok; break; + case SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127: + alt1_ok = false; + alt6_ok = lc3_a127_ok; + break; case SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB: default: /* LC3-SWB has same transport requirements as msbc. diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf index d08ff0bb9..799f8e795 100644 --- a/spa/plugins/bluez5/bluez-hardware.conf +++ b/spa/plugins/bluez5/bluez-hardware.conf @@ -15,6 +15,7 @@ # sbc-xq "nonstandard" SBC codec setting with better sound quality # faststream FastStream codec support # a2dp-duplex A2DP duplex codec support +# lc3-a127 HFP LC3-24KHz codec support # # Features are disabled with the key "no-features" whose value is an # array of strings in the match rule. @@ -83,6 +84,9 @@ bluez5.features.adapter = [ # Realtek Semiconductor Corp. { bus-type = "usb", vendor-id = "usb:0bda" }, + # Mediatek MT7925, #pipewire-5213 + { bus-type = "usb", vendor-id = "usb:0e8d", product-id = "e025", no-features = [ lc3-a127 ] }, + # Generic USB adapters { bus-type = "usb", no-features = [ msbc-alt1-rtl ] }, diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 24a85a5c9..5e69bb757 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -811,6 +811,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_LC3_A127 = (1 << 8), }; struct spa_bt_quirks; diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c index c4b293e68..23bcec0d4 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_lc3_a127; 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 }, + { "lc3-a127", SPA_BT_FEATURE_LC3_A127 }, }; 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_lc3_a127 = parse_force_flag(info, "bluez5.enable-lc3-a127"); 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_lc3_a127 != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_LC3_A127, this->force_lc3_a127); + return 0; } From 04af44d5c3c8b9ba16831c825c7975e867e54406 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 19 Apr 2026 13:20:06 +0300 Subject: [PATCH 8/8] bluez5: more MT7925 quirks The MT7925 chipset has several alternative USB ids recognized by kernel, list them all. (cherry picked from commit cee1bdfb5a5dcf80a84bcb1a0eeadd6c015489cb) --- spa/plugins/bluez5/bluez-hardware.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf index 799f8e795..0148a4ee4 100644 --- a/spa/plugins/bluez5/bluez-hardware.conf +++ b/spa/plugins/bluez5/bluez-hardware.conf @@ -85,7 +85,10 @@ bluez5.features.adapter = [ { bus-type = "usb", vendor-id = "usb:0bda" }, # Mediatek MT7925, #pipewire-5213 - { bus-type = "usb", vendor-id = "usb:0e8d", product-id = "e025", no-features = [ lc3-a127 ] }, + { bus-type = "usb", vendor-id = "usb:0e8d", product-id = "~(e025)", no-features = [ lc3-a127 ] }, + { bus-type = "usb", vendor-id = "usb:0489", product-id = "~(e111|e113|e118|e11e|e124|e139|e14e|e14f|e150|e151)", no-features = [ lc3-a127 ] }, + { bus-type = "usb", vendor-id = "usb:13d3", product-id = "~(3602|3603|3604|3608|3613|3627|3628|3630)", no-features = [ lc3-a127 ] }, + { bus-type = "usb", vendor-id = "usb:2c7c", product-id = "~(7009)", no-features = [ lc3-a127 ] }, # Generic USB adapters { bus-type = "usb", no-features = [ msbc-alt1-rtl ] },