bluez5: set some BAP Context metadata value on streams

Some devices refuse to enable microphone if Streaming Context metadata
is just Unspecified.

Set some reasonable values for the stream context we create along TMAP,
and try follow CAP rules for selecting the PAC.
This commit is contained in:
Pauli Virtanen 2025-10-12 02:15:56 +03:00 committed by Wim Taymans
parent 2010a525d3
commit 8d59ad2713
4 changed files with 182 additions and 21 deletions

View file

@ -82,6 +82,22 @@
#define LC3_MAX_CHANNELS 28
/* Metadata types */
#define BAP_META_TYPE_PREFERRED_CONTEXT 0x01
#define BAP_META_TYPE_STREAMING_CONTEXT 0x02
#define BAP_META_TYPE_PROGRAM_INFO 0x03
#define BAP_META_TYPE_LANGUAGE 0x04
#define BAP_META_TYPE_CCID_LIST 0x05
#define BAP_META_TYPE_PARENTAL_RATING 0x06
#define BAP_META_TYPE_PROGRAM_INFO_URI 0x07
#define BAP_META_TYPE_AUDIO_ACTIVE_STATE 0x08
#define BAP_META_TYPE_BCAST_IMMEDIATE 0x09
#define BAP_META_TYPE_ASSISTED_LISTENING 0x0a
#define BAP_META_TYPE_BCAST_NAME 0x0b
#define BAP_META_TYPE_EXTENDED 0xfe
#define BAP_META_TYPE_VENDOR 0xff
#define BAP_CHANNEL_MONO 0x00000000 /* mono */
#define BAP_CHANNEL_FL 0x00000001 /* front left */
#define BAP_CHANNEL_FR 0x00000002 /* front right */

View file

