Merge branch 'lhdc' into 'master'

Draft: bluez5: add LHDC V3 A2DP vendor codec

See merge request pipewire/pipewire!1786
This commit is contained in:
anonymix007 2025-10-27 19:41:59 +00:00
commit 9f830293b5
8 changed files with 1097 additions and 0 deletions

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

@ -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

@ -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

@ -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' ],