From a2c09923ff4b6c91f62601f169d9cefaf649fd41 Mon Sep 17 00:00:00 2001 From: zuozhiwei Date: Fri, 10 Jul 2026 17:29:42 +0800 Subject: [PATCH] bluez5: complete Opus 05 companion caps like AAC ELD After a6f529b5, opus_05_51/71 only set endpoint_companion. The owner fill_caps still advertised up to MAX_CHANNELS, there was no combine_caps, and validate_config was not id-gated. Disabled surround companions remained visible in endpoint caps, and SetConfiguration could bind a 5.1/7.1 config to the stereo opus_05 object (wrong bitrate limits / channel profile). Mirror a2dp-codec-aac.c: - split fill_caps per codec id (2ch / 5.1 / 7.1; duplex 2+2; pro wide) - add combine_caps on endpoint owners - id-gate validate_config so the config-aware resolver picks the companion - give companions their own fill_caps for the monitor merge path --- spa/plugins/bluez5/a2dp-codec-opus.c | 160 ++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 28 deletions(-) diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c index e881c1f58..7e04bf5ba 100644 --- a/spa/plugins/bluez5/a2dp-codec-opus.c +++ b/spa/plugins/bluez5/a2dp-codec-opus.c @@ -551,43 +551,113 @@ static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direc return 0; } +static const uint8_t opus_05_frame_durations = + OPUS_05_FRAME_DURATION_25 | + OPUS_05_FRAME_DURATION_50 | + OPUS_05_FRAME_DURATION_100 | + OPUS_05_FRAME_DURATION_200 | + OPUS_05_FRAME_DURATION_400; + +/* + * Each codec object advertises only its own channel profile. Companions on the + * same endpoint (51/71 on "opus_05", pro on "opus_05_duplex") are merged by the + * monitor via combine_caps when enabled — same model as AAC LC + AAC-ELD. + */ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) { - a2dp_opus_05_t a2dp_opus_05 = { - .info = codec->vendor, - .main = { - .channels = SPA_MIN(255u, MAX_CHANNELS), - .frame_duration = (OPUS_05_FRAME_DURATION_25 | - OPUS_05_FRAME_DURATION_50 | - OPUS_05_FRAME_DURATION_100 | - OPUS_05_FRAME_DURATION_200 | - OPUS_05_FRAME_DURATION_400), - OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY) - OPUS_05_INIT_BITRATE(0) - }, - .bidi = { - .channels = SPA_MIN(255u, MAX_CHANNELS), - .frame_duration = (OPUS_05_FRAME_DURATION_25 | - OPUS_05_FRAME_DURATION_50 | - OPUS_05_FRAME_DURATION_100 | - OPUS_05_FRAME_DURATION_200 | - OPUS_05_FRAME_DURATION_400), - OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY) - OPUS_05_INIT_BITRATE(0) - } - }; + uint8_t channels; + uint32_t location; + bool has_bidi = false; + uint8_t bidi_channels = 0; + uint32_t bidi_location = 0; + a2dp_opus_05_t a2dp_opus_05; - /* Only duplex/pro codec has bidi, since bluez5-device has to know early - * whether to show nodes or not. */ - if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX && - codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) - spa_zero(a2dp_opus_05.bidi); + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05: + channels = 2; + location = BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51: + channels = surround_encoders[5].channels; + location = surround_encoders[5].location; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71: + channels = surround_encoders[7].channels; + location = surround_encoders[7].location; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX: + channels = 2; + location = BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR; + has_bidi = true; + bidi_channels = 2; + bidi_location = BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO: + /* Pro Audio allows arbitrary layouts; keep the wide advertisement. */ + channels = SPA_MIN(255u, MAX_CHANNELS); + location = BT_AUDIO_LOCATION_ANY; + has_bidi = true; + bidi_channels = SPA_MIN(255u, MAX_CHANNELS); + bidi_location = BT_AUDIO_LOCATION_ANY; + break; + default: + return -ENOTSUP; + } + + spa_zero(a2dp_opus_05); + a2dp_opus_05.info = codec->vendor; + a2dp_opus_05.main.channels = channels; + a2dp_opus_05.main.frame_duration = opus_05_frame_durations; + OPUS_05_SET_LOCATION(a2dp_opus_05.main, location); + OPUS_05_SET_BITRATE(a2dp_opus_05.main, 0); + + if (has_bidi) { + a2dp_opus_05.bidi.channels = bidi_channels; + a2dp_opus_05.bidi.frame_duration = opus_05_frame_durations; + OPUS_05_SET_LOCATION(a2dp_opus_05.bidi, bidi_location); + OPUS_05_SET_BITRATE(a2dp_opus_05.bidi, 0); + } memcpy(caps, &a2dp_opus_05, sizeof(a2dp_opus_05)); return sizeof(a2dp_opus_05); } +static void combine_direction_caps(a2dp_opus_05_direction_t *dst, + const a2dp_opus_05_direction_t *src) +{ + uint32_t dst_bitrate, src_bitrate; + + dst->channels = SPA_MAX(dst->channels, src->channels); + dst->frame_duration |= src->frame_duration; + OPUS_05_SET_LOCATION(*dst, + OPUS_05_GET_LOCATION(*dst) | OPUS_05_GET_LOCATION(*src)); + + /* bitrate 0 means unlimited */ + dst_bitrate = OPUS_05_GET_BITRATE(*dst); + src_bitrate = OPUS_05_GET_BITRATE(*src); + if (dst_bitrate == 0 || src_bitrate == 0) + OPUS_05_SET_BITRATE(*dst, 0); + else + OPUS_05_SET_BITRATE(*dst, SPA_MAX(dst_bitrate, src_bitrate)); +} + +static int codec_combine_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE], int caps_size, + const uint8_t *other, int other_size) +{ + a2dp_opus_05_t *dst = (a2dp_opus_05_t *)caps; + const a2dp_opus_05_t *src = (const a2dp_opus_05_t *)other; + + if (caps_size != (int)sizeof(*dst) || other_size != (int)sizeof(*src)) + return -EINVAL; + + combine_direction_caps(&dst->main, &src->main); + combine_direction_caps(&dst->bidi, &src->bidi); + + return caps_size; +} + static int codec_select_config(const struct media_codec *codec, uint32_t flags, const void *caps, size_t caps_size, const struct media_codec_audio_info *info, @@ -831,6 +901,35 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags dir2 = &conf->main; } + /* + * Each codec object only accepts its own channel profile. This lets the + * monitor's config-aware resolver attribute the transport to the correct + * companion (opus_05 vs opus_05_51/71, duplex vs pro), matching AAC LC/ELD. + */ + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05: + if (dir1->channels > 2 || dir2->channels != 0) + return -EINVAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51: + if (dir1->channels != surround_encoders[5].channels || dir2->channels != 0) + return -EINVAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71: + if (dir1->channels != surround_encoders[7].channels || dir2->channels != 0) + return -EINVAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX: + if (dir1->channels > 2 || dir2->channels > 2) + return -EINVAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO: + case 0: /* duplex bidi return channel */ + break; + default: + return -EINVAL; + } + info->info.raw.channels = dir1->channels; if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0) @@ -1382,6 +1481,7 @@ const struct media_codec a2dp_codec_opus_05 = { .name = "opus_05", .description = "Opus 05", .fill_caps = codec_fill_caps, + .combine_caps = codec_combine_caps, }; const struct media_codec a2dp_codec_opus_05_51 = { @@ -1391,6 +1491,7 @@ const struct media_codec a2dp_codec_opus_05_51 = { .description = "Opus 05 5.1 Surround", .endpoint_name = "opus_05", .endpoint_companion = true, + .fill_caps = codec_fill_caps, }; const struct media_codec a2dp_codec_opus_05_71 = { @@ -1400,6 +1501,7 @@ const struct media_codec a2dp_codec_opus_05_71 = { .description = "Opus 05 7.1 Surround", .endpoint_name = "opus_05", .endpoint_companion = true, + .fill_caps = codec_fill_caps, }; /* Bidi return channel codec: doesn't have endpoints */ @@ -1417,6 +1519,7 @@ const struct media_codec a2dp_codec_opus_05_duplex = { .description = "Opus 05 Duplex", .duplex_codec = &a2dp_codec_opus_05_return, .fill_caps = codec_fill_caps, + .combine_caps = codec_combine_caps, }; const struct media_codec a2dp_codec_opus_05_pro = { @@ -1429,6 +1532,7 @@ const struct media_codec a2dp_codec_opus_05_pro = { .duplex_codec = &a2dp_codec_opus_05_return, .endpoint_name = "opus_05_duplex", .endpoint_companion = true, + .fill_caps = codec_fill_caps, }; MEDIA_CODEC_EXPORT_DEF(