bluez5: add aptx-LL codec

Support the low-latency variant of the aptx codec.

The magic mostly seems to be on the device side, since the stream is the
same as standard aptx, but latency is smaller even if stream/packet
sizes are the same.

Sound output latency is noticeably less than with the standard aptx.
Tested on Sennheiser HD 250 / Avantree Aria Pro.

The codec in principle also supports bidirectional duplex streams,
but that is not implemented here.
This commit is contained in:
Pauli Virtanen 2021-08-15 20:15:58 +03:00 committed by Wim Taymans
parent 73994feda9
commit 90b62ba2b3
6 changed files with 143 additions and 21 deletions

View file

@ -44,6 +44,7 @@ enum spa_bluetooth_audio_codec {
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
/* HFP */
SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100,

View file

@ -48,6 +48,7 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx", NULL },
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_hd", NULL },
{ SPA_BLUETOOTH_AUDIO_CODEC_LDAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "ldac", NULL },
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll", NULL },
{ SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL },
{ SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL },

View file

@ -35,6 +35,18 @@
#include "rtp.h"
#include "a2dp-codecs.h"
#define APTX_LL_LEVEL1(level) (((level) >> 8) & 0xFF)
#define APTX_LL_LEVEL2(level) (((level) >> 0) & 0xFF)
#define APTX_LL_LEVEL(level1, level2) ((((level1) & 0xFF) << 8) | (((level2) & 0xFF) << 0))
/*
* XXX: Bump requested device buffer levels up by 50% from defaults,
* XXX: increasing latency similarly. This seems to be necessary for
* XXX: stable output when moving headphones. It might be possible to
* XXX: reduce this by changing the scheduling of the socket writes.
*/
#define LL_LEVEL_ADJUSTMENT 3/2
struct impl {
struct aptx_context *aptx;
@ -49,13 +61,25 @@ struct impl {
bool hd;
};
static inline bool codec_is_hd(const struct a2dp_codec *codec) {
static inline bool codec_is_hd(const struct a2dp_codec *codec)
{
return codec->vendor.codec_id == APTX_HD_CODEC_ID
&& codec->vendor.vendor_id == APTX_HD_VENDOR_ID;
}
static inline size_t codec_get_caps_size(const struct a2dp_codec *codec) {
return codec_is_hd(codec) ? sizeof(a2dp_aptx_hd_t) : sizeof(a2dp_aptx_t);
static inline bool codec_is_ll(const struct a2dp_codec *codec)
{
return codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL;
}
static inline size_t codec_get_caps_size(const struct a2dp_codec *codec)
{
if (codec_is_hd(codec))
return sizeof(a2dp_aptx_hd_t);
else if (codec_is_ll(codec))
return sizeof(a2dp_aptx_ll_t);
else
return sizeof(a2dp_aptx_t);
}
static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
@ -72,7 +96,15 @@ static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
.channel_mode =
APTX_CHANNEL_MODE_STEREO,
};
memcpy(caps, &a2dp_aptx, sizeof(a2dp_aptx));
const a2dp_aptx_ll_t a2dp_aptx_ll = {
.aptx = a2dp_aptx,
.bidirect_link = false,
.has_new_caps = false,
};
if (codec_is_ll(codec))
memcpy(caps, &a2dp_aptx_ll, sizeof(a2dp_aptx_ll));
else
memcpy(caps, &a2dp_aptx, sizeof(a2dp_aptx));
return actual_conf_size;
}
@ -120,6 +152,58 @@ static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
return actual_conf_size;
}
static int codec_select_config_ll(const struct a2dp_codec *codec, uint32_t flags,
const void *caps, size_t caps_size,
const struct a2dp_codec_audio_info *info,
const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
{
a2dp_aptx_ll_ext_t conf = { 0 };
size_t actual_conf_size;
int res;
/* caps may contain only conf.base, or also the extended attributes */
if (caps_size < sizeof(conf.base))
return -EINVAL;
memcpy(&conf, caps, SPA_MIN(caps_size, sizeof(conf)));
actual_conf_size = conf.base.has_new_caps ? sizeof(conf) : sizeof(conf.base);
if (caps_size < actual_conf_size)
return -EINVAL;
if ((res = codec_select_config(codec, flags, caps, caps_size, info, settings, config)) < 0)
return res;
memcpy(&conf.base.aptx, config, sizeof(conf.base.aptx));
if (conf.base.has_new_caps) {
int target_level = APTX_LL_LEVEL(conf.target_level1, conf.target_level2);
int initial_level = APTX_LL_LEVEL(conf.initial_level1, conf.initial_level2);
int good_working_level = APTX_LL_LEVEL(conf.good_working_level1, conf.good_working_level2);
target_level = SPA_MAX(target_level, APTX_LL_TARGET_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
initial_level = SPA_MAX(initial_level, APTX_LL_INITIAL_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
good_working_level = SPA_MAX(good_working_level, APTX_LL_GOOD_WORKING_LEVEL * LL_LEVEL_ADJUSTMENT);
conf.target_level1 = APTX_LL_LEVEL1(target_level);
conf.target_level2 = APTX_LL_LEVEL2(target_level);
conf.initial_level1 = APTX_LL_LEVEL1(initial_level);
conf.initial_level2 = APTX_LL_LEVEL2(initial_level);
conf.good_working_level1 = APTX_LL_LEVEL1(good_working_level);
conf.good_working_level2 = APTX_LL_LEVEL2(good_working_level);
if (conf.sra_max_rate == 0)
conf.sra_max_rate = APTX_LL_SRA_MAX_RATE;
if (conf.sra_avg_time == 0)
conf.sra_avg_time = APTX_LL_SRA_AVG_TIME;
}
memcpy(config, &conf, actual_conf_size);
return actual_conf_size;
}
static int codec_enum_config(const struct a2dp_codec *codec,
const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
struct spa_pod_builder *b, struct spa_pod **param)
@ -396,3 +480,37 @@ const struct a2dp_codec a2dp_codec_aptx_hd = {
.reduce_bitpool = codec_reduce_bitpool,
.increase_bitpool = codec_increase_bitpool,
};
#define APTX_LL_COMMON_DEFS \
.id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, \
.codec_id = A2DP_CODEC_VENDOR, \
.name = "aptx_ll", \
.description = "aptX-LL", \
.fill_caps = codec_fill_caps, \
.select_config = codec_select_config_ll, \
.enum_config = codec_enum_config, \
.init = codec_init, \
.deinit = codec_deinit, \
.get_block_size = codec_get_block_size, \
.abr_process = codec_abr_process, \
.start_encode = codec_start_encode, \
.encode = codec_encode, \
.start_decode = codec_start_decode, \
.decode = codec_decode, \
.reduce_bitpool = codec_reduce_bitpool, \
.increase_bitpool = codec_increase_bitpool
const struct a2dp_codec a2dp_codec_aptx_ll_0 = {
APTX_LL_COMMON_DEFS,
.vendor = { .vendor_id = APTX_LL_VENDOR_ID,
.codec_id = APTX_LL_CODEC_ID },
.endpoint_name = "aptx_ll_0",
};
const struct a2dp_codec a2dp_codec_aptx_ll_1 = {
APTX_LL_COMMON_DEFS,
.vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
.codec_id = APTX_LL_CODEC_ID },
.endpoint_name = "aptx_ll_1",
};

View file

@ -132,6 +132,8 @@ extern struct a2dp_codec a2dp_codec_mpeg;
#if ENABLE_APTX
extern struct a2dp_codec a2dp_codec_aptx;
extern struct a2dp_codec a2dp_codec_aptx_hd;
extern struct a2dp_codec a2dp_codec_aptx_ll_0;
extern struct a2dp_codec a2dp_codec_aptx_ll_1;
#endif
static const struct a2dp_codec * const a2dp_codec_list[] = {
@ -150,6 +152,10 @@ static const struct a2dp_codec * const a2dp_codec_list[] = {
#endif
&a2dp_codec_sbc,
&a2dp_codec_sbc_xq,
#if ENABLE_APTX
&a2dp_codec_aptx_ll_0,
&a2dp_codec_aptx_ll_1,
#endif
NULL
};

View file

@ -1933,24 +1933,20 @@ int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t)
if (t->a2dp_codec == NULL)
return 30 * SPA_NSEC_PER_MSEC;
switch (t->a2dp_codec->codec_id) {
case A2DP_CODEC_SBC:
switch (t->a2dp_codec->id) {
case SPA_BLUETOOTH_AUDIO_CODEC_SBC:
case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ:
return 200 * SPA_NSEC_PER_MSEC;
case A2DP_CODEC_MPEG24:
case SPA_BLUETOOTH_AUDIO_CODEC_MPEG:
case SPA_BLUETOOTH_AUDIO_CODEC_AAC:
return 200 * SPA_NSEC_PER_MSEC;
case A2DP_CODEC_VENDOR:
{
uint32_t vendor_id = t->a2dp_codec->vendor.vendor_id;
uint16_t codec_id = t->a2dp_codec->vendor.codec_id;
if (vendor_id == APTX_VENDOR_ID && codec_id == APTX_CODEC_ID)
return 150 * SPA_NSEC_PER_MSEC;
if (vendor_id == APTX_HD_VENDOR_ID && codec_id == APTX_HD_CODEC_ID)
return 150 * SPA_NSEC_PER_MSEC;
if (vendor_id == LDAC_VENDOR_ID && codec_id == LDAC_CODEC_ID)
return 175 * SPA_NSEC_PER_MSEC;
break;
}
case SPA_BLUETOOTH_AUDIO_CODEC_APTX:
case SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD:
return 150 * SPA_NSEC_PER_MSEC;
case SPA_BLUETOOTH_AUDIO_CODEC_LDAC:
return 175 * SPA_NSEC_PER_MSEC;
case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL:
return 40 * SPA_NSEC_PER_MSEC;
default:
break;
};

View file

@ -27,7 +27,7 @@ properties = {
#bluez5.headset-roles = [ hsp_hs hsp_ag hfp_hf hfp_ag ]
# Enabled A2DP codecs (default: all).
#bluez5.codecs = [ sbc aac ldac aptx aptx_hd sbc_xq ]
#bluez5.codecs = [ sbc aac ldac aptx aptx_hd aptx_ll sbc_xq ]
# Properties for the A2DP codec configuration
#bluez5.default.rate = 48000