From b11cff0c2fd436e64c98b77943bb20a2d3d0ea77 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 13 Jan 2021 21:09:11 +0100 Subject: [PATCH] Support apt-X (HD) Sink when decoder is available --- NEWS | 1 + README.md | 2 +- configure.ac | 6 +- src/a2dp-audio.c | 222 ++++++++++++++++++++++++++++++++++++++++++++++- src/a2dp.c | 11 ++- test/test-io.c | 58 +++++++++++-- 6 files changed, 285 insertions(+), 15 deletions(-) diff --git a/NEWS b/NEWS index 16d2b9598..fb50ae5af 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,7 @@ unreleased ========== - higher PCM bit depth for apt-X HD (24-bit) and LDAC (32-bit) +- support for A2DP Sink with apt-X (HD) if decoder is available - support for A2DP Sink with LDAC codec if decoder is available - use rst2man (docutils) instead of pandoc to build man-pages diff --git a/README.md b/README.md index dab86953e..ca9304f94 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Dependencies: - [mp3lame](https://lame.sourceforge.net/) (when MP3 support is enabled with `--enable-mp3lame`) - [mpg123](https://www.mpg123.org/) (when MPEG decoding support is enabled with `--enable-mpg123`) - [fdk-aac](https://github.com/mstorsjo/fdk-aac) (when AAC support is enabled with `--enable-aac`) -- [openaptx](https://github.com/Arkq/openaptx) (when apt-X encoding support is enabled with +- [openaptx](https://github.com/Arkq/openaptx) (when apt-X support is enabled with `--enable-aptx` and/or `--enable-aptx-hd`) - [libldac](https://github.com/EHfive/ldacBT) (when LDAC support is enabled with `--enable-ldac`) - [docutils](https://docutils.sourceforge.io) (when man pages build is enabled with `--enable-manpages`) diff --git a/configure.ac b/configure.ac index 18800e1bb..4bd1077fb 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ # BlueALSA - configure.ac -# Copyright (c) 2016-2020 Arkadiusz Bokowy +# Copyright (c) 2016-2021 Arkadiusz Bokowy AC_PREREQ([2.60]) AC_INIT([BlueALSA], @@ -96,7 +96,7 @@ AM_COND_IF([ENABLE_AAC], [ ]) AC_ARG_ENABLE([aptx], - [AS_HELP_STRING([--enable-aptx], [enable apt-X encoding support])]) + [AS_HELP_STRING([--enable-aptx], [enable apt-X support])]) AM_CONDITIONAL([ENABLE_APTX], [test "x$enable_aptx" = "xyes"]) AM_COND_IF([ENABLE_APTX], [ PKG_CHECK_MODULES([APTX], [openaptx >= 1.2.0]) @@ -104,7 +104,7 @@ AM_COND_IF([ENABLE_APTX], [ ]) AC_ARG_ENABLE([aptx_hd], - [AS_HELP_STRING([--enable-aptx-hd], [enable apt-X HD encoding support])]) + [AS_HELP_STRING([--enable-aptx-hd], [enable apt-X HD support])]) AM_CONDITIONAL([ENABLE_APTX_HD], [test "x$enable_aptx_hd" = "xyes"]) AM_COND_IF([ENABLE_APTX_HD], [ PKG_CHECK_MODULES([APTX_HD], [openaptxhd >= 1.2.0]) diff --git a/src/a2dp-audio.c b/src/a2dp-audio.c index f6d0589d1..f1754932d 100644 --- a/src/a2dp-audio.c +++ b/src/a2dp-audio.c @@ -1,6 +1,6 @@ /* * BlueALSA - a2dp-audio.c - * Copyright (c) 2016-2020 Arkadiusz Bokowy + * Copyright (c) 2016-2021 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -1556,6 +1556,105 @@ static void *a2dp_source_aac(struct ba_transport_thread *th) { } #endif +#if ENABLE_APTX && OPENAPTX_DECODER +static void *a2dp_sink_aptx(struct ba_transport_thread *th) { + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_thread_cleanup), th); + + struct ba_transport *t = th->t; + struct io_thread_data io = { + .th = th, + .t_locked = !ba_transport_thread_cleanup_lock(th), + }; + + if (a2dp_validate_bt_sink(t) != 0) + goto fail_open; + + APTXDEC handle = malloc(SizeofAptxbtdec()); + pthread_cleanup_push(PTHREAD_CLEANUP(free), handle); + pthread_cleanup_push(PTHREAD_CLEANUP(aptxbtdec_destroy), handle); + + if (handle == NULL || + aptxbtdec_init(handle, __BYTE_ORDER == __LITTLE_ENDIAN) != 0) { + error("Couldn't initialize apt-X decoder: %s", strerror(errno)); + goto fail_init; + } + + ffb_t bt = { 0 }; + ffb_t pcm = { 0 }; + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt); + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &pcm); + + if (ffb_init_int16_t(&pcm, t->mtu_read / 4 * 8) == -1 || + ffb_init_uint8_t(&bt, t->mtu_read) == -1) { + error("Couldn't create data buffers: %s", strerror(errno)); + goto fail_ffb; + } + + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_thread_cleanup_lock), th); + + ba_transport_thread_cleanup_unlock(th); + io.t_locked = false; + + debug("Starting IO loop: %s", ba_transport_type_to_string(t->type)); + for (;;) { + + ssize_t len; + if ((len = a2dp_poll_and_read_bt(&io, &bt)) <= 0) { + if (len == -1) + error("BT poll and read error: %s", strerror(errno)); + goto fail; + } + + if (t->a2dp.pcm.fd == -1) + continue; + + uint16_t *input = bt.data; + size_t input_codewords = len / sizeof(uint16_t); + + ffb_rewind(&pcm); + int16_t *output = pcm.data; + + while (input_codewords >= 2) { + + int32_t pcm_l[4], pcm_r[4]; + if (aptxbtdec_decodestereo(handle, pcm_l, pcm_r, input) != 0) { + error("Apt-X decoding error: %s", strerror(errno)); + break; + } + + input += 2; + input_codewords -= 2; + + ffb_seek(&pcm, 2 * ARRAYSIZE(pcm_l)); + for (size_t i = 0; i < ARRAYSIZE(pcm_l); i++) { + *output++ = pcm_l[i]; + *output++ = pcm_r[i]; + } + + } + + if (ba_transport_pcm_write(&t->a2dp.pcm, pcm.data, ffb_len_out(&pcm)) == -1) + error("FIFO write error: %s", strerror(errno)); + + } + +fail: + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_pop(!io.t_locked); +fail_ffb: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_init: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_open: + pthread_cleanup_pop(1); + return NULL; +} +#endif + #if ENABLE_APTX static void *a2dp_source_aptx(struct ba_transport_thread *th) { @@ -1675,6 +1774,117 @@ static void *a2dp_source_aptx(struct ba_transport_thread *th) { } #endif +#if ENABLE_APTX_HD && OPENAPTX_DECODER +static void *a2dp_sink_aptx_hd(struct ba_transport_thread *th) { + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_thread_cleanup), th); + + struct ba_transport *t = th->t; + struct io_thread_data io = { + .th = th, + .rtp_seq_number = -1, + .t_locked = !ba_transport_thread_cleanup_lock(th), + }; + + if (a2dp_validate_bt_sink(t) != 0) + goto fail_open; + + APTXDEC handle = malloc(SizeofAptxbtdec()); + pthread_cleanup_push(PTHREAD_CLEANUP(free), handle); + pthread_cleanup_push(PTHREAD_CLEANUP(aptxhdbtdec_destroy), handle); + + if (handle == NULL || + aptxhdbtdec_init(handle, false) != 0) { + error("Couldn't initialize apt-X decoder: %s", strerror(errno)); + goto fail_init; + } + + ffb_t bt = { 0 }; + ffb_t pcm = { 0 }; + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt); + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &pcm); + + if (ffb_init_int32_t(&pcm, t->mtu_read / 6 * 8) == -1 || + ffb_init_uint8_t(&bt, t->mtu_read) == -1) { + error("Couldn't create data buffers: %s", strerror(errno)); + goto fail_ffb; + } + + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_thread_cleanup_lock), th); + + ba_transport_thread_cleanup_unlock(th); + io.t_locked = false; + + debug("Starting IO loop: %s", ba_transport_type_to_string(t->type)); + for (;;) { + + ssize_t len; + if ((len = a2dp_poll_and_read_bt(&io, &bt)) <= 0) { + if (len == -1) + error("BT poll and read error: %s", strerror(errno)); + goto fail; + } + + if (t->a2dp.pcm.fd == -1) { + io.rtp_seq_number = -1; + continue; + } + + const uint8_t *rtp_payload; + if ((rtp_payload = a2dp_validate_rtp(bt.data, &io)) == NULL) + continue; + + size_t rtp_payload_len = len - (rtp_payload - (uint8_t *)bt.data); + size_t input_codewords = rtp_payload_len / 3; + + ffb_rewind(&pcm); + int32_t *output = pcm.data; + + while (input_codewords >= 2) { + + int32_t pcm_l[4]; + int32_t pcm_r[4]; + + uint32_t code[2] = { + (rtp_payload[0] << 16) | (rtp_payload[1] << 8) | rtp_payload[2], + (rtp_payload[3] << 16) | (rtp_payload[4] << 8) | rtp_payload[5] }; + if (aptxhdbtdec_decodestereo(handle, pcm_l, pcm_r, code) != 0) { + error("Apt-X decoding error: %s", strerror(errno)); + break; + } + + rtp_payload += 6; + input_codewords -= 2; + + ffb_seek(&pcm, 2 * ARRAYSIZE(pcm_l)); + for (size_t i = 0; i < ARRAYSIZE(pcm_l); i++) { + *output++ = pcm_l[i]; + *output++ = pcm_r[i]; + } + + } + + if (ba_transport_pcm_write(&t->a2dp.pcm, pcm.data, ffb_len_out(&pcm)) == -1) + error("FIFO write error: %s", strerror(errno)); + + } + +fail: + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_pop(!io.t_locked); +fail_ffb: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_init: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_open: + pthread_cleanup_pop(1); + return NULL; +} +#endif + #if ENABLE_APTX_HD static void *a2dp_source_aptx_hd(struct ba_transport_thread *th) { @@ -2086,6 +2296,7 @@ static void *a2dp_source_ldac(struct ba_transport_thread *th) { } #endif +#if DEBUG /** * Dump incoming BT data to a file. */ static void *a2dp_sink_dump(struct ba_transport_thread *th) { @@ -2141,6 +2352,7 @@ static void *a2dp_sink_dump(struct ba_transport_thread *th) { pthread_cleanup_pop(1); return NULL; } +#endif int a2dp_audio_thread_create(struct ba_transport *t) { @@ -2193,6 +2405,14 @@ int a2dp_audio_thread_create(struct ba_transport *t) { case A2DP_CODEC_MPEG24: return ba_transport_thread_create(th, a2dp_sink_aac, "ba-a2dp-aac"); #endif +#if ENABLE_APTX && OPENAPTX_DECODER + case A2DP_CODEC_VENDOR_APTX: + return ba_transport_thread_create(th, a2dp_sink_aptx, "ba-a2dp-aptx"); +#endif +#if ENABLE_APTX_HD && OPENAPTX_DECODER + case A2DP_CODEC_VENDOR_APTX_HD: + return ba_transport_thread_create(th, a2dp_sink_aptx_hd, "ba-a2dp-aptx-hd"); +#endif #if ENABLE_LDAC && HAVE_LDAC_DECODE case A2DP_CODEC_VENDOR_LDAC: return ba_transport_thread_create(th, a2dp_sink_ldac, "ba-a2dp-ldac"); diff --git a/src/a2dp.c b/src/a2dp.c index 81458f96e..30ee442cc 100644 --- a/src/a2dp.c +++ b/src/a2dp.c @@ -1,6 +1,6 @@ /* * BlueALSA - a2dp.c - * Copyright (c) 2016-2020 Arkadiusz Bokowy + * Copyright (c) 2016-2021 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -15,6 +15,9 @@ #include #include +#if ENABLE_APTX || ENABLE_APTX_HD +# include +#endif #include "a2dp-codecs.h" #include "bluealsa.h" @@ -471,9 +474,15 @@ const struct a2dp_codec *a2dp_codecs[] = { #endif #if ENABLE_APTX_HD &a2dp_codec_source_aptx_hd, +# if OPENAPTX_DECODER + &a2dp_codec_sink_aptx_hd, +# endif #endif #if ENABLE_APTX &a2dp_codec_source_aptx, +# if OPENAPTX_DECODER + &a2dp_codec_sink_aptx, +# endif #endif #if ENABLE_FASTSTREAM &a2dp_codec_source_faststream, diff --git a/test/test-io.c b/test/test-io.c index 7047c48c1..09de63931 100644 --- a/test/test-io.c +++ b/test/test-io.c @@ -1,6 +1,6 @@ /* * test-io.c - * Copyright (c) 2016-2020 Arkadiusz Bokowy + * Copyright (c) 2016-2021 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -104,6 +104,25 @@ static const char *input_pcm_file = NULL; static unsigned int aging_duration = 0; static bool dump_data = false; +static void *write_test_pcm_async(void *userdata) { + + int fd_out = (uintptr_t)userdata; + int fd_in; + + ck_assert_int_ne(fd_in = open(input_pcm_file, O_RDONLY), -1); + + struct pollfd pfds[] = {{ fd_out, POLLOUT, 0 }}; + ssize_t len; + + do { + ck_assert_int_ne(poll(pfds, ARRAYSIZE(pfds), -1), -1); + ck_assert_int_ne(len = sendfile(fd_out, fd_in, NULL, 0x7FFFF000), -1); + } while (len > 0); + + close(fd_in); + return NULL; +} + /** * Write test PCM signal to the file descriptor. */ static void write_test_pcm(int fd, int channels) { @@ -111,13 +130,12 @@ static void write_test_pcm(int fd, int channels) { static int16_t sample_pcm_mono[5 * 1024]; static int16_t sample_pcm_stereo[10 * 1024]; static bool initialized = false; + pthread_t thread; FILE *f; - int ffd; if (input_pcm_file != NULL) { - ck_assert_int_ne(ffd = open(input_pcm_file, O_RDONLY), -1); - ck_assert_int_ne(sendfile(fd, ffd, NULL, 0x7FFFF000), -1); - close(ffd); + pthread_create(&thread, NULL, write_test_pcm_async, (void *)(uintptr_t)fd); + pthread_detach(thread); return; } @@ -174,6 +192,15 @@ static void bt_data_push(uint8_t *data, size_t len) { bt_data_end = bt_data_end->next; } +static void bt_data_write(int fd) { + struct bt_data *bt_data_head = &bt_data; + struct pollfd pfds[] = {{ fd, POLLOUT, 0 }}; + for (; bt_data_head != bt_data_end; bt_data_head = bt_data_head->next) { + ck_assert_int_ne(poll(pfds, ARRAYSIZE(pfds), -1), -1); + ck_assert_int_eq(write(fd, bt_data_head->data, bt_data_head->len), bt_data_head->len); + } +} + /** * Helper function for timed thread join. * @@ -306,10 +333,8 @@ static void test_a2dp(struct ba_transport *t1, struct ba_transport *t2, if (enc == test_io_thread_a2dp_dump_pcm) { ck_assert_int_eq(ba_transport_thread_create(&t2->thread, dec, dec_name), 0); - struct bt_data *bt_data_head = &bt_data; - for (; bt_data_head != bt_data_end; bt_data_head = bt_data_head->next) - ck_assert_int_eq(write(bt_fds[1], bt_data_head->data, bt_data_head->len), bt_data_head->len); ck_assert_int_eq(ba_transport_thread_create(&t1->thread, enc, enc_name), 0); + bt_data_write(bt_fds[1]); } else { ck_assert_int_eq(ba_transport_thread_create(&t1->thread, enc, enc_name), 0); @@ -489,10 +514,17 @@ START_TEST(test_a2dp_aptx) { t1->release = t2->release = test_transport_release_bt_a2dp; if (aging_duration) { +#if OPENAPTX_DECODER + t1->mtu_write = t2->mtu_read = 400; + test_a2dp(t1, t2, a2dp_source_aptx, a2dp_sink_aptx); +#endif } else { t1->mtu_write = t2->mtu_read = 40; test_a2dp(t1, t2, a2dp_source_aptx, test_io_thread_a2dp_dump_bt); +#if OPENAPTX_DECODER + test_a2dp(t1, t2, test_io_thread_a2dp_dump_pcm, a2dp_sink_aptx); +#endif }; } END_TEST @@ -513,10 +545,17 @@ START_TEST(test_a2dp_aptx_hd) { t1->release = t2->release = test_transport_release_bt_a2dp; if (aging_duration) { +#if OPENAPTX_DECODER + t1->mtu_write = t2->mtu_read = 600; + test_a2dp(t1, t2, a2dp_source_aptx_hd, a2dp_sink_aptx_hd); +#endif } else { t1->mtu_write = t2->mtu_read = 60; test_a2dp(t1, t2, a2dp_source_aptx_hd, test_io_thread_a2dp_dump_bt); +#if OPENAPTX_DECODER + test_a2dp(t1, t2, test_io_thread_a2dp_dump_pcm, a2dp_sink_aptx_hd); +#endif }; } END_TEST @@ -659,7 +698,8 @@ int main(int argc, char *argv[]) { SRunner *sr = srunner_create(s); suite_add_tcase(s, tc); - tcase_set_timeout(tc, aging_duration + 5); + tcase_set_timeout(tc, aging_duration + + (input_pcm_file != NULL ? 180 : 5)); if (enabled_codecs & TEST_CODEC_SBC) tcase_add_test(tc, test_a2dp_sbc);