This patch provides support for aptX codec in bluetooth A2DP profile. In pulseaudio it is implemented as a new profile a2dp_aptx_sink. For aptX encoding it uses open source LGPLv2.1+ licensed libopenaptx library which can be found at https://github.com/pali/libopenaptx. Limitations: Codec selection (either SBC or aptX) is done by bluez itself and it does not provide API for switching codec. Therefore pulseaudio is not able to change codec and it is up to bluez if it decide to use aptX or not. Only standard aptX codec is supported for now. Support for other variants like aptX HD, aptX Low Latency, FastStream may come up later. --- configure.ac | 19 ++ src/Makefile.am | 5 + src/modules/bluetooth/a2dp-codecs.h | 118 ++++++++++- src/modules/bluetooth/bluez5-util.c | 48 ++++- src/modules/bluetooth/bluez5-util.h | 2 + src/modules/bluetooth/module-bluez5-device.c | 65 +++++- src/modules/bluetooth/pa-a2dp-codec-aptx.c | 297 +++++++++++++++++++++++++++ src/modules/bluetooth/pa-a2dp-codec.h | 1 + 8 files changed, 548 insertions(+), 7 deletions(-) create mode 100644 src/modules/bluetooth/pa-a2dp-codec-aptx.c diff --git a/configure.ac b/configure.ac index d2bfab23b..c2d13fa53 100644 --- a/configure.ac +++ b/configure.ac @@ -1094,6 +1094,23 @@ AC_SUBST(HAVE_BLUEZ_5_NATIVE_HEADSET) AM_CONDITIONAL([HAVE_BLUEZ_5_NATIVE_HEADSET], [test "x$HAVE_BLUEZ_5_NATIVE_HEADSET" = x1]) AS_IF([test "x$HAVE_BLUEZ_5_NATIVE_HEADSET" = "x1"], AC_DEFINE([HAVE_BLUEZ_5_NATIVE_HEADSET], 1, [Bluez 5 native headset backend enabled])) +#### Bluetooth A2DP aptX codec (optional) ### + +AC_ARG_ENABLE([aptx], + AS_HELP_STRING([--disable-aptx],[Disable optional bluetooth A2DP aptX codec support (via libopenaptx)])) + +AS_IF([test "x$HAVE_BLUEZ_5" = "x1" && test "x$enable_aptx" != "xno"], + [AC_CHECK_HEADER([openaptx.h], + [AC_CHECK_LIB([openaptx], [aptx_init], [HAVE_OPENAPTX=1], [HAVE_OPENAPTX=0])], + [HAVE_OPENAPTX=0])]) + +AS_IF([test "x$HAVE_BLUEZ_5" = "x1" && test "x$enable_aptx" = "xyes" && test "x$HAVE_OPENAPTX" = "x0"], + [AC_MSG_ERROR([*** libopenaptx from https://github.com/pali/libopenaptx not found])]) + +AC_SUBST(HAVE_OPENAPTX) +AM_CONDITIONAL([HAVE_OPENAPTX], [test "x$HAVE_OPENAPTX" = "x1"]) +AS_IF([test "x$HAVE_OPENAPTX" = "x1"], AC_DEFINE([HAVE_OPENAPTX], 1, [Have openaptx codec library])) + #### UDEV support (optional) #### AC_ARG_ENABLE([udev], @@ -1579,6 +1596,7 @@ AS_IF([test "x$HAVE_SYSTEMD_JOURNAL" = "x1"], ENABLE_SYSTEMD_JOURNAL=yes, ENABLE AS_IF([test "x$HAVE_BLUEZ_5" = "x1"], ENABLE_BLUEZ_5=yes, ENABLE_BLUEZ_5=no) AS_IF([test "x$HAVE_BLUEZ_5_OFONO_HEADSET" = "x1"], ENABLE_BLUEZ_5_OFONO_HEADSET=yes, ENABLE_BLUEZ_5_OFONO_HEADSET=no) AS_IF([test "x$HAVE_BLUEZ_5_NATIVE_HEADSET" = "x1"], ENABLE_BLUEZ_5_NATIVE_HEADSET=yes, ENABLE_BLUEZ_5_NATIVE_HEADSET=no) +AS_IF([test "x$HAVE_OPENAPTX" = "x1"], ENABLE_APTX=yes, ENABLE_APTX=no) AS_IF([test "x$HAVE_HAL_COMPAT" = "x1"], ENABLE_HAL_COMPAT=yes, ENABLE_HAL_COMPAT=no) AS_IF([test "x$HAVE_TCPWRAP" = "x1"], ENABLE_TCPWRAP=yes, ENABLE_TCPWRAP=no) AS_IF([test "x$HAVE_LIBSAMPLERATE" = "x1"], ENABLE_LIBSAMPLERATE="yes (DEPRECATED)", ENABLE_LIBSAMPLERATE=no) @@ -1637,6 +1655,7 @@ echo " Enable BlueZ 5: ${ENABLE_BLUEZ_5} Enable ofono headsets: ${ENABLE_BLUEZ_5_OFONO_HEADSET} Enable native headsets: ${ENABLE_BLUEZ_5_NATIVE_HEADSET} + Enable aptX codec: ${ENABLE_APTX} Enable udev: ${ENABLE_UDEV} Enable HAL->udev compat: ${ENABLE_HAL_COMPAT} Enable systemd diff --git a/src/Makefile.am b/src/Makefile.am index 411b9e5e5..bbd797589 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2136,6 +2136,11 @@ libbluez5_util_la_SOURCES += modules/bluetooth/pa-a2dp-codec-sbc.c libbluez5_util_la_LIBADD += $(SBC_LIBS) libbluez5_util_la_CFLAGS += $(SBC_CFLAGS) +if HAVE_OPENAPTX +libbluez5_util_la_SOURCES += modules/bluetooth/pa-a2dp-codec-aptx.c +libbluez5_util_la_LIBADD += -lopenaptx +endif + module_bluez5_discover_la_SOURCES = modules/bluetooth/module-bluez5-discover.c module_bluez5_discover_la_LDFLAGS = $(MODULE_LDFLAGS) module_bluez5_discover_la_LIBADD = $(MODULE_LIBADD) $(DBUS_LIBS) libbluez5-util.la diff --git a/src/modules/bluetooth/a2dp-codecs.h b/src/modules/bluetooth/a2dp-codecs.h index 004975586..0c3583434 100644 --- a/src/modules/bluetooth/a2dp-codecs.h +++ b/src/modules/bluetooth/a2dp-codecs.h @@ -4,6 +4,7 @@ * * Copyright (C) 2006-2010 Nokia Corporation * Copyright (C) 2004-2010 Marcel Holtmann <marcel at holtmann.org> + * Copyright (C) 2018 Pali Rohár <pali.rohar at gmail.com> * * * This library is free software; you can redistribute it and/or @@ -24,7 +25,18 @@ #define A2DP_CODEC_SBC 0x00 #define A2DP_CODEC_MPEG12 0x01 #define A2DP_CODEC_MPEG24 0x02 -#define A2DP_CODEC_ATRAC 0x03 +#define A2DP_CODEC_ATRAC 0x04 +#define A2DP_CODEC_VENDOR 0xFF + +#define A2DP_VENDOR_APT_LIC_LTD 0x0000004f +#define APT_LIC_LTD_CODEC_APTX 0x0001 + +#define A2DP_VENDOR_QTIL 0x0000000a +#define QTIL_CODEC_FASTSTREAM 0x0001 +#define QTIL_CODEC_APTX_LL 0x0002 + +#define A2DP_VENDOR_QUALCOMM_TECH_INC 0x000000d7 +#define QUALCOMM_TECH_INC_CODEC_APTX_HD 0x0024 #define SBC_SAMPLING_FREQ_16000 (1 << 3) #define SBC_SAMPLING_FREQ_32000 (1 << 2) @@ -66,6 +78,26 @@ #define MPEG_SAMPLING_FREQ_44100 (1 << 1) #define MPEG_SAMPLING_FREQ_48000 1 +#define APTX_CHANNEL_MODE_MONO 0x1 +#define APTX_CHANNEL_MODE_STEREO 0x2 + +#define APTX_SAMPLING_FREQ_16000 0x8 +#define APTX_SAMPLING_FREQ_32000 0x4 +#define APTX_SAMPLING_FREQ_44100 0x2 +#define APTX_SAMPLING_FREQ_48000 0x1 + +#define FASTSTREAM_DIRECTION_SINK 0x1 +#define FASTSTREAM_DIRECTION_SOURCE 0x2 + +#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2 +#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1 + +#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2 + +typedef struct { + uint32_t vendor_id; + uint16_t codec_id; +} __attribute__ ((packed)) a2dp_vendor_codec_t; #if __BYTE_ORDER == __LITTLE_ENDIAN @@ -89,6 +121,48 @@ typedef struct { uint16_t bitrate; } __attribute__ ((packed)) a2dp_mpeg_t; +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t sink_frequency:4; + uint8_t source_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +typedef struct { + uint8_t reserved; + uint16_t target_codec_level; + uint16_t initial_codec_level; + uint8_t sra_max_rate; + uint8_t sra_avg_time; + uint16_t good_working_level; +} __attribute__ ((packed)) a2dp_aptx_ll_new_caps_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t bidirect_link:1; + uint8_t has_new_caps:1; + uint8_t reserved:6; + a2dp_aptx_ll_new_caps_t new_caps[0]; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t reserved0; + uint8_t reserved1; + uint8_t reserved2; + uint8_t reserved3; +} __attribute__ ((packed)) a2dp_aptx_hd_t; + #elif __BYTE_ORDER == __BIG_ENDIAN typedef struct { @@ -111,6 +185,48 @@ typedef struct { uint16_t bitrate; } __attribute__ ((packed)) a2dp_mpeg_t; +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t source_frequency:4; + uint8_t sink_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +typedef struct { + uint8_t reserved; + uint16_t target_codec_level; + uint16_t initial_codec_level; + uint8_t sra_max_rate; + uint8_t sra_avg_time; + uint16_t good_working_level; +} __attribute__ ((packed)) a2dp_aptx_ll_new_caps_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t reserved:6; + uint8_t has_new_caps:1; + uint8_t bidirect_link:1; + a2dp_aptx_ll_new_caps_t new_caps[0]; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t reserved0; + uint8_t reserved1; + uint8_t reserved2; + uint8_t reserved3; +} __attribute__ ((packed)) a2dp_aptx_hd_t; + #else #error "Unknown byte order" #endif diff --git a/src/modules/bluetooth/bluez5-util.c b/src/modules/bluetooth/bluez5-util.c index 9c4e3367b..c139f7fc3 100644 --- a/src/modules/bluetooth/bluez5-util.c +++ b/src/modules/bluetooth/bluez5-util.c @@ -50,7 +50,9 @@ #define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported" #define A2DP_SOURCE_SBC_ENDPOINT "/MediaEndpoint/A2DPSourceSBC" +#define A2DP_SOURCE_APTX_ENDPOINT "/MediaEndpoint/A2DPSourceAPTX" #define A2DP_SINK_SBC_ENDPOINT "/MediaEndpoint/A2DPSinkSBC" +#define A2DP_SINK_APTX_ENDPOINT "/MediaEndpoint/A2DPSinkAPTX" #define ENDPOINT_INTROSPECT_XML \ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ @@ -173,8 +175,22 @@ static bool device_supports_profile(pa_bluetooth_device *device, pa_bluetooth_pr switch (profile) { case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK); + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: +#ifdef HAVE_OPENAPTX + /* TODO: Implement once bluez provides API to check if codec is supported */ + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK); +#else + return false; +#endif case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE); + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: +#ifdef HAVE_OPENAPTX + /* TODO: Implement once bluez provides API to check if codec is supported */ + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE); +#else + return false; +#endif case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS) || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS_ALT) @@ -961,7 +977,9 @@ static void parse_interfaces_and_properties(pa_bluetooth_discovery *y, DBusMessa if (!a->valid) return; + register_endpoint(y, path, A2DP_SOURCE_APTX_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SOURCE); register_endpoint(y, path, A2DP_SOURCE_SBC_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SOURCE); + register_endpoint(y, path, A2DP_SINK_APTX_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SINK); register_endpoint(y, path, A2DP_SINK_SBC_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SINK); } else if (pa_streq(interface, BLUEZ_DEVICE_INTERFACE)) { @@ -1258,8 +1276,12 @@ const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile) { switch(profile) { case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: return "a2dp_sink"; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: + return "a2dp_aptx_sink"; case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: return "a2dp_source"; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: + return "a2dp_aptx_source"; case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: return "headset_head_unit"; case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: @@ -1275,8 +1297,12 @@ const char *pa_bluetooth_profile_to_a2dp_endpoint(pa_bluetooth_profile_t profile switch (profile) { case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: return A2DP_SOURCE_SBC_ENDPOINT; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: + return A2DP_SOURCE_APTX_ENDPOINT; case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: return A2DP_SINK_SBC_ENDPOINT; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: + return A2DP_SINK_APTX_ENDPOINT; default: return NULL; } @@ -1289,6 +1315,11 @@ const pa_a2dp_codec_t *pa_bluetooth_profile_to_a2dp_codec(pa_bluetooth_profile_t case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: return &pa_a2dp_codec_sbc; +#ifdef HAVE_OPENAPTX + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: + return &pa_a2dp_codec_aptx; +#endif default: return NULL; } @@ -1299,6 +1330,10 @@ const pa_a2dp_codec_t *pa_bluetooth_profile_to_a2dp_codec(pa_bluetooth_profile_t const pa_a2dp_codec_t *pa_a2dp_endpoint_to_a2dp_codec(const char *endpoint) { if (pa_streq(endpoint, A2DP_SOURCE_SBC_ENDPOINT) || pa_streq(endpoint, A2DP_SINK_SBC_ENDPOINT)) return &pa_a2dp_codec_sbc; +#ifdef HAVE_OPENAPTX + else if (pa_streq(endpoint, A2DP_SOURCE_APTX_ENDPOINT) || pa_streq(endpoint, A2DP_SINK_APTX_ENDPOINT)) + return &pa_a2dp_codec_aptx; +#endif else return NULL; } @@ -1359,9 +1394,15 @@ static DBusMessage *endpoint_set_configuration(DBusConnection *conn, DBusMessage if (pa_streq(endpoint_path, A2DP_SOURCE_SBC_ENDPOINT)) { if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) p = PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK; + } else if (pa_streq(endpoint_path, A2DP_SOURCE_APTX_ENDPOINT)) { + if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) + p = PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK; } else if (pa_streq(endpoint_path, A2DP_SINK_SBC_ENDPOINT)) { if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) p = PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE; + } else if (pa_streq(endpoint_path, A2DP_SINK_APTX_ENDPOINT)) { + if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) + p = PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE; } if (p == PA_BLUETOOTH_PROFILE_OFF) { @@ -1546,7 +1587,8 @@ static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, voi pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); - if (!pa_streq(path, A2DP_SOURCE_SBC_ENDPOINT) && !pa_streq(path, A2DP_SINK_SBC_ENDPOINT)) + if (!pa_streq(path, A2DP_SOURCE_SBC_ENDPOINT) && !pa_streq(path, A2DP_SINK_SBC_ENDPOINT) && + !pa_streq(path, A2DP_SOURCE_APTX_ENDPOINT) && !pa_streq(path, A2DP_SINK_APTX_ENDPOINT)) return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { @@ -1652,7 +1694,9 @@ pa_bluetooth_discovery* pa_bluetooth_discovery_get(pa_core *c, int headset_backe } y->matches_added = true; + endpoint_init(y, PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK); endpoint_init(y, PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK); + endpoint_init(y, PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE); endpoint_init(y, PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE); get_managed_objects(y); @@ -1721,7 +1765,9 @@ void pa_bluetooth_discovery_unref(pa_bluetooth_discovery *y) { if (y->filter_added) dbus_connection_remove_filter(pa_dbus_connection_get(y->connection), filter_cb, y); + endpoint_done(y, PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK); endpoint_done(y, PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK); + endpoint_done(y, PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE); endpoint_done(y, PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE); pa_dbus_connection_unref(y->connection); diff --git a/src/modules/bluetooth/bluez5-util.h b/src/modules/bluetooth/bluez5-util.h index 365b9ef6f..29d862fe1 100644 --- a/src/modules/bluetooth/bluez5-util.h +++ b/src/modules/bluetooth/bluez5-util.h @@ -55,7 +55,9 @@ typedef enum pa_bluetooth_hook { typedef enum profile { PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK, + PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK, PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE, + PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY, PA_BLUETOOTH_PROFILE_OFF diff --git a/src/modules/bluetooth/module-bluez5-device.c b/src/modules/bluetooth/module-bluez5-device.c index e626e80e9..e8a07d067 100644 --- a/src/modules/bluetooth/module-bluez5-device.c +++ b/src/modules/bluetooth/module-bluez5-device.c @@ -706,7 +706,8 @@ static void transport_config_mtu(struct userdata *u) { if (u->sink) { pa_sink_set_max_request_within_thread(u->sink, u->write_block_size); pa_sink_set_fixed_latency_within_thread(u->sink, - (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK ? + ((u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK || + u->profile == PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK) ? FIXED_LATENCY_PLAYBACK_A2DP : FIXED_LATENCY_PLAYBACK_SCO) + pa_bytes_to_usec(u->write_block_size, &u->sample_spec)); @@ -752,7 +753,7 @@ static void setup_stream(struct userdata *u) { if (setsockopt(u->stream_fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0) pa_log_warn("Failed to enable SO_TIMESTAMP: %s", pa_cstrerror(errno)); - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK) + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK) update_buffer_size(u); pa_log_debug("Stream properly set up, we're ready to roll!"); @@ -925,6 +926,7 @@ static int add_source(struct userdata *u) { if (!u->transport_acquired) switch (u->profile) { case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: data.suspend_cause = PA_SUSPEND_USER; break; @@ -937,6 +939,7 @@ static int add_source(struct userdata *u) { pa_assert_not_reached(); break; case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: case PA_BLUETOOTH_PROFILE_OFF: default: pa_assert_not_reached(); @@ -1111,8 +1114,10 @@ static int add_sink(struct userdata *u) { pa_assert_not_reached(); break; case PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK: + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: /* Profile switch should have failed */ case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: case PA_BLUETOOTH_PROFILE_OFF: default: pa_assert_not_reached(); @@ -1145,7 +1150,7 @@ static void transport_config(struct userdata *u) { u->sample_spec.rate = 8000; } else { const pa_a2dp_codec_t *codec = pa_bluetooth_profile_to_a2dp_codec(u->profile); - bool is_sink = (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK); + bool is_sink = (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK); pa_assert(codec); pa_assert(u->transport); codec->init(is_sink ? &u->encoder_info : &u->decoder_info, &u->sample_spec, is_sink, u->transport->config, u->transport->config_size); @@ -1169,7 +1174,7 @@ static int setup_transport(struct userdata *u) { u->transport = t; - if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) transport_acquire(u, true); /* In case of error, the sink/sources will be created suspended */ else { int transport_error; @@ -1188,7 +1193,9 @@ static int setup_transport(struct userdata *u) { static pa_direction_t get_profile_direction(pa_bluetooth_profile_t p) { static const pa_direction_t profile_direction[] = { [PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK] = PA_DIRECTION_OUTPUT, + [PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK] = PA_DIRECTION_OUTPUT, [PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE] = PA_DIRECTION_INPUT, + [PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE] = PA_DIRECTION_INPUT, [PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, [PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, [PA_BLUETOOTH_PROFILE_OFF] = 0 @@ -1520,7 +1527,7 @@ static int start_thread(struct userdata *u) { /* If we are in the headset role or the device is an a2dp source, * the source should not become default unless there is no other * sound device available. */ - if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE) + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE) u->source->priority = 1500; pa_source_put(u->source); @@ -1745,6 +1752,18 @@ static pa_card_profile *create_card_profile(struct userdata *u, pa_bluetooth_pro p = PA_CARD_PROFILE_DATA(cp); break; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK: + cp = pa_card_profile_new(name, _("High Fidelity aptX Playback (A2DP Sink)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 50; + cp->n_sinks = 1; + cp->n_sources = 0; + cp->max_sink_channels = 2; + cp->max_source_channels = 0; + pa_hashmap_put(output_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + case PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE: cp = pa_card_profile_new(name, _("High Fidelity SBC Capture (A2DP Source)"), sizeof(pa_bluetooth_profile_t)); cp->priority = 20; @@ -1757,6 +1776,18 @@ static pa_card_profile *create_card_profile(struct userdata *u, pa_bluetooth_pro p = PA_CARD_PROFILE_DATA(cp); break; + case PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE: + cp = pa_card_profile_new(name, _("High Fidelity aptX Capture (A2DP Source)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 25; + cp->n_sinks = 0; + cp->n_sources = 1; + cp->max_sink_channels = 0; + cp->max_source_channels = 2; + pa_hashmap_put(input_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: cp = pa_card_profile_new(name, _("Headset Head Unit (HSP/HFP)"), sizeof(pa_bluetooth_profile_t)); cp->priority = 30; @@ -1840,8 +1871,10 @@ off: } static int uuid_to_profile(const char *uuid, pa_bluetooth_profile_t *_r) { + /* FIXME: PA_BLUETOOTH_UUID_A2DP_SINK maps to both SBC and APTX */ if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) *_r = PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK; + /* FIXME: PA_BLUETOOTH_UUID_A2DP_SOURCE maps to both SBC and APTX */ else if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) *_r = PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE; else if (pa_bluetooth_uuid_is_hsp_hs(uuid) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_HF)) @@ -1907,6 +1940,28 @@ static int add_card(struct userdata *u) { pa_hashmap_put(data.profiles, cp->name, cp); } + PA_HASHMAP_FOREACH(uuid, d->uuids, state) { + pa_bluetooth_profile_t profile; + + /* FIXME: PA_BLUETOOTH_UUID_A2DP_SINK maps to both SBC and APTX */ + /* FIXME: PA_BLUETOOTH_UUID_A2DP_SOURCE maps to both SBC and APTX */ + if (uuid_to_profile(uuid, &profile) < 0) + continue; + + /* Handle APTX */ + if (profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SINK) + profile = PA_BLUETOOTH_PROFILE_A2DP_APTX_SINK; + else if (profile == PA_BLUETOOTH_PROFILE_A2DP_SBC_SOURCE) + profile = PA_BLUETOOTH_PROFILE_A2DP_APTX_SOURCE; + else + continue; + + if (!pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile))) { + cp = create_card_profile(u, profile, data.ports); + pa_hashmap_put(data.profiles, cp->name, cp); + } + } + pa_assert(!pa_hashmap_isempty(data.profiles)); cp = pa_card_profile_new("off", _("Off"), sizeof(pa_bluetooth_profile_t)); diff --git a/src/modules/bluetooth/pa-a2dp-codec-aptx.c b/src/modules/bluetooth/pa-a2dp-codec-aptx.c new file mode 100644 index 000000000..8ce1fc67c --- /dev/null +++ b/src/modules/bluetooth/pa-a2dp-codec-aptx.c @@ -0,0 +1,297 @@ +/*** + This file is part of PulseAudio. + + Copyright 2018 Pali Rohár <pali.rohar at gmail.com> + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/log.h> +#include <pulsecore/macro.h> +#include <pulsecore/once.h> +#include <pulse/sample.h> + +#include <openaptx.h> + +#include "a2dp-codecs.h" +#include "pa-a2dp-codec.h" + +static size_t pa_aptx_fill_capabilities(uint8_t *capabilities_buffer, size_t capabilities_size) { + a2dp_aptx_t *capabilities = (a2dp_aptx_t *) capabilities_buffer; + + if (capabilities_size < sizeof(*capabilities)) { + pa_log_error("Invalid size of capabilities buffer"); + return 0; + } + + pa_zero(*capabilities); + + capabilities->info.vendor_id = A2DP_VENDOR_APT_LIC_LTD; + capabilities->info.codec_id = APT_LIC_LTD_CODEC_APTX; + capabilities->channel_mode = APTX_CHANNEL_MODE_STEREO; + capabilities->frequency = APTX_SAMPLING_FREQ_16000 | APTX_SAMPLING_FREQ_32000 | APTX_SAMPLING_FREQ_44100 | + APTX_SAMPLING_FREQ_48000; + + return sizeof(*capabilities); +} + +static bool pa_aptx_validate_configuration(const uint8_t *config_buffer, size_t config_size) { + a2dp_aptx_t *config = (a2dp_aptx_t *) config_buffer; + + if (config_size != sizeof(*config)) { + pa_log_error("Invalid size of config buffer"); + return false; + } + + if (config->info.vendor_id != A2DP_VENDOR_APT_LIC_LTD || config->info.codec_id != APT_LIC_LTD_CODEC_APTX) { + pa_log_error("Invalid vendor codec information in configuration"); + return false; + } + + if (config->frequency != APTX_SAMPLING_FREQ_16000 && config->frequency != APTX_SAMPLING_FREQ_32000 && + config->frequency != APTX_SAMPLING_FREQ_44100 && config->frequency != APTX_SAMPLING_FREQ_48000) { + pa_log_error("Invalid sampling frequency in configuration"); + return false; + } + + if (config->channel_mode != APTX_CHANNEL_MODE_STEREO) { + pa_log_error("Invalid channel mode in configuration"); + return false; + } + + return true; +} + +static size_t pa_aptx_select_configuration(const pa_sample_spec *sample_spec, const uint8_t *capabilities_buffer, size_t capabilities_size, uint8_t *config_buffer, size_t config_size) { + a2dp_aptx_t *config = (a2dp_aptx_t *) config_buffer; + a2dp_aptx_t *capabilities = (a2dp_aptx_t *) capabilities_buffer; + int i; + + static const struct { + uint32_t rate; + uint8_t cap; + } freq_table[] = { + { 16000U, APTX_SAMPLING_FREQ_16000 }, + { 32000U, APTX_SAMPLING_FREQ_32000 }, + { 44100U, APTX_SAMPLING_FREQ_44100 }, + { 48000U, APTX_SAMPLING_FREQ_48000 } + }; + + if (capabilities_size != sizeof(*capabilities)) { + pa_log_error("Invalid size of capabilities buffer"); + return 0; + } + + if (config_size < sizeof(*config)) { + pa_log_error("Invalid size of config buffer"); + return 0; + } + + pa_zero(*config); + + if (capabilities->info.vendor_id != A2DP_VENDOR_APT_LIC_LTD || capabilities->info.codec_id != APT_LIC_LTD_CODEC_APTX) { + pa_log_error("No supported vendor codec information"); + return 0; + } + + config->info.vendor_id = A2DP_VENDOR_APT_LIC_LTD; + config->info.codec_id = APT_LIC_LTD_CODEC_APTX; + + if (sample_spec->channels != 2 || !(capabilities->channel_mode & APTX_CHANNEL_MODE_STEREO)) { + pa_log_error("No supported channel modes"); + return 0; + } + + config->channel_mode = APTX_CHANNEL_MODE_STEREO; + + /* Find the lowest freq that is at least as high as the requested sampling rate */ + for (i = 0; (unsigned) i < PA_ELEMENTSOF(freq_table); i++) { + if (freq_table[i].rate >= sample_spec->rate && (capabilities->frequency & freq_table[i].cap)) { + config->frequency = freq_table[i].cap; + break; + } + } + + if ((unsigned) i == PA_ELEMENTSOF(freq_table)) { + for (--i; i >= 0; i--) { + if (capabilities->frequency & freq_table[i].cap) { + config->frequency = freq_table[i].cap; + break; + } + } + + if (i < 0) { + pa_log_error("Not suitable sample rate"); + return 0; + } + } + + return sizeof(*config); +} + +static void pa_aptx_init(void **info_ptr, pa_sample_spec *sample_spec, bool is_a2dp_sink, const uint8_t *config_buffer, size_t config_size) { + a2dp_aptx_t *config = (a2dp_aptx_t *) config_buffer; + + pa_assert(config_size == sizeof(*config)); + + sample_spec->format = PA_SAMPLE_S24LE; + + if (*info_ptr) + aptx_reset((struct aptx_context *) *info_ptr); + else + *info_ptr = (void *)aptx_init(0); + + switch (config->frequency) { + case APTX_SAMPLING_FREQ_16000: + sample_spec->rate = 16000U; + break; + case APTX_SAMPLING_FREQ_32000: + sample_spec->rate = 32000U; + break; + case APTX_SAMPLING_FREQ_44100: + sample_spec->rate = 44100U; + break; + case APTX_SAMPLING_FREQ_48000: + sample_spec->rate = 48000U; + break; + default: + pa_assert_not_reached(); + } + + switch (config->channel_mode) { + case APTX_CHANNEL_MODE_STEREO: + sample_spec->channels = 2; + break; + default: + pa_assert_not_reached(); + } +} + +static void pa_aptx_finish(void **info_ptr) { + struct aptx_context *aptx_c = (struct aptx_context *) *info_ptr; + + if (aptx_c) { + aptx_finish(aptx_c); + *info_ptr = NULL; + } +} + +static void pa_aptx_setup(void **info_ptr) { +} + +static void pa_aptx_fill_blocksize(void **info_ptr, size_t read_link_mtu, size_t write_link_mtu, size_t *read_block_size, size_t *write_block_size) { + *write_block_size = (write_link_mtu/(8*4)) * 8*4*6; + *read_block_size = (read_link_mtu/(8*4)) * 8*4*6; +} + +static bool pa_aptx_fix_latency(void **info_ptr) { + return false; +} + +static size_t pa_aptx_encode(void **info_ptr, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size) { + struct aptx_context *aptx_c = (struct aptx_context *) *info_ptr; + uint8_t *d; + const uint8_t *p; + size_t to_write, to_encode; + + p = input_buffer; + to_encode = input_size; + + d = output_buffer; + to_write = output_size; + + while (PA_LIKELY(to_encode > 0 && to_write > 0)) { + size_t written; + size_t encoded; + encoded = aptx_encode(aptx_c, p, to_encode, d, to_write, &written); + + if (PA_UNLIKELY(encoded == 0)) { + pa_log_error("aptX encoding error"); + return 0; + } + + pa_assert_fp((size_t) encoded <= to_encode); + pa_assert_fp((size_t) written <= to_write); + + p += encoded; + to_encode -= encoded; + + d += written; + to_write -= written; + } + + pa_assert(to_encode == 0); + + PA_ONCE_BEGIN { + pa_log_debug("Using aptX encoder implementation: libopenaptx from https://github.com/pali/libopenaptx"); + } PA_ONCE_END; + + return d - output_buffer; +} + +static size_t pa_aptx_decode(void **info_ptr, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed) { + struct aptx_context *aptx_c = (struct aptx_context *) *info_ptr; + + const uint8_t *p; + uint8_t *d; + size_t to_write, to_decode; + + p = input_buffer; + to_decode = input_size; + + d = output_buffer; + to_write = output_size; + + while (PA_LIKELY(to_decode > 0)) { + size_t written; + size_t decoded; + + decoded = aptx_decode(aptx_c, p, to_decode, d, to_write, &written); + + if (PA_UNLIKELY(decoded == 0)) { + pa_log_error("aptX decoding error"); + *processed = p - input_buffer; + return 0; + } + + pa_assert_fp((size_t) decoded <= to_decode); + + p += decoded; + to_decode -= decoded; + + d += written; + to_write -= written; + } + + *processed = p - input_buffer; + return d - output_buffer; +} + +const pa_a2dp_codec_t pa_a2dp_codec_aptx = { + .codec_id = A2DP_CODEC_VENDOR, + .fill_capabilities = pa_aptx_fill_capabilities, + .validate_configuration = pa_aptx_validate_configuration, + .select_configuration = pa_aptx_select_configuration, + .init = pa_aptx_init, + .finish = pa_aptx_finish, + .setup = pa_aptx_setup, + .fill_blocksize = pa_aptx_fill_blocksize, + .fix_latency = pa_aptx_fix_latency, + .encode = pa_aptx_encode, + .decode = pa_aptx_decode, +}; diff --git a/src/modules/bluetooth/pa-a2dp-codec.h b/src/modules/bluetooth/pa-a2dp-codec.h index 68b1619c2..26e7653a6 100644 --- a/src/modules/bluetooth/pa-a2dp-codec.h +++ b/src/modules/bluetooth/pa-a2dp-codec.h @@ -36,5 +36,6 @@ typedef struct pa_a2dp_codec { } pa_a2dp_codec_t; extern const pa_a2dp_codec_t pa_a2dp_codec_sbc; +extern const pa_a2dp_codec_t pa_a2dp_codec_aptx; #endif -- 2.11.0