[v3,3/4] Add A2DP aptX, aptX HD codecs support

Submitted by Huang-Huang Bao on Dec. 29, 2018, 3:48 p.m.

Details

Message ID 20181229154808.17235-4-eh5@sokka.cn
State New
Series "bluetooth: Adds A2DP AAC, aptX(HD), LDAC codecs"
Headers show

Commit Message

Huang-Huang Bao Dec. 29, 2018, 3:48 p.m.
Optional (build with --disable-bluez5-aptx-codec)
---
 configure.ac                           |  18 +-
 src/Makefile.am                        |   8 +
 src/modules/bluetooth/a2dp/a2dp-api.h  |  12 +
 src/modules/bluetooth/a2dp/a2dp_aptx.c | 617 +++++++++++++++++++++++++
 src/modules/bluetooth/a2dp/a2dp_util.c |  62 +++
 5 files changed, 716 insertions(+), 1 deletion(-)
 create mode 100644 src/modules/bluetooth/a2dp/a2dp_aptx.c

Patch hide | download patch | download mbox

diff --git a/configure.ac b/configure.ac
index 0f9d7fb6c..586077e5b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1061,7 +1061,7 @@  PA_MACHINE_ID_FALLBACK="${localstatedir}/lib/dbus/machine-id"
 AX_DEFINE_DIR(PA_MACHINE_ID_FALLBACK, PA_MACHINE_ID_FALLBACK,
 	      [Fallback machine-id file])
 
-#### BlueZ support (optional, dependent on D-Bus and SBC and FDK-AAC) ####
+#### BlueZ support (optional, dependent on D-Bus, SBC, FDK-AAC and FFmpeg) ####
 
 AC_ARG_ENABLE([bluez5],
     AS_HELP_STRING([--disable-bluez5],[Disable optional BlueZ 5 support]))
@@ -1093,6 +1093,20 @@  AC_SUBST(HAVE_FDK_AAC)
 AM_CONDITIONAL([HAVE_FDK_AAC], [test "x$HAVE_FDK_AAC" = x1])
 AS_IF([test "x$HAVE_FDK_AAC" = "x1"], AC_DEFINE([HAVE_FDK_AAC], 1, [Bluez 5 A2DP AAC codec enabled]))
 
