diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h index 1d950a900..66e0ad899 100644 --- a/spa/plugins/bluez5/bap-codec-caps.h +++ b/spa/plugins/bluez5/bap-codec-caps.h @@ -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 */ diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index ae2e9f10f..be08fd5a5 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -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, diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 9bfe141d0..9111586ab 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -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) diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h index 62507c7d1..76bd00571 100644 --- a/spa/plugins/bluez5/media-codecs.h +++ b/spa/plugins/bluez5/media-codecs.h @@ -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.