[v5,6/7] bluetooth: Add A2DP aptX and aptX HD codecs support

Submitted by Pali Rohár on Jan. 19, 2019, 5:11 p.m.

Details

Message ID 20190119171156.15810-7-pali.rohar@gmail.com
State Superseded
Headers show
Series "New API for Bluetooth A2DP codecs" ( rev: 19 18 17 16 15 14 13 ) in PulseAudio

Not browsing as part of any series.

Commit Message

Pali Rohár Jan. 19, 2019, 5:11 p.m.
This patch provides support for aptX and aptX HD codecs in bluetooth A2DP
profile. It uses open source LGPLv2.1+ licensed libopenaptx library which
can be found at https://github.com/pali/libopenaptx.

aptX for s24 stereo samples provides fixed 6:1 compression ratio and
bitrate 352.8 kbit/s, aptX HD provides fixed 4:1 compression ratio and
bitrate 529.2 kbit/s.

According to soundexpert research, aptX codec used in bluetooth A2DP is no
better than SBC High Quality settings. And you cannot hear difference
between aptX and SBC High Quality, aptX is just a copper-less overpriced
audio cable.

aptX HD is high-bitrate version of aptX. It has clearly noticeable increase
in sound quality (not dramatic though taking into account the increase in
bitrate).

http://soundexpert.org/news/-/blogs/audio-quality-of-bluetooth-aptx
---
 configure.ac                            |  36 +++
 src/Makefile.am                         |   6 +
 src/modules/bluetooth/a2dp-codec-aptx.c | 547 ++++++++++++++++++++++++++++++++
 src/modules/bluetooth/a2dp-codec-util.c |   8 +
 4 files changed, 597 insertions(+)
 create mode 100644 src/modules/bluetooth/a2dp-codec-aptx.c

Patch hide | download patch | download mbox

