Skip to content

Commit

Permalink
Add support for FastStream A2DP source and sink
Browse files Browse the repository at this point in the history
  • Loading branch information
arkq committed Jul 8, 2021
1 parent ad63d45 commit 6aca5d5
Show file tree
Hide file tree
Showing 18 changed files with 569 additions and 49 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ jobs:
- --enable-debug
- --enable-debug --enable-aac --enable-msbc
- --enable-debug --enable-mp3lame --enable-mpg123
- --enable-mp3lame --enable-ofono --enable-upower
- --enable-faststream --enable-mp3lame
- --enable-ofono --enable-upower
- --enable-cli --enable-rfcomm --enable-manpages
fail-fast: false
runs-on: ubuntu-18.04
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql-analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
run: |
${{ github.workspace }}/configure \
--enable-aac \
--enable-faststream \
--enable-mp3lame \
--enable-mpg123 \
--enable-msbc \
Expand Down
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
unreleased
==========

- optional support for A2DP FastStream codec (music & voice)
- connection keep-alive timeout for all BT profiles

bluez-alsa v3.1.0 (2021-06-01)
Expand Down
7 changes: 7 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ AM_COND_IF([ENABLE_APTX_HD], [
AM_CONDITIONAL([ENABLE_APTX_OR_APTX_HD], [
test "x$enable_aptx" = "xyes" -o "x$enable_aptx_hd" = "xyes"])

AC_ARG_ENABLE([faststream],
[AS_HELP_STRING([--enable-faststream], [enable FastStream support])])
AM_CONDITIONAL([ENABLE_FASTSTREAM], [test "x$enable_faststream" = "xyes"])
AM_COND_IF([ENABLE_FASTSTREAM], [
AC_DEFINE([ENABLE_FASTSTREAM], [1], [Define to 1 if FastStream is enabled.])
])

AC_ARG_ENABLE([ldac],
[AS_HELP_STRING([--enable-ldac], [enable LDAC support])])
AM_CONDITIONAL([ENABLE_LDAC], [test "x$enable_ldac" = "xyes"])
Expand Down
5 changes: 5 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ bluealsa_SOURCES += \
codec-aptx.c
endif

if ENABLE_FASTSTREAM
bluealsa_SOURCES += \
a2dp-faststream.c
endif

if ENABLE_MSBC
bluealsa_SOURCES += \
codec-msbc.c
Expand Down
8 changes: 3 additions & 5 deletions src/a2dp-audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ static void *a2dp_sink_sbc(struct ba_transport_thread *th) {
};

sbc_t sbc;

if ((errno = -sbc_init_a2dp(&sbc, 0, t->a2dp.configuration,
t->a2dp.codec->capabilities_size)) != 0) {
error("Couldn't initialize SBC codec: %s", strerror(errno));
Expand Down Expand Up @@ -224,7 +223,6 @@ static void *a2dp_source_sbc(struct ba_transport_thread *th) {
};

sbc_t sbc;

if ((errno = -sbc_init_a2dp(&sbc, 0, t->a2dp.configuration,
t->a2dp.codec->capabilities_size)) != 0) {
error("Couldn't initialize SBC codec: %s", strerror(errno));
Expand All @@ -238,7 +236,7 @@ static void *a2dp_source_sbc(struct ba_transport_thread *th) {
pthread_cleanup_push(PTHREAD_CLEANUP(sbc_finish), &sbc);

const a2dp_sbc_t *configuration = (a2dp_sbc_t *)t->a2dp.configuration;
const size_t sbc_pcm_samples = sbc_get_codesize(&sbc) / sizeof(int16_t);
const size_t sbc_frame_samples = sbc_get_codesize(&sbc) / sizeof(int16_t);
const unsigned int channels = t->a2dp.pcm.channels;
const unsigned int samplerate = t->a2dp.pcm.sampling;

Expand All @@ -259,7 +257,7 @@ static void *a2dp_source_sbc(struct ba_transport_thread *th) {
warn("Writing MTU too small for one single SBC frame: %zu < %zu",
t->mtu_write, RTP_HEADER_LEN + sizeof(rtp_media_header_t) + sbc_frame_len);

if (ffb_init_int16_t(&pcm, sbc_pcm_samples * (mtu_write_payload / sbc_frame_len)) == -1 ||
if (ffb_init_int16_t(&pcm, sbc_frame_samples * (mtu_write_payload / sbc_frame_len)) == -1 ||
ffb_init_uint8_t(&bt, t->mtu_write) == -1) {
error("Couldn't create data buffers: %s", strerror(errno));
goto fail_ffb;
Expand Down Expand Up @@ -301,7 +299,7 @@ static void *a2dp_source_sbc(struct ba_transport_thread *th) {
/* Generate as many SBC frames as possible, but less than a 4-bit media
* header frame counter can contain. The size of the output buffer is
* based on the socket MTU, so such transfer should be most efficient. */
while (input_samples >= sbc_pcm_samples &&
while (input_samples >= sbc_frame_samples &&
output_len >= sbc_frame_len &&
sbc_frames < ((1 << 4) - 1)) {

Expand Down
287 changes: 287 additions & 0 deletions src/a2dp-faststream.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* BlueALSA - a2dp-faststream.c
* Copyright (c) 2016-2021 Arkadiusz Bokowy
*
* This file is a part of bluez-alsa.
*
* This project is licensed under the terms of the MIT license.
*
*/

#include "a2dp-faststream.h"

#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#include <glib.h>

#include <sbc/sbc.h>

#include "a2dp.h"
#include "a2dp-codecs.h"
#include "codec-sbc.h"
#include "io.h"
#include "utils.h"
#include "shared/defs.h"
#include "shared/ffb.h"
#include "shared/log.h"
#include "shared/rt.h"

void a2dp_faststream_transport_set_codec(struct ba_transport *t) {

const struct a2dp_codec *codec = t->a2dp.codec;

t->a2dp.pcm.format = BA_TRANSPORT_PCM_FORMAT_S16_2LE;
t->a2dp.pcm_bc.format = BA_TRANSPORT_PCM_FORMAT_S16_2LE;

if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_MUSIC) {
t->a2dp.pcm.channels = 2;
t->a2dp.pcm.sampling = a2dp_codec_lookup_frequency(codec,
((a2dp_faststream_t *)t->a2dp.configuration)->frequency_music, false);
}

if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_VOICE) {
t->a2dp.pcm_bc.channels = 1;
t->a2dp.pcm_bc.sampling = a2dp_codec_lookup_frequency(codec,
((a2dp_faststream_t *)t->a2dp.configuration)->frequency_voice, true);
}

}

static void *a2dp_faststream_enc_thread(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_poll io = { .timeout = -1 };

/* determine encoder operation mode: music or voice */
const bool is_voice = t->type.profile & BA_TRANSPORT_PROFILE_A2DP_SINK;
struct ba_transport_pcm *t_a2dp_pcm = is_voice ? &t->a2dp.pcm_bc : &t->a2dp.pcm;

sbc_t sbc;
if ((errno = -sbc_init_a2dp_faststream(&sbc, 0, t->a2dp.configuration,
t->a2dp.codec->capabilities_size, is_voice)) != 0) {
error("Couldn't initialize FastStream SBC codec: %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);
pthread_cleanup_push(PTHREAD_CLEANUP(sbc_finish), &sbc);

const unsigned int channels = t_a2dp_pcm->channels;
const size_t sbc_frame_len = sbc_get_frame_length(&sbc);
const size_t sbc_frame_samples = sbc_get_codesize(&sbc) / sizeof(int16_t);

if (ffb_init_int16_t(&pcm, sbc_frame_samples * 3) == -1 ||
ffb_init_uint8_t(&bt, t->mtu_write) == -1) {
error("Couldn't create data buffers: %s", strerror(ENOMEM));
goto fail_ffb;
}

debug_transport_thread_loop(th, "START");
for (ba_transport_thread_set_state_running(th);;) {


ssize_t samples = ffb_len_in(&pcm);
if ((samples = io_poll_and_read_pcm(&io, t_a2dp_pcm, pcm.tail, samples)) <= 0) {
if (samples == -1)
error("PCM poll and read error: %s", strerror(errno));
ba_transport_stop_if_no_clients(t);
continue;
}

ffb_seek(&pcm, samples);
samples = ffb_len_out(&pcm);

const int16_t *input = pcm.data;
size_t input_len = samples;
size_t output_len = ffb_len_in(&bt);
size_t pcm_frames = 0;
size_t sbc_frames = 0;

while (input_len >= sbc_frame_samples &&
output_len >= sbc_frame_len &&
sbc_frames < 3) {

ssize_t len;
ssize_t encoded;

if ((len = sbc_encode(&sbc, input, input_len * sizeof(int16_t),
bt.tail, output_len, &encoded)) < 0) {
error("FastStream SBC encoding error: %s", strerror(-len));
break;
}

len = len / sizeof(int16_t);
input += len;
input_len -= len;
ffb_seek(&bt, encoded);
output_len -= encoded;
pcm_frames += len / channels;
sbc_frames += 1;

}

if (sbc_frames > 0) {

ssize_t len = ffb_blen_out(&bt);
if ((len = io_bt_write(th, bt.data, len)) <= 0) {
if (len == -1)
error("BT write error: %s", strerror(errno));
goto fail;
}

/* make room for new FastStream frames */
ffb_rewind(&bt);

/* keep data transfer at a constant bit rate */
asrsync_sync(&io.asrs, pcm_frames);

/* update busy delay (encoding overhead) */
t_a2dp_pcm->delay = asrsync_get_busy_usec(&io.asrs) / 100;

/* If the input buffer was not consumed (due to codesize limit), we
* have to append new data to the existing one. Since we do not use
* ring buffer, we will simply move unprocessed data to the front
* of our linear buffer. */
ffb_shift(&pcm, samples - input_len);

}

}

fail:
debug_transport_thread_loop(th, "EXIT");
ba_transport_thread_set_state_stopping(th);
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
fail_ffb:
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
fail_init:
pthread_cleanup_pop(1);
return NULL;
}

static void *a2dp_faststream_dec_thread(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_poll io = { .timeout = -1 };

/* determine decoder operation mode: music or voice */
const bool is_voice = t->type.profile & BA_TRANSPORT_PROFILE_A2DP_SOURCE;
struct ba_transport_pcm *t_a2dp_pcm = is_voice ? &t->a2dp.pcm_bc : &t->a2dp.pcm;

sbc_t sbc;
if ((errno = -sbc_init_a2dp_faststream(&sbc, 0, t->a2dp.configuration,
t->a2dp.codec->capabilities_size, is_voice)) != 0) {
error("Couldn't initialize FastStream SBC codec: %s", strerror(errno));
goto fail_init;
}

const size_t sbc_frame_len = sbc_get_frame_length(&sbc);
const size_t sbc_frame_samples = sbc_get_codesize(&sbc) / sizeof(int16_t);

ffb_t bt = { 0 };
ffb_t pcm = { 0 };
pthread_cleanup_push(PTHREAD_CLEANUP(sbc_finish), &sbc);
pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt);
pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &pcm);

if (ffb_init_int16_t(&pcm, sbc_frame_samples) == -1 ||
ffb_init_uint8_t(&bt, t->mtu_read) == -1) {
error("Couldn't create data buffers: %s", strerror(errno));
goto fail_ffb;
}

debug_transport_thread_loop(th, "START");
for (ba_transport_thread_set_state_running(th);;) {

ssize_t len = ffb_blen_in(&bt);
if ((len = io_poll_and_read_bt(&io, th, bt.tail, len)) <= 0) {
if (len == -1)
error("BT poll and read error: %s", strerror(errno));
goto fail;
}

if (!ba_transport_pcm_is_active(t_a2dp_pcm))
continue;

uint8_t *input = bt.data;
size_t input_len = len;

/* decode retrieved SBC frames */
while (input_len >= sbc_frame_len) {

ssize_t len;
size_t decoded;

if ((len = sbc_decode(&sbc, input, input_len,
pcm.data, ffb_blen_in(&pcm), &decoded)) < 0) {
error("FastStream SBC decoding error: %s", strerror(-len));
break;
}

input += len;
input_len -= len;

const size_t samples = decoded / sizeof(int16_t);
io_pcm_scale(t_a2dp_pcm, pcm.data, samples);
if (io_pcm_write(t_a2dp_pcm, pcm.data, samples) == -1)
error("FIFO write error: %s", strerror(errno));

}

}

fail:
debug_transport_thread_loop(th, "EXIT");
ba_transport_thread_set_state_stopping(th);
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
fail_ffb:
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
fail_init:
pthread_cleanup_pop(1);
return NULL;
}

int a2dp_faststream_transport_start(struct ba_transport *t) {

struct ba_transport_thread *th_enc = &t->thread_enc;
struct ba_transport_thread *th_dec = &t->thread_dec;
int rv = 0;

if (t->type.profile & BA_TRANSPORT_PROFILE_A2DP_SOURCE) {
if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_MUSIC)
rv |= ba_transport_thread_create(th_enc, a2dp_faststream_enc_thread, "ba-a2dp-fs-m", true);
if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_VOICE)
rv |= ba_transport_thread_create(th_dec, a2dp_faststream_dec_thread, "ba-a2dp-fs-v", false);
return rv;
}

if (t->type.profile & BA_TRANSPORT_PROFILE_A2DP_SINK) {
if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_MUSIC)
rv |= ba_transport_thread_create(th_dec, a2dp_faststream_dec_thread, "ba-a2dp-fs-m", true);
if (((a2dp_faststream_t *)t->a2dp.configuration)->direction & FASTSTREAM_DIRECTION_VOICE)
rv |= ba_transport_thread_create(th_enc, a2dp_faststream_enc_thread, "ba-a2dp-fs-v", false);
return rv;
}

g_assert_not_reached();
return -1;
}
Loading

0 comments on commit 6aca5d5

Please sign in to comment.