mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
Currently, the PipeWire daemon registers BlueZ LE Media Endpoints with audio capabilities covering all settings defined in the BAP spec. However, some scenarios might require the capabilities to be restricted to specific configurations. This adds a method to read LC3 codec specific capabilities from the Wireplumber config file, and provide those settings when registering Media Endpoint objects with BlueZ. If the values are not present in the config file, all settings will be used by default. Below is an example of how to set the LC3 capabilities in the config file, to support the 16_2 setting from the BAP spec: bluez5.bap-server-capabilities.rates = [16000] bluez5.bap-server-capabilities.durations = [10] bluez5.bap-server-capabilities.channels = [1, 2] bluez5.bap-server-capabilities.framelen_min = 40 bluez5.bap-server-capabilities.framelen_max = 40 bluez5.bap-server-capabilities.max_frames = 2
627 lines
15 KiB
C
627 lines
15 KiB
C
/* Spa A2DP FastStream codec */
|
|
/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
|
|
/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */
|
|
/* SPDX-License-Identifier: MIT */
|
|
|
|
#include <unistd.h>
|
|
#include <stddef.h>
|
|
#include <errno.h>
|
|
#include <arpa/inet.h>
|
|
|
|
#include <spa/param/audio/format.h>
|
|
#include <spa/param/audio/format-utils.h>
|
|
#include <spa/utils/endian.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,
|
|
const struct spa_dict *settings, 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 > 1)
|
|
choice->body.type = SPA_CHOICE_Enum;
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
if (i == 0)
|
|
return -EINVAL;
|
|
|
|
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;
|
|
}
|
|
|
|
static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder)
|
|
{
|
|
if (encoder)
|
|
*encoder = 73;
|
|
if (decoder)
|
|
*decoder = 0;
|
|
}
|
|
|
|
/* 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, \
|
|
.get_delay = codec_get_delay
|
|
|
|
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
|
|
);
|