diff --git a/configure.ac b/configure.ac
index c45254e80..cdcf59df4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1106,6 +1106,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 and aptX HD codecs 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],
@@ -1591,6 +1625,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)
@@ -1649,6 +1684,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+aptXHD codecs: ${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 5ef870e32..a303578bb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2154,6 +2154,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..3452d9153
--- /dev/null
+++ b/src/modules/bluetooth/a2dp-codec-aptx.c
@@ -0,0 +1,547 @@ 
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2018-2019 Pali Rohár <pali.rohar@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 "a2dp-codec-api.h"
+
+static bool aptx_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 bool aptx_hd_accept_capabilities(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding) {
+    const a2dp_aptx_hd_t *capabilities = (const a2dp_aptx_hd_t *) capabilities_buffer;
+
+    if (capabilities_size != sizeof(*capabilities))
+        return false;
+
+    if (A2DP_GET_VENDOR_ID(capabilities->info) != APTX_HD_VENDOR_ID || A2DP_GET_CODEC_ID(capabilities->info) != APTX_HD_CODEC_ID)
+        return false;
+
+    if (!(capabilities->frequency & (APTX_HD_SAMPLING_FREQ_16000 | APTX_HD_SAMPLING_FREQ_32000 |
+                                     APTX_HD_SAMPLING_FREQ_44100 | APTX_HD_SAMPLING_FREQ_48000)))
+        return false;
+
+    if (!(capabilities->channel_mode & APTX_HD_CHANNEL_MODE_STEREO))
+        return false;
+
+    return true;
+}
+
+static const char *aptx_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 (aptx_accept_capabilities(a2dp_capabilities->buffer, a2dp_capabilities->size, for_encoding))
+            return key;
+    }
+
+    return NULL;
+}
+
+static const char *aptx_hd_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 (aptx_hd_accept_capabilities(a2dp_capabilities->buffer, a2dp_capabilities->size, for_encoding))
+            return key;
+    }
+
+    return NULL;
+}
+
+static uint8_t aptx_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 uint8_t aptx_hd_fill_capabilities(uint8_t capabilities_buffer[254]) {
+    a2dp_aptx_hd_t *capabilities = (a2dp_aptx_hd_t *) capabilities_buffer;
+
+    pa_zero(*capabilities);
+
+    capabilities->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID);
+    capabilities->channel_mode = APTX_HD_CHANNEL_MODE_STEREO;
+    capabilities->frequency = APTX_HD_SAMPLING_FREQ_16000 | APTX_HD_SAMPLING_FREQ_32000 |
+                              APTX_HD_SAMPLING_FREQ_44100 | APTX_HD_SAMPLING_FREQ_48000;
+
+    return sizeof(*capabilities);
+}
+
+static bool aptx_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 bool aptx_hd_validate_configuration(const uint8_t *config_buffer, uint8_t config_size) {
+    const a2dp_aptx_hd_t *config = (const a2dp_aptx_hd_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_HD_VENDOR_ID || A2DP_GET_CODEC_ID(config->info) != APTX_HD_CODEC_ID) {
+        pa_log_error("Invalid vendor codec information in configuration");
+        return false;
+    }
+
+    if (config->frequency != APTX_HD_SAMPLING_FREQ_16000 && config->frequency != APTX_HD_SAMPLING_FREQ_32000 &&
+        config->frequency != APTX_HD_SAMPLING_FREQ_44100 && config->frequency != APTX_HD_SAMPLING_FREQ_48000) {
+        pa_log_error("Invalid sampling frequency in configuration");
+        return false;
+    }
+
+    if (config->channel_mode != APTX_HD_CHANNEL_MODE_STEREO) {
+        pa_log_error("Invalid channel mode in configuration");
+        return false;
+    }
+
+    return true;
+}
+
+static uint8_t aptx_fill_preferred_configuration(const pa_sample_spec *default_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 (!(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 >= default_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 uint8_t aptx_hd_fill_preferred_configuration(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[254]) {
+    a2dp_aptx_hd_t *config = (a2dp_aptx_hd_t *) config_buffer;
+    const a2dp_aptx_hd_t *capabilities = (const a2dp_aptx_hd_t *) capabilities_buffer;
+    int i;
+
+    static const struct {
+        uint32_t rate;
+        uint8_t cap;
+    } freq_table[] = {
+        { 16000U, APTX_HD_SAMPLING_FREQ_16000 },
+        { 32000U, APTX_HD_SAMPLING_FREQ_32000 },
+        { 44100U, APTX_HD_SAMPLING_FREQ_44100 },
+        { 48000U, APTX_HD_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_HD_VENDOR_ID || A2DP_GET_CODEC_ID(capabilities->info) != APTX_HD_CODEC_ID) {
+        pa_log_error("No supported vendor codec information");
+        return 0;
+    }
+
+    config->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID);
+
+    if (!(capabilities->channel_mode & APTX_HD_CHANNEL_MODE_STEREO)) {
+        pa_log_error("No supported channel modes");
+        return 0;
+    }
+
+    config->channel_mode = APTX_HD_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 >= default_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 *aptx_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;
+    struct aptx_context *aptx_c;
+
+    pa_assert(config_size == sizeof(*config));
+    pa_assert(!for_backchannel);
+
+    aptx_c = aptx_init(0);
+    if (!aptx_c) {
+        pa_log_error("libopenaptx initialization failed");
+        return NULL;
+    }
+
+    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
+        /* libopenaptx 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 aptx_c;
+}
+
+static void *aptx_hd_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;
+    struct aptx_context *aptx_c;
+
+    pa_assert(config_size == sizeof(*config));
+    pa_assert(!for_backchannel);
+
+    aptx_c = aptx_init(1);
+    if (!aptx_c) {
+        pa_log_error("libopenaptx initialization failed");
+        return NULL;
+    }
+
+    sample_spec->format = PA_SAMPLE_S24LE;
+
+    switch (config->frequency) {
+        case APTX_HD_SAMPLING_FREQ_16000:
+            sample_spec->rate = 16000U;
+            break;
+        case APTX_HD_SAMPLING_FREQ_32000:
+            sample_spec->rate = 32000U;
+            break;
+        case APTX_HD_SAMPLING_FREQ_44100:
+            sample_spec->rate = 44100U;
+            break;
+        case APTX_HD_SAMPLING_FREQ_48000:
+            sample_spec->rate = 48000U;
+            break;
+        default:
+            pa_assert_not_reached();
+    }
+
+    switch (config->channel_mode) {
+        case APTX_HD_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
+        /* libopenaptx 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 HD codec implementation: libopenaptx %d.%d.%d from https://github.com/pali/libopenaptx", aptx_major, aptx_minor, aptx_patch);
+    } PA_ONCE_END;
+
+    return aptx_c;
+}
+
+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 size_t aptx_get_block_size(void *codec_info, size_t link_mtu) {
+    /* aptX compression ratio is 6:1 and we need to process 4 aptX bytes at once */
+    return (link_mtu/4) * 4*6;
+}
+
+static size_t aptx_hd_get_block_size(void *codec_info, size_t link_mtu) {
+    /* aptX HD compression ratio is 4:1 and we need to process 4 aptX HD bytes at once */
+    return (link_mtu/4) * 4*4;
+}
+
+static size_t reduce_encoder_bitrate(void *codec_info, size_t write_link_mtu) {
+    return 0;
+}
+
+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 = aptx_accept_capabilities,
+    .choose_capabilities = aptx_choose_capabilities,
+    .fill_capabilities = aptx_fill_capabilities,
+    .validate_configuration = aptx_validate_configuration,
+    .fill_preferred_configuration = aptx_fill_preferred_configuration,
+    .init_codec = aptx_init_codec,
+    .finish_codec = finish_codec,
+    .reset_codec = reset_codec,
+    .get_read_block_size = aptx_get_block_size,
+    .get_write_block_size = aptx_get_block_size,
+    .reduce_encoder_bitrate = reduce_encoder_bitrate,
+    .encode_buffer = encode_buffer,
+    .decode_buffer = decode_buffer,
+};
+
+const pa_a2dp_codec pa_a2dp_codec_aptx_hd = {
+    .codec_name = "aptx_hd",
+    .codec_description = "aptX HD",
+    .codec_id = { A2DP_CODEC_VENDOR, APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID },
+    .support_backchannel = false,
+    .accept_capabilities = aptx_hd_accept_capabilities,
+    .choose_capabilities = aptx_hd_choose_capabilities,
+    .fill_capabilities = aptx_hd_fill_capabilities,
+    .validate_configuration = aptx_hd_validate_configuration,
+    .fill_preferred_configuration = aptx_hd_fill_preferred_configuration,
+    .init_codec = aptx_hd_init_codec,
+    .finish_codec = finish_codec,
+    .reset_codec = reset_codec,
+    .get_read_block_size = aptx_hd_get_block_size,
+    .get_write_block_size = aptx_hd_get_block_size,
+    .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 27128d8ae..c9c117bb6 100644
--- a/src/modules/bluetooth/a2dp-codec-util.c
+++ b/src/modules/bluetooth/a2dp-codec-util.c
@@ -27,11 +27,19 @@ 
 #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;
+extern const pa_a2dp_codec pa_a2dp_codec_aptx_hd;
+#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,
+    &pa_a2dp_codec_aptx_hd,
+#endif
 };
 
 unsigned int pa_bluetooth_a2dp_codec_count(void) {