Compare commits

...

18 commits

Author SHA1 Message Date
anonymix007
9f830293b5 Merge branch 'lhdc' into 'master'
Draft: bluez5: add LHDC V3 A2DP vendor codec

See merge request pipewire/pipewire!1786
2025-10-27 19:41:59 +00:00
Jonas Holmberg
76a31a47c2 module-echo-cancel: Avoid discontinuity
Keep the samples in the ringbuffer that are needed the next cycle to
avoid discontinuity when the aec blocksize is not equal to or divisible
by quantum.
2025-10-27 14:39:29 +01:00
Wim Taymans
23c449af5d test: add test for an array with odd number of items
We have to use the relax version to get the expected container type
correct.
2025-10-27 14:20:25 +01:00
Wim Taymans
94d0d8bc09 spa: add spa_json_init_relax
spa_json_init assumes that we start in an object and always requires a
key/value pair. If the last part is a key, it returns and error and does
not want to return the key value.

This causes problems when parsing AUX0,AUX1,AUX2 or any relaxed array
withand odd number of elements.

Make a new spa_json_init_relax that takes the type of the container
we're assuming we're in and set the state of the parser to array when we
are parsing a relaxed array.

Fixes #4944
2025-10-27 13:32:03 +01:00
Wim Taymans
0276bb5b06 modules: ringbuffer avail is signed 2025-10-27 11:43:04 +01:00
Jonas Holmberg
614186a590 module-echo-cancel: Sync capture and sink buffers
Call process() when capture and sink ringbuffers contain data from the
same graph cycle and only process the latest block from them to avoid
adding latency that can accumulate if one of the streams gets more than
one buffer before the other gets its first buffer when starting up.
2025-10-27 08:43:08 +01:00
Pauli Virtanen
c6d0b364ab spa: param: add size checks for spa_audio_info* structs
In the API that take struct size for spa_audio_info*, also check the
struct size.
2025-10-26 18:23:17 +02:00
Pauli Virtanen
8a23b13798 spa: param: pass correct struct size to spa_format_audio_raw_ext_parse/build 2025-10-26 17:44:03 +02:00
Pauli Virtanen
3d08c0557f properties: fix assign + conditional expression 2025-10-26 14:12:19 +00:00
Pauli Virtanen
68dc45cc62 audioconvert: simplify volume ramp generation
Don't use floating point accumulators, interpolate from sample position.
2025-10-26 14:12:19 +00:00
Pauli Virtanen
b0e308e0dc spa: examples: fix getopt usage + typos in adapter-control 2025-10-26 14:12:19 +00:00
Pauli Virtanen
fe2c62b9b1 meson.build: set project cc flags also for native builds
Use the build flags also for all native build targets.
Avoids spurious warnings in spa-json-dump
2025-10-26 14:12:19 +00:00
Pauli Virtanen
3febf09b85 alsa: fix typoed braces in condition + assign 2025-10-26 14:12:19 +00:00
Pauli Virtanen
93495d3a75 spa: param: infer raw audio channels from position if unset
The behavior before b8eeb2db45 was that spa_audio_info_raw_update()
always sets audio.channels when audio.position is updated.  The new
behavior does not set audio.channels when parsing audio.position.

This breaks things e.g. when combine-sink and loopback nodes are created
with only audio.position specified.

Restore the previous behavior.
2025-10-26 15:47:48 +02:00
Pauli Virtanen
9f1a149876 ci: add file package, for coverity
Try to fix coverity by adding missing 'file' package to container
2025-10-25 12:42:45 +03:00
anonymix007
f2b3b63f21 bluez5: add LHDC V5 A2DP decoder 2025-04-20 18:20:26 +03:00
anonymix007
a9ba34da23 bluez5: add LHDC V3 A2DP decoder 2025-04-19 18:12:14 +03:00
anonymix007
87cb3ea4a1 bluez5: add LHDC V3 A2DP vendor codec 2025-04-19 18:09:20 +03:00
26 changed files with 1435 additions and 92 deletions

