This patch provides support for aptX codec in bluetooth A2DP profile. It uses open source LGPLv2.1+ licensed libopenaptx library which can be found at https://github.com/pali/libopenaptx. Only standard aptX codec is supported for now. Support for other variants like aptX HD or aptX Low Latency may come up later. --- configure.ac | 36 ++++ src/Makefile.am | 6 + src/modules/bluetooth/a2dp-codec-aptx.c | 330 ++++++++++++++++++++++++++++++++ src/modules/bluetooth/a2dp-codec-util.c | 6 + 4 files changed, 378 insertions(+) create mode 100644 src/modules/bluetooth/a2dp-codec-aptx.c diff --git a/configure.ac b/configure.ac index 2512d3c95..ae5c3210e 100644 --- a/configure.ac +++ b/configure.ac @@ -1104,6 +1104,40 @@ 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)])) +AC_ARG_VAR([OPENAPTX_CPPFLAGS], [C preprocessor flags for openaptx]) +AC_ARG_VAR([OPENAPTX_LDFLAGS], [linker flags for openaptx]) + +CPPFLAGS_SAVE="$CPPFLAGS" +LDFLAGS_SAVE="$LDFLAGS" +LIBS_SAVE="$LIBS" + +CPPFLAGS="$CPPFLAGS $OPENAPTX_CPPFLAGS" +LDFLAGS="$LDFLAGS $OPENAPTX_LDFLAGS" + +AS_IF([test "x$HAVE_BLUEZ_5" = "x1" && test "x$enable_aptx" != "xno"], + [AC_CHECK_HEADER([openaptx.h], + [AC_SEARCH_LIBS([aptx_init], [openaptx], + [HAVE_OPENAPTX=1; AS_IF([test "x$ac_cv_search_aptx_init" != "xnone required"], [OPENAPTX_LDFLAGS="$OPENAPTX_LDFLAGS $ac_cv_search_aptx_init"])], + [HAVE_OPENAPTX=0])], + [HAVE_OPENAPTX=0])]) + +CPPFLAGS="$CPPFLAGS_SAVE" +LDFLAGS="$LDFLAGS_SAVE" +LIBS="$LIBS_SAVE" + +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(OPENAPTX_CPPFLAGS) +AC_SUBST(OPENAPTX_LDFLAGS) +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], @@ -1589,6 +1623,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) @@ -1647,6 +1682,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 f65783308..7c7f1b564 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2146,6 +2146,12 @@ libbluez5_util_la_SOURCES += modules/bluetooth/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/a2dp-codec-aptx.c +libbluez5_util_la_CPPFLAGS += $(OPENAPTX_CPPFLAGS) +libbluez5_util_la_LDFLAGS += $(OPENAPTX_LDFLAGS) +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-codec-aptx.c b/src/modules/bluetooth/a2dp-codec-aptx.c new file mode 100644 index 000000000..8cde8b908 --- /dev/null +++ b/src/modules/bluetooth/a2dp-codec-aptx.c @@ -0,0 +1,330 @@ +/*** + This file is part of PulseAudio. + + Copyright 2018-2019 Pali Rohár <pali.rohar@xxxxxxxxx> + + 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 "a2dp-codec-api.h" + +static bool accept_capabilities(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding) { + const a2dp_aptx_t *capabilities = (const a2dp_aptx_t *) capabilities_buffer; + + if (capabilities_size != sizeof(*capabilities)) + return false; + + if (A2DP_GET_VENDOR_ID(capabilities->info) != APTX_VENDOR_ID || A2DP_GET_CODEC_ID(capabilities->info) != APTX_CODEC_ID) + return false; + + if (!(capabilities->frequency & (APTX_SAMPLING_FREQ_16000 | APTX_SAMPLING_FREQ_32000 | + APTX_SAMPLING_FREQ_44100 | APTX_SAMPLING_FREQ_48000))) + return false; + + if (!(capabilities->channel_mode & APTX_CHANNEL_MODE_STEREO)) + return false; + + return true; +} + +static const char *choose_capabilities(const pa_hashmap *capabilities_hashmap, bool for_encoding) { + const pa_a2dp_codec_capabilities *a2dp_capabilities; + const char *key; + void *state; + + /* There is no preference, just choose random valid entry */ + PA_HASHMAP_FOREACH_KV(key, a2dp_capabilities, capabilities_hashmap, state) { + if (accept_capabilities(a2dp_capabilities->buffer, a2dp_capabilities->size, for_encoding)) + return key; + } + + return NULL; +} + +static uint8_t fill_capabilities(uint8_t capabilities_buffer[254]) { + a2dp_aptx_t *capabilities = (a2dp_aptx_t *) capabilities_buffer; + + pa_zero(*capabilities); + + capabilities->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_VENDOR_ID, APTX_CODEC_ID); + 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 validate_configuration(const uint8_t *config_buffer, uint8_t config_size) { + const a2dp_aptx_t *config = (const a2dp_aptx_t *) config_buffer; + + if (config_size != sizeof(*config)) { + pa_log_error("Invalid size of config buffer"); + return false; + } + + if (A2DP_GET_VENDOR_ID(config->info) != APTX_VENDOR_ID || A2DP_GET_CODEC_ID(config->info) != APTX_CODEC_ID) { + 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 uint8_t fill_preferred_configuration(const pa_sample_spec *sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[254]) { + a2dp_aptx_t *config = (a2dp_aptx_t *) config_buffer; + const a2dp_aptx_t *capabilities = (const 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; + } + + pa_zero(*config); + + if (A2DP_GET_VENDOR_ID(capabilities->info) != APTX_VENDOR_ID || A2DP_GET_CODEC_ID(capabilities->info) != APTX_CODEC_ID) { + pa_log_error("No supported vendor codec information"); + return 0; + } + + config->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_VENDOR_ID, APTX_CODEC_ID); + + 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 *init_codec(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec) { + const a2dp_aptx_t *config = (const a2dp_aptx_t *) config_buffer; + + pa_assert(config_size == sizeof(*config)); + pa_assert(!for_backchannel); + + sample_spec->format = PA_SAMPLE_S24LE; + + 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(); + } + + PA_ONCE_BEGIN { +#if OPENAPTX_MAJOR == 0 && OPENAPTX_MINOR == 0 && OPENAPTX_PATCH == 0 + /* Version 0.0.0 does not export version global variables */ + const int aptx_major = OPENAPTX_MAJOR; + const int aptx_minor = OPENAPTX_MINOR; + const int aptx_patch = OPENAPTX_PATCH; +#endif + pa_log_debug("Using aptX codec implementation: libopenaptx %d.%d.%d from https://github.com/pali/libopenaptx", aptx_major, aptx_minor, aptx_patch); + } PA_ONCE_END; + + return (void *)aptx_init(0); +} + +static void finish_codec(void *codec_info) { + struct aptx_context *aptx_c = (struct aptx_context *) codec_info; + + aptx_finish(aptx_c); +} + +static void reset_codec(void *codec_info) { + struct aptx_context *aptx_c = (struct aptx_context *) codec_info; + + aptx_reset(aptx_c); +} + +static void fill_blocksize(void *codec_info, size_t read_link_mtu, size_t write_link_mtu, size_t *read_block_size, size_t *write_block_size) { + /* Input sequence is four s24 stereo samples. At one time we need to process multiple + * of eight of them due to synchronization of aptX codec. aptX compression ratio is 6:1. + * Therefore 8*4*6 bytes are on input and 8*4 bytes are on output. */ + *write_block_size = (write_link_mtu/(8*4)) * 8*4*6; + *read_block_size = (read_link_mtu/(8*4)) * 8*4*6; +} + +static bool reduce_encoder_bitrate(void *codec_info, size_t write_link_mtu, size_t *write_block_size) { + return false; +} + +static size_t encode_buffer(void *codec_info, uint32_t timestamp, 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 *) codec_info; + 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"); + *processed = p - input_buffer; + 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; + } + + *processed = p - input_buffer; + return d - output_buffer; +} + +static size_t decode_buffer(void *codec_info, 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 *) codec_info; + + 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 pa_a2dp_codec_aptx = { + .codec_name = "aptx", + .codec_description = "aptX", + .codec_id = { A2DP_CODEC_VENDOR, APTX_VENDOR_ID, APTX_CODEC_ID }, + .support_backchannel = false, + .accept_capabilities = accept_capabilities, + .choose_capabilities = choose_capabilities, + .fill_capabilities = fill_capabilities, + .validate_configuration = validate_configuration, + .fill_preferred_configuration = fill_preferred_configuration, + .init_codec = init_codec, + .finish_codec = finish_codec, + .reset_codec = reset_codec, + .fill_blocksize = fill_blocksize, + .reduce_encoder_bitrate = reduce_encoder_bitrate, + .encode_buffer = encode_buffer, + .decode_buffer = decode_buffer, +}; diff --git a/src/modules/bluetooth/a2dp-codec-util.c b/src/modules/bluetooth/a2dp-codec-util.c index 40e84783c..1f685f2d3 100644 --- a/src/modules/bluetooth/a2dp-codec-util.c +++ b/src/modules/bluetooth/a2dp-codec-util.c @@ -27,11 +27,17 @@ #include "a2dp-codec-util.h" extern const pa_a2dp_codec pa_a2dp_codec_sbc; +#ifdef HAVE_OPENAPTX +extern const pa_a2dp_codec pa_a2dp_codec_aptx; +#endif /* This is list of supported codecs. Their order is important. * Codec with higher index has higher priority. */ const pa_a2dp_codec *pa_a2dp_codecs[] = { &pa_a2dp_codec_sbc, +#ifdef HAVE_OPENAPTX + &pa_a2dp_codec_aptx, +#endif }; unsigned int pa_bluetooth_a2dp_codec_count(void) { -- 2.11.0 _______________________________________________ pulseaudio-discuss mailing list pulseaudio-discuss@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/pulseaudio-discuss