1 /* Spa A2DP FastStream codec
2 *
3 * Copyright © 2020 Wim Taymans
4 * Copyright © 2021 Pauli Virtanen
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice (including the next
14 * paragraph) shall be included in all copies or substantial portions of the
15 * Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 * DEALINGS IN THE SOFTWARE.
24 */
25
26 #include <unistd.h>
27 #include <stddef.h>
28 #include <errno.h>
29 #include <arpa/inet.h>
30 #if __BYTE_ORDER != __LITTLE_ENDIAN
31 #include <byteswap.h>
32 #endif
33
34 #include <spa/param/audio/format.h>
35 #include <spa/param/audio/format-utils.h>
36
37 #include <sbc/sbc.h>
38
39 #include "a2dp-codecs.h"
40
41 struct impl {
42 sbc_t sbc;
43
44 size_t mtu;
45 int codesize;
46 int frame_count;
47 int max_frames;
48 };
49
50 struct duplex_impl {
51 sbc_t sbc;
52 };
53
codec_fill_caps(const struct a2dp_codec * codec,uint32_t flags,uint8_t caps[A2DP_MAX_CAPS_SIZE])54 static int codec_fill_caps(const struct a2dp_codec *codec, uint32_t flags,
55 uint8_t caps[A2DP_MAX_CAPS_SIZE])
56 {
57 const a2dp_faststream_t a2dp_faststream = {
58 .info = codec->vendor,
59 .direction = FASTSTREAM_DIRECTION_SINK |
60 (codec->duplex_codec ? FASTSTREAM_DIRECTION_SOURCE : 0),
61 .sink_frequency =
62 FASTSTREAM_SINK_SAMPLING_FREQ_44100 |
63 FASTSTREAM_SINK_SAMPLING_FREQ_48000,
64 .source_frequency =
65 FASTSTREAM_SOURCE_SAMPLING_FREQ_16000,
66 };
67
68 memcpy(caps, &a2dp_faststream, sizeof(a2dp_faststream));
69 return sizeof(a2dp_faststream);
70 }
71
72 static const struct a2dp_codec_config
73 frequencies[] = {
74 { FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 },
75 { FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 },
76 };
77
78 static const struct a2dp_codec_config
79 duplex_frequencies[] = {
80 { FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 },
81 };
82
codec_select_config(const struct a2dp_codec * codec,uint32_t flags,const void * caps,size_t caps_size,const struct a2dp_codec_audio_info * info,const struct spa_dict * settings,uint8_t config[A2DP_MAX_CAPS_SIZE])83 static int codec_select_config(const struct a2dp_codec *codec, uint32_t flags,
84 const void *caps, size_t caps_size,
85 const struct a2dp_codec_audio_info *info,
86 const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
87 {
88 a2dp_faststream_t conf;
89 int i;
90
91 if (caps_size < sizeof(conf))
92 return -EINVAL;
93
94 memcpy(&conf, caps, sizeof(conf));
95
96 if (codec->vendor.vendor_id != conf.info.vendor_id ||
97 codec->vendor.codec_id != conf.info.codec_id)
98 return -ENOTSUP;
99
100 if (codec->duplex_codec && !(conf.direction & FASTSTREAM_DIRECTION_SOURCE))
101 return -ENOTSUP;
102
103 if (!(conf.direction & FASTSTREAM_DIRECTION_SINK))
104 return -ENOTSUP;
105
106 conf.direction = FASTSTREAM_DIRECTION_SINK;
107
108 if (codec->duplex_codec)
109 conf.direction |= FASTSTREAM_DIRECTION_SOURCE;
110
111 if ((i = a2dp_codec_select_config(frequencies,
112 SPA_N_ELEMENTS(frequencies),
113 conf.sink_frequency,
114 info ? info->rate : A2DP_CODEC_DEFAULT_RATE
115 )) < 0)
116 return -ENOTSUP;
117 conf.sink_frequency = frequencies[i].config;
118
119 if ((i = a2dp_codec_select_config(duplex_frequencies,
120 SPA_N_ELEMENTS(duplex_frequencies),
121 conf.source_frequency,
122 16000
123 )) < 0)
124 return -ENOTSUP;
125 conf.source_frequency = duplex_frequencies[i].config;
126
127 memcpy(config, &conf, sizeof(conf));
128
129 return sizeof(conf);
130 }
131
codec_enum_config(const struct a2dp_codec * codec,const void * caps,size_t caps_size,uint32_t id,uint32_t idx,struct spa_pod_builder * b,struct spa_pod ** param)132 static int codec_enum_config(const struct a2dp_codec *codec,
133 const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
134 struct spa_pod_builder *b, struct spa_pod **param)
135 {
136 a2dp_faststream_t conf;
137 struct spa_pod_frame f[2];
138 struct spa_pod_choice *choice;
139 uint32_t position[SPA_AUDIO_MAX_CHANNELS];
140 uint32_t i = 0;
141
142 if (caps_size < sizeof(conf))
143 return -EINVAL;
144
145 memcpy(&conf, caps, sizeof(conf));
146
147 if (idx > 0)
148 return 0;
149
150 spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
151 spa_pod_builder_add(b,
152 SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
153 SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
154 SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
155 0);
156 spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
157
158 spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
159 choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
160 i = 0;
161 if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_48000) {
162 if (i++ == 0)
163 spa_pod_builder_int(b, 48000);
164 spa_pod_builder_int(b, 48000);
165 }
166 if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_44100) {
167 if (i++ == 0)
168 spa_pod_builder_int(b, 44100);
169 spa_pod_builder_int(b, 44100);
170 }
171 if (i == 0)
172 return -EINVAL;
173 if (i > 1)
174 choice->body.type = SPA_CHOICE_Enum;
175 spa_pod_builder_pop(b, &f[1]);
176
177 position[0] = SPA_AUDIO_CHANNEL_FL;
178 position[1] = SPA_AUDIO_CHANNEL_FR;
179 spa_pod_builder_add(b,
180 SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
181 SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
182 SPA_TYPE_Id, 2, position),
183 0);
184
185 *param = spa_pod_builder_pop(b, &f[0]);
186 return *param == NULL ? -EIO : 1;
187 }
188
codec_reduce_bitpool(void * data)189 static int codec_reduce_bitpool(void *data)
190 {
191 return -ENOTSUP;
192 }
193
codec_increase_bitpool(void * data)194 static int codec_increase_bitpool(void *data)
195 {
196 return -ENOTSUP;
197 }
198
codec_get_block_size(void * data)199 static int codec_get_block_size(void *data)
200 {
201 struct impl *this = data;
202 return this->codesize;
203 }
204
ceil2(size_t v)205 static size_t ceil2(size_t v)
206 {
207 if (v % 2 != 0 && v < SIZE_MAX)
208 v += 1;
209 return v;
210 }
211
codec_init(const struct a2dp_codec * codec,uint32_t flags,void * config,size_t config_len,const struct spa_audio_info * info,void * props,size_t mtu)212 static void *codec_init(const struct a2dp_codec *codec, uint32_t flags,
213 void *config, size_t config_len, const struct spa_audio_info *info,
214 void *props, size_t mtu)
215 {
216 a2dp_faststream_t *conf = config;
217 struct impl *this;
218 bool sbc_initialized = false;
219 int res;
220
221 if ((this = calloc(1, sizeof(struct impl))) == NULL)
222 goto error_errno;
223
224 if ((res = sbc_init(&this->sbc, 0)) < 0)
225 goto error;
226
227 sbc_initialized = true;
228 this->sbc.endian = SBC_LE;
229 this->mtu = mtu;
230
231 if (info->media_type != SPA_MEDIA_TYPE_audio ||
232 info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
233 info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
234 res = -EINVAL;
235 goto error;
236 }
237
238 switch (conf->sink_frequency) {
239 case FASTSTREAM_SINK_SAMPLING_FREQ_44100:
240 this->sbc.frequency = SBC_FREQ_44100;
241 break;
242 case FASTSTREAM_SINK_SAMPLING_FREQ_48000:
243 this->sbc.frequency = SBC_FREQ_48000;
244 break;
245 default:
246 res = -EINVAL;
247 goto error;
248 }
249
250 this->sbc.mode = SBC_MODE_JOINT_STEREO;
251 this->sbc.subbands = SBC_SB_8;
252 this->sbc.allocation = SBC_AM_LOUDNESS;
253 this->sbc.blocks = SBC_BLK_16;
254 this->sbc.bitpool = 29;
255
256 this->codesize = sbc_get_codesize(&this->sbc);
257
258 this->max_frames = 3;
259 if (this->mtu < this->max_frames * ceil2(sbc_get_frame_length(&this->sbc))) {
260 res = -EINVAL;
261 goto error;
262 }
263
264 return this;
265
266 error_errno:
267 res = -errno;
268 goto error;
269
270 error:
271 if (sbc_initialized)
272 sbc_finish(&this->sbc);
273 free(this);
274 errno = -res;
275 return NULL;
276 }
277
codec_deinit(void * data)278 static void codec_deinit(void *data)
279 {
280 struct impl *this = data;
281 sbc_finish(&this->sbc);
282 free(this);
283 }
284
codec_abr_process(void * data,size_t unsent)285 static int codec_abr_process (void *data, size_t unsent)
286 {
287 return -ENOTSUP;
288 }
289
codec_start_encode(void * data,void * dst,size_t dst_size,uint16_t seqnum,uint32_t timestamp)290 static int codec_start_encode (void *data,
291 void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
292 {
293 struct impl *this = data;
294 this->frame_count = 0;
295 return 0;
296 }
297
codec_encode(void * data,const void * src,size_t src_size,void * dst,size_t dst_size,size_t * dst_out,int * need_flush)298 static int codec_encode(void *data,
299 const void *src, size_t src_size,
300 void *dst, size_t dst_size,
301 size_t *dst_out, int *need_flush)
302 {
303 struct impl *this = data;
304 int res;
305
306 res = sbc_encode(&this->sbc, src, src_size,
307 dst, dst_size, (ssize_t*)dst_out);
308 if (SPA_UNLIKELY(res < 0))
309 return -EINVAL;
310 spa_assert(res == this->codesize);
311
312 if (*dst_out % 2 != 0 && *dst_out < dst_size) {
313 /* Pad similarly as in input stream */
314 *((uint8_t *)dst + *dst_out) = 0;
315 ++*dst_out;
316 }
317
318 this->frame_count += res / this->codesize;
319 *need_flush = this->frame_count >= this->max_frames;
320 return res;
321 }
322
codec_start_decode(void * data,const void * src,size_t src_size,uint16_t * seqnum,uint32_t * timestamp)323 static SPA_UNUSED int codec_start_decode (void *data,
324 const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
325 {
326 return 0;
327 }
328
do_decode(sbc_t * sbc,const void * src,size_t src_size,void * dst,size_t dst_size,size_t * dst_out)329 static int do_decode(sbc_t *sbc,
330 const void *src, size_t src_size,
331 void *dst, size_t dst_size,
332 size_t *dst_out)
333 {
334 size_t processed = 0;
335 int res;
336
337 *dst_out = 0;
338
339 /* Scan for SBC syncword.
340 * We could probably assume 1-byte paddings instead,
341 * which devices seem to be sending.
342 */
343 while (src_size >= 1) {
344 if (*(uint8_t*)src == 0x9C)
345 break;
346 src = (uint8_t*)src + 1;
347 --src_size;
348 ++processed;
349 }
350
351 res = sbc_decode(sbc, src, src_size,
352 dst, dst_size, dst_out);
353 if (res <= 0)
354 res = SPA_MIN((size_t)1, src_size); /* skip bad payload */
355
356 processed += res;
357 return processed;
358 }
359
codec_decode(void * data,const void * src,size_t src_size,void * dst,size_t dst_size,size_t * dst_out)360 static SPA_UNUSED int codec_decode(void *data,
361 const void *src, size_t src_size,
362 void *dst, size_t dst_size,
363 size_t *dst_out)
364 {
365 struct impl *this = data;
366 return do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
367 }
368
369 /*
370 * Duplex codec
371 *
372 * When connected as SRC to SNK, FastStream sink may send back SBC data.
373 */
374
duplex_enum_config(const struct a2dp_codec * codec,const void * caps,size_t caps_size,uint32_t id,uint32_t idx,struct spa_pod_builder * b,struct spa_pod ** param)375 static int duplex_enum_config(const struct a2dp_codec *codec,
376 const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
377 struct spa_pod_builder *b, struct spa_pod **param)
378 {
379 a2dp_faststream_t conf;
380 struct spa_audio_info_raw info = { 0, };
381
382 if (caps_size < sizeof(conf))
383 return -EINVAL;
384
385 memcpy(&conf, caps, sizeof(conf));
386
387 if (idx > 0)
388 return 0;
389
390 switch (conf.source_frequency) {
391 case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
392 info.rate = 16000;
393 break;
394 default:
395 return -EINVAL;
396 }
397
398 /*
399 * Some headsets send mono stream, others stereo. This information
400 * is contained in the SBC headers, and becomes known only when
401 * stream arrives. To be able to work in both cases, we will
402 * produce 2-channel output, and will double the channels
403 * in the decoding step if mono stream was received.
404 */
405 info.format = SPA_AUDIO_FORMAT_S16_LE;
406 info.channels = 2;
407 info.position[0] = SPA_AUDIO_CHANNEL_FL;
408 info.position[1] = SPA_AUDIO_CHANNEL_FR;
409
410 *param = spa_format_audio_raw_build(b, id, &info);
411 return *param == NULL ? -EIO : 1;
412 }
413
duplex_validate_config(const struct a2dp_codec * codec,uint32_t flags,const void * caps,size_t caps_size,struct spa_audio_info * info)414 static int duplex_validate_config(const struct a2dp_codec *codec, uint32_t flags,
415 const void *caps, size_t caps_size,
416 struct spa_audio_info *info)
417 {
418 spa_zero(*info);
419 info->media_type = SPA_MEDIA_TYPE_audio;
420 info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
421 info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE;
422 info->info.raw.channels = 2;
423 info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
424 info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
425 info->info.raw.rate = 16000;
426 return 0;
427 }
428
duplex_reduce_bitpool(void * data)429 static int duplex_reduce_bitpool(void *data)
430 {
431 return -ENOTSUP;
432 }
433
duplex_increase_bitpool(void * data)434 static int duplex_increase_bitpool(void *data)
435 {
436 return -ENOTSUP;
437 }
438
duplex_get_block_size(void * data)439 static int duplex_get_block_size(void *data)
440 {
441 return 0;
442 }
443
duplex_init(const struct a2dp_codec * codec,uint32_t flags,void * config,size_t config_len,const struct spa_audio_info * info,void * props,size_t mtu)444 static void *duplex_init(const struct a2dp_codec *codec, uint32_t flags,
445 void *config, size_t config_len, const struct spa_audio_info *info,
446 void *props, size_t mtu)
447 {
448 a2dp_faststream_t *conf = config;
449 struct duplex_impl *this = NULL;
450 int res;
451
452 if (info->media_type != SPA_MEDIA_TYPE_audio ||
453 info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
454 info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) {
455 res = -EINVAL;
456 goto error;
457 }
458
459 if ((this = calloc(1, sizeof(struct duplex_impl))) == NULL)
460 goto error_errno;
461
462 if ((res = sbc_init(&this->sbc, 0)) < 0)
463 goto error;
464
465 switch (conf->source_frequency) {
466 case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
467 this->sbc.frequency = SBC_FREQ_16000;
468 break;
469 default:
470 res = -EINVAL;
471 goto error;
472 }
473
474 this->sbc.endian = SBC_LE;
475 this->sbc.mode = SBC_MODE_MONO;
476 this->sbc.subbands = SBC_SB_8;
477 this->sbc.allocation = SBC_AM_LOUDNESS;
478 this->sbc.blocks = SBC_BLK_16;
479 this->sbc.bitpool = 32;
480
481 return this;
482
483 error_errno:
484 res = -errno;
485 goto error;
486 error:
487 free(this);
488 errno = -res;
489 return NULL;
490 }
491
duplex_deinit(void * data)492 static void duplex_deinit(void *data)
493 {
494 struct duplex_impl *this = data;
495 sbc_finish(&this->sbc);
496 free(this);
497 }
498
duplex_abr_process(void * data,size_t unsent)499 static int duplex_abr_process (void *data, size_t unsent)
500 {
501 return -ENOTSUP;
502 }
503
duplex_start_encode(void * data,void * dst,size_t dst_size,uint16_t seqnum,uint32_t timestamp)504 static int duplex_start_encode (void *data,
505 void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
506 {
507 return -ENOTSUP;
508 }
509
duplex_encode(void * data,const void * src,size_t src_size,void * dst,size_t dst_size,size_t * dst_out,int * need_flush)510 static int duplex_encode(void *data,
511 const void *src, size_t src_size,
512 void *dst, size_t dst_size,
513 size_t *dst_out, int *need_flush)
514 {
515 return -ENOTSUP;
516 }
517
duplex_start_decode(void * data,const void * src,size_t src_size,uint16_t * seqnum,uint32_t * timestamp)518 static int duplex_start_decode (void *data,
519 const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
520 {
521 return 0;
522 }
523
524 /** Convert S16LE stereo -> S16LE mono, in-place (only for testing purposes) */
convert_s16le_c2_to_c1(int16_t * data,size_t size,size_t max_size)525 static SPA_UNUSED size_t convert_s16le_c2_to_c1(int16_t *data, size_t size, size_t max_size)
526 {
527 size_t i;
528 for (i = 0; i < size / 2; ++i)
529 #if __BYTE_ORDER == __LITTLE_ENDIAN
530 data[i] = data[2*i]/2 + data[2*i+1]/2;
531 #else
532 data[i] = bswap_16(bswap_16(data[2*i])/2 + bswap_16(data[2*i+1])/2);
533 #endif
534 return size / 2;
535 }
536
537 /** Convert S16LE mono -> S16LE stereo, in-place */
convert_s16le_c1_to_c2(uint8_t * data,size_t size,size_t max_size)538 static size_t convert_s16le_c1_to_c2(uint8_t *data, size_t size, size_t max_size)
539 {
540 size_t pos;
541
542 pos = 2 * SPA_MIN(size / 2, max_size / 4);
543 size = 2 * pos;
544
545 /* We'll trust the compiler to optimize this */
546 while (pos >= 2) {
547 pos -= 2;
548 data[2*pos+3] = data[pos+1];
549 data[2*pos+2] = data[pos];
550 data[2*pos+1] = data[pos+1];
551 data[2*pos] = data[pos];
552 }
553
554 return size;
555 }
556
duplex_decode(void * data,const void * src,size_t src_size,void * dst,size_t dst_size,size_t * dst_out)557 static int duplex_decode(void *data,
558 const void *src, size_t src_size,
559 void *dst, size_t dst_size,
560 size_t *dst_out)
561 {
562 struct duplex_impl *this = data;
563 int res;
564
565 *dst_out = 0;
566 res = do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
567
568 /*
569 * Depending on headers of first frame, libsbc may output either
570 * 1 or 2 channels. This function should always produce 2 channels,
571 * so we'll just double the channels here.
572 */
573 if (this->sbc.mode == SBC_MODE_MONO)
574 *dst_out = convert_s16le_c1_to_c2(dst, *dst_out, dst_size);
575
576 return res;
577 }
578
579 /* Voice channel SBC, not a real A2DP codec */
580 static const struct a2dp_codec duplex_codec = {
581 .codec_id = A2DP_CODEC_VENDOR,
582 .name = "faststream_sbc",
583 .description = "FastStream duplex SBC",
584 .fill_caps = codec_fill_caps,
585 .select_config = codec_select_config,
586 .enum_config = duplex_enum_config,
587 .validate_config = duplex_validate_config,
588 .init = duplex_init,
589 .deinit = duplex_deinit,
590 .get_block_size = duplex_get_block_size,
591 .abr_process = duplex_abr_process,
592 .start_encode = duplex_start_encode,
593 .encode = duplex_encode,
594 .start_decode = duplex_start_decode,
595 .decode = duplex_decode,
596 .reduce_bitpool = duplex_reduce_bitpool,
597 .increase_bitpool = duplex_increase_bitpool,
598 };
599
600 #define FASTSTREAM_COMMON_DEFS \
601 .codec_id = A2DP_CODEC_VENDOR, \
602 .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \
603 .codec_id = FASTSTREAM_CODEC_ID }, \
604 .description = "FastStream", \
605 .fill_caps = codec_fill_caps, \
606 .select_config = codec_select_config, \
607 .enum_config = codec_enum_config, \
608 .init = codec_init, \
609 .deinit = codec_deinit, \
610 .get_block_size = codec_get_block_size, \
611 .abr_process = codec_abr_process, \
612 .start_encode = codec_start_encode, \
613 .encode = codec_encode, \
614 .reduce_bitpool = codec_reduce_bitpool, \
615 .increase_bitpool = codec_increase_bitpool
616
617 const struct a2dp_codec a2dp_codec_faststream = {
618 FASTSTREAM_COMMON_DEFS,
619 .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
620 .name = "faststream",
621 };
622
623 const struct a2dp_codec a2dp_codec_faststream_duplex = {
624 FASTSTREAM_COMMON_DEFS,
625 .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
626 .name = "faststream_duplex",
627 .duplex_codec = &duplex_codec,
628 };
629
630 A2DP_CODEC_EXPORT_DEF(
631 "faststream",
632 &a2dp_codec_faststream,
633 &a2dp_codec_faststream_duplex
634 );
635