View file

@ -38,7 +38,7 @@ include:
.fedora:
variables:
# 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_PACKAGES: >-
alsa-lib-devel
@ -48,6 +48,7 @@ include:
dbus-devel
doxygen
fdk-aac-free-devel
file
findutils
gcc
gcc-c++

View file

@ -118,8 +118,8 @@ cc_flags = common_flags + [
'-DSPA_AUDIO_MAX_CHANNELS=128u',
]
add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
cc_flags_native = cc_native.get_supported_arguments(cc_flags)
add_project_arguments(cc_native.get_supported_arguments(cc_flags),
language: 'c', native: true)
have_cpp = add_languages('cpp', native: false, required : false)

View file

@ -133,6 +133,10 @@ option('bluez5-codec-ldac-dec',
description: 'Enable LDAC Sony open source codec decoding',
type: 'feature',
value: 'auto')
option('bluez5-codec-lhdc',
description: 'Enable LHDC open source codec implementation',
type: 'feature',
value: 'auto')
option('bluez5-codec-aac',
description: 'Enable Fraunhofer FDK AAC open source codec implementation',
type: 'feature',

View file

@ -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_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
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);
return res;
}
@ -647,7 +647,7 @@ static int make_nodes(struct data *data)
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);
return res;
}
@ -987,7 +987,7 @@ int main(int argc, char *argv[])
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) {
case 'h':
show_help(&data, argv[0], false);

View file

@ -46,20 +46,61 @@ extern "C" {
#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_format_audio_ext_parse(const struct spa_pod *format, struct spa_audio_info *info, size_t size)
{
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;
if (info->media_type != SPA_MEDIA_TYPE_audio)
if (media_type != SPA_MEDIA_TYPE_audio)
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:
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:
return spa_format_audio_dsp_parse(format, &info->info.dsp);
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,
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) {
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:
return spa_format_audio_dsp_build(builder, id, &info->info.dsp);
case SPA_MEDIA_SUBTYPE_iec958:

View file

@ -59,6 +59,8 @@ struct spa_audio_info {
struct spa_audio_info_dts dts;
struct spa_audio_info_mpegh mpegh;
} info;
/* padding follows here when info has flexible size */
};
/**

View file

@ -60,6 +60,10 @@ spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size,
{
uint32_t v;
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 (force || info->format == 0)
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) {
if (v > max_position)
return -ECHRNG;
info->channels = v;
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;
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size))
return -EINVAL;
memset(info, 0, size);
SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
if (dict) {

View file

@ -35,6 +35,9 @@ spa_format_audio_raw_ext_parse(const struct spa_pod *format, struct spa_audio_in
int res;
uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size);
if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size))
return -EINVAL;
info->flags = 0;
res = spa_pod_parse_object(format,
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;
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_add(builder,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),

View file

@ -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_VALID_SIZE(size) ((size) >= offsetof(struct spa_audio_info_raw, position))
#define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string,
* Ex. "S16LE" */

View file

@ -25,6 +25,8 @@ enum spa_bluetooth_audio_codec {
SPA_BLUETOOTH_AUDIO_CODEC_APTX,
SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
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_DUPLEX,
SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,

View file

@ -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_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_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_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 },

View file

@ -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);
}
#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 })
SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub)

View file

@ -105,7 +105,7 @@ SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter,
spa_json_init(iter, data, size);
res = spa_json_enter_container(iter, iter, type);
if (res == -EPROTO && relax)
spa_json_init(iter, data, size);
spa_json_init_relax(iter, type, data, size);
else if (res <= 0)
return res;
return 1;

View file

@ -106,6 +106,42 @@ if get_option('spa-plugins').allowed()
endif
endif
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()
error('bluez5-codec-opus enabled, but opus dependency not found')
endif

View file

@ -773,7 +773,7 @@ static void bind_ctl_event(struct spa_source *source)
snd_ctl_elem_id_alloca(&bound_id);
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;
if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM)

View file