+## aptX, aptX HD support (FFmpeg libavcodec) ##
+AC_ARG_ENABLE([bluez5-aptx-codec],
+    AS_HELP_STRING([--disable-bluez5-aptx-codec],[Disable optional A2DP aptX, aptX HD codecs support (Bluez 5)]))
+AS_IF([test "x$HAVE_BLUEZ_5" = "x1" && test "x$enable_bluez5_aptx_codec" != "xno"],
+    [PKG_CHECK_MODULES(FF_AVCODEC, [ libavcodec >= 58.18.100 ], HAVE_FF_AVCODEC=1, HAVE_FF_AVCODEC=0)],
+    HAVE_FF_AVCODEC=0)
+AS_IF([test "x$HAVE_BLUEZ_5" = "x1" && test "x$enable_bluez5_aptx_codec" != "xno"],
+    [PKG_CHECK_MODULES(FF_AVUTIL, [ libavcodec >= 56.14.100 ], HAVE_FF_AVUTIL=1, HAVE_FF_AVUTIL=0)],
+    HAVE_FF_AVUTIL=0)
+AS_IF([test "x$HAVE_FF_AVCODEC" = "x1" && test "x$HAVE_FF_AVUTIL" = "x1"], HAVE_FF_APTX=1, HAVE_FF_APTX=0)
+AC_SUBST(HAVE_FF_APTX)
+AM_CONDITIONAL([HAVE_FF_APTX], [test "x$HAVE_FF_APTX" = x1])
+AS_IF([test "x$HAVE_FF_APTX" = "x1"], AC_DEFINE([HAVE_FF_APTX], 1, [Bluez 5 A2DP aptX, aptX HD codecs enabled]))
+
 ## Bluetooth Headset profiles backend ##
 
 AC_ARG_ENABLE([bluez5-ofono-headset],
@@ -1598,6 +1612,7 @@  AS_IF([test "x$HAVE_SYSTEMD_LOGIN" = "x1"], ENABLE_SYSTEMD_LOGIN=yes, ENABLE_SYS
 AS_IF([test "x$HAVE_SYSTEMD_JOURNAL" = "x1"], ENABLE_SYSTEMD_JOURNAL=yes, ENABLE_SYSTEMD_JOURNAL=no)
 AS_IF([test "x$HAVE_BLUEZ_5" = "x1"], ENABLE_BLUEZ_5=yes, ENABLE_BLUEZ_5=no)
 AS_IF([test "x$HAVE_FDK_AAC" = "x1"], ENABLE_BLUEZ_5_AAC_CODEC=yes, ENABLE_BLUEZ_5_AAC_CODEC=no)
+AS_IF([test "x$HAVE_FF_APTX" = "x1"], ENABLE_BLUEZ_5_APTX_CODEC=yes, ENABLE_BLUEZ_5_APTX_CODEC=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_HAL_COMPAT" = "x1"], ENABLE_HAL_COMPAT=yes, ENABLE_HAL_COMPAT=no)
@@ -1657,6 +1672,7 @@  echo "
     Enable D-Bus:                  ${ENABLE_DBUS}
       Enable BlueZ 5:              ${ENABLE_BLUEZ_5}
         Enable A2DP AAC codec:     ${ENABLE_BLUEZ_5_AAC_CODEC}
+        Enable A2DP aptX(HD):      ${ENABLE_BLUEZ_5_APTX_CODEC}
         Enable ofono headsets:     ${ENABLE_BLUEZ_5_OFONO_HEADSET}
         Enable native headsets:    ${ENABLE_BLUEZ_5_NATIVE_HEADSET}
     Enable udev:                   ${ENABLE_UDEV}
diff --git a/src/Makefile.am b/src/Makefile.am
index c44a65f05..5bb43c3a6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2132,6 +2132,10 @@  if HAVE_FDK_AAC
 libbluez5_util_la_SOURCES += \
 		modules/bluetooth/a2dp/a2dp_aac.c
 endif
+if HAVE_FF_APTX
+libbluez5_util_la_SOURCES += \
+		modules/bluetooth/a2dp/a2dp_aptx.c
+endif
 if HAVE_BLUEZ_5_OFONO_HEADSET
 libbluez5_util_la_SOURCES += \
 		modules/bluetooth/backend-ofono.c
@@ -2148,6 +2152,10 @@  if HAVE_FDK_AAC
 libbluez5_util_la_LIBADD += $(FDK_AAC_LIBS)
 libbluez5_util_la_CFLAGS += $(FDK_AAC_CFLAGS)
 endif
+if HAVE_FF_APTX
+libbluez5_util_la_LIBADD += $(FF_AVCODEC_LIBS) $(FF_AVUTIL_LIBS)
+libbluez5_util_la_CFLAGS += $(FF_AVCODEC_CFLAGS) $(FF_AVUTIL_CFLAGS)
+endif
 
 module_bluez5_discover_la_SOURCES = modules/bluetooth/module-bluez5-discover.c
 module_bluez5_discover_la_LDFLAGS = $(MODULE_LDFLAGS)
diff --git a/src/modules/bluetooth/a2dp/a2dp-api.h b/src/modules/bluetooth/a2dp/a2dp-api.h
index 1e601a9c5..59ec21a8b 100644
--- a/src/modules/bluetooth/a2dp/a2dp-api.h
+++ b/src/modules/bluetooth/a2dp/a2dp-api.h
@@ -40,6 +40,10 @@  extern const pa_a2dp_codec_t pa_a2dp_sbc;
 #ifdef HAVE_FDK_AAC
 extern const pa_a2dp_codec_t pa_a2dp_aac;
 #endif
+#ifdef HAVE_FF_APTX
+extern const pa_a2dp_codec_t pa_a2dp_aptx;
+extern const pa_a2dp_codec_t pa_a2dp_aptx_hd;
+#endif
 
 /* Run from <pa_a2dp_sink_t>.encode */
 
@@ -54,12 +58,20 @@  typedef enum pa_a2dp_codec_index {
     PA_A2DP_SINK_SBC,
 #ifdef HAVE_FDK_AAC
     PA_A2DP_SINK_AAC,
+#endif
+#ifdef HAVE_FF_APTX
+    PA_A2DP_SINK_APTX,
+    PA_A2DP_SINK_APTX_HD,
 #endif
     PA_A2DP_SINK_MAX,
     PA_A2DP_SOURCE_MIN = PA_A2DP_SINK_MAX,
     PA_A2DP_SOURCE_SBC,
 #ifdef HAVE_FDK_AAC
     PA_A2DP_SOURCE_AAC,
+#endif
+#ifdef HAVE_FF_APTX
+    PA_A2DP_SOURCE_APTX,
+    PA_A2DP_SOURCE_APTX_HD,
 #endif
     PA_A2DP_SOURCE_MAX,
     PA_A2DP_CODEC_INDEX_UNAVAILABLE
diff --git a/src/modules/bluetooth/a2dp/a2dp_aptx.c b/src/modules/bluetooth/a2dp/a2dp_aptx.c
new file mode 100644
index 000000000..732693de7
--- /dev/null
+++ b/src/modules/bluetooth/a2dp/a2dp_aptx.c
@@ -0,0 +1,617 @@ 
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2018 Huang-Huang Bao
+
+  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/>.
+***/
+
+#include <arpa/inet.h>
+#include <string.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/samplefmt.h>
+
+#include <pulse/xmalloc.h>
+
+#include "a2dp-api.h"
+
+
+#define streq(a, b) (!strcmp((a),(b)))
+
+
+typedef struct aptx_info {
+    pa_a2dp_source_read_cb_t read_pcm;
+    pa_a2dp_source_read_buf_free_cb_t read_buf_free;
+
+    bool is_a2dp_sink;
+    bool is_hd;
+
+    size_t aptx_frame_size;
+    int nb_samples;
+    const AVCodec *av_codec;
+    AVCodecContext *av_codec_ctx;
+    int channel_mode;
+    uint16_t seq_num;
+
+
+    size_t block_size;
+
+
+} aptx_info_t;
+
+static const AVCodec *av_codec_aptx_decoder = NULL;
+static const AVCodec *av_codec_aptx_encoder = NULL;
+
+static const AVCodec *av_codec_aptx_hd_decoder = NULL;
+static const AVCodec *av_codec_aptx_hd_encoder = NULL;
+
+
+static bool pa_aptx_decoder_load() {
+    if (av_codec_aptx_decoder)
+        return true;
+    av_codec_aptx_decoder = avcodec_find_decoder(AV_CODEC_ID_APTX);
+    if (!av_codec_aptx_decoder) {
+        pa_log_debug("Cannot find aptX decoder in FFmpeg avcodec library");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pa_aptx_encoder_load() {
+    if (av_codec_aptx_encoder)
+        return true;
+    av_codec_aptx_encoder = avcodec_find_encoder(AV_CODEC_ID_APTX);
+    if (!av_codec_aptx_encoder) {
+        pa_log_debug("Cannot find aptX encoder in FFmpeg avcodec library");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pa_aptx_hd_decoder_load() {
+    if (av_codec_aptx_hd_decoder)
+        return true;
+    av_codec_aptx_hd_decoder = avcodec_find_decoder(AV_CODEC_ID_APTX_HD);
+    if (!av_codec_aptx_hd_decoder) {
+        pa_log_debug("Cannot find aptX HD decoder in FFmpeg avcodec library");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pa_aptx_hd_encoder_load() {
+    if (av_codec_aptx_hd_encoder)
+        return true;
+    av_codec_aptx_hd_encoder = avcodec_find_encoder(AV_CODEC_ID_APTX_HD);
+    if (!av_codec_aptx_hd_encoder) {
+        pa_log_debug("Cannot find aptX HD encoder in FFmpeg avcodec library");
+        return false;
+    }
+
+    return true;
+}
+
+static bool _internal_pa_dual_decoder_init(bool is_hd, void **codec_data) {
+    aptx_info_t *info = pa_xmalloc0(sizeof(aptx_info_t));
+    *codec_data = info;
+    info->is_hd = is_hd;
+    info->is_a2dp_sink = true;
+    info->aptx_frame_size = is_hd ? 6 : 4;
+    info->av_codec = is_hd ? av_codec_aptx_hd_decoder : av_codec_aptx_decoder;
+    return true;
+}
+
+static bool
+_internal_pa_dual_encoder_init(bool is_hd, pa_a2dp_source_read_cb_t read_cb, pa_a2dp_source_read_buf_free_cb_t free_cb,
+                               void **codec_data) {
+    aptx_info_t *info = pa_xmalloc0(sizeof(aptx_info_t));
+    *codec_data = info;
+    info->is_hd = is_hd;
+    info->is_a2dp_sink = false;
+    info->read_pcm = read_cb;
+    info->read_buf_free = free_cb;
+    info->aptx_frame_size = is_hd ? 6 : 4;
+    info->av_codec = is_hd ? av_codec_aptx_hd_encoder : av_codec_aptx_encoder;
+    return true;
+}
+
+static int pa_dual_update_user_config(pa_proplist *user_config, void **codec_data) {
+    return 0;
+}
+
+static size_t
+pa_dual_decode(const void *read_buf, size_t read_buf_size, void *write_buf, size_t write_buf_size, size_t *_decoded,
+               uint32_t *timestamp, void **codec_data) {
+    const struct rtp_header *header;
+    void *p;
+    int ret;
+    size_t i;
+    AVPacket *pkt;
+    size_t to_decode;
+    size_t total_written = 0;
+    AVFrame *av_frame = NULL;
+    aptx_info_t *aptx_info = *codec_data;
+
+
+    pa_assert(aptx_info);
+    pa_assert(aptx_info->av_codec);
+    pa_assert(aptx_info->av_codec_ctx);
+
+    if (aptx_info->is_hd) {
+        header = read_buf;
+
+        *timestamp = ntohl(header->timestamp);
+
+        p = (uint8_t *) read_buf + sizeof(*header);
+        to_decode = read_buf_size - sizeof(*header);
+    } else {
+        *timestamp = (uint32_t) - 1;
+
+        p = (uint8_t *) read_buf;
+        to_decode = read_buf_size;
+    }
+
+    av_frame = av_frame_alloc();
+    pkt = av_packet_alloc();
+    pkt->data = p;
+    pkt->size = (int) to_decode;
+
+
+    *_decoded = 0;
+
+    ret = avcodec_send_packet(aptx_info->av_codec_ctx, pkt);
+    if (PA_UNLIKELY(ret < 0)) {
+        pa_log_debug("Error submitting the packet to the aptX decoder");
+        goto done;
+    }
+    ret = avcodec_receive_frame(aptx_info->av_codec_ctx, av_frame);
+    if (PA_UNLIKELY(ret < 0)) {
+        pa_log_debug("Error during aptX decoding");
+        goto done;
+    }
+
+    *_decoded = aptx_info->aptx_frame_size * av_frame->nb_samples / 4;
+
+    total_written = (size_t) av_frame->nb_samples * (4 * 2);
+
+    pa_assert_fp(_decoded <= read_buf_size);
+    pa_assert_fp(total_written <= write_buf_size);
+
+    for (i = 0; i < av_frame->nb_samples * sizeof(uint32_t); i += sizeof(uint32_t)) {
+        memcpy((uint8_t *) write_buf + i * 2, av_frame->data[0] + i, sizeof(uint32_t));
+        memcpy((uint8_t *) write_buf + i * 2 + sizeof(uint32_t), av_frame->data[1] + i, sizeof(uint32_t));
+    }
+
+done:
+    av_frame_free(&av_frame);
+    av_packet_free(&pkt);
+    return total_written;
+}
+
+static size_t
+pa_dual_encode(uint32_t timestamp, void *write_buf, size_t write_buf_size, size_t *_encoded, void *read_cb_data,
+               void **codec_data) {
+    struct rtp_header *header;
+    size_t nbytes;
+    void *d;
+    const void *p;
+    AVFrame *av_frame;
+    AVPacket *pkt;
+    aptx_info_t *aptx_info = *codec_data;
+    int ret;
+    size_t i;
+
+    pa_assert(aptx_info);
+    pa_assert(aptx_info->av_codec);
+    pa_assert(aptx_info->av_codec_ctx);
+
+    pa_assert_fp(aptx_info->av_codec_ctx->frame_size <= write_buf_size);
+
+    aptx_info->read_pcm(&p, (size_t) aptx_info->block_size, read_cb_data);
+
+    if (aptx_info->is_hd) {
+        header = write_buf;
+        memset(write_buf, 0, sizeof(*header));
+        header->v = 2;
+        header->pt = 1;
+        header->sequence_number = htons(aptx_info->seq_num++);
+        header->timestamp = htonl(timestamp);
+        header->ssrc = htonl(1);
+        d = (uint8_t *) write_buf + sizeof(*header);
+    } else {
+        d = (uint8_t *) write_buf;
+    }
+
+    av_frame = av_frame_alloc();
+    av_frame->nb_samples = aptx_info->nb_samples;
+    av_frame->format = aptx_info->av_codec_ctx->sample_fmt;
+    av_frame->channel_layout = aptx_info->av_codec_ctx->channel_layout;
+
+    pkt = av_packet_alloc();
+
+    pa_assert_se(av_frame_get_buffer(av_frame, 0) >= 0);
+    pa_assert_se(av_frame_make_writable(av_frame) >= 0);
+
+
+    for (i = 0; i < av_frame->nb_samples * sizeof(uint32_t); i += sizeof(uint32_t)) {
+        memcpy(av_frame->data[0] + i, (uint8_t *) p + i * 2, sizeof(uint32_t));
+        memcpy(av_frame->data[1] + i, (uint8_t *) p + i * 2 + sizeof(uint32_t), sizeof(uint32_t));
+    }
+    *_encoded = 0;
+
+    ret = avcodec_send_frame(aptx_info->av_codec_ctx, av_frame);
+
+    if (PA_UNLIKELY(ret < 0)) {
+        fprintf(stderr, "Error sending the frame to the aptX encoder\n");
+        nbytes = 0;
+        goto done;
+    }
+
+    ret = avcodec_receive_packet(aptx_info->av_codec_ctx, pkt);
+
+    if (PA_UNLIKELY(ret != 0)) {
+        fprintf(stderr, "Error receiving the packet from the aptX encoder\n");
+        nbytes = 0;
+        goto done;
+    }
+
+    memcpy(d, pkt->data, (size_t) pkt->size);
+    d = (uint8_t *) d + pkt->size;
+
+    nbytes = (uint8_t *) d - (uint8_t *) write_buf;
+    *_encoded += aptx_info->block_size;
+
+done:
+    av_frame_free(&av_frame);
+    av_packet_free(&pkt);
+    aptx_info->read_buf_free(&p, read_cb_data);
+    return nbytes;
+}
+
+static void
+pa_dual_config_transport(pa_sample_spec default_sample_spec, const void *configuration, size_t configuration_size,
+                         pa_sample_spec *sample_spec, void **codec_data) {
+    aptx_info_t *aptx_info = *codec_data;
+    a2dp_aptx_t *config = (a2dp_aptx_t *) configuration;
+    AVCodecContext *aptx_ctx;
+    pa_assert(aptx_info);
+    pa_assert(aptx_info->av_codec);
+    pa_assert_se(configuration_size == (aptx_info->is_hd ? sizeof(a2dp_aptx_hd_t) : sizeof(a2dp_aptx_t)));
+
+    if (aptx_info->av_codec_ctx)
+        avcodec_free_context(&aptx_info->av_codec_ctx);
+
+    aptx_info->av_codec_ctx = avcodec_alloc_context3(aptx_info->av_codec);
+    aptx_ctx = aptx_info->av_codec_ctx;
+
+    aptx_ctx->sample_fmt = AV_SAMPLE_FMT_S32P;
+    sample_spec->format = PA_SAMPLE_S32LE;
+
+    switch (config->frequency) {
+        case APTX_SAMPLING_FREQ_16000:
+            aptx_ctx->sample_rate = 16000;
+            aptx_ctx->bit_rate = 16000;
+            sample_spec->rate = 16000U;
+            break;
+        case APTX_SAMPLING_FREQ_32000:
+            aptx_ctx->sample_rate = 32000;
+            aptx_ctx->bit_rate = 32000;
+            sample_spec->rate = 32000U;
+            break;
+        case APTX_SAMPLING_FREQ_44100:
+            aptx_ctx->sample_rate = 44100;
+            aptx_ctx->bit_rate = 44100;
+            sample_spec->rate = 44100U;
+            break;
+        case APTX_SAMPLING_FREQ_48000:
+            aptx_ctx->sample_rate = 48000;
+            aptx_ctx->bit_rate = 48000;
+            sample_spec->rate = 48000U;
+            break;
+        default:
+            pa_assert_not_reached();
+    }
+
+    switch (config->channel_mode) {
+        case APTX_CHANNEL_MODE_STEREO:
+            aptx_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
+            aptx_ctx->channels = 2;
+            sample_spec->channels = 2;
+            break;
+        default:
+            pa_assert_not_reached();
+    }
+
+    pa_assert_se(avcodec_open2(aptx_info->av_codec_ctx, aptx_info->av_codec, NULL) == 0);
+
+};
+
+static void pa_dual_get_read_block_size(size_t read_link_mtu, size_t *read_block_size, void **codec_data) {
+    aptx_info_t *aptx_info = *codec_data;
+    size_t aptx_frame_size = aptx_info->aptx_frame_size;
+    size_t rtp_use_size = aptx_info->is_hd ? sizeof(struct rtp_header) : 0;
+    pa_assert(aptx_info);
+
+    /*
+     * PCM 32-bit, 2 channel (4 bytes * 2)
+     * PCM frames/APTX frames == 4
+     * */
+    *read_block_size = (read_link_mtu - rtp_use_size) / aptx_frame_size * (4 * 2) * 4;
+    aptx_info->block_size = *read_block_size;
+};
+
+static void pa_dual_get_write_block_size(size_t write_link_mtu, size_t *write_block_size, void **codec_data) {
+    aptx_info_t *aptx_info = *codec_data;
+    size_t aptx_frame_size = aptx_info->aptx_frame_size;
+    size_t rtp_use_size = aptx_info->is_hd ? sizeof(struct rtp_header) : 0;
+    pa_assert(aptx_info);
+
+    /*
+     * PCM 32-bit, 2 channel (4 bytes * 2)
+     * PCM frames/APTX frames == 4
+     * */
+    *write_block_size = (write_link_mtu - rtp_use_size) / aptx_frame_size * (4 * 2) * 4;
+    aptx_info->block_size = *write_block_size;
+};
+
+
+static void pa_dual_setup_stream(void **codec_data) {
+    aptx_info_t *aptx_info = *codec_data;
+    pa_assert(aptx_info);
+    aptx_info->nb_samples = (int) (aptx_info->block_size / (4 * 2));
+    aptx_info->av_codec_ctx->frame_size = (int) (aptx_info->aptx_frame_size * aptx_info->nb_samples / 4);
+};
+
+static void pa_dual_free(void **codec_data) {
+    aptx_info_t *aptx_info = *codec_data;
+    if (!aptx_info)
+        return;
+    if (aptx_info->av_codec_ctx)
+        avcodec_free_context(&aptx_info->av_codec_ctx);
+    pa_xfree(aptx_info);
+    *codec_data = NULL;
+
+};
+
+static size_t _internal_pa_dual_get_capabilities(bool is_hd, void **_capabilities) {
+
+    const size_t cap_size = is_hd ? sizeof(a2dp_aptx_hd_t) : sizeof(a2dp_aptx_t);
+    a2dp_aptx_t *capabilities = (a2dp_aptx_t *) pa_xmalloc0(cap_size);
+
+    if (is_hd) {
+        capabilities->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID);
+    } else {
+        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;
+    *_capabilities = capabilities;
+
+    return cap_size;
+};
+
+
+static size_t
+_internal_pa_dual_select_configuration(bool is_hd, const pa_sample_spec default_sample_spec,
+                                       const uint8_t *supported_capabilities,
+                                       const size_t capabilities_size, void **configuration) {
+    a2dp_aptx_t *cap;
+    a2dp_aptx_t *config;
+    const size_t cap_size = is_hd ? sizeof(a2dp_aptx_hd_t) : sizeof(a2dp_aptx_t);
+    pa_a2dp_freq_cap_t aptx_freq_cap, aptx_freq_table[] = {
+            {16000U, APTX_SAMPLING_FREQ_16000},
+            {32000U, APTX_SAMPLING_FREQ_32000},
+            {44100U, APTX_SAMPLING_FREQ_44100},
+            {48000U, APTX_SAMPLING_FREQ_48000}
+    };
+
+    cap = (a2dp_aptx_t *) supported_capabilities;
+
+    if (capabilities_size != cap_size)
+        return 0;
+
+    config = (a2dp_aptx_t *) pa_xmalloc0(cap_size);
+
+    if (is_hd) {
+        config->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID);
+    } else {
+        config->info = A2DP_SET_VENDOR_ID_CODEC_ID(APTX_VENDOR_ID, APTX_CODEC_ID);
+    }
+
+    if (!pa_a2dp_select_cap_frequency(cap->frequency, default_sample_spec, aptx_freq_table,
+                                      PA_ELEMENTSOF(aptx_freq_table), &aptx_freq_cap))
+        return 0;
+
+    config->frequency = (uint8_t) aptx_freq_cap.cap;
+
+    if (cap->channel_mode & APTX_CHANNEL_MODE_STEREO)
+        config->channel_mode = APTX_CHANNEL_MODE_STEREO;
+    else {
+        pa_log_error("No supported channel modes");
+        return 0;
+    }
+
+    *configuration = config;
+    return cap_size;
+};
+
+static void pa_dual_free_capabilities(void **capabilities) {
+    if (!capabilities || !*capabilities)
+        return;
+    pa_xfree(*capabilities);
+    *capabilities = NULL;
+}
+
+static bool _internal_pa_dual_validate_configuration(bool is_hd, const uint8_t *selected_configuration,
+                                                const size_t configuration_size) {
+    a2dp_aptx_t *c = (a2dp_aptx_t *) selected_configuration;
+
+    if (configuration_size != (is_hd ? sizeof(a2dp_aptx_hd_t) : sizeof(a2dp_aptx_t))) {
+        pa_log_error("APTX configuration array of invalid size");
+        return false;
+    }
+
+    switch (c->frequency) {
+        case APTX_SAMPLING_FREQ_16000:
+        case APTX_SAMPLING_FREQ_32000:
+        case APTX_SAMPLING_FREQ_44100:
+        case APTX_SAMPLING_FREQ_48000:
+            break;
+        default:
+            pa_log_error("Invalid sampling frequency in aptX configuration");
+            return false;
+    }
+
+    switch (c->channel_mode) {
+        case APTX_CHANNEL_MODE_STEREO:
+            break;
+        default:
+            pa_log_error("Invalid channel mode in aptX Configuration");
+            return false;
+    }
+    return true;
+};
+
+
+static bool pa_aptx_decoder_init(void **codec_data) {
+    return _internal_pa_dual_decoder_init(false, codec_data);
+}
+
+static bool pa_aptx_hd_decoder_init(void **codec_data) {
+    return _internal_pa_dual_decoder_init(true, codec_data);
+}
+
+static bool
+pa_aptx_encoder_init(pa_a2dp_source_read_cb_t read_cb, pa_a2dp_source_read_buf_free_cb_t free_cb, void **codec_data) {
+    return _internal_pa_dual_encoder_init(false, read_cb, free_cb, codec_data);
+}
+
+static bool
+pa_aptx_hd_encoder_init(pa_a2dp_source_read_cb_t read_cb, pa_a2dp_source_read_buf_free_cb_t free_cb,
+                        void **codec_data) {
+    return _internal_pa_dual_encoder_init(true, read_cb, free_cb, codec_data);
+}
+
+
+static size_t pa_aptx_get_capabilities(void **_capabilities) {
+    return _internal_pa_dual_get_capabilities(false, _capabilities);
+}
+
+static size_t pa_aptx_select_configuration(const pa_sample_spec default_sample_spec,
+                                           const uint8_t *supported_capabilities,
+                                           const size_t capabilities_size, void **configuration) {
+    return _internal_pa_dual_select_configuration(false, default_sample_spec, supported_capabilities, capabilities_size,
+                                                  configuration);
+}
+
+static bool pa_aptx_validate_configuration(const uint8_t *selected_configuration,
+                                      const size_t configuration_size) {
+    return _internal_pa_dual_validate_configuration(false, selected_configuration, configuration_size);
+}
+
+static size_t pa_aptx_hd_get_capabilities(void **_capabilities) {
+    return _internal_pa_dual_get_capabilities(true, _capabilities);
+}
+
+static size_t pa_aptx_hd_select_configuration(const pa_sample_spec default_sample_spec,
+                                              const uint8_t *supported_capabilities,
+                                              const size_t capabilities_size, void **configuration) {
+    return _internal_pa_dual_select_configuration(true, default_sample_spec, supported_capabilities, capabilities_size,
+                                                  configuration);
+}
+
+static bool pa_aptx_hd_validate_configuration(const uint8_t *selected_configuration,
+                                         const size_t configuration_size) {
+    return _internal_pa_dual_validate_configuration(true, selected_configuration, configuration_size);
+}
+
+static pa_a2dp_source_t pa_aptx_source = {
+        .encoder_load = pa_aptx_encoder_load,
+        .init = pa_aptx_encoder_init,
+        .update_user_config = pa_dual_update_user_config,
+        .encode = pa_dual_encode,
+        .config_transport = pa_dual_config_transport,
+        .get_block_size = pa_dual_get_write_block_size,
+        .setup_stream = pa_dual_setup_stream,
+        .set_tx_length = NULL,
+        .decrease_quality = NULL,
+        .free = pa_dual_free
+};
+
+static pa_a2dp_sink_t pa_aptx_sink = {
+        .decoder_load = pa_aptx_decoder_load,
+        .init = pa_aptx_decoder_init,
+        .update_user_config = pa_dual_update_user_config,
+        .config_transport = pa_dual_config_transport,
+        .get_block_size = pa_dual_get_read_block_size,
+        .setup_stream = pa_dual_setup_stream,
+        .decode = pa_dual_decode,
+        .free = pa_dual_free
+};
+
+const pa_a2dp_codec_t pa_a2dp_aptx = {
+        .name = "aptX",
+        .codec = A2DP_CODEC_VENDOR,
+        .vendor_codec = &A2DP_SET_VENDOR_ID_CODEC_ID(APTX_VENDOR_ID, APTX_CODEC_ID),
+        .a2dp_sink = &pa_aptx_sink,
+        .a2dp_source = &pa_aptx_source,
+        .get_capabilities = pa_aptx_get_capabilities,
+        .select_configuration = pa_aptx_select_configuration,
+        .free_capabilities = pa_dual_free_capabilities,
+        .free_configuration = pa_dual_free_capabilities,
+        .validate_configuration = pa_aptx_validate_configuration
+};
+
+static pa_a2dp_source_t pa_aptx_hd_source = {
+        .encoder_load = pa_aptx_hd_encoder_load,
+        .init = pa_aptx_hd_encoder_init,
+        .update_user_config = pa_dual_update_user_config,
+        .encode = pa_dual_encode,
+        .config_transport = pa_dual_config_transport,
+        .get_block_size = pa_dual_get_write_block_size,
+        .setup_stream = pa_dual_setup_stream,
+        .set_tx_length = NULL,
+        .decrease_quality = NULL,
+        .free = pa_dual_free
+};
+
+static pa_a2dp_sink_t pa_aptx_hd_sink = {
+        .decoder_load = pa_aptx_hd_decoder_load,
+        .init = pa_aptx_hd_decoder_init,
+        .update_user_config = pa_dual_update_user_config,
+        .config_transport = pa_dual_config_transport,
+        .get_block_size = pa_dual_get_read_block_size,
+        .setup_stream = pa_dual_setup_stream,
+        .decode = pa_dual_decode,
+        .free = pa_dual_free
+};
+
+const pa_a2dp_codec_t pa_a2dp_aptx_hd = {
+        .name = "aptX_HD",
+        .codec = A2DP_CODEC_VENDOR,
+        .vendor_codec = &A2DP_SET_VENDOR_ID_CODEC_ID(APTX_HD_VENDOR_ID, APTX_HD_CODEC_ID),
+        .a2dp_sink = &pa_aptx_hd_sink,
+        .a2dp_source = &pa_aptx_hd_source,
+        .get_capabilities = pa_aptx_hd_get_capabilities,
+        .select_configuration = pa_aptx_hd_select_configuration,
+        .free_capabilities = pa_dual_free_capabilities,
+        .free_configuration = pa_dual_free_capabilities,
+        .validate_configuration = pa_aptx_hd_validate_configuration
+};
diff --git a/src/modules/bluetooth/a2dp/a2dp_util.c b/src/modules/bluetooth/a2dp/a2dp_util.c
index edd28b5cc..8f4703518 100644
--- a/src/modules/bluetooth/a2dp/a2dp_util.c
+++ b/src/modules/bluetooth/a2dp/a2dp_util.c
@@ -36,6 +36,12 @@ 
 #define A2DP_VENDOR_SRC_ENDPOINT A2DP_SOURCE_ENDPOINT "/VENDOR"
 #define A2DP_VENDOR_SNK_ENDPOINT A2DP_SINK_ENDPOINT "/VENDOR"
 
+#define A2DP_APTX_SRC_ENDPOINT A2DP_VENDOR_SRC_ENDPOINT "/APTX"
+#define A2DP_APTX_SNK_ENDPOINT A2DP_VENDOR_SNK_ENDPOINT "/APTX"
+
+#define A2DP_APTX_HD_SRC_ENDPOINT A2DP_VENDOR_SRC_ENDPOINT "/APTXHD"
+#define A2DP_APTX_HD_SNK_ENDPOINT A2DP_VENDOR_SNK_ENDPOINT "/APTXHD"
+
 
 #define PA_A2DP_PRIORITY_DISABLE 0
 #define PA_A2DP_PRIORITY_MIN 1
@@ -219,6 +225,20 @@  void pa_a2dp_codec_index_to_endpoint(pa_a2dp_codec_index_t codec_index, const ch
         case PA_A2DP_SOURCE_AAC:
             *endpoint = A2DP_AAC_SRC_ENDPOINT;
             break;
+#endif
+#ifdef HAVE_FF_APTX
+        case PA_A2DP_SINK_APTX:
+            *endpoint = A2DP_APTX_SNK_ENDPOINT;
+            break;
+        case PA_A2DP_SOURCE_APTX:
+            *endpoint = A2DP_APTX_SRC_ENDPOINT;
+            break;
+        case PA_A2DP_SINK_APTX_HD:
+            *endpoint = A2DP_APTX_HD_SNK_ENDPOINT;
+            break;
+        case PA_A2DP_SOURCE_APTX_HD:
+            *endpoint = A2DP_APTX_HD_SRC_ENDPOINT;
+            break;
 #endif
         default:
             *endpoint = NULL;
@@ -235,6 +255,16 @@  void pa_a2dp_endpoint_to_codec_index(const char *endpoint, pa_a2dp_codec_index_t
         *codec_index = PA_A2DP_SINK_AAC;
     else if (streq(endpoint, A2DP_AAC_SRC_ENDPOINT))
         *codec_index = PA_A2DP_SOURCE_AAC;
+#endif
+#ifdef HAVE_FF_APTX
+    else if (streq(endpoint, A2DP_APTX_SNK_ENDPOINT))
+        *codec_index = PA_A2DP_SINK_APTX;
+    else if (streq(endpoint, A2DP_APTX_SRC_ENDPOINT))
+        *codec_index = PA_A2DP_SOURCE_APTX;
+    else if (streq(endpoint, A2DP_APTX_HD_SNK_ENDPOINT))
+        *codec_index = PA_A2DP_SINK_APTX_HD;
+    else if (streq(endpoint, A2DP_APTX_HD_SRC_ENDPOINT))
+        *codec_index = PA_A2DP_SOURCE_APTX_HD;
 #endif
     else
         *codec_index = PA_A2DP_CODEC_INDEX_UNAVAILABLE;
@@ -251,6 +281,16 @@  void pa_a2dp_codec_index_to_a2dp_codec(pa_a2dp_codec_index_t codec_index, const
         case PA_A2DP_SOURCE_AAC:
             *a2dp_codec = &pa_a2dp_aac;
             break;
+#endif
+#ifdef HAVE_FF_APTX
+        case PA_A2DP_SINK_APTX:
+        case PA_A2DP_SOURCE_APTX:
+            *a2dp_codec = &pa_a2dp_aptx;
+            break;
+        case PA_A2DP_SINK_APTX_HD:
+        case PA_A2DP_SOURCE_APTX_HD:
+            *a2dp_codec = &pa_a2dp_aptx_hd;
+            break;
 #endif
         default:
             *a2dp_codec = NULL;
@@ -277,6 +317,17 @@  void pa_a2dp_a2dp_codec_to_codec_index(const pa_a2dp_codec_t *a2dp_codec, bool i
                 *codec_index = PA_A2DP_CODEC_INDEX_UNAVAILABLE;
                 return;
             }
+#ifdef HAVE_FF_APTX
+            else if (A2DP_GET_VENDOR_ID(*a2dp_codec->vendor_codec) == APTX_VENDOR_ID &&
+                     A2DP_GET_CODEC_ID(*a2dp_codec->vendor_codec) == APTX_CODEC_ID) {
+                *codec_index = is_a2dp_sink ? PA_A2DP_SINK_APTX : PA_A2DP_SOURCE_APTX;
+                return;
+            } else if (A2DP_GET_VENDOR_ID(*a2dp_codec->vendor_codec) == APTX_HD_VENDOR_ID &&
+                       A2DP_GET_CODEC_ID(*a2dp_codec->vendor_codec) == APTX_HD_CODEC_ID) {
+                *codec_index = is_a2dp_sink ? PA_A2DP_SINK_APTX_HD : PA_A2DP_SOURCE_APTX_HD;
+                return;
+            }
+#endif
             *codec_index = PA_A2DP_CODEC_INDEX_UNAVAILABLE;
             break;
         default:
@@ -300,6 +351,17 @@  pa_a2dp_get_a2dp_codec(uint8_t codec, const a2dp_vendor_codec_t *vendor_codec, c
                 *a2dp_codec = NULL;
                 pa_assert_not_reached();
             }
+#ifdef HAVE_FF_APTX
+            else if (A2DP_GET_VENDOR_ID(*vendor_codec) == APTX_VENDOR_ID &&
+                     A2DP_GET_CODEC_ID(*vendor_codec) == APTX_CODEC_ID) {
+                *a2dp_codec = &pa_a2dp_aptx;
+                return;
+            } else if (A2DP_GET_VENDOR_ID(*vendor_codec) == APTX_HD_VENDOR_ID &&
+                       A2DP_GET_CODEC_ID(*vendor_codec) == APTX_HD_CODEC_ID) {
+                *a2dp_codec = &pa_a2dp_aptx_hd;
+                return;
+            }
+#endif
             *a2dp_codec = NULL;
             break;
         default: