diff options
Diffstat (limited to '')
-rw-r--r-- | spa/plugins/bluez5/a2dp-codec-faststream.c | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c new file mode 100644 index 0000000..a579ead --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-faststream.c @@ -0,0 +1,640 @@ +/* Spa A2DP FastStream codec + * + * Copyright © 2020 Wim Taymans + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <unistd.h> +#include <stddef.h> +#include <errno.h> +#include <arpa/inet.h> +#if __BYTE_ORDER != __LITTLE_ENDIAN +#include <byteswap.h> +#endif + +#include <spa/param/audio/format.h> +#include <spa/param/audio/format-utils.h> + +#include <sbc/sbc.h> + +#include "media-codecs.h" + +struct impl { + sbc_t sbc; + + size_t mtu; + int codesize; + int frame_count; + int max_frames; +}; + +struct duplex_impl { + sbc_t sbc; +}; + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + const a2dp_faststream_t a2dp_faststream = { + .info = codec->vendor, + .direction = FASTSTREAM_DIRECTION_SINK | + (codec->duplex_codec ? FASTSTREAM_DIRECTION_SOURCE : 0), + .sink_frequency = + FASTSTREAM_SINK_SAMPLING_FREQ_44100 | + FASTSTREAM_SINK_SAMPLING_FREQ_48000, + .source_frequency = + FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, + }; + + memcpy(caps, &a2dp_faststream, sizeof(a2dp_faststream)); + return sizeof(a2dp_faststream); +} + +static const struct media_codec_config +frequencies[] = { + { FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 }, + { FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 }, +}; + +static const struct media_codec_config +duplex_frequencies[] = { + { FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 }, +}; + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_faststream_t conf; + int i; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + if (codec->duplex_codec && !(conf.direction & FASTSTREAM_DIRECTION_SOURCE)) + return -ENOTSUP; + + if (!(conf.direction & FASTSTREAM_DIRECTION_SINK)) + return -ENOTSUP; + + conf.direction = FASTSTREAM_DIRECTION_SINK; + + if (codec->duplex_codec) + conf.direction |= FASTSTREAM_DIRECTION_SOURCE; + + if ((i = media_codec_select_config(frequencies, + SPA_N_ELEMENTS(frequencies), + conf.sink_frequency, + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + conf.sink_frequency = frequencies[i].config; + + if ((i = media_codec_select_config(duplex_frequencies, + SPA_N_ELEMENTS(duplex_frequencies), + conf.source_frequency, + 16000 + )) < 0) + return -ENOTSUP; + conf.source_frequency = duplex_frequencies[i].config; + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_faststream_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_48000) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_44100) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static size_t ceil2(size_t v) +{ + if (v % 2 != 0 && v < SIZE_MAX) + v += 1; + return v; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + a2dp_faststream_t *conf = config; + struct impl *this; + bool sbc_initialized = false; + int res; + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + if ((res = sbc_init(&this->sbc, 0)) < 0) + goto error; + + sbc_initialized = true; + this->sbc.endian = SBC_LE; + this->mtu = mtu; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16) { + res = -EINVAL; + goto error; + } + + switch (conf->sink_frequency) { + case FASTSTREAM_SINK_SAMPLING_FREQ_44100: + this->sbc.frequency = SBC_FREQ_44100; + break; + case FASTSTREAM_SINK_SAMPLING_FREQ_48000: + this->sbc.frequency = SBC_FREQ_48000; + break; + default: + res = -EINVAL; + goto error; + } + + this->sbc.mode = SBC_MODE_JOINT_STEREO; + this->sbc.subbands = SBC_SB_8; + this->sbc.allocation = SBC_AM_LOUDNESS; + this->sbc.blocks = SBC_BLK_16; + this->sbc.bitpool = 29; + + this->codesize = sbc_get_codesize(&this->sbc); + + this->max_frames = 3; + if (this->mtu < this->max_frames * ceil2(sbc_get_frame_length(&this->sbc))) { + res = -EINVAL; + goto error; + } + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (sbc_initialized) + sbc_finish(&this->sbc); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + sbc_finish(&this->sbc); + free(this); +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + this->frame_count = 0; + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res; + + res = sbc_encode(&this->sbc, src, src_size, + dst, dst_size, (ssize_t*)dst_out); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + spa_assert(res == this->codesize); + + if (*dst_out % 2 != 0 && *dst_out < dst_size) { + /* Pad similarly as in input stream */ + *((uint8_t *)dst + *dst_out) = 0; + ++*dst_out; + } + + this->frame_count += res / this->codesize; + *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO; + return res; +} + +static SPA_UNUSED int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +static int do_decode(sbc_t *sbc, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + size_t processed = 0; + int res; + + *dst_out = 0; + + /* Scan for SBC syncword. + * We could probably assume 1-byte paddings instead, + * which devices seem to be sending. + */ + while (src_size >= 1) { + if (*(uint8_t*)src == 0x9C) + break; + src = (uint8_t*)src + 1; + --src_size; + ++processed; + } + + res = sbc_decode(sbc, src, src_size, + dst, dst_size, dst_out); + if (res <= 0) + res = SPA_MIN((size_t)1, src_size); /* skip bad payload */ + + processed += res; + return processed; +} + +static SPA_UNUSED int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + return do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out); +} + +/* + * Duplex codec + * + * When connected as SRC to SNK, FastStream sink may send back SBC data. + */ + +static int duplex_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_faststream_t conf; + struct spa_audio_info_raw info = { 0, }; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + switch (conf.source_frequency) { + case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000: + info.rate = 16000; + break; + default: + return -EINVAL; + } + + /* + * Some headsets send mono stream, others stereo. This information + * is contained in the SBC headers, and becomes known only when + * stream arrives. To be able to work in both cases, we will + * produce 2-channel output, and will double the channels + * in the decoding step if mono stream was received. + */ + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.channels = 2; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + + *param = spa_format_audio_raw_build(b, id, &info); + return *param == NULL ? -EIO : 1; +} + +static int duplex_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + info->info.raw.rate = 16000; + return 0; +} + +static int duplex_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int duplex_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int duplex_get_block_size(void *data) +{ + return 0; +} + +static void *duplex_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + a2dp_faststream_t *conf = config; + struct duplex_impl *this = NULL; + int res; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct duplex_impl))) == NULL) + goto error_errno; + + if ((res = sbc_init(&this->sbc, 0)) < 0) + goto error; + + switch (conf->source_frequency) { + case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000: + this->sbc.frequency = SBC_FREQ_16000; + break; + default: + res = -EINVAL; + goto error; + } + + this->sbc.endian = SBC_LE; + this->sbc.mode = SBC_MODE_MONO; + this->sbc.subbands = SBC_SB_8; + this->sbc.allocation = SBC_AM_LOUDNESS; + this->sbc.blocks = SBC_BLK_16; + this->sbc.bitpool = 32; + + return this; + +error_errno: + res = -errno; + goto error; +error: + free(this); + errno = -res; + return NULL; +} + +static void duplex_deinit(void *data) +{ + struct duplex_impl *this = data; + sbc_finish(&this->sbc); + free(this); +} + +static int duplex_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int duplex_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + return -ENOTSUP; +} + +static int duplex_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + return -ENOTSUP; +} + +static int duplex_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +/** Convert S16LE stereo -> S16LE mono, in-place (only for testing purposes) */ +static SPA_UNUSED size_t convert_s16le_c2_to_c1(int16_t *data, size_t size, size_t max_size) +{ + size_t i; + for (i = 0; i < size / 2; ++i) +#if __BYTE_ORDER == __LITTLE_ENDIAN + data[i] = data[2*i]/2 + data[2*i+1]/2; +#else + data[i] = bswap_16(bswap_16(data[2*i])/2 + bswap_16(data[2*i+1])/2); +#endif + return size / 2; +} + +/** Convert S16LE mono -> S16LE stereo, in-place */ +static size_t convert_s16le_c1_to_c2(uint8_t *data, size_t size, size_t max_size) +{ + size_t pos; + + pos = 2 * SPA_MIN(size / 2, max_size / 4); + size = 2 * pos; + + /* We'll trust the compiler to optimize this */ + while (pos >= 2) { + pos -= 2; + data[2*pos+3] = data[pos+1]; + data[2*pos+2] = data[pos]; + data[2*pos+1] = data[pos+1]; + data[2*pos] = data[pos]; + } + + return size; +} + +static int duplex_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct duplex_impl *this = data; + int res; + + *dst_out = 0; + res = do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out); + + /* + * Depending on headers of first frame, libsbc may output either + * 1 or 2 channels. This function should always produce 2 channels, + * so we'll just double the channels here. + */ + if (this->sbc.mode == SBC_MODE_MONO) + *dst_out = convert_s16le_c1_to_c2(dst, *dst_out, dst_size); + + return res; +} + +/* Voice channel SBC, not a real A2DP codec */ +static const struct media_codec duplex_codec = { + .codec_id = A2DP_CODEC_VENDOR, + .name = "faststream_sbc", + .description = "FastStream duplex SBC", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = duplex_enum_config, + .validate_config = duplex_validate_config, + .init = duplex_init, + .deinit = duplex_deinit, + .get_block_size = duplex_get_block_size, + .abr_process = duplex_abr_process, + .start_encode = duplex_start_encode, + .encode = duplex_encode, + .start_decode = duplex_start_decode, + .decode = duplex_decode, + .reduce_bitpool = duplex_reduce_bitpool, + .increase_bitpool = duplex_increase_bitpool, +}; + +#define FASTSTREAM_COMMON_DEFS \ + .codec_id = A2DP_CODEC_VENDOR, \ + .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \ + .codec_id = FASTSTREAM_CODEC_ID }, \ + .description = "FastStream", \ + .fill_caps = codec_fill_caps, \ + .select_config = codec_select_config, \ + .enum_config = codec_enum_config, \ + .init = codec_init, \ + .deinit = codec_deinit, \ + .get_block_size = codec_get_block_size, \ + .abr_process = codec_abr_process, \ + .start_encode = codec_start_encode, \ + .encode = codec_encode, \ + .reduce_bitpool = codec_reduce_bitpool, \ + .increase_bitpool = codec_increase_bitpool + +const struct media_codec a2dp_codec_faststream = { + FASTSTREAM_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, + .name = "faststream", +}; + +static const struct spa_dict_item duplex_info_items[] = { + { "duplex.boost", "true" }, +}; +static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items); + +const struct media_codec a2dp_codec_faststream_duplex = { + FASTSTREAM_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, + .name = "faststream_duplex", + .duplex_codec = &duplex_codec, + .info = &duplex_info, +}; + +MEDIA_CODEC_EXPORT_DEF( + "faststream", + &a2dp_codec_faststream, + &a2dp_codec_faststream_duplex +); |