@ -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;
spa_log_info(this->log, "volume ramp samples calculated from time is %d", samples);
}
if (!samples)
samples = -1;
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)
samples = vrp->volume_ramp_step_samples;
else if (vrp->volume_ramp_step_time) {
/* convert the step time which is in nano seconds to seconds */
samples = (vrp->volume_ramp_step_time/1000) * (vrp->rate/1000);
/* convert the step time which is in nano seconds to seconds, round up */
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);
}
if (!samples)
samples = -1;
return samples;
}
@ -1568,76 +1564,52 @@ static float get_volume_at_scale(struct volume_ramp_params *vrp, float value)
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)
{
struct spa_pod_dynamic_builder b;
struct spa_pod_frame f[1];
float start = vrp->start, end = vrp->end, volume_accum = start;
int ramp_samples = get_ramp_samples(this, vrp);
int ramp_step_samples = get_ramp_step_samples(this, vrp);
float volume_step = ((end - start) / (ramp_samples / ramp_step_samples));
uint32_t volume_offs = 0;
float start = vrp->start, end = vrp->end;
int samples = get_ramp_samples(this, vrp);
int step = get_ramp_step_samples(this, vrp);
int offs = 0;
if (samples < 0 || step < 0 || (samples > 0 && step == 0))
return NULL;
spa_pod_dynamic_builder_init(&b, buffer, size, 4096);
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"
" 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_log_info(this->log, "generating ramp sequence from %f to %f with "
"step %d/%d at scale %d", start, end, step, samples, vrp->scale);
while (1) {
float pos = (samples == 0) ? end :
SPA_CLAMP(start + (end - start) * offs / samples,
SPA_MIN(start, end), SPA_MAX(start, end));
float vas = get_volume_at_scale(vrp, pos);
spa_log_trace(this->log, "volume %d accum %f", offs, vas);
spa_pod_builder_control(&b.b, 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]);
if (offs >= samples)
break;
offs = SPA_MIN(samples, offs + step);
}
static struct spa_pod *generate_ramp_down_seq(struct impl *this, struct volume_ramp_params *vrp,
void *buffer, size_t size)
{
struct spa_pod_dynamic_builder b;
struct spa_pod_frame f[1];
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_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_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 void generate_volume_ramp(struct impl *this, struct volume_ramp_params *vrp,
void *buffer, size_t size)
{
void *sequence = NULL;
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);
void *sequence;
sequence = generate_ramp_seq(this, vrp, buffer, size);
if (!sequence)
spa_log_error(this->log, "unable to generate sequence");

View file

@ -125,7 +125,7 @@ sparesampledumpcoeffs_sources = [
sparesampledumpcoeffs = executable(
'spa-resample-dump-coeffs',
sparesampledumpcoeffs_sources,
c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ],
c_args : [ '-DRESAMPLE_DISABLE_PRECOMP' ],
dependencies : [ spa_dep, mathlib_native ],
install : false,
native : true,

View file

@ -194,6 +194,53 @@
#define LDAC_SAMPLING_FREQ_176400 0x02
#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_CODEC_ID 0x0001
@ -378,6 +425,46 @@ typedef struct {
uint8_t source_frequency:4;
} __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
typedef struct {

View 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
);

View file

@ -32,6 +32,8 @@ static int codec_order(const struct media_codec *c)
{
static const enum spa_bluetooth_audio_codec order[] = {
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_APTX_HD,
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("faststream"),
MEDIA_CODEC_FACTORY_LIB("ldac"),
MEDIA_CODEC_FACTORY_LIB("lhdc"),
MEDIA_CODEC_FACTORY_LIB("sbc"),
MEDIA_CODEC_FACTORY_LIB("lc3plus"),
MEDIA_CODEC_FACTORY_LIB("opus"),

View file

@ -152,6 +152,17 @@ if ldac_dep.found()
install_dir : spa_plugindir / 'bluez5')
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()
bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
[ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],

View file

@ -229,8 +229,10 @@ struct impl {
struct spa_audio_aec *aec;
uint32_t aec_blocksize;
unsigned int capture_ready:1;
unsigned int sink_ready:1;
struct spa_io_position *capture_position;
struct spa_io_position *sink_position;
uint32_t capture_cycle;
uint32_t sink_cycle;
unsigned int do_disconnect:1;
@ -307,13 +309,24 @@ static void process(struct impl *impl)
const float *play_delayed[impl->play_info.channels];
float *out[impl->out_info.channels];
struct spa_data *dd;
uint32_t i, size;
uint32_t rindex, pindex, oindex, pdindex, avail;
uint32_t i;
uint32_t rindex, pindex, oindex, pdindex, size;
int32_t avail, pavail, pdavail;
size = impl->aec_blocksize;
/* First read a block from the playback and capture ring buffers */
spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
/* First read a block from the capture ring buffer */
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++) {
/* captured samples, with echo from sink */
@ -331,19 +344,34 @@ static void process(struct impl *impl)
out[i] = &out_buf[i][0];
}
spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
pw_log_debug("out of playback buffers: %m");
/* playback stream may not yet be in streaming state, drop play
* data to avoid introducing additional playback latency */
spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail);
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail);
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++) {
/* echo from sink */
play[i] = &play_buf[i][0];
@ -431,7 +459,7 @@ static void process(struct impl *impl)
* available on the source */
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) {
for (i = 0; i < impl->out_info.channels; i++) {
dd = &cout->buffer->datas[i];
@ -454,8 +482,8 @@ static void process(struct impl *impl)
}
done:
impl->sink_ready = false;
impl->capture_ready = false;
impl->capture_cycle = 0;
impl->sink_cycle = 0;
}
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_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
impl->sink_ready = false;
impl->capture_ready = false;
impl->capture_cycle = 0;
impl->sink_cycle = 0;
}
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);
if (avail + size >= impl->aec_blocksize) {
impl->capture_ready = true;
if (impl->sink_ready)
if (impl->capture_position)
impl->capture_cycle = impl->capture_position->clock.cycle;
else
pw_log_warn("no capture position");
if (impl->capture_cycle == impl->sink_cycle)
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 = {
PW_VERSION_STREAM_EVENTS,
.destroy = capture_destroy,
.state_changed = capture_state_changed,
.process = capture_process,
.param_changed = input_param_changed
.param_changed = input_param_changed,
.io_changed = capture_io_changed
};
static void source_destroy(void *d)
@ -930,10 +975,15 @@ static void sink_process(void *data)
SPA_PTROFF(d->data, offs, void), 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) {
impl->sink_ready = true;
if (impl->capture_ready)
if (impl->sink_position)
impl->sink_cycle = impl->sink_position->clock.cycle;
else
pw_log_warn("no sink position");
if (impl->capture_cycle == impl->sink_cycle)
process(impl);
}
@ -955,12 +1005,27 @@ static const struct pw_stream_events playback_events = {
.state_changed = playback_state_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 = {
PW_VERSION_STREAM_EVENTS,
.destroy = sink_destroy,
.process = sink_process,
.state_changed = sink_state_changed,
.param_changed = output_param_changed
.param_changed = output_param_changed,
.io_changed = sink_io_changed
};
#define MAX_PARAMS 512u

View file

@ -252,7 +252,8 @@ static int update_string(struct pw_properties *props, const char *str, size_t si
continue;
}
/* 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;
break;
}

View file

@ -112,6 +112,7 @@ test('test-spa',
executable('test-spa',
'test-spa-buffer.c',
'test-spa-control.c',
'test-spa-format.c',
'test-spa-json.c',
'test-spa-utils.c',
'test-spa-log.c',

129
test/test-spa-format.c Normal file
View 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;
}

View file

@ -609,7 +609,7 @@ static void test_array(const char *str, const char * const vals[])
spa_json_init(&it[0], str, strlen(str));
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++) {
pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
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 FC", (const char *[]){ "FL", "FR", "FC", NULL });
return PWTEST_PASS;
}