@ -46,6 +46,8 @@ struct impl {
struct settings {
uint32_t locations;
uint32_t channel_allocation;
uint16_t supported_context;
uint16_t available_context;
bool sink;
bool duplex;
char *qos_name;
@ -55,13 +57,11 @@ struct settings {
int framing;
};
struct config_data {
struct settings settings;
};
struct pac_data {
const void *data;
size_t size;
const void *metadata;
size_t metadata_size;
int index;
const struct settings *settings;
};
@ -86,9 +86,16 @@ typedef struct {
uint8_t n_blks;
bool sink;
bool duplex;
uint16_t preferred_context;
unsigned int priority;
} bap_lc3_t;
struct config_data {
bap_lc3_t conf;
int pac_index;
struct settings settings;
};
#define BAP_QOS(name_, rate_, duration_, framing_, framelen_, rtn_, latency_, delay_, priority_) \
((struct bap_qos){ .name = (name_), .rate = (rate_), .frame_duration = (duration_), .framing = (framing_), \
.framelen = (framelen_), .retransmission = (rtn_), .latency = (latency_), \
@ -370,7 +377,7 @@ static void debugc_ltv(struct spa_debug_context *debug_ctx, int pac, struct ltv
}
}
static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS],
static int parse_bluez_pacs_data(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS],
struct spa_debug_context *debug_ctx)
{
/*
@ -379,7 +386,7 @@ static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_da
*/
int pac = 0;
pacs[pac] = (struct pac_data){ data, 0 };
pacs[pac] = (struct pac_data){ .data = data };
while (data_size > 0) {
struct ltv *ltv = (struct ltv *)data;
@ -390,7 +397,7 @@ static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_da
break;
++pac;
pacs[pac] = (struct pac_data){ data + 1, 0, pac };
pacs[pac] = (struct pac_data){ .data = data + 1, .index = pac };
} else if (ltv->len >= data_size) {
return -EINVAL;
} else {
@ -404,6 +411,28 @@ static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_da
return pac + 1;
}
static int parse_bluez_pacs(const uint8_t *data, size_t data_size,
const uint8_t *metadata, size_t metadata_size, struct pac_data pacs[MAX_PACS],
struct spa_debug_context *debug_ctx)
{
struct pac_data meta[MAX_PACS];
int pac_count, meta_count;
pac_count = parse_bluez_pacs_data(data, data_size, pacs, debug_ctx);
if (pac_count < 0)
return pac_count;
meta_count = parse_bluez_pacs_data(metadata, metadata_size, meta, debug_ctx);
if (meta_count == pac_count) {
for (int i = 0; i < pac_count; ++i) {
pacs[i].metadata = meta[i].data;
pacs[i].metadata_size = meta[i].size;
}
}
return pac_count;
}
static uint8_t get_channel_count(uint32_t channels)
{
uint8_t num;
@ -536,6 +565,7 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp
uint8_t max_channels = 0;
uint8_t duration_mask = 0;
uint16_t rate_mask = 0;
uint16_t preferred_context = 0;
struct bap_qos bap_qos;
unsigned int i;
bool found = false;
@ -584,6 +614,20 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp
return false;
}
data = pac->metadata;
data_size = pac->metadata_size;
while ((ltv = ltv_next(&data, &data_size))) {
switch (ltv->type) {
case BAP_META_TYPE_PREFERRED_CONTEXT:
if (ltv->len != 3)
break;
preferred_context = ltv->value[0] + (ltv->value[1] << 8);
break;
}
}
if (data)
spa_debugc(debug_ctx, "malformed metadata");
for (i = 0; i < 8; ++i)
if (channel_counts & (1u << i))
max_channels = i + 1;
@ -621,8 +665,8 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp
* and reassemble SDUs as needed.
*/
if (pac->settings->duplex) {
/* 16KHz input is mandatory in BAP v1.0.1 Table 3.5, so prefer
* it or 32kHz for now for input rate in duplex configuration.
/* 16KHz input is mandatory in BAP v1.0.1 Table 3.5, and 32KHz in TMAP,
* so prefer those for now for input rate in duplex configuration.
*
* It appears few devices support 48kHz out + input, so in duplex mode
* try 32 kHz or 16 kHz also for output direction.
@ -645,6 +689,7 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp
conf->frame_duration = bap_qos.frame_duration;
conf->framelen = bap_qos.framelen;
conf->priority = bap_qos.priority;
conf->preferred_context = preferred_context;
return true;
}
@ -700,7 +745,32 @@ static bool parse_conf(bap_lc3_t *conf, const void *data, size_t data_size)
return true;
}
static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, int res2)
static uint16_t get_wanted_context(const struct settings *settings)
{
uint16_t context;
/* Stick with contexts specified in TMAP. Anything else is probably crapshoot due
* to interesting device firmware behavior.
*
* Eg. some Earfun firmwares fail if we set MEDIA | CONVERSATIONAL, other versions
* disconnect if LIVE is set, Samsung Galaxy Buds fail on microphone Enable if
* CONVERSATIONAL is not set.
*/
if (settings->sink || settings->duplex)
context = BAP_CONTEXT_CONVERSATIONAL;
else
context = BAP_CONTEXT_MEDIA;
/* CAP v1.0.1 Sec 7.3.1.2.1: drop contexts if not available, otherwise unspecified */
context &= settings->available_context;
if (!context)
context = BAP_CONTEXT_UNSPECIFIED & settings->available_context;
return context;
}
static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, int res2,
const struct settings *settings)
{
const bap_lc3_t *conf;
int a, b;
@ -725,6 +795,9 @@ static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, in
PREFER_EXPR(conf->priority == UINT_MAX);
/* CAP v1.0.1 Sec 7.3.1.2.4: should use PAC preferred context if possible */
PREFER_BOOL(conf->preferred_context & get_wanted_context(settings));
PREFER_BOOL(conf->channels & LC3_CHAN_2);
PREFER_BOOL(conf->channels & LC3_CHAN_1);
@ -750,7 +823,7 @@ static int pac_cmp(const void *p1, const void *p2)
res1 = select_config(&conf1, pac1, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL;
res2 = select_config(&conf2, pac2, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL;
return conf_cmp(&conf1, res1, &conf2, res2);
return conf_cmp(&conf1, res1, &conf2, res2, pac1->settings);
}
static void parse_settings(struct settings *s, const struct spa_dict *settings,
@ -789,6 +862,12 @@ static void parse_settings(struct settings *s, const struct spa_dict *settings,
if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.channel-allocation"), &value, 0))
s->channel_allocation = value;
if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.supported-context"), &value, 0))
s->supported_context = value;
if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.available-context"), &value, 0))
s->available_context = value;
if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug")))
*debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_DEBUG);
else
@ -832,6 +911,8 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
spa_autoptr(config_data) d = NULL;
int i, ret;
struct ltv_writer writer = LTV_WRITER(config, A2DP_MAX_CAPS_SIZE);
const void *metadata = NULL;
uint32_t metadata_len = 0;
if (caps == NULL)
return -EINVAL;
@ -842,8 +923,13 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
parse_settings(&d->settings, settings, &debug_ctx);
if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.metadata"), &metadata_len, 0))
metadata = spa_dict_lookup(settings, "bluez5.bap.metadata");
if (!metadata)
metadata_len = 0;
/* Select best conf from those possible */
npacs = parse_bluez_pacs(caps, caps_size, pacs, &debug_ctx.ctx);
npacs = parse_bluez_pacs(caps, caps_size, metadata, metadata_len, pacs, &debug_ctx.ctx);
if (npacs < 0) {
spa_debugc(&debug_ctx.ctx, "malformed PACS");
return npacs;
@ -862,6 +948,9 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags,
if (!select_config(&conf, &pacs[0], &debug_ctx.ctx))
return -ENOTSUP;
d->conf = conf;
d->pac_index = pacs[0].index;
ltv_writer_uint8(&writer, LC3_TYPE_FREQ, conf.rate);
ltv_writer_uint8(&writer, LC3_TYPE_DUR, conf.frame_duration);
@ -884,13 +973,22 @@ static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t f
const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
{
bap_lc3_t conf1, conf2;
int res1, res2;
int res1, res2, res;
void *data1 = NULL;
void *data2 = NULL;
const struct config_data *d;
/* Order selected configurations by preference */
res1 = codec->select_config(codec, 0, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1, NULL);
res2 = codec->select_config(codec, 0, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2, NULL);
res1 = codec->select_config(codec, 0, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1, &data1);
res2 = codec->select_config(codec, 0, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2, &data2);
return conf_cmp(&conf1, res1, &conf2, res2);
d = data1 ? data1 : data2;
res = conf_cmp(&conf1, res1, &conf2, res2, d ? &d->settings : NULL);
codec->free_config_data(codec, data1);
codec->free_config_data(codec, data2);
return res;
}
static uint8_t channels_to_positions(uint32_t channels, uint32_t *position, uint32_t max_position)
@ -1054,7 +1152,6 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags
}
static int codec_get_qos(const struct media_codec *codec,
const void *config, size_t config_size,
const struct bap_endpoint_qos *endpoint_qos,
const void *config_data,
struct bap_codec_qos *qos)
@ -1068,8 +1165,8 @@ static int codec_get_qos(const struct media_codec *codec,
if (!d)
return -EINVAL;
if (!parse_conf(&conf, config, config_size))
return -EINVAL;
conf = d->conf;
found = select_bap_qos(&bap_qos, &d->settings, get_rate_mask(conf.rate), get_duration_mask(conf.frame_duration),
conf.framelen, conf.framelen);
@ -1114,6 +1211,22 @@ static int codec_get_qos(const struct media_codec *codec,
return 0;
}
static int codec_get_metadata(const struct media_codec *codec, const void *config_data,
uint8_t *meta, size_t meta_max_size)
{
const struct config_data *d = config_data;
struct ltv_writer writer = LTV_WRITER(meta, meta_max_size);
uint16_t ctx;
ctx = get_wanted_context(&d->settings);
if (!ctx)
ctx = BAP_CONTEXT_UNSPECIFIED;
ltv_writer_uint16(&writer, BAP_META_TYPE_STREAMING_CONTEXT, ctx);
return ltv_writer_end(&writer);
}
static void codec_free_config_data(const struct media_codec *codec, void *config_data)
{
free_config_data(config_data);
@ -1442,6 +1555,7 @@ const struct media_codec bap_codec_lc3 = {
.enum_config = codec_enum_config,
.validate_config = codec_validate_config,
.get_qos = codec_get_qos,
.get_metadata = codec_get_metadata,
.free_config_data = codec_free_config_data,
.caps_preference_cmp = codec_caps_preference_cmp,
.init = codec_init,

View file

@ -1035,6 +1035,9 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
void *config_data = NULL;
char locations[64] = {0};
char channel_allocation[64] = {0};
char supported_context[64] = {0};
char available_context[64] = {0};
char metadata_len[64] = {0};
int conf_size;
DBusMessageIter dict;
@ -1092,6 +1095,10 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
if (ep->qos.channel_allocation)
spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, ep->qos.channel_allocation);
spa_scnprintf(supported_context, sizeof(supported_context), "%"PRIu16, ep->qos.supported_context);
spa_scnprintf(available_context, sizeof(available_context), "%"PRIu16, ep->qos.context);
spa_scnprintf(metadata_len, sizeof(metadata_len), "%zu", ep->metadata_len);
if (!ep->device->preferred_profiles)
ep->device->preferred_profiles = ep->device->profiles;
@ -1100,9 +1107,13 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
i = 0;
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.locations", locations);
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.channel-allocation", channel_allocation);
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.supported-context", supported_context);
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.available-context", available_context);
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.sink", sink ? "true" : "false");
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.duplex", duplex ? "true" : "false");
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.debug", "true");
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.metadata", (void *)ep->metadata);
setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.metadata-len", metadata_len);
if (ep->device->settings)
for (j = 0; j < ep->device->settings->n_items && i < SPA_N_ELEMENTS(setting_items); ++i, ++j)
setting_items[i] = ep->device->settings->items[j];
@ -1136,7 +1147,7 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
spa_zero(qos);
res = codec->get_qos(codec, config, conf_size, &ep->qos, config_data, &qos);
res = codec->get_qos(codec, &ep->qos, config_data, &qos);
if (res < 0) {
spa_log_error(monitor->log, "can't select QOS config: %d (%s)",
res, spa_strerror(res));
@ -1184,6 +1195,25 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe
dbus_message_iter_close_container(&dict, &entry);
}
if (codec->get_metadata) {
uint8_t meta[4096] = {};
size_t meta_size;
meta_size = res = codec->get_metadata(codec, config_data, meta, sizeof(meta));
if (res < 0) {
spa_log_error(monitor->log, "can't select metadata config: %d (%s)",
res, spa_strerror(res));
goto error_invalid;
}
if (meta_size) {
spa_log_info(monitor->log, "%p: selected metadata %d", monitor, (int)meta_size);
spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', meta, meta_size);
append_basic_array_variant_dict_entry(&dict, "Metadata", "ay", "y", DBUS_TYPE_BYTE, &meta, meta_size);
}
}
dbus_message_iter_close_container(&iter, &dict);
if (config_data && codec->free_config_data)

View file

@ -113,10 +113,11 @@ struct media_codec {
const void *caps, size_t caps_size,
struct spa_audio_info *info);
int (*get_qos)(const struct media_codec *codec,
const void *config, size_t config_size,
const struct bap_endpoint_qos *endpoint_qos,
const void *config_data,
struct bap_codec_qos *qos);
int (*get_metadata)(const struct media_codec *codec, const void *config_data,
uint8_t *meta, size_t meta_max_size);
void (*free_config_data)(const struct media_codec *codec, void *config_data);
/** qsort comparison sorting caps in order of preference for the codec.