mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
Compare commits
18 commits
21c697f5c8
...
9f830293b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f830293b5 | ||
|
|
76a31a47c2 | ||
|
|
23c449af5d | ||
|
|
94d0d8bc09 | ||
|
|
0276bb5b06 | ||
|
|
614186a590 | ||
|
|
c6d0b364ab | ||
|
|
8a23b13798 | ||
|
|
3d08c0557f | ||
|
|
68dc45cc62 | ||
|
|
b0e308e0dc | ||
|
|
fe2c62b9b1 | ||
|
|
3febf09b85 | ||
|
|
93495d3a75 | ||
|
|
9f1a149876 | ||
|
|
f2b3b63f21 | ||
|
|
a9ba34da23 | ||
|
|
87cb3ea4a1 |
26 changed files with 1435 additions and 92 deletions
|
|
@ -38,7 +38,7 @@ include:
|
||||||
.fedora:
|
.fedora:
|
||||||
variables:
|
variables:
|
||||||
# Update this tag when you want to trigger a rebuild
|
# Update this tag when you want to trigger a rebuild
|
||||||
FDO_DISTRIBUTION_TAG: '2025-10-15.0'
|
FDO_DISTRIBUTION_TAG: '2025-10-22.0'
|
||||||
FDO_DISTRIBUTION_VERSION: '42'
|
FDO_DISTRIBUTION_VERSION: '42'
|
||||||
FDO_DISTRIBUTION_PACKAGES: >-
|
FDO_DISTRIBUTION_PACKAGES: >-
|
||||||
alsa-lib-devel
|
alsa-lib-devel
|
||||||
|
|
@ -48,6 +48,7 @@ include:
|
||||||
dbus-devel
|
dbus-devel
|
||||||
doxygen
|
doxygen
|
||||||
fdk-aac-free-devel
|
fdk-aac-free-devel
|
||||||
|
file
|
||||||
findutils
|
findutils
|
||||||
gcc
|
gcc
|
||||||
gcc-c++
|
gcc-c++
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,8 @@ cc_flags = common_flags + [
|
||||||
'-DSPA_AUDIO_MAX_CHANNELS=128u',
|
'-DSPA_AUDIO_MAX_CHANNELS=128u',
|
||||||
]
|
]
|
||||||
add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
|
add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
|
||||||
|
add_project_arguments(cc_native.get_supported_arguments(cc_flags),
|
||||||
cc_flags_native = cc_native.get_supported_arguments(cc_flags)
|
language: 'c', native: true)
|
||||||
|
|
||||||
have_cpp = add_languages('cpp', native: false, required : false)
|
have_cpp = add_languages('cpp', native: false, required : false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,10 @@ option('bluez5-codec-ldac-dec',
|
||||||
description: 'Enable LDAC Sony open source codec decoding',
|
description: 'Enable LDAC Sony open source codec decoding',
|
||||||
type: 'feature',
|
type: 'feature',
|
||||||
value: 'auto')
|
value: 'auto')
|
||||||
|
option('bluez5-codec-lhdc',
|
||||||
|
description: 'Enable LHDC open source codec implementation',
|
||||||
|
type: 'feature',
|
||||||
|
value: 'auto')
|
||||||
option('bluez5-codec-aac',
|
option('bluez5-codec-aac',
|
||||||
description: 'Enable Fraunhofer FDK AAC open source codec implementation',
|
description: 'Enable Fraunhofer FDK AAC open source codec implementation',
|
||||||
type: 'feature',
|
type: 'feature',
|
||||||
|
|
|
||||||
|
|
@ -578,7 +578,7 @@ static int make_nodes(struct data *data)
|
||||||
SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
|
SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
|
||||||
SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
|
SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
|
||||||
SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
|
SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
|
||||||
if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param) < 0)) {
|
if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param)) < 0) {
|
||||||
printf("can't setup source node %d\n", res);
|
printf("can't setup source node %d\n", res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
@ -647,7 +647,7 @@ static int make_nodes(struct data *data)
|
||||||
SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
|
SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
|
||||||
|
|
||||||
|
|
||||||
if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param) < 0)) {
|
if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param)) < 0) {
|
||||||
printf("can't setup sink node %d\n", res);
|
printf("can't setup sink node %d\n", res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
@ -987,7 +987,7 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
setlocale(LC_ALL, "");
|
setlocale(LC_ALL, "");
|
||||||
|
|
||||||
while ((c = getopt_long(argc, argv, "hdmstiac:", long_options, NULL)) != -1) {
|
while ((c = getopt_long(argc, argv, "hd:m:s:t:i:a:c:", long_options, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'h':
|
case 'h':
|
||||||
show_help(&data, argv[0], false);
|
show_help(&data, argv[0], false);
|
||||||
|
|
|
||||||
|
|
@ -46,20 +46,61 @@ extern "C" {
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
SPA_API_AUDIO_FORMAT_UTILS bool
|
||||||
|
spa_format_audio_ext_valid_size(uint32_t media_subtype, size_t size)
|
||||||
|
{
|
||||||
|
switch (media_subtype) {
|
||||||
|
case SPA_MEDIA_SUBTYPE_raw:
|
||||||
|
return size >= offsetof(struct spa_audio_info, info.raw) &&
|
||||||
|
SPA_AUDIO_INFO_RAW_VALID_SIZE(size - offsetof(struct spa_audio_info, info.raw));
|
||||||
|
|
||||||
|
#define _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(format) \
|
||||||
|
case SPA_MEDIA_SUBTYPE_ ## format: \
|
||||||
|
return size >= offsetof(struct spa_audio_info, info.format) + sizeof(struct spa_audio_info_ ## format);
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dsp)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(iec958)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dsd)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(mp3)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(aac)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(vorbis)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(wma)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ra)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(amr)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(alac)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(flac)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ape)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ac3)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(eac3)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(truehd)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dts)
|
||||||
|
_SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(mpegh)
|
||||||
|
#undef _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
SPA_API_AUDIO_FORMAT_UTILS int
|
SPA_API_AUDIO_FORMAT_UTILS int
|
||||||
spa_format_audio_ext_parse(const struct spa_pod *format, struct spa_audio_info *info, size_t size)
|
spa_format_audio_ext_parse(const struct spa_pod *format, struct spa_audio_info *info, size_t size)
|
||||||
{
|
{
|
||||||
int res;
|
int res;
|
||||||
|
uint32_t media_type, media_subtype;
|
||||||
|
|
||||||
if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0)
|
if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
if (info->media_type != SPA_MEDIA_TYPE_audio)
|
if (media_type != SPA_MEDIA_TYPE_audio)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
switch (info->media_subtype) {
|
if (!spa_format_audio_ext_valid_size(media_subtype, size))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
info->media_type = media_type;
|
||||||
|
info->media_subtype = media_subtype;
|
||||||
|
|
||||||
|
switch (media_subtype) {
|
||||||
case SPA_MEDIA_SUBTYPE_raw:
|
case SPA_MEDIA_SUBTYPE_raw:
|
||||||
return spa_format_audio_raw_ext_parse(format, &info->info.raw, size);
|
return spa_format_audio_raw_ext_parse(format, &info->info.raw,
|
||||||
|
size - offsetof(struct spa_audio_info, info.raw));
|
||||||
case SPA_MEDIA_SUBTYPE_dsp:
|
case SPA_MEDIA_SUBTYPE_dsp:
|
||||||
return spa_format_audio_dsp_parse(format, &info->info.dsp);
|
return spa_format_audio_dsp_parse(format, &info->info.dsp);
|
||||||
case SPA_MEDIA_SUBTYPE_iec958:
|
case SPA_MEDIA_SUBTYPE_iec958:
|
||||||
|
|
@ -108,9 +149,15 @@ SPA_API_AUDIO_FORMAT_UTILS struct spa_pod *
|
||||||
spa_format_audio_ext_build(struct spa_pod_builder *builder, uint32_t id,
|
spa_format_audio_ext_build(struct spa_pod_builder *builder, uint32_t id,
|
||||||
const struct spa_audio_info *info, size_t size)
|
const struct spa_audio_info *info, size_t size)
|
||||||
{
|
{
|
||||||
|
if (!spa_format_audio_ext_valid_size(info->media_subtype, size)) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
switch (info->media_subtype) {
|
switch (info->media_subtype) {
|
||||||
case SPA_MEDIA_SUBTYPE_raw:
|
case SPA_MEDIA_SUBTYPE_raw:
|
||||||
return spa_format_audio_raw_ext_build(builder, id, &info->info.raw, size);
|
return spa_format_audio_raw_ext_build(builder, id, &info->info.raw,
|
||||||
|
size - offsetof(struct spa_audio_info, info.raw));
|
||||||
case SPA_MEDIA_SUBTYPE_dsp:
|
case SPA_MEDIA_SUBTYPE_dsp:
|
||||||
return spa_format_audio_dsp_build(builder, id, &info->info.dsp);
|
return spa_format_audio_dsp_build(builder, id, &info->info.dsp);
|
||||||
case SPA_MEDIA_SUBTYPE_iec958:
|
case SPA_MEDIA_SUBTYPE_iec958:
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ struct spa_audio_info {
|
||||||
struct spa_audio_info_dts dts;
|
struct spa_audio_info_dts dts;
|
||||||
struct spa_audio_info_mpegh mpegh;
|
struct spa_audio_info_mpegh mpegh;
|
||||||
} info;
|
} info;
|
||||||
|
|
||||||
|
/* padding follows here when info has flexible size */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size,
|
||||||
{
|
{
|
||||||
uint32_t v;
|
uint32_t v;
|
||||||
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
||||||
|
|
||||||
|
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
if (spa_streq(key, SPA_KEY_AUDIO_FORMAT)) {
|
if (spa_streq(key, SPA_KEY_AUDIO_FORMAT)) {
|
||||||
if (force || info->format == 0)
|
if (force || info->format == 0)
|
||||||
info->format = (enum spa_audio_format)spa_type_audio_format_from_short_name(val);
|
info->format = (enum spa_audio_format)spa_type_audio_format_from_short_name(val);
|
||||||
|
|
@ -78,6 +82,7 @@ spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size,
|
||||||
max_position, &v) > 0) {
|
max_position, &v) > 0) {
|
||||||
if (v > max_position)
|
if (v > max_position)
|
||||||
return -ECHRNG;
|
return -ECHRNG;
|
||||||
|
info->channels = v;
|
||||||
SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +104,9 @@ spa_audio_info_raw_ext_init_dict_keys_va(struct spa_audio_info_raw *info, size_t
|
||||||
{
|
{
|
||||||
int res;
|
int res;
|
||||||
|
|
||||||
|
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
memset(info, 0, size);
|
memset(info, 0, size);
|
||||||
SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
||||||
if (dict) {
|
if (dict) {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ spa_format_audio_raw_ext_parse(const struct spa_pod *format, struct spa_audio_in
|
||||||
int res;
|
int res;
|
||||||
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
||||||
|
|
||||||
|
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
info->flags = 0;
|
info->flags = 0;
|
||||||
res = spa_pod_parse_object(format,
|
res = spa_pod_parse_object(format,
|
||||||
SPA_TYPE_OBJECT_Format, NULL,
|
SPA_TYPE_OBJECT_Format, NULL,
|
||||||
|
|
@ -64,6 +67,11 @@ spa_format_audio_raw_ext_build(struct spa_pod_builder *builder, uint32_t id,
|
||||||
struct spa_pod_frame f;
|
struct spa_pod_frame f;
|
||||||
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
|
||||||
|
|
||||||
|
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size)) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
|
spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
|
||||||
spa_pod_builder_add(builder,
|
spa_pod_builder_add(builder,
|
||||||
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
|
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,8 @@ struct spa_audio_info_raw {
|
||||||
|
|
||||||
#define SPA_AUDIO_INFO_RAW_MAX_POSITION(size) (((size)-offsetof(struct spa_audio_info_raw,position))/sizeof(uint32_t))
|
#define SPA_AUDIO_INFO_RAW_MAX_POSITION(size) (((size)-offsetof(struct spa_audio_info_raw,position))/sizeof(uint32_t))
|
||||||
|
|
||||||
|
#define SPA_AUDIO_INFO_RAW_VALID_SIZE(size) ((size) >= offsetof(struct spa_audio_info_raw, position))
|
||||||
|
|
||||||
|
|
||||||
#define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string,
|
#define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string,
|
||||||
* Ex. "S16LE" */
|
* Ex. "S16LE" */
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ enum spa_bluetooth_audio_codec {
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
|
SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
|
||||||
|
SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V3,
|
||||||
|
SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V5,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
|
SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx", NULL },
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_hd", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_hd", NULL },
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_LDAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "ldac", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_LDAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "ldac", NULL },
|
||||||
|
{ SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lhdc_v3", NULL },
|
||||||
|
{ SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V5, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lhdc_v5", NULL },
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll", NULL },
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll_duplex", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll_duplex", NULL },
|
||||||
{ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream", NULL },
|
{ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream", NULL },
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,15 @@ SPA_API_JSON void spa_json_init(struct spa_json * iter, const char *data, size_t
|
||||||
{
|
{
|
||||||
*iter = SPA_JSON_INIT(data, size);
|
*iter = SPA_JSON_INIT(data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define SPA_JSON_INIT_RELAX(type,data,size) \
|
||||||
|
((struct spa_json) { (data), (data)+(size), NULL, (uint32_t)((type) == '[' ? 0x10 : 0x0), 0 })
|
||||||
|
|
||||||
|
SPA_API_JSON void spa_json_init_relax(struct spa_json * iter, char type, const char *data, size_t size)
|
||||||
|
{
|
||||||
|
*iter = SPA_JSON_INIT_RELAX(type, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 })
|
#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 })
|
||||||
|
|
||||||
SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
|
SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter,
|
||||||
spa_json_init(iter, data, size);
|
spa_json_init(iter, data, size);
|
||||||
res = spa_json_enter_container(iter, iter, type);
|
res = spa_json_enter_container(iter, iter, type);
|
||||||
if (res == -EPROTO && relax)
|
if (res == -EPROTO && relax)
|
||||||
spa_json_init(iter, data, size);
|
spa_json_init_relax(iter, type, data, size);
|
||||||
else if (res <= 0)
|
else if (res <= 0)
|
||||||
return res;
|
return res;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,42 @@ if get_option('spa-plugins').allowed()
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
summary({'LC3plus': lc3plus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
summary({'LC3plus': lc3plus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
||||||
|
lhdc_enc_dep = dependency('lhdcBT-enc', required : false)
|
||||||
|
if not lhdc_enc_dep.found()
|
||||||
|
lhdc_enc_lhdc_h_dep = cc.find_library('lhdcBT_enc', has_headers: ['lhdcBT.h'], required : false)
|
||||||
|
if lhdc_enc_lhdc_h_dep.found()
|
||||||
|
lhdc_enc_dep = declare_dependency(dependencies : [ lhdc_enc_lhdc_h_dep ])
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
summary({'LHDC V3 Encoder': lhdc_enc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
||||||
|
|
||||||
|
lhdc_dec_dep = dependency('lhdcBT-dec', required : false)
|
||||||
|
if not lhdc_dec_dep.found()
|
||||||
|
lhdc_dec_lhdc_h_dep = cc.find_library('lhdcBT_dec', has_headers: ['lhdcBT_dec.h'], required : false)
|
||||||
|
if lhdc_dec_lhdc_h_dep.found()
|
||||||
|
lhdc_dec_dep = declare_dependency(dependencies : [ lhdc_dec_lhdc_h_dep ])
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
summary({'LHDC V3 Decoder': lhdc_enc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
||||||
|
|
||||||
|
lhdc_v5_enc_dep = dependency('lhdcv5BT-enc', required : false)
|
||||||
|
if not lhdc_v5_enc_dep.found()
|
||||||
|
lhdc_v5_enc_lhdc_h_dep = cc.find_library('lhdcv5BT_enc', has_headers: ['lhdcv5BT.h'], required : false)
|
||||||
|
if lhdc_v5_enc_lhdc_h_dep.found()
|
||||||
|
lhdc_v5_enc_dep = declare_dependency(dependencies : [ lhdc_v5_enc_lhdc_h_dep ])
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
summary({'LHDC V5 Encoder': lhdc_v5_enc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
||||||
|
|
||||||
|
lhdc_v5_dec_dep = dependency('lhdcv5BT-dec', required : false)
|
||||||
|
if not lhdc_v5_dec_dep.found()
|
||||||
|
lhdc_v5_dec_lhdc_h_dep = cc.find_library('lhdcv5BT_dec', has_headers: ['lhdcv5BT_dec.h'], required : false)
|
||||||
|
if lhdc_v5_dec_lhdc_h_dep.found()
|
||||||
|
lhdc_v5_dec_dep = declare_dependency(dependencies : [ lhdc_v5_dec_lhdc_h_dep ])
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
summary({'LHDC V5 Decoder': lhdc_v5_enc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
|
||||||
|
|
||||||
if get_option('bluez5-codec-opus').enabled() and not opus_dep.found()
|
if get_option('bluez5-codec-opus').enabled() and not opus_dep.found()
|
||||||
error('bluez5-codec-opus enabled, but opus dependency not found')
|
error('bluez5-codec-opus enabled, but opus dependency not found')
|
||||||
endif
|
endif
|
||||||
|
|
|
||||||
|
|
@ -773,7 +773,7 @@ static void bind_ctl_event(struct spa_source *source)
|
||||||
snd_ctl_elem_id_alloca(&bound_id);
|
snd_ctl_elem_id_alloca(&bound_id);
|
||||||
snd_ctl_elem_value_alloca(&old_value);
|
snd_ctl_elem_value_alloca(&old_value);
|
||||||
|
|
||||||
while ((err = snd_ctl_read(state->ctl, ev) > 0)) {
|
while ((err = snd_ctl_read(state->ctl, ev)) > 0) {
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM)
|
if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM)
|
||||||
|
|
|
||||||
|
|
@ -1538,8 +1538,6 @@ static int get_ramp_samples(struct impl *this, struct volume_ramp_params *vrp)
|
||||||
samples = (vrp->volume_ramp_time * vrp->rate) / 1000;
|
samples = (vrp->volume_ramp_time * vrp->rate) / 1000;
|
||||||
spa_log_info(this->log, "volume ramp samples calculated from time is %d", samples);
|
spa_log_info(this->log, "volume ramp samples calculated from time is %d", samples);
|
||||||
}
|
}
|
||||||
if (!samples)
|
|
||||||
samples = -1;
|
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1550,12 +1548,10 @@ static int get_ramp_step_samples(struct impl *this, struct volume_ramp_params *v
|
||||||
if (vrp->volume_ramp_step_samples)
|
if (vrp->volume_ramp_step_samples)
|
||||||
samples = vrp->volume_ramp_step_samples;
|
samples = vrp->volume_ramp_step_samples;
|
||||||
else if (vrp->volume_ramp_step_time) {
|
else if (vrp->volume_ramp_step_time) {
|
||||||
/* convert the step time which is in nano seconds to seconds */
|
/* convert the step time which is in nano seconds to seconds, round up */
|
||||||
samples = (vrp->volume_ramp_step_time/1000) * (vrp->rate/1000);
|
samples = SPA_MAX(1u, vrp->volume_ramp_step_time/1000) * (vrp->rate/1000);
|
||||||
spa_log_debug(this->log, "volume ramp step samples calculated from time is %d", samples);
|
spa_log_debug(this->log, "volume ramp step samples calculated from time is %d", samples);
|
||||||
}
|
}
|
||||||
if (!samples)
|
|
||||||
samples = -1;
|
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1568,76 +1564,52 @@ static float get_volume_at_scale(struct volume_ramp_params *vrp, float value)
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct spa_pod *generate_ramp_up_seq(struct impl *this, struct volume_ramp_params *vrp,
|
static struct spa_pod *generate_ramp_seq(struct impl *this, struct volume_ramp_params *vrp,
|
||||||
void *buffer, size_t size)
|
void *buffer, size_t size)
|
||||||
{
|
{
|
||||||
struct spa_pod_dynamic_builder b;
|
struct spa_pod_dynamic_builder b;
|
||||||
struct spa_pod_frame f[1];
|
struct spa_pod_frame f[1];
|
||||||
float start = vrp->start, end = vrp->end, volume_accum = start;
|
float start = vrp->start, end = vrp->end;
|
||||||
int ramp_samples = get_ramp_samples(this, vrp);
|
int samples = get_ramp_samples(this, vrp);
|
||||||
int ramp_step_samples = get_ramp_step_samples(this, vrp);
|
int step = get_ramp_step_samples(this, vrp);
|
||||||
float volume_step = ((end - start) / (ramp_samples / ramp_step_samples));
|
int offs = 0;
|
||||||
uint32_t volume_offs = 0;
|
|
||||||
|
if (samples < 0 || step < 0 || (samples > 0 && step == 0))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
spa_pod_dynamic_builder_init(&b, buffer, size, 4096);
|
spa_pod_dynamic_builder_init(&b, buffer, size, 4096);
|
||||||
|
|
||||||
spa_pod_builder_push_sequence(&b.b, &f[0], 0);
|
spa_pod_builder_push_sequence(&b.b, &f[0], 0);
|
||||||
spa_log_info(this->log, "generating ramp up sequence from %f to %f with a"
|
spa_log_info(this->log, "generating ramp sequence from %f to %f with "
|
||||||
" step value %f at scale %d", start, end, volume_step, vrp->scale);
|
"step %d/%d at scale %d", start, end, step, samples, vrp->scale);
|
||||||
do {
|
|
||||||
float vas = get_volume_at_scale(vrp, volume_accum);
|
|
||||||
spa_log_trace(this->log, "volume accum %f", vas);
|
|
||||||
spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties);
|
|
||||||
spa_pod_builder_add_object(&b.b,
|
|
||||||
SPA_TYPE_OBJECT_Props, 0,
|
|
||||||
SPA_PROP_volume, SPA_POD_Float(vas));
|
|
||||||
volume_accum += volume_step;
|
|
||||||
volume_offs += ramp_step_samples;
|
|
||||||
} while (volume_accum < end);
|
|
||||||
return spa_pod_builder_pop(&b.b, &f[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct spa_pod *generate_ramp_down_seq(struct impl *this, struct volume_ramp_params *vrp,
|
while (1) {
|
||||||
void *buffer, size_t size)
|
float pos = (samples == 0) ? end :
|
||||||
{
|
SPA_CLAMP(start + (end - start) * offs / samples,
|
||||||
struct spa_pod_dynamic_builder b;
|
SPA_MIN(start, end), SPA_MAX(start, end));
|
||||||
struct spa_pod_frame f[1];
|
float vas = get_volume_at_scale(vrp, pos);
|
||||||
int ramp_samples = get_ramp_samples(this, vrp);
|
|
||||||
int ramp_step_samples = get_ramp_step_samples(this, vrp);
|
|
||||||
float start = vrp->start, end = vrp->end, volume_accum = start;
|
|
||||||
float volume_step = ((start - end) / (ramp_samples / ramp_step_samples));
|
|
||||||
uint32_t volume_offs = 0;
|
|
||||||
|
|
||||||
spa_pod_dynamic_builder_init(&b, buffer, size, 4096);
|
spa_log_trace(this->log, "volume %d accum %f", offs, vas);
|
||||||
|
spa_pod_builder_control(&b.b, offs, SPA_CONTROL_Properties);
|
||||||
spa_pod_builder_push_sequence(&b.b, &f[0], 0);
|
|
||||||
spa_log_info(this->log, "generating ramp down sequence from %f to %f with a"
|
|
||||||
" step value %f at scale %d", start, end, volume_step, vrp->scale);
|
|
||||||
do {
|
|
||||||
float vas = get_volume_at_scale(vrp, volume_accum);
|
|
||||||
spa_log_trace(this->log, "volume accum %f", vas);
|
|
||||||
spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties);
|
|
||||||
spa_pod_builder_add_object(&b.b,
|
spa_pod_builder_add_object(&b.b,
|
||||||
SPA_TYPE_OBJECT_Props, 0,
|
SPA_TYPE_OBJECT_Props, 0,
|
||||||
SPA_PROP_volume, SPA_POD_Float(vas));
|
SPA_PROP_volume, SPA_POD_Float(vas));
|
||||||
|
|
||||||
volume_accum -= volume_step;
|
if (offs >= samples)
|
||||||
volume_offs += ramp_step_samples;
|
break;
|
||||||
} while (volume_accum > end);
|
|
||||||
|
offs = SPA_MIN(samples, offs + step);
|
||||||
|
}
|
||||||
|
|
||||||
return spa_pod_builder_pop(&b.b, &f[0]);
|
return spa_pod_builder_pop(&b.b, &f[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void generate_volume_ramp(struct impl *this, struct volume_ramp_params *vrp,
|
static void generate_volume_ramp(struct impl *this, struct volume_ramp_params *vrp,
|
||||||
void *buffer, size_t size)
|
void *buffer, size_t size)
|
||||||
{
|
{
|
||||||
void *sequence = NULL;
|
void *sequence;
|
||||||
if (vrp->start == vrp->end)
|
|
||||||
spa_log_error(this->log, "no change in volume, cannot ramp volume");
|
|
||||||
else if (vrp->end > vrp->start)
|
|
||||||
sequence = generate_ramp_up_seq(this, vrp, buffer, size);
|
|
||||||
else
|
|
||||||
sequence = generate_ramp_down_seq(this, vrp, buffer, size);
|
|
||||||
|
|
||||||
|
sequence = generate_ramp_seq(this, vrp, buffer, size);
|
||||||
if (!sequence)
|
if (!sequence)
|
||||||
spa_log_error(this->log, "unable to generate sequence");
|
spa_log_error(this->log, "unable to generate sequence");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ sparesampledumpcoeffs_sources = [
|
||||||
sparesampledumpcoeffs = executable(
|
sparesampledumpcoeffs = executable(
|
||||||
'spa-resample-dump-coeffs',
|
'spa-resample-dump-coeffs',
|
||||||
sparesampledumpcoeffs_sources,
|
sparesampledumpcoeffs_sources,
|
||||||
c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ],
|
c_args : [ '-DRESAMPLE_DISABLE_PRECOMP' ],
|
||||||
dependencies : [ spa_dep, mathlib_native ],
|
dependencies : [ spa_dep, mathlib_native ],
|
||||||
install : false,
|
install : false,
|
||||||
native : true,
|
native : true,
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,53 @@
|
||||||
#define LDAC_SAMPLING_FREQ_176400 0x02
|
#define LDAC_SAMPLING_FREQ_176400 0x02
|
||||||
#define LDAC_SAMPLING_FREQ_192000 0x01
|
#define LDAC_SAMPLING_FREQ_192000 0x01
|
||||||
|
|
||||||
|
#define LHDC_V3_VENDOR_ID 0x0000053a
|
||||||
|
#define LHDC_V3_CODEC_ID 0x4c33
|
||||||
|
|
||||||
|
#define LHDC_V5_VENDOR_ID 0x0000053a
|
||||||
|
#define LHDC_V5_CODEC_ID 0x4c35
|
||||||
|
|
||||||
|
#define LHDC_CHANNEL_MODE_STEREO 0x03
|
||||||
|
|
||||||
|
#define LHDC_BIT_DEPTH_16 0x02
|
||||||
|
#define LHDC_BIT_DEPTH_24 0x01
|
||||||
|
|
||||||
|
#define LHDC_VER3 0x01
|
||||||
|
|
||||||
|
#define LHDC_SAMPLING_FREQ_44100 0x08
|
||||||
|
#define LHDC_SAMPLING_FREQ_48000 0x04
|
||||||
|
#define LHDC_SAMPLING_FREQ_88200 0x02
|
||||||
|
#define LHDC_SAMPLING_FREQ_96000 0x01
|
||||||
|
|
||||||
|
#define LHDC_MAX_BIT_RATE_400K 0x02
|
||||||
|
#define LHDC_MAX_BIT_RATE_500K 0x01
|
||||||
|
#define LHDC_MAX_BIT_RATE_900K 0x00
|
||||||
|
|
||||||
|
#define LHDC_CH_SPLIT_MODE_NONE 0x01
|
||||||
|
#define LHDC_CH_SPLIT_MODE_TWS 0x02
|
||||||
|
#define LHDC_CH_SPLIT_MODE_TWS_PLUS 0x04
|
||||||
|
|
||||||
|
#define LHDCV5_SAMPLING_FREQ_44100 (1 << 5)
|
||||||
|
#define LHDCV5_SAMPLING_FREQ_48000 (1 << 4)
|
||||||
|
#define LHDCV5_SAMPLING_FREQ_96000 (1 << 2)
|
||||||
|
#define LHDCV5_SAMPLING_FREQ_192000 (1 << 0)
|
||||||
|
|
||||||
|
#define LHDCV5_BIT_DEPTH_16 (1 << 2)
|
||||||
|
#define LHDCV5_BIT_DEPTH_24 (1 << 1)
|
||||||
|
#define LHDCV5_BIT_DEPTH_32 (1 << 0)
|
||||||
|
|
||||||
|
#define LHDCV5_MAX_BITRATE_900K (3)
|
||||||
|
#define LHDCV5_MAX_BITRATE_500K (2)
|
||||||
|
#define LHDCV5_MAX_BITRATE_400K (1)
|
||||||
|
#define LHDCV5_MAX_BITRATE_1000K (0)
|
||||||
|
|
||||||
|
#define LHDCV5_MIN_BITRATE_400K (3)
|
||||||
|
#define LHDCV5_MIN_BITRATE_256K (2)
|
||||||
|
#define LHDCV5_MIN_BITRATE_160K (1)
|
||||||
|
#define LHDCV5_MIN_BITRATE_64K (0)
|
||||||
|
|
||||||
|
#define LHDCV5_VER_1 (1 << 0)
|
||||||
|
|
||||||
#define FASTSTREAM_VENDOR_ID 0x0000000a
|
#define FASTSTREAM_VENDOR_ID 0x0000000a
|
||||||
#define FASTSTREAM_CODEC_ID 0x0001
|
#define FASTSTREAM_CODEC_ID 0x0001
|
||||||
|
|
||||||
|
|
@ -378,6 +425,46 @@ typedef struct {
|
||||||
uint8_t source_frequency:4;
|
uint8_t source_frequency:4;
|
||||||
} __attribute__ ((packed)) a2dp_faststream_t;
|
} __attribute__ ((packed)) a2dp_faststream_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
a2dp_vendor_codec_t info;
|
||||||
|
uint8_t frequency:4;
|
||||||
|
uint8_t bit_depth:2;
|
||||||
|
uint8_t jas:1;
|
||||||
|
uint8_t ar:1;
|
||||||
|
uint8_t version:4;
|
||||||
|
uint8_t max_bit_rate:2;
|
||||||
|
uint8_t low_latency:1;
|
||||||
|
uint8_t llac:1;
|
||||||
|
uint8_t ch_split_mode:4;
|
||||||
|
uint8_t meta:1;
|
||||||
|
uint8_t min_bitrate:1;
|
||||||
|
uint8_t larc:1;
|
||||||
|
uint8_t lhdc_v4:1;
|
||||||
|
} __attribute__ ((packed)) a2dp_lhdc_v3_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
a2dp_vendor_codec_t info;
|
||||||
|
uint8_t sampling_freq:6;
|
||||||
|
uint8_t rfa1:2;
|
||||||
|
uint8_t bit_depth:3;
|
||||||
|
uint8_t rfa2:1;
|
||||||
|
uint8_t max_bitrate:2;
|
||||||
|
uint8_t min_bitrate:2;
|
||||||
|
uint8_t version:4;
|
||||||
|
uint8_t frame_len_5ms:1;
|
||||||
|
uint8_t rfa3:3;
|
||||||
|
uint8_t ar:1;
|
||||||
|
uint8_t jas:1;
|
||||||
|
uint8_t meta:1;
|
||||||
|
uint8_t rfa4:1;
|
||||||
|
uint8_t lossless_96k:1;
|
||||||
|
uint8_t lossless_24b:1;
|
||||||
|
uint8_t low_latency:1;
|
||||||
|
uint8_t lossless_48k:1;
|
||||||
|
uint8_t rfa5:7;
|
||||||
|
uint8_t lossless_raw_48k:1;
|
||||||
|
} __attribute__ ((packed)) a2dp_lhdc_v5_t;
|
||||||
|
|
||||||
#elif __BYTE_ORDER == __BIG_ENDIAN
|
#elif __BYTE_ORDER == __BIG_ENDIAN
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
||||||
952
spa/plugins/bluez5/a2dp-codec-lhdc.c
Normal file
952
spa/plugins/bluez5/a2dp-codec-lhdc.c
Normal file
|
|
@ -0,0 +1,952 @@
|
||||||
|
/* Spa A2DP LHDC codec */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2025 anonymix007 */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#include <spa/utils/string.h>
|
||||||
|
#include <spa/utils/dict.h>
|
||||||
|
#include <spa/pod/parser.h>
|
||||||
|
#include <spa/param/props.h>
|
||||||
|
#include <spa/param/audio/format.h>
|
||||||
|
|
||||||
|
#include <lhdcBT.h>
|
||||||
|
#include <lhdcBT_dec.h>
|
||||||
|
|
||||||
|
#include <lhdcv5BT_dec.h>
|
||||||
|
|
||||||
|
#include "rtp.h"
|
||||||
|
#include "media-codecs.h"
|
||||||
|
|
||||||
|
static struct spa_log *log;
|
||||||
|
|
||||||
|
struct props_v3 {
|
||||||
|
LHDCBT_QUALITY_T eqmid;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct rtp_lhdc_payload {
|
||||||
|
uint8_t seq_num;
|
||||||
|
uint8_t latency:2;
|
||||||
|
uint8_t frame_count:6;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(struct rtp_lhdc_payload) == sizeof(uint16_t), "LHDC payload header must be 2 bytes");
|
||||||
|
|
||||||
|
struct impl_v3 {
|
||||||
|
HANDLE_LHDC_BT lhdc;
|
||||||
|
|
||||||
|
bool dec_initialized;
|
||||||
|
|
||||||
|
struct rtp_header *header;
|
||||||
|
struct rtp_lhdc_payload *payload;
|
||||||
|
|
||||||
|
int mtu;
|
||||||
|
int eqmid;
|
||||||
|
int frequency;
|
||||||
|
int bit_depth;
|
||||||
|
int codesize;
|
||||||
|
int block_size;
|
||||||
|
uint8_t seq_num;
|
||||||
|
int32_t buf[2][LHDCV2_BT_ENC_BLOCK_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct impl_v5 {
|
||||||
|
HANDLE_LHDCV5_BT dec;
|
||||||
|
|
||||||
|
struct rtp_header *header;
|
||||||
|
struct rtp_lhdc_payload *payload;
|
||||||
|
|
||||||
|
int mtu;
|
||||||
|
int eqmid;
|
||||||
|
int frequency;
|
||||||
|
int bit_depth;
|
||||||
|
int frame_samples;
|
||||||
|
uint8_t seq_num;
|
||||||
|
int32_t buf[2][LHDCV5_MAX_SAMPLE_FRAME];
|
||||||
|
};
|
||||||
|
|
||||||
|
static int codec_fill_caps_v3(const struct media_codec *codec, uint32_t flags,
|
||||||
|
const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
|
||||||
|
{
|
||||||
|
static const a2dp_lhdc_v3_t a2dp_lhdc = {
|
||||||
|
.info.vendor_id = LHDC_V3_VENDOR_ID,
|
||||||
|
.info.codec_id = LHDC_V3_CODEC_ID,
|
||||||
|
.frequency = LHDC_SAMPLING_FREQ_44100 | LHDC_SAMPLING_FREQ_48000 | LHDC_SAMPLING_FREQ_96000,
|
||||||
|
.bit_depth = LHDC_BIT_DEPTH_16 | LHDC_BIT_DEPTH_24,
|
||||||
|
.jas = 0,
|
||||||
|
.ar = 0,
|
||||||
|
.version = LHDC_VER3,
|
||||||
|
.max_bit_rate = LHDC_MAX_BIT_RATE_900K,
|
||||||
|
.low_latency = 0,
|
||||||
|
.llac = 0,
|
||||||
|
.ch_split_mode = LHDC_CH_SPLIT_MODE_NONE,
|
||||||
|
.meta = 0,
|
||||||
|
.min_bitrate = 0,
|
||||||
|
.larc = 0,
|
||||||
|
.lhdc_v4 = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
memcpy(caps, &a2dp_lhdc, sizeof(a2dp_lhdc));
|
||||||
|
return sizeof(a2dp_lhdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_fill_caps_v5(const struct media_codec *codec, uint32_t flags,
|
||||||
|
const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE])
|
||||||
|
{
|
||||||
|
static const a2dp_lhdc_v5_t a2dp_lhdc = {
|
||||||
|
.info.vendor_id = LHDC_V5_VENDOR_ID,
|
||||||
|
.info.codec_id = LHDC_V5_CODEC_ID,
|
||||||
|
.sampling_freq = LHDCV5_SAMPLING_FREQ_44100 | LHDCV5_SAMPLING_FREQ_48000 | LHDCV5_SAMPLING_FREQ_96000,
|
||||||
|
.bit_depth = LHDCV5_BIT_DEPTH_16 | LHDCV5_BIT_DEPTH_24,
|
||||||
|
.max_bitrate = LHDCV5_MAX_BITRATE_1000K,
|
||||||
|
.min_bitrate = LHDCV5_MIN_BITRATE_64K,
|
||||||
|
.version = LHDCV5_VER_1,
|
||||||
|
.frame_len_5ms = 1,
|
||||||
|
.ar = 0,
|
||||||
|
.jas = 0,
|
||||||
|
.meta = 0,
|
||||||
|
.lossless_96k = 0,
|
||||||
|
.lossless_24b = 0,
|
||||||
|
.low_latency = 0,
|
||||||
|
.lossless_48k = 0,
|
||||||
|
.lossless_raw_48k = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
memcpy(caps, &a2dp_lhdc, sizeof(a2dp_lhdc));
|
||||||
|
return sizeof(a2dp_lhdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct media_codec_config
|
||||||
|
lhdc_frequencies_v3[] = {
|
||||||
|
{ LHDC_SAMPLING_FREQ_44100, 44100, 0 },
|
||||||
|
{ LHDC_SAMPLING_FREQ_48000, 48000, 2 },
|
||||||
|
{ LHDC_SAMPLING_FREQ_96000, 96000, 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct media_codec_config
|
||||||
|
lhdc_frequencies_v5[] = {
|
||||||
|
{ LHDCV5_SAMPLING_FREQ_44100, 44100, 0 },
|
||||||
|
{ LHDCV5_SAMPLING_FREQ_48000, 48000, 2 },
|
||||||
|
{ LHDCV5_SAMPLING_FREQ_96000, 96000, 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static int codec_select_config_v3(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_lhdc_v3_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 ((i = media_codec_select_config(lhdc_frequencies_v3,
|
||||||
|
SPA_N_ELEMENTS(lhdc_frequencies_v3),
|
||||||
|
conf.frequency,
|
||||||
|
info ? info->rate : A2DP_CODEC_DEFAULT_RATE
|
||||||
|
)) < 0)
|
||||||
|
return -ENOTSUP;
|
||||||
|
conf.frequency = lhdc_frequencies_v3[i].config;
|
||||||
|
|
||||||
|
conf.low_latency = 0;
|
||||||
|
conf.llac = 0;
|
||||||
|
conf.lhdc_v4 = 1;
|
||||||
|
conf.bit_depth = LHDC_BIT_DEPTH_24;
|
||||||
|
|
||||||
|
memcpy(config, &conf, sizeof(conf));
|
||||||
|
|
||||||
|
return sizeof(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_select_config_v5(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_lhdc_v5_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 ((i = media_codec_select_config(lhdc_frequencies_v5,
|
||||||
|
SPA_N_ELEMENTS(lhdc_frequencies_v5),
|
||||||
|
conf.sampling_freq,
|
||||||
|
info ? info->rate : A2DP_CODEC_DEFAULT_RATE
|
||||||
|
)) < 0)
|
||||||
|
return -ENOTSUP;
|
||||||
|
conf.sampling_freq = lhdc_frequencies_v5[i].config;
|
||||||
|
|
||||||
|
conf.bit_depth = LHDCV5_BIT_DEPTH_24;
|
||||||
|
|
||||||
|
memcpy(config, &conf, sizeof(conf));
|
||||||
|
|
||||||
|
return sizeof(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_enum_config_v3(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_lhdc_v3_t conf;
|
||||||
|
struct spa_pod_frame f[2];
|
||||||
|
struct spa_pod_choice *choice;
|
||||||
|
uint32_t i = 0;
|
||||||
|
uint32_t position[SPA_AUDIO_MAX_CHANNELS];
|
||||||
|
|
||||||
|
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_S32),
|
||||||
|
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.frequency & LHDC_SAMPLING_FREQ_48000) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 48000);
|
||||||
|
spa_pod_builder_int(b, 48000);
|
||||||
|
}
|
||||||
|
if (conf.frequency & LHDC_SAMPLING_FREQ_44100) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 44100);
|
||||||
|
spa_pod_builder_int(b, 44100);
|
||||||
|
}
|
||||||
|
if (conf.frequency & LHDC_SAMPLING_FREQ_96000) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 96000);
|
||||||
|
spa_pod_builder_int(b, 96000);
|
||||||
|
}
|
||||||
|
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_enum_config_v5(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_lhdc_v5_t conf;
|
||||||
|
struct spa_pod_frame f[2];
|
||||||
|
struct spa_pod_choice *choice;
|
||||||
|
uint32_t i = 0;
|
||||||
|
uint32_t position[SPA_AUDIO_MAX_CHANNELS];
|
||||||
|
|
||||||
|
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_S32),
|
||||||
|
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.sampling_freq & LHDCV5_SAMPLING_FREQ_48000) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 48000);
|
||||||
|
spa_pod_builder_int(b, 48000);
|
||||||
|
}
|
||||||
|
if (conf.sampling_freq & LHDCV5_SAMPLING_FREQ_44100) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 44100);
|
||||||
|
spa_pod_builder_int(b, 44100);
|
||||||
|
}
|
||||||
|
if (conf.sampling_freq & LHDCV5_SAMPLING_FREQ_96000) {
|
||||||
|
if (i++ == 0)
|
||||||
|
spa_pod_builder_int(b, 96000);
|
||||||
|
spa_pod_builder_int(b, 96000);
|
||||||
|
}
|
||||||
|
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_get_block_size_v3(void *data)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
return this->codesize;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_get_block_size_v5(void *data)
|
||||||
|
{
|
||||||
|
struct impl_v5 *this = data;
|
||||||
|
return this->frame_samples * 4 * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct { const char *name; int v; } eqmids_v3[] = {
|
||||||
|
{ "low0", .v = LHDCBT_QUALITY_LOW0 },
|
||||||
|
{ "low1", .v = LHDCBT_QUALITY_LOW1 },
|
||||||
|
{ "low2", .v = LHDCBT_QUALITY_LOW2 },
|
||||||
|
{ "low3", .v = LHDCBT_QUALITY_LOW3 },
|
||||||
|
{ "low4", .v = LHDCBT_QUALITY_LOW4 },
|
||||||
|
{ "low", .v = LHDCBT_QUALITY_LOW },
|
||||||
|
{ "mid", .v = LHDCBT_QUALITY_MID },
|
||||||
|
{ "high", .v = LHDCBT_QUALITY_HIGH },
|
||||||
|
{ "auto", .v = LHDCBT_QUALITY_AUTO },
|
||||||
|
{ 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static int string_to_eqmid_v3(const char * eqmid)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; eqmids_v3[i].name; i++) {
|
||||||
|
if (spa_streq(eqmids_v3[i].name, eqmid))
|
||||||
|
return eqmids_v3[i].v;
|
||||||
|
}
|
||||||
|
return LHDCBT_QUALITY_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *codec_init_props_v3(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
|
||||||
|
{
|
||||||
|
struct props_v3 *p = calloc(1, sizeof(struct props_v3));
|
||||||
|
const char *str;
|
||||||
|
|
||||||
|
if (p == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.lhdc_v3.quality")) == NULL)
|
||||||
|
str = "auto";
|
||||||
|
|
||||||
|
p->eqmid = string_to_eqmid_v3(str);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *codec_init_props_v5(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
|
||||||
|
{
|
||||||
|
return malloc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void codec_clear_props_v3(void *props)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_enum_props_v3(void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
|
||||||
|
struct spa_pod_builder *b, struct spa_pod **param)
|
||||||
|
{
|
||||||
|
struct props_v3 *p = props;
|
||||||
|
struct spa_pod_frame f[2];
|
||||||
|
switch (id) {
|
||||||
|
case SPA_PARAM_PropInfo:
|
||||||
|
{
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
|
||||||
|
spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
|
||||||
|
spa_pod_builder_id(b, SPA_PROP_quality);
|
||||||
|
spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
|
||||||
|
spa_pod_builder_string(b, "LHDC quality");
|
||||||
|
|
||||||
|
spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
|
||||||
|
spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
|
||||||
|
spa_pod_builder_int(b, p->eqmid);
|
||||||
|
for (size_t i = 0; eqmids_v3[i].name; i++) {
|
||||||
|
spa_pod_builder_int(b, eqmids_v3[i].v);
|
||||||
|
}
|
||||||
|
spa_pod_builder_pop(b, &f[1]);
|
||||||
|
|
||||||
|
spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0);
|
||||||
|
spa_pod_builder_push_struct(b, &f[1]);
|
||||||
|
for (size_t i = 0; eqmids_v3[i].name; i++) {
|
||||||
|
spa_pod_builder_int(b, eqmids_v3[i].v);
|
||||||
|
spa_pod_builder_string(b, eqmids_v3[i].name);
|
||||||
|
}
|
||||||
|
spa_pod_builder_pop(b, &f[1]);
|
||||||
|
|
||||||
|
*param = spa_pod_builder_pop(b, &f[0]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SPA_PARAM_Props:
|
||||||
|
{
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
*param = spa_pod_builder_add_object(b,
|
||||||
|
SPA_TYPE_OBJECT_Props, id,
|
||||||
|
SPA_PROP_quality, SPA_POD_Int(p->eqmid));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_enum_props_v5(void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
|
||||||
|
struct spa_pod_builder *b, struct spa_pod **param) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_set_props_v3(void *props, const struct spa_pod *param)
|
||||||
|
{
|
||||||
|
struct props_v3 *p = props;
|
||||||
|
const LHDCBT_QUALITY_T prev_eqmid = p->eqmid;
|
||||||
|
if (param == NULL) {
|
||||||
|
p->eqmid = LHDCBT_QUALITY_AUTO;
|
||||||
|
} else {
|
||||||
|
spa_pod_parse_object(param,
|
||||||
|
SPA_TYPE_OBJECT_Props, NULL,
|
||||||
|
SPA_PROP_quality, SPA_POD_OPT_Int(&p->eqmid));
|
||||||
|
if (p->eqmid > LHDCBT_QUALITY_AUTO || p->eqmid < LHDCBT_QUALITY_LOW0)
|
||||||
|
p->eqmid = prev_eqmid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev_eqmid != p->eqmid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_set_props_v5(void *props, const struct spa_pod *param)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LHDC_VERSION_SETUP get_version_v3(const a2dp_lhdc_v3_t *configuration) {
|
||||||
|
if (configuration->llac) {
|
||||||
|
return LLAC;
|
||||||
|
} else if (configuration->lhdc_v4) {
|
||||||
|
return LHDC_V4;
|
||||||
|
} else {
|
||||||
|
return LHDC_V3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_encoder_interval_v3(const a2dp_lhdc_v3_t *configuration) {
|
||||||
|
if (configuration->low_latency) {
|
||||||
|
return 10;
|
||||||
|
} else {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_bit_depth_v3(const a2dp_lhdc_v3_t *configuration) {
|
||||||
|
if (configuration->bit_depth == LHDC_BIT_DEPTH_16) {
|
||||||
|
return 16;
|
||||||
|
} else {
|
||||||
|
return 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_bit_depth_v5(const a2dp_lhdc_v5_t *configuration) {
|
||||||
|
if (configuration->bit_depth == LHDCV5_BIT_DEPTH_16) {
|
||||||
|
return 16;
|
||||||
|
} else {
|
||||||
|
return 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LHDCBT_QUALITY_T get_max_bitrate_v3(const a2dp_lhdc_v3_t *configuration) {
|
||||||
|
if (configuration->max_bit_rate == LHDC_MAX_BIT_RATE_400K) {
|
||||||
|
return LHDCBT_QUALITY_LOW;
|
||||||
|
} else if (configuration->max_bit_rate == LHDC_MAX_BIT_RATE_500K) {
|
||||||
|
return LHDCBT_QUALITY_MID;
|
||||||
|
} else {
|
||||||
|
return LHDCBT_QUALITY_HIGH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_version_setup_v3(const a2dp_lhdc_v3_t *configuration) {
|
||||||
|
if (configuration->llac) {
|
||||||
|
return VERSION_LLAC;
|
||||||
|
} else if (configuration->lhdc_v4) {
|
||||||
|
return VERSION_4;
|
||||||
|
} else {
|
||||||
|
return VERSION_3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *codec_init_v3(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)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this;
|
||||||
|
a2dp_lhdc_v3_t *conf = config;
|
||||||
|
int res;
|
||||||
|
struct props_v3 *p = props;
|
||||||
|
|
||||||
|
this = calloc(1, sizeof(struct impl_v3));
|
||||||
|
if (this == NULL)
|
||||||
|
goto error_errno;
|
||||||
|
|
||||||
|
this->lhdc = lhdcBT_get_handle(get_version_v3(conf));
|
||||||
|
if (this->lhdc == NULL)
|
||||||
|
goto error_errno;
|
||||||
|
|
||||||
|
if (p == NULL) {
|
||||||
|
this->eqmid = LHDCBT_QUALITY_AUTO;
|
||||||
|
} else {
|
||||||
|
this->eqmid = p->eqmid;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mtu = mtu;
|
||||||
|
this->frequency = info->info.raw.rate;
|
||||||
|
this->bit_depth = get_bit_depth_v3(conf);
|
||||||
|
|
||||||
|
lhdcBT_set_hasMinBitrateLimit(this->lhdc, conf->min_bitrate);
|
||||||
|
lhdcBT_set_max_bitrate(this->lhdc, get_max_bitrate_v3(conf));
|
||||||
|
|
||||||
|
res = lhdcBT_init_encoder(this->lhdc,
|
||||||
|
this->frequency,
|
||||||
|
this->bit_depth,
|
||||||
|
this->eqmid,
|
||||||
|
conf->ch_split_mode > LHDC_CH_SPLIT_MODE_NONE,
|
||||||
|
0,
|
||||||
|
this->mtu,
|
||||||
|
get_encoder_interval_v3(conf));
|
||||||
|
if (res < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
tLHDCV3_DEC_CONFIG dec_config = {
|
||||||
|
.version = get_version_setup_v3(conf),
|
||||||
|
.sample_rate = this->frequency,
|
||||||
|
.bits_depth = this->bit_depth,
|
||||||
|
};
|
||||||
|
|
||||||
|
this->dec_initialized = false;
|
||||||
|
|
||||||
|
if (lhdcBT_dec_init_decoder(&dec_config) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
this->dec_initialized = true;
|
||||||
|
|
||||||
|
this->block_size = lhdcBT_get_block_Size(this->lhdc);
|
||||||
|
this->codesize = info->info.raw.channels * lhdcBT_get_block_Size(this->lhdc);
|
||||||
|
|
||||||
|
switch (info->info.raw.format) {
|
||||||
|
case SPA_AUDIO_FORMAT_S32:
|
||||||
|
this->codesize *= 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
res = -EINVAL;
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
|
||||||
|
error_errno:
|
||||||
|
res = -errno;
|
||||||
|
error:
|
||||||
|
if (this && this->lhdc)
|
||||||
|
lhdcBT_free_handle(this->lhdc);
|
||||||
|
free(this);
|
||||||
|
errno = -res;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *codec_init_v5(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)
|
||||||
|
{
|
||||||
|
struct impl_v5 *this;
|
||||||
|
a2dp_lhdc_v5_t *conf = config;
|
||||||
|
int res;
|
||||||
|
|
||||||
|
this = calloc(1, sizeof(struct impl_v5));
|
||||||
|
if (this == NULL)
|
||||||
|
goto error_errno;
|
||||||
|
|
||||||
|
this->mtu = mtu;
|
||||||
|
this->frequency = info->info.raw.rate;
|
||||||
|
this->bit_depth = get_bit_depth_v5(conf);
|
||||||
|
|
||||||
|
tLHDCV5_DEC_CONFIG dec_config = {
|
||||||
|
.version = VERSION_5,
|
||||||
|
.sample_rate = this->frequency,
|
||||||
|
.bits_depth = this->bit_depth,
|
||||||
|
.bit_rate = LHDCV5BT_BIT_RATE_1000K,
|
||||||
|
.lossless_enable = 0,
|
||||||
|
.lossless_raw_enable = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((res = lhdcv5BT_dec_init_decoder(&this->dec, &dec_config)) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
this->frame_samples = (50 * (this->frequency == 44100 ? 48000 : this->frequency)) / 10000;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
error_errno:
|
||||||
|
res = -errno;
|
||||||
|
error:
|
||||||
|
if (this && this->dec)
|
||||||
|
lhdcv5BT_dec_deinit_decoder(this->dec);
|
||||||
|
free(this);
|
||||||
|
errno = -res;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void codec_deinit_v3(void *data)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
if (this->lhdc)
|
||||||
|
lhdcBT_free_handle(this->lhdc);
|
||||||
|
if (this->dec_initialized)
|
||||||
|
lhdcBT_dec_deinit_decoder();
|
||||||
|
free(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void codec_deinit_v5(void *data)
|
||||||
|
{
|
||||||
|
struct impl_v5 *this = data;
|
||||||
|
if (this->dec)
|
||||||
|
lhdcv5BT_dec_deinit_decoder(this->dec);
|
||||||
|
free(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_update_props_v3(void *data, void *props)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
struct props_v3 *p = props;
|
||||||
|
int res;
|
||||||
|
|
||||||
|
if (p == NULL)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
this->eqmid = p->eqmid;
|
||||||
|
|
||||||
|
if ((res = lhdcBT_set_bitrate(this->lhdc, this->eqmid)) < 0)
|
||||||
|
goto error;
|
||||||
|
return 0;
|
||||||
|
error:
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int codec_update_props_v5(void *data, void *props)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_abr_process_v3(void *data, size_t unsent)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
return this->eqmid == LHDCBT_QUALITY_AUTO ? lhdcBT_adjust_bitrate(this->lhdc, unsent / this->mtu) : -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_abr_process_v5(void *data, size_t unsent)
|
||||||
|
{
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_start_encode_v3(void *data,
|
||||||
|
void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
|
||||||
|
this->header = (struct rtp_header *)dst;
|
||||||
|
this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_lhdc_payload);
|
||||||
|
memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_lhdc_payload));
|
||||||
|
|
||||||
|
this->payload->frame_count = 0;
|
||||||
|
this->header->v = 2;
|
||||||
|
this->header->pt = 96;
|
||||||
|
this->header->sequence_number = htons(seqnum);
|
||||||
|
this->header->timestamp = htonl(timestamp);
|
||||||
|
this->header->ssrc = htonl(1);
|
||||||
|
return sizeof(struct rtp_header) + sizeof(struct rtp_lhdc_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_start_encode_v5(void *data,
|
||||||
|
void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
|
||||||
|
{
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deinterleave_32_c2(int32_t * SPA_RESTRICT * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT src, size_t n_samples)
|
||||||
|
{
|
||||||
|
/* We'll trust the compiler to optimize this */
|
||||||
|
const size_t n_channels = 2;
|
||||||
|
size_t i, j;
|
||||||
|
for (j = 0; j < n_samples; ++j)
|
||||||
|
for (i = 0; i < n_channels; ++i)
|
||||||
|
dst[i][j] = *src++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_encode_v3(void *data,
|
||||||
|
const void *src, size_t src_size,
|
||||||
|
void *dst, size_t dst_size,
|
||||||
|
size_t *dst_out, int *need_flush)
|
||||||
|
{
|
||||||
|
struct impl_v3 *this = data;
|
||||||
|
int res, src_used;
|
||||||
|
uint32_t dst_used, frame_num = 0;
|
||||||
|
int32_t *inputs[2] = { this->buf[0], inputs[1] = this->buf[1] };
|
||||||
|
|
||||||
|
src_used = this->codesize;
|
||||||
|
dst_used = dst_size;
|
||||||
|
|
||||||
|
deinterleave_32_c2(inputs, src, this->block_size);
|
||||||
|
|
||||||
|
res = lhdcBT_encode_stereo(this->lhdc, inputs[0], inputs[1], dst, &dst_used, &frame_num);
|
||||||
|
if (SPA_UNLIKELY(res < 0))
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
*dst_out = dst_used;
|
||||||
|
|
||||||
|
this->payload->frame_count += frame_num;
|
||||||
|
|
||||||
|
*need_flush = (this->payload->frame_count > 0) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
|
||||||
|
|
||||||
|
if (this->payload->frame_count > 0)
|
||||||
|
this->payload->seq_num = this->seq_num++;
|
||||||
|
|
||||||
|
return src_used;
|
||||||
|
}
|
||||||
|
static int codec_encode_v5(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 codec_start_decode(void *data,
|
||||||
|
const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
|
||||||
|
{
|
||||||
|
const struct rtp_header *header = src;
|
||||||
|
size_t header_size = sizeof(struct rtp_header);
|
||||||
|
|
||||||
|
spa_return_val_if_fail (src_size > header_size, -EINVAL);
|
||||||
|
|
||||||
|
if (seqnum)
|
||||||
|
*seqnum = ntohs(header->sequence_number);
|
||||||
|
if (timestamp)
|
||||||
|
*timestamp = ntohl(header->timestamp);
|
||||||
|
|
||||||
|
return header_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *dec_errors_v3[] = {
|
||||||
|
[-LHDCBT_DEC_FUNC_SUCCEED] = "OK",
|
||||||
|
[-LHDCBT_DEC_FUNC_FAIL] = "General error",
|
||||||
|
[-LHDCBT_DEC_FUNC_INPUT_NOT_ENOUGH] = "Not enough input data",
|
||||||
|
[-LHDCBT_DEC_FUNC_OUTPUT_NOT_ENOUGH] = "Not enough output space",
|
||||||
|
[-LHDCBT_DEC_FUNC_INVALID_SEQ_NO] = "Invalid sequence number",
|
||||||
|
};
|
||||||
|
|
||||||
|
static int codec_decode_v3(void *data,
|
||||||
|
const void *src, size_t src_size,
|
||||||
|
void *dst, size_t dst_size,
|
||||||
|
size_t *dst_out)
|
||||||
|
{
|
||||||
|
uint32_t decoded = dst_size;
|
||||||
|
uint32_t consumed = 0;
|
||||||
|
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
if ((err = lhdcBT_dec_check_frame_data_enough(src, src_size, &consumed)) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
consumed += sizeof(struct rtp_lhdc_payload);
|
||||||
|
|
||||||
|
if ((err = lhdcBT_dec_decode(src, consumed, dst, &decoded, 24)) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
int32_t *samples = dst;
|
||||||
|
for (size_t i = 0; i < decoded / 4; i++)
|
||||||
|
samples[i] *= (1 << 8);
|
||||||
|
|
||||||
|
if (dst_out)
|
||||||
|
*dst_out = decoded;
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
|
||||||
|
error:
|
||||||
|
spa_log_error(log, "lhdcBT_dec_decode: %s (%d)!", dec_errors_v3[-err], err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *dec_errors_v5[] = {
|
||||||
|
[-LHDCV5BT_DEC_API_SUCCEED] = "OK",
|
||||||
|
[-LHDCV5BT_DEC_API_FAIL] = "General error",
|
||||||
|
[-LHDCV5BT_DEC_API_INVALID_INPUT] = "Invalid input",
|
||||||
|
[-LHDCV5BT_DEC_API_INVALID_OUTPUT] = "Invalid output",
|
||||||
|
[-LHDCV5BT_DEC_API_INVALID_SEQ_NO] = "Invalid sequence number",
|
||||||
|
[-LHDCV5BT_DEC_API_INIT_DECODER_FAIL] = "Decoder initialization error",
|
||||||
|
[-LHDCV5BT_DEC_API_CHANNEL_SETUP_FAIL] = "Channel setup error",
|
||||||
|
[-LHDCV5BT_DEC_API_FRAME_INFO_FAIL] = "Failed to fetch frame info",
|
||||||
|
[-LHDCV5BT_DEC_API_INPUT_NOT_ENOUGH] = "Not enough input data",
|
||||||
|
[-LHDCV5BT_DEC_API_OUTPUT_NOT_ENOUGH] = "Invalid sequence number",
|
||||||
|
[-LHDCV5BT_DEC_API_DECODE_FAIL] = "Decode error",
|
||||||
|
[-LHDCV5BT_DEC_API_ALLOC_MEM_FAIL] = "Out of memory",
|
||||||
|
};
|
||||||
|
|
||||||
|
static int codec_decode_v5(void *data,
|
||||||
|
const void *src, size_t src_size,
|
||||||
|
void *dst, size_t dst_size,
|
||||||
|
size_t *dst_out)
|
||||||
|
{
|
||||||
|
struct impl_v5 *this = data;
|
||||||
|
|
||||||
|
uint32_t decoded = dst_size;
|
||||||
|
uint32_t consumed = 0;
|
||||||
|
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
if ((err = lhdcv5BT_dec_check_frame_data_enough(this->dec, src, src_size, &consumed)) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
consumed += sizeof(struct rtp_lhdc_payload);
|
||||||
|
|
||||||
|
if ((err = lhdcv5BT_dec_decode(this->dec, src, consumed, dst, &decoded, 24)) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
int32_t *samples = dst;
|
||||||
|
for (size_t i = 0; i < decoded / 4; i++)
|
||||||
|
samples[i] *= (1 << 8);
|
||||||
|
|
||||||
|
if (dst_out)
|
||||||
|
*dst_out = decoded;
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
|
||||||
|
error:
|
||||||
|
spa_log_error(log, "lhdcv5BT_dec_decode: %s (%d)!", dec_errors_v5[-err], err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_reduce_bitpool(void *data)
|
||||||
|
{
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int codec_increase_bitpool(void *data)
|
||||||
|
{
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void codec_set_log(struct spa_log *global_log)
|
||||||
|
{
|
||||||
|
log = global_log;
|
||||||
|
spa_log_topic_init(log, &codec_plugin_log_topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct media_codec a2dp_codec_lhdc_v3 = {
|
||||||
|
.id = SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V3,
|
||||||
|
.codec_id = A2DP_CODEC_VENDOR,
|
||||||
|
.vendor = { .vendor_id = LHDC_V3_VENDOR_ID,
|
||||||
|
.codec_id = LHDC_V3_CODEC_ID },
|
||||||
|
.name = "lhdc_v3",
|
||||||
|
.description = "LHDC V3",
|
||||||
|
.fill_caps = codec_fill_caps_v3,
|
||||||
|
.select_config = codec_select_config_v3,
|
||||||
|
.enum_config = codec_enum_config_v3,
|
||||||
|
.init_props = codec_init_props_v3,
|
||||||
|
.enum_props = codec_enum_props_v3,
|
||||||
|
.set_props = codec_set_props_v3,
|
||||||
|
.clear_props = codec_clear_props_v3,
|
||||||
|
.init = codec_init_v3,
|
||||||
|
.deinit = codec_deinit_v3,
|
||||||
|
.update_props = codec_update_props_v3,
|
||||||
|
.get_block_size = codec_get_block_size_v3,
|
||||||
|
.abr_process = codec_abr_process_v3,
|
||||||
|
.start_encode = codec_start_encode_v3,
|
||||||
|
.encode = codec_encode_v3,
|
||||||
|
.start_decode = codec_start_decode,
|
||||||
|
.decode = codec_decode_v3,
|
||||||
|
.reduce_bitpool = codec_reduce_bitpool,
|
||||||
|
.increase_bitpool = codec_increase_bitpool,
|
||||||
|
.set_log = codec_set_log,
|
||||||
|
};
|
||||||
|
|
||||||
|
const struct media_codec a2dp_codec_lhdc_v5 = {
|
||||||
|
.id = SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V5,
|
||||||
|
.codec_id = A2DP_CODEC_VENDOR,
|
||||||
|
.vendor = { .vendor_id = LHDC_V5_VENDOR_ID,
|
||||||
|
.codec_id = LHDC_V5_CODEC_ID },
|
||||||
|
.name = "lhdc_v5",
|
||||||
|
.description = "LHDC V5",
|
||||||
|
.fill_caps = codec_fill_caps_v5,
|
||||||
|
.select_config = codec_select_config_v5,
|
||||||
|
.enum_config = codec_enum_config_v5,
|
||||||
|
.init_props = codec_init_props_v5,
|
||||||
|
.enum_props = codec_enum_props_v5,
|
||||||
|
.set_props = codec_set_props_v5,
|
||||||
|
.clear_props = codec_clear_props_v3,
|
||||||
|
.init = codec_init_v5,
|
||||||
|
.deinit = codec_deinit_v5,
|
||||||
|
.update_props = codec_update_props_v5,
|
||||||
|
.get_block_size = codec_get_block_size_v5,
|
||||||
|
.abr_process = codec_abr_process_v5,
|
||||||
|
.start_encode = codec_start_encode_v5,
|
||||||
|
.encode = codec_encode_v5,
|
||||||
|
.start_decode = codec_start_decode,
|
||||||
|
.decode = codec_decode_v5,
|
||||||
|
.reduce_bitpool = codec_reduce_bitpool,
|
||||||
|
.increase_bitpool = codec_increase_bitpool,
|
||||||
|
.set_log = codec_set_log,
|
||||||
|
};
|
||||||
|
|
||||||
|
MEDIA_CODEC_EXPORT_DEF(
|
||||||
|
"lhdc",
|
||||||
|
&a2dp_codec_lhdc_v3,
|
||||||
|
&a2dp_codec_lhdc_v5
|
||||||
|
);
|
||||||
|
|
@ -32,6 +32,8 @@ static int codec_order(const struct media_codec *c)
|
||||||
{
|
{
|
||||||
static const enum spa_bluetooth_audio_codec order[] = {
|
static const enum spa_bluetooth_audio_codec order[] = {
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_LC3,
|
SPA_BLUETOOTH_AUDIO_CODEC_LC3,
|
||||||
|
SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V5,
|
||||||
|
SPA_BLUETOOTH_AUDIO_CODEC_LHDC_V3,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
|
SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
|
||||||
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
|
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
|
||||||
|
|
@ -184,6 +186,7 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo
|
||||||
MEDIA_CODEC_FACTORY_LIB("aptx"),
|
MEDIA_CODEC_FACTORY_LIB("aptx"),
|
||||||
MEDIA_CODEC_FACTORY_LIB("faststream"),
|
MEDIA_CODEC_FACTORY_LIB("faststream"),
|
||||||
MEDIA_CODEC_FACTORY_LIB("ldac"),
|
MEDIA_CODEC_FACTORY_LIB("ldac"),
|
||||||
|
MEDIA_CODEC_FACTORY_LIB("lhdc"),
|
||||||
MEDIA_CODEC_FACTORY_LIB("sbc"),
|
MEDIA_CODEC_FACTORY_LIB("sbc"),
|
||||||
MEDIA_CODEC_FACTORY_LIB("lc3plus"),
|
MEDIA_CODEC_FACTORY_LIB("lc3plus"),
|
||||||
MEDIA_CODEC_FACTORY_LIB("opus"),
|
MEDIA_CODEC_FACTORY_LIB("opus"),
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,17 @@ if ldac_dep.found()
|
||||||
install_dir : spa_plugindir / 'bluez5')
|
install_dir : spa_plugindir / 'bluez5')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if lhdc_enc_dep.found()
|
||||||
|
lhdc_args = codec_args
|
||||||
|
bluez_codec_ldac = shared_library('spa-codec-bluez5-lhdc',
|
||||||
|
[ 'a2dp-codec-lhdc.c', 'media-codecs.c' ],
|
||||||
|
include_directories : [ configinc ],
|
||||||
|
c_args : lhdc_args,
|
||||||
|
dependencies : [ spa_dep, lhdc_enc_dep, lhdc_dec_dep, lhdc_v5_enc_dep, lhdc_v5_dec_dep ],
|
||||||
|
install : true,
|
||||||
|
install_dir : spa_plugindir / 'bluez5')
|
||||||
|
endif
|
||||||
|
|
||||||
if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
|
if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
|
||||||
bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
|
bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
|
||||||
[ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],
|
[ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,10 @@ struct impl {
|
||||||
struct spa_audio_aec *aec;
|
struct spa_audio_aec *aec;
|
||||||
uint32_t aec_blocksize;
|
uint32_t aec_blocksize;
|
||||||
|
|
||||||
unsigned int capture_ready:1;
|
struct spa_io_position *capture_position;
|
||||||
unsigned int sink_ready:1;
|
struct spa_io_position *sink_position;
|
||||||
|
uint32_t capture_cycle;
|
||||||
|
uint32_t sink_cycle;
|
||||||
|
|
||||||
unsigned int do_disconnect:1;
|
unsigned int do_disconnect:1;
|
||||||
|
|
||||||
|
|
@ -307,13 +309,24 @@ static void process(struct impl *impl)
|
||||||
const float *play_delayed[impl->play_info.channels];
|
const float *play_delayed[impl->play_info.channels];
|
||||||
float *out[impl->out_info.channels];
|
float *out[impl->out_info.channels];
|
||||||
struct spa_data *dd;
|
struct spa_data *dd;
|
||||||
uint32_t i, size;
|
uint32_t i;
|
||||||
uint32_t rindex, pindex, oindex, pdindex, avail;
|
uint32_t rindex, pindex, oindex, pdindex, size;
|
||||||
|
int32_t avail, pavail, pdavail;
|
||||||
|
|
||||||
size = impl->aec_blocksize;
|
size = impl->aec_blocksize;
|
||||||
|
|
||||||
/* First read a block from the playback and capture ring buffers */
|
/* First read a block from the capture ring buffer */
|
||||||
spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
||||||
|
while (avail >= (int32_t)size * 2) {
|
||||||
|
/* drop samples that are not needed this or next cycle. Note
|
||||||
|
* that samples are kept in the ringbuffer until next cycle if
|
||||||
|
* size is not equal to or divisible by quantum, to avoid
|
||||||
|
* discontinuity */
|
||||||
|
pw_log_debug("avail %d", avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->rec_ring, rindex + size);
|
||||||
|
avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
||||||
|
pw_log_debug("new avail %d, size %u", avail, size);
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < impl->rec_info.channels; i++) {
|
for (i = 0; i < impl->rec_info.channels; i++) {
|
||||||
/* captured samples, with echo from sink */
|
/* captured samples, with echo from sink */
|
||||||
|
|
@ -331,19 +344,34 @@ static void process(struct impl *impl)
|
||||||
out[i] = &out_buf[i][0];
|
out[i] = &out_buf[i][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
||||||
spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
||||||
|
|
||||||
if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
|
if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
|
||||||
pw_log_debug("out of playback buffers: %m");
|
pw_log_debug("out of playback buffers: %m");
|
||||||
|
|
||||||
/* playback stream may not yet be in streaming state, drop play
|
/* playback stream may not yet be in streaming state, drop play
|
||||||
* data to avoid introducing additional playback latency */
|
* data to avoid introducing additional playback latency */
|
||||||
spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
|
spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail);
|
||||||
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
|
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail);
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pavail > avail) {
|
||||||
|
/* drop too old samples from previous graph cycles */
|
||||||
|
pw_log_debug("pavail %d, dropping %d", pavail, pavail - avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail - avail);
|
||||||
|
pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
||||||
|
pw_log_debug("new pavail %d, avail %d", pavail, avail);
|
||||||
|
}
|
||||||
|
if (pdavail > avail) {
|
||||||
|
/* drop too old samples from previous graph cycles */
|
||||||
|
pw_log_debug("pdavail %d, dropping %d", pdavail, pdavail - avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail - avail);
|
||||||
|
pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
||||||
|
pw_log_debug("new pdavail %d, avail %d", pdavail, avail);
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < impl->play_info.channels; i++) {
|
for (i = 0; i < impl->play_info.channels; i++) {
|
||||||
/* echo from sink */
|
/* echo from sink */
|
||||||
play[i] = &play_buf[i][0];
|
play[i] = &play_buf[i][0];
|
||||||
|
|
@ -431,7 +459,7 @@ static void process(struct impl *impl)
|
||||||
* available on the source */
|
* available on the source */
|
||||||
|
|
||||||
avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
|
avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
|
||||||
while (avail >= size) {
|
while (avail >= (int32_t)size) {
|
||||||
if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) {
|
if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) {
|
||||||
for (i = 0; i < impl->out_info.channels; i++) {
|
for (i = 0; i < impl->out_info.channels; i++) {
|
||||||
dd = &cout->buffer->datas[i];
|
dd = &cout->buffer->datas[i];
|
||||||
|
|
@ -454,8 +482,8 @@ static void process(struct impl *impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
done:
|
done:
|
||||||
impl->sink_ready = false;
|
impl->capture_cycle = 0;
|
||||||
impl->capture_ready = false;
|
impl->sink_cycle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reset_buffers(struct impl *impl)
|
static void reset_buffers(struct impl *impl)
|
||||||
|
|
@ -479,8 +507,8 @@ static void reset_buffers(struct impl *impl)
|
||||||
spa_ringbuffer_get_read_index(&impl->play_ring, &index);
|
spa_ringbuffer_get_read_index(&impl->play_ring, &index);
|
||||||
spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
|
spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
|
||||||
|
|
||||||
impl->sink_ready = false;
|
impl->capture_cycle = 0;
|
||||||
impl->capture_ready = false;
|
impl->sink_cycle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void capture_destroy(void *d)
|
static void capture_destroy(void *d)
|
||||||
|
|
@ -546,8 +574,11 @@ static void capture_process(void *data)
|
||||||
spa_ringbuffer_write_update(&impl->rec_ring, index + size);
|
spa_ringbuffer_write_update(&impl->rec_ring, index + size);
|
||||||
|
|
||||||
if (avail + size >= impl->aec_blocksize) {
|
if (avail + size >= impl->aec_blocksize) {
|
||||||
impl->capture_ready = true;
|
if (impl->capture_position)
|
||||||
if (impl->sink_ready)
|
impl->capture_cycle = impl->capture_position->clock.cycle;
|
||||||
|
else
|
||||||
|
pw_log_warn("no capture position");
|
||||||
|
if (impl->capture_cycle == impl->sink_cycle)
|
||||||
process(impl);
|
process(impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -740,12 +771,26 @@ static void input_param_changed(void *data, uint32_t id, const struct spa_pod* p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void capture_io_changed(void *data, uint32_t id, void *area, uint32_t size)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_IO_Position:
|
||||||
|
impl->capture_position = area;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const struct pw_stream_events capture_events = {
|
static const struct pw_stream_events capture_events = {
|
||||||
PW_VERSION_STREAM_EVENTS,
|
PW_VERSION_STREAM_EVENTS,
|
||||||
.destroy = capture_destroy,
|
.destroy = capture_destroy,
|
||||||
.state_changed = capture_state_changed,
|
.state_changed = capture_state_changed,
|
||||||
.process = capture_process,
|
.process = capture_process,
|
||||||
.param_changed = input_param_changed
|
.param_changed = input_param_changed,
|
||||||
|
.io_changed = capture_io_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
static void source_destroy(void *d)
|
static void source_destroy(void *d)
|
||||||
|
|
@ -930,10 +975,15 @@ static void sink_process(void *data)
|
||||||
SPA_PTROFF(d->data, offs, void), size);
|
SPA_PTROFF(d->data, offs, void), size);
|
||||||
}
|
}
|
||||||
spa_ringbuffer_write_update(&impl->play_ring, index + size);
|
spa_ringbuffer_write_update(&impl->play_ring, index + size);
|
||||||
|
spa_ringbuffer_get_write_index(&impl->play_delayed_ring, &index);
|
||||||
|
spa_ringbuffer_write_update(&impl->play_delayed_ring, index + size);
|
||||||
|
|
||||||
if (avail + size >= impl->aec_blocksize) {
|
if (avail + size >= impl->aec_blocksize) {
|
||||||
impl->sink_ready = true;
|
if (impl->sink_position)
|
||||||
if (impl->capture_ready)
|
impl->sink_cycle = impl->sink_position->clock.cycle;
|
||||||
|
else
|
||||||
|
pw_log_warn("no sink position");
|
||||||
|
if (impl->capture_cycle == impl->sink_cycle)
|
||||||
process(impl);
|
process(impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -955,12 +1005,27 @@ static const struct pw_stream_events playback_events = {
|
||||||
.state_changed = playback_state_changed,
|
.state_changed = playback_state_changed,
|
||||||
.param_changed = output_param_changed
|
.param_changed = output_param_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void sink_io_changed(void *data, uint32_t id, void *area, uint32_t size)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_IO_Position:
|
||||||
|
impl->sink_position = area;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const struct pw_stream_events sink_events = {
|
static const struct pw_stream_events sink_events = {
|
||||||
PW_VERSION_STREAM_EVENTS,
|
PW_VERSION_STREAM_EVENTS,
|
||||||
.destroy = sink_destroy,
|
.destroy = sink_destroy,
|
||||||
.process = sink_process,
|
.process = sink_process,
|
||||||
.state_changed = sink_state_changed,
|
.state_changed = sink_state_changed,
|
||||||
.param_changed = output_param_changed
|
.param_changed = output_param_changed,
|
||||||
|
.io_changed = sink_io_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
#define MAX_PARAMS 512u
|
#define MAX_PARAMS 512u
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,8 @@ static int update_string(struct pw_properties *props, const char *str, size_t si
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* item changed or added, apply changes later */
|
/* item changed or added, apply changes later */
|
||||||
if ((errno = -add_item(&changes, key, false, val, true) < 0)) {
|
if ((res = add_item(&changes, key, false, val, true)) < 0) {
|
||||||
|
errno = -res;
|
||||||
it[0].state = SPA_JSON_ERROR_FLAG;
|
it[0].state = SPA_JSON_ERROR_FLAG;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ test('test-spa',
|
||||||
executable('test-spa',
|
executable('test-spa',
|
||||||
'test-spa-buffer.c',
|
'test-spa-buffer.c',
|
||||||
'test-spa-control.c',
|
'test-spa-control.c',
|
||||||
|
'test-spa-format.c',
|
||||||
'test-spa-json.c',
|
'test-spa-json.c',
|
||||||
'test-spa-utils.c',
|
'test-spa-utils.c',
|
||||||
'test-spa-log.c',
|
'test-spa-log.c',
|
||||||
|
|
|
||||||
129
test/test-spa-format.c
Normal file
129
test/test-spa-format.c
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/* Simple Plugin API */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
#include <spa/param/audio/format.h>
|
||||||
|
|
||||||
|
#include "pwtest.h"
|
||||||
|
|
||||||
|
PWTEST(audio_format_sizes)
|
||||||
|
{
|
||||||
|
union {
|
||||||
|
uint8_t buf[1024];
|
||||||
|
struct spa_audio_info align;
|
||||||
|
} data;
|
||||||
|
struct spa_audio_info info;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
memset(&info, 0xf3, sizeof(info));
|
||||||
|
info.media_type = SPA_MEDIA_TYPE_audio;
|
||||||
|
info.media_subtype = SPA_MEDIA_SUBTYPE_raw;
|
||||||
|
info.info.raw.channels = 5;
|
||||||
|
info.info.raw.format = SPA_AUDIO_FORMAT_F32P;
|
||||||
|
info.info.raw.rate = 12345;
|
||||||
|
info.info.raw.flags = 0;
|
||||||
|
info.info.raw.position[0] = 1;
|
||||||
|
info.info.raw.position[1] = 2;
|
||||||
|
info.info.raw.position[2] = 3;
|
||||||
|
info.info.raw.position[3] = 4;
|
||||||
|
info.info.raw.position[4] = 5;
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(data.buf); ++i) {
|
||||||
|
struct spa_pod *pod;
|
||||||
|
uint8_t buf[4096];
|
||||||
|
struct spa_pod_builder b;
|
||||||
|
|
||||||
|
spa_pod_builder_init(&b, buf, sizeof(buf));
|
||||||
|
memcpy(data.buf, &info, sizeof(info));
|
||||||
|
|
||||||
|
pod = spa_format_audio_ext_build(&b, 123, (void *)data.buf, i);
|
||||||
|
if (i < offsetof(struct spa_audio_info, info.raw)
|
||||||
|
+ offsetof(struct spa_audio_info_raw, position))
|
||||||
|
pwtest_bool_true(!pod);
|
||||||
|
else
|
||||||
|
pwtest_bool_true(pod);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(data.buf); ++i) {
|
||||||
|
struct spa_pod *pod;
|
||||||
|
uint8_t buf[4096];
|
||||||
|
struct spa_pod_builder b;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
spa_pod_builder_init(&b, buf, sizeof(buf));
|
||||||
|
pod = spa_format_audio_ext_build(&b, 123, &info, sizeof(info));
|
||||||
|
pwtest_bool_true(pod);
|
||||||
|
|
||||||
|
memset(data.buf, 0xf3, sizeof(data.buf));
|
||||||
|
|
||||||
|
ret = spa_format_audio_ext_parse(pod, (void *)data.buf, i);
|
||||||
|
if (i < offsetof(struct spa_audio_info, info.raw)
|
||||||
|
+ offsetof(struct spa_audio_info_raw, position)
|
||||||
|
+ info.info.raw.channels*sizeof(uint32_t)) {
|
||||||
|
for (size_t j = i; j < sizeof(data.buf); ++j)
|
||||||
|
pwtest_int_eq(data.buf[j], 0xf3);
|
||||||
|
pwtest_int_lt(ret, 0);
|
||||||
|
} else {
|
||||||
|
pwtest_int_ge(ret, 0);
|
||||||
|
pwtest_bool_true(memcmp(data.buf, &info, SPA_MIN(i, sizeof(info))) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&info, 0xf3, sizeof(info));
|
||||||
|
info.media_type = SPA_MEDIA_TYPE_audio;
|
||||||
|
info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
|
||||||
|
info.info.aac.rate = 12345;
|
||||||
|
info.info.aac.channels = 6;
|
||||||
|
info.info.aac.bitrate = 54321;
|
||||||
|
info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM;
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(data.buf); ++i) {
|
||||||
|
struct spa_pod *pod;
|
||||||
|
uint8_t buf[4096];
|
||||||
|
struct spa_pod_builder b;
|
||||||
|
|
||||||
|
spa_pod_builder_init(&b, buf, sizeof(buf));
|
||||||
|
memcpy(data.buf, &info, sizeof(info));
|
||||||
|
|
||||||
|
pod = spa_format_audio_ext_build(&b, 123, (void *)data.buf, i);
|
||||||
|
if (i < offsetof(struct spa_audio_info, info.raw)
|
||||||
|
+ sizeof(struct spa_audio_info_aac))
|
||||||
|
pwtest_bool_true(!pod);
|
||||||
|
else
|
||||||
|
pwtest_bool_true(pod);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(data.buf); ++i) {
|
||||||
|
struct spa_pod *pod;
|
||||||
|
uint8_t buf[4096];
|
||||||
|
struct spa_pod_builder b;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
spa_pod_builder_init(&b, buf, sizeof(buf));
|
||||||
|
pod = spa_format_audio_ext_build(&b, 123, &info, sizeof(info));
|
||||||
|
pwtest_bool_true(pod);
|
||||||
|
|
||||||
|
memset(data.buf, 0xf3, sizeof(data.buf));
|
||||||
|
|
||||||
|
ret = spa_format_audio_ext_parse(pod, (void *)data.buf, i);
|
||||||
|
if (i < offsetof(struct spa_audio_info, info.raw)
|
||||||
|
+ sizeof(struct spa_audio_info_aac)) {
|
||||||
|
for (size_t j = i; j < sizeof(data.buf); ++j)
|
||||||
|
pwtest_int_eq(data.buf[j], 0xf3);
|
||||||
|
pwtest_int_lt(ret, 0);
|
||||||
|
} else {
|
||||||
|
pwtest_int_ge(ret, 0);
|
||||||
|
pwtest_bool_true(memcmp(data.buf, &info, SPA_MIN(i, sizeof(info))) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PWTEST_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PWTEST_SUITE(spa_format)
|
||||||
|
{
|
||||||
|
pwtest_add(audio_format_sizes, PWTEST_NOARG);
|
||||||
|
|
||||||
|
return PWTEST_PASS;
|
||||||
|
}
|
||||||
|
|
@ -609,7 +609,7 @@ static void test_array(const char *str, const char * const vals[])
|
||||||
|
|
||||||
spa_json_init(&it[0], str, strlen(str));
|
spa_json_init(&it[0], str, strlen(str));
|
||||||
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
|
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
|
||||||
spa_json_init(&it[1], str, strlen(str));
|
spa_json_init_relax(&it[1], '[', str, strlen(str));
|
||||||
for (i = 0; vals[i]; i++) {
|
for (i = 0; vals[i]; i++) {
|
||||||
pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
|
pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
|
||||||
pwtest_str_eq(val, vals[i]);
|
pwtest_str_eq(val, vals[i]);
|
||||||
|
|
@ -624,6 +624,7 @@ PWTEST(json_array)
|
||||||
test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL });
|
test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL });
|
||||||
test_array("FL FR", (const char *[]){ "FL", "FR", NULL });
|
test_array("FL FR", (const char *[]){ "FL", "FR", NULL });
|
||||||
test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL });
|
test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL });
|
||||||
|
test_array("FL FR FC", (const char *[]){ "FL", "FR", "FC", NULL });
|
||||||
|
|
||||||
return PWTEST_PASS;
|
return PWTEST_PASS;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue