diff --git a/.github/spellcheck-wordlist.txt b/.github/spellcheck-wordlist.txt index 20fab1fef..b9fed88a5 100644 --- a/.github/spellcheck-wordlist.txt +++ b/.github/spellcheck-wordlist.txt @@ -2,6 +2,7 @@ AAC ABR ADDR +AOSP AVRCP BT CBR diff --git a/configure.ac b/configure.ac index 99be7217b..a1b9ae825 100644 --- a/configure.ac +++ b/configure.ac @@ -183,6 +183,15 @@ AM_COND_IF([ENABLE_LDAC], [ AC_DEFINE([ENABLE_LDAC], [1], [Define to 1 if LDAC is enabled.]) ]) +AC_ARG_ENABLE([lhdc], + [AS_HELP_STRING([--enable-lhdc], [enable LHDC support])]) +AM_CONDITIONAL([ENABLE_LHDC], [test "x$enable_lhdc" = "xyes"]) +AM_COND_IF([ENABLE_LHDC], [ + AC_DEFINE([ENABLE_LHDC], [1], [Define to 1 if LHDC is enabled.]) + PKG_CHECK_MODULES([LHDC_DEC], [ldhcBT-dec >= 4.0.2]) + PKG_CHECK_MODULES([LHDC_ENC], [lhdcBT-enc >= 4.0.6]) +]) + AC_ARG_ENABLE([mp3lame], [AS_HELP_STRING([--enable-mp3lame], [enable MP3 support])]) AM_CONDITIONAL([ENABLE_MP3LAME], [test "x$enable_mp3lame" = "xyes"]) diff --git a/src/Makefile.am b/src/Makefile.am index d3160170c..ef464813a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -83,6 +83,11 @@ bluealsa_SOURCES += \ a2dp-ldac.c endif +if ENABLE_LHDC +bluealsa_SOURCES += \ + a2dp-lhdc.c +endif + if ENABLE_MPEG bluealsa_SOURCES += \ a2dp-mpeg.c @@ -114,6 +119,8 @@ AM_CFLAGS = \ @LDAC_ABR_CFLAGS@ \ @LDAC_DEC_CFLAGS@ \ @LDAC_ENC_CFLAGS@ \ + @LHDC_DEC_CFLAGS@ \ + @LHDC_ENC_CFLAGS@ \ @LIBBSD_CFLAGS@ \ @LIBUNWIND_CFLAGS@ \ @MPG123_CFLAGS@ \ @@ -131,6 +138,8 @@ LDADD = \ @LDAC_ABR_LIBS@ \ @LDAC_DEC_LIBS@ \ @LDAC_ENC_LIBS@ \ + @LHDC_DEC_LIBS@ \ + @LHDC_ENC_LIBS@ \ @LIBUNWIND_LIBS@ \ @MP3LAME_LIBS@ \ @MPG123_LIBS@ \ diff --git a/src/a2dp-lhdc.c b/src/a2dp-lhdc.c new file mode 100644 index 000000000..4b878c564 --- /dev/null +++ b/src/a2dp-lhdc.c @@ -0,0 +1,535 @@ +/* + * BlueALSA - a2dp-lhdc.c + * Copyright (c) 2016-2023 Arkadiusz Bokowy + * Copyright (c) 2023 anonymix007 + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#include "a2dp-lhdc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "a2dp.h" +#include "audio.h" +#include "ba-transport.h" +#include "ba-transport-pcm.h" +#include "bluealsa-config.h" +#include "io.h" +#include "rtp.h" +#include "utils.h" +#include "shared/a2dp-codecs.h" +#include "shared/defs.h" +#include "shared/ffb.h" +#include "shared/log.h" +#include "shared/rt.h" + +static LHDC_VERSION_SETUP get_version(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(const a2dp_lhdc_v3_t *configuration) { + if (configuration->low_latency) { + return 10; + } else { + return 20; + } +} + +static int get_bit_depth(const a2dp_lhdc_v3_t *configuration) { + if (configuration->bit_depth == LHDC_BIT_DEPTH_16) { + return 16; + } else { + return 24; + } +} + +static LHDCBT_QUALITY_T get_max_bitrate(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; + } +} + +void *a2dp_lhdc_enc_thread(struct ba_transport_pcm *t_pcm) { + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_pcm_thread_cleanup), t_pcm); + + struct ba_transport *t = t_pcm->t; + struct io_poll io = { .timeout = -1 }; + + const a2dp_lhdc_v3_t *configuration = &t->a2dp.configuration.lhdc_v3; + + HANDLE_LHDC_BT handle; + if ((handle = lhdcBT_get_handle(get_version(configuration))) == NULL) { + error("Couldn't get LHDC handle: %s", strerror(errno)); + goto fail_open_lhdc; + } + + pthread_cleanup_push(PTHREAD_CLEANUP(lhdcBT_free_handle), handle); + + const unsigned int channels = t_pcm->channels; + const unsigned int samplerate = t_pcm->sampling; + const unsigned int bitdepth = get_bit_depth(configuration); + + lhdcBT_set_hasMinBitrateLimit(handle, configuration->min_bit_rate); + lhdcBT_set_max_bitrate(handle, get_max_bitrate(configuration)); + + if (lhdcBT_init_encoder(handle, samplerate, bitdepth, config.lhdc_eqmid, + configuration->ch_split_mode > LHDC_CH_SPLIT_MODE_NONE, 0, t->mtu_write, + get_encoder_interval(configuration)) == -1) { + error("Couldn't initialize LHDC encoder"); + goto fail_init; + } + + const size_t lhdc_ch_samples = lhdcBT_get_block_Size(handle); + const size_t lhdc_pcm_samples = lhdc_ch_samples * channels; + + ffb_t bt = { 0 }; + ffb_t pcm = { 0 }; + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt); + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &pcm); + + int32_t *pcm_ch1 = malloc(lhdc_ch_samples * sizeof(int32_t)); + int32_t *pcm_ch2 = malloc(lhdc_ch_samples * sizeof(int32_t)); + pthread_cleanup_push(PTHREAD_CLEANUP(free), pcm_ch1); + pthread_cleanup_push(PTHREAD_CLEANUP(free), pcm_ch2); + + if (ffb_init_int32_t(&pcm, lhdc_pcm_samples) == -1 || + ffb_init_uint8_t(&bt, t->mtu_write) == -1) { + error("Couldn't create data buffers: %s", strerror(errno)); + goto fail_ffb; + } + + rtp_header_t *rtp_header; + struct { + uint8_t seq_num; + uint8_t latency:2; + uint8_t frames:6; + } *lhdc_media_header; + /* initialize RTP headers and get anchor for payload */ + uint8_t *rtp_payload = rtp_a2dp_init(bt.data, &rtp_header, + (void **)&lhdc_media_header, sizeof(*lhdc_media_header)); + + struct rtp_state rtp = { .synced = false }; + /* RTP clock frequency equal to audio samplerate */ + rtp_state_init(&rtp, samplerate, samplerate); + + uint8_t seq_num = 0; + + debug_transport_pcm_thread_loop(t_pcm, "START"); + for (ba_transport_pcm_state_set_running(t_pcm);;) { + + ssize_t samples = ffb_len_in(&pcm); + switch (samples = io_poll_and_read_pcm(&io, t_pcm, pcm.tail, samples)) { + case -1: + if (errno == ESTALE) { + ffb_rewind(&pcm); + continue; + } + error("PCM poll and read error: %s", strerror(errno)); + /* fall-through */ + case 0: + ba_transport_stop_if_no_clients(t); + continue; + } + + ffb_seek(&pcm, samples); + samples = ffb_len_out(&pcm); + + int *input = pcm.data; + size_t input_len = samples; + + /* encode and transfer obtained data */ + while (input_len >= lhdc_pcm_samples) { + + /* anchor for RTP payload */ + bt.tail = rtp_payload; + + audio_deinterleave_s32_4le(input, lhdc_ch_samples, channels, pcm_ch1, pcm_ch2); + + uint32_t encoded; + uint32_t frames; + + if (lhdcBT_encode_stereo(handle, pcm_ch1, pcm_ch2, bt.tail, &encoded, &frames) < 0) { + error("LHDC encoding error"); + break; + } + + input += lhdc_pcm_samples; + input_len -= lhdc_pcm_samples; + ffb_seek(&bt, encoded); + + if (encoded > 0) { + + lhdc_media_header->seq_num = seq_num++; + lhdc_media_header->latency = 0; + lhdc_media_header->frames = frames; + + rtp_state_new_frame(&rtp, rtp_header); + + /* Try to get the number of bytes queued in the + * socket output buffer. */ + int queued_bytes = 0; + if (ioctl(t->bt_fd, TIOCOUTQ, &queued_bytes) != -1) + queued_bytes = abs(t->a2dp.bt_fd_coutq_init - queued_bytes); + + errno = 0; + + ssize_t len = ffb_blen_out(&bt); + if ((len = io_bt_write(t_pcm, bt.data, len)) <= 0) { + if (len == -1) + error("BT write error: %s", strerror(errno)); + goto fail; + } + + if (errno == EAGAIN) + /* The io_bt_write() call was blocking due to not enough + * space in the BT socket. Set the queued_bytes to some + * arbitrary big value. */ + queued_bytes = 1024 * 16; + + if (config.lhdc_eqmid == LHDCBT_QUALITY_AUTO) + lhdcBT_adjust_bitrate(handle, queued_bytes / t->mtu_write); + } + + unsigned int pcm_frames = lhdc_pcm_samples / channels; + /* keep data transfer at a constant bit rate */ + asrsync_sync(&io.asrs, pcm_frames); + /* move forward RTP timestamp clock */ + rtp_state_update(&rtp, pcm_frames); + + /* update busy delay (encoding overhead) */ + t_pcm->delay = asrsync_get_busy_usec(&io.asrs) / 100; + + } + + /* If the input buffer was not consumed (due to codesize limit), we + * have to append new data to the existing one. Since we do not use + * ring buffer, we will simply move unprocessed data to the front + * of our linear buffer. */ + ffb_shift(&pcm, samples - input_len); + + } + +fail: + debug_transport_pcm_thread_loop(t_pcm, "EXIT"); +fail_ffb: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_init: + pthread_cleanup_pop(1); +fail_open_lhdc: + pthread_cleanup_pop(1); + return NULL; +} + +static const int versions[5] = { + [LHDC_V2] = VERSION_2, + [LHDC_V3] = VERSION_3, + [LHDC_V4] = VERSION_4, + [LLAC] = VERSION_LLAC, +}; + +void *a2dp_lhdc_dec_thread(struct ba_transport_pcm *t_pcm) { + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_pcm_thread_cleanup), t_pcm); + + struct ba_transport *t = t_pcm->t; + struct io_poll io = { .timeout = -1 }; + + const a2dp_lhdc_v3_t *configuration = &t->a2dp.configuration.lhdc_v3; + const size_t sample_size = BA_TRANSPORT_PCM_FORMAT_BYTES(t_pcm->format); + const unsigned int channels = t_pcm->channels; + const unsigned int samplerate = t_pcm->sampling; + const unsigned int bitdepth = get_bit_depth(configuration); + + tLHDCV3_DEC_CONFIG dec_config = { + .version = versions[get_version(configuration)], + .sample_rate = samplerate, + .bits_depth = bitdepth, + }; + + if (lhdcBT_dec_init_decoder(&dec_config) < 0) { + error("Couldn't initialise LHDC decoder: %s", strerror(errno)); + goto fail_open; + } + + pthread_cleanup_push(PTHREAD_CLEANUP(lhdcBT_dec_deinit_decoder), NULL); + + ffb_t bt = { 0 }; + ffb_t pcm = { 0 }; + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt); + pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &pcm); + + if (ffb_init_int32_t(&pcm, 16 * 256 * channels) == -1 || + ffb_init_uint8_t(&bt, t->mtu_read) == -1) { + error("Couldn't create data buffers: %s", strerror(errno)); + goto fail_ffb; + } + + struct rtp_state rtp = { .synced = false }; + /* RTP clock frequency equal to audio samplerate */ + rtp_state_init(&rtp, samplerate, samplerate); + + debug_transport_pcm_thread_loop(t_pcm, "START"); + for (ba_transport_pcm_state_set_running(t_pcm);;) { + + ssize_t len = ffb_blen_in(&bt); + if ((len = io_poll_and_read_bt(&io, t_pcm, bt.data, len)) <= 0) { + if (len == -1) + error("BT poll and read error: %s", strerror(errno)); + goto fail; + } + + const rtp_header_t *rtp_header = bt.data; + const void *lhdc_media_header; + if ((lhdc_media_header = rtp_a2dp_get_payload(rtp_header)) == NULL) + continue; + + int missing_rtp_frames = 0; + rtp_state_sync_stream(&rtp, rtp_header, &missing_rtp_frames, NULL); + + if (!ba_transport_pcm_is_active(t_pcm)) { + rtp.synced = false; + continue; + } + + const uint8_t *rtp_payload = (uint8_t *) lhdc_media_header; + size_t rtp_payload_len = len - (rtp_payload - (uint8_t *)bt.data); + + uint32_t decoded = 16 * 256 * sizeof(int32_t) * channels; + + lhdcBT_dec_decode(rtp_payload, rtp_payload_len, pcm.data, &decoded, 24); + + const size_t samples = decoded / sample_size; + io_pcm_scale(t_pcm, pcm.data, samples); + if (io_pcm_write(t_pcm, pcm.data, samples) == -1) + error("FIFO write error: %s", strerror(errno)); + + /* update local state with decoded PCM frames */ + rtp_state_update(&rtp, samples / channels); + + } + +fail: + debug_transport_pcm_thread_loop(t_pcm, "EXIT"); +fail_ffb: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); +fail_open: + pthread_cleanup_pop(1); + return NULL; +} + +static const struct a2dp_channel_mode a2dp_lhdc_channels[] = { + { A2DP_CHM_STEREO, 2, LHDC_CHANNEL_MODE_STEREO }, + { 0 }, +}; + +static const struct a2dp_sampling a2dp_lhdc_samplings[] = { + { 44100, LHDC_SAMPLING_FREQ_44100 }, + { 48000, LHDC_SAMPLING_FREQ_48000 }, + { 96000, LHDC_SAMPLING_FREQ_96000 }, + { 0 }, +}; + +static int a2dp_lhdc_configuration_select( + const struct a2dp_codec *codec, + void *capabilities) { + + warn("LHDC: LLAC/V3/V4 switch logic is not implemented"); + + a2dp_lhdc_v3_t *caps = capabilities; + const a2dp_lhdc_v3_t saved = *caps; + + /* narrow capabilities to values supported by BlueALSA */ + if (a2dp_filter_capabilities(codec, &codec->capabilities, + caps, sizeof(*caps)) != 0) + return -1; + + if (caps->bit_depth & LHDC_BIT_DEPTH_24) { + caps->bit_depth = LHDC_BIT_DEPTH_24; + } else if (caps->bit_depth & LHDC_BIT_DEPTH_16) { + caps->bit_depth = LHDC_BIT_DEPTH_16; + } else { + error("LHDC: No supported bit depths: %#x", saved.bit_depth); + return errno = ENOTSUP, -1; + } + + const struct a2dp_sampling *sampling; + if ((sampling = a2dp_sampling_select(a2dp_lhdc_samplings, caps->frequency)) != NULL) + caps->frequency = sampling->value; + else { + error("LHDC: No supported sampling frequencies: %#x", saved.frequency); + return errno = ENOTSUP, -1; + } + + // const struct a2dp_channel_mode *chm; + // if ((chm = a2dp_channel_mode_select(a2dp_lhdc_channels, caps->channel_mode)) != NULL) + // caps->channel_mode = chm->value; + // else { + // error("LHDC: No supported channel modes: %#x", saved.channel_mode); + // return errno = ENOTSUP, -1; + // } + + return 0; +} + +static int a2dp_lhdc_configuration_check( + const struct a2dp_codec *codec, + const void *configuration) { + + const a2dp_lhdc_v3_t *conf = configuration; + a2dp_lhdc_v3_t conf_v = *conf; + + /* validate configuration against BlueALSA capabilities */ + if (a2dp_filter_capabilities(codec, &codec->capabilities, + &conf_v, sizeof(conf_v)) != 0) + return A2DP_CHECK_ERR_SIZE; + + if (a2dp_sampling_lookup(a2dp_lhdc_samplings, conf_v.frequency) == NULL) { + debug("LHDC: Invalid sampling frequency: %#x", conf->frequency); + return A2DP_CHECK_ERR_SAMPLING; + } + + // if (a2dp_channel_mode_lookup(a2dp_lhdc_channels, conf_v.channel_mode) == NULL) { + // debug("LHDC: Invalid channel mode: %#x", conf->channel_mode); + // return A2DP_CHECK_ERR_CHANNEL_MODE; + // } + + return A2DP_CHECK_OK; +} + +static int a2dp_lhdc_transport_init(struct ba_transport *t) { + + // const struct a2dp_channel_mode *chm; + // if ((chm = a2dp_channel_mode_lookup(a2dp_lhdc_channels, + // t->a2dp.configuration.lhdc_v3.channel_mode)) == NULL) + // return -1; + + const struct a2dp_sampling *sampling; + if ((sampling = a2dp_sampling_lookup(a2dp_lhdc_samplings, + t->a2dp.configuration.lhdc_v3.frequency)) == NULL) + return -1; + + if (t->a2dp.codec->dir == A2DP_SINK) { + t->a2dp.pcm.format = BA_TRANSPORT_PCM_FORMAT_S24_4LE; + } else { + t->a2dp.pcm.format = BA_TRANSPORT_PCM_FORMAT_S32_4LE; + } + + t->a2dp.pcm.channels = 2; + t->a2dp.pcm.sampling = sampling->frequency; + + return 0; +} + +static int a2dp_lhdc_source_init(struct a2dp_codec *codec) { + if (config.a2dp.force_44100) + codec->capabilities.lhdc_v3.frequency = LHDC_SAMPLING_FREQ_44100; + return 0; +} + +static int a2dp_lhdc_source_transport_start(struct ba_transport *t) { + return ba_transport_pcm_start(&t->a2dp.pcm, a2dp_lhdc_enc_thread, "ba-a2dp-lhdc"); +} + +struct a2dp_codec a2dp_lhdc_source = { + .dir = A2DP_SOURCE, + .codec_id = A2DP_CODEC_VENDOR_LHDC_V3, + .synopsis = "A2DP Source (LHDC V3)", + .capabilities.lhdc_v3 = { + .info = A2DP_SET_VENDOR_ID_CODEC_ID(LHDC_V3_VENDOR_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, // TODO: copy LLAC/V3/V4 logic from AOSP patches + .ch_split_mode = LHDC_CH_SPLIT_MODE_NONE, + .meta = 0, + .min_bit_rate = 0, + .larc = 0, + .lhdc_v4 = 1, + }, + .capabilities_size = sizeof(a2dp_lhdc_v3_t), + .init = a2dp_lhdc_source_init, + .configuration_select = a2dp_lhdc_configuration_select, + .configuration_check = a2dp_lhdc_configuration_check, + .transport_init = a2dp_lhdc_transport_init, + .transport_start = a2dp_lhdc_source_transport_start, +}; + +static int a2dp_lhdc_sink_transport_start(struct ba_transport *t) { + return ba_transport_pcm_start(&t->a2dp.pcm, a2dp_lhdc_dec_thread, "ba-a2dp-lhdc"); +} + +struct a2dp_codec a2dp_lhdc_sink = { + .dir = A2DP_SINK, + .codec_id = A2DP_CODEC_VENDOR_LHDC_V3, + .synopsis = "A2DP Sink (LHDC V3)", + .capabilities.lhdc_v3 = { + .info = A2DP_SET_VENDOR_ID_CODEC_ID(LHDC_V3_VENDOR_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 = 1, + .ch_split_mode = LHDC_CH_SPLIT_MODE_NONE, + .meta = 0, + .min_bit_rate = 0, + .larc = 0, + .lhdc_v4 = 1, + }, + .capabilities_size = sizeof(a2dp_lhdc_v3_t), + .configuration_select = a2dp_lhdc_configuration_select, + .configuration_check = a2dp_lhdc_configuration_check, + .transport_init = a2dp_lhdc_transport_init, + .transport_start = a2dp_lhdc_sink_transport_start, +}; diff --git a/src/a2dp-lhdc.h b/src/a2dp-lhdc.h new file mode 100644 index 000000000..bc05ada98 --- /dev/null +++ b/src/a2dp-lhdc.h @@ -0,0 +1,25 @@ +/* + * BlueALSA - a2dp-ldac.h + * Copyright (c) 2016-2021 Arkadiusz Bokowy + * Copyright (c) 2023 anonymix007 + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#pragma once +#ifndef BLUEALSA_A2DPLHDC_H_ +#define BLUEALSA_A2DPLHDC_H_ + +#if HAVE_CONFIG_H +# include +#endif + +#include "a2dp.h" + +extern struct a2dp_codec a2dp_lhdc_sink; +extern struct a2dp_codec a2dp_lhdc_source; + +#endif diff --git a/src/a2dp.c b/src/a2dp.c index 2af4aff35..2e425e668 100644 --- a/src/a2dp.c +++ b/src/a2dp.c @@ -36,6 +36,9 @@ #if ENABLE_LDAC # include "a2dp-ldac.h" #endif +#if ENABLE_LHDC +# include "a2dp-lhdc.h" +#endif #if ENABLE_MPEG # include "a2dp-mpeg.h" #endif @@ -51,6 +54,10 @@ struct a2dp_codec * const a2dp_codecs[] = { &a2dp_lc3plus_source, &a2dp_lc3plus_sink, #endif +#if ENABLE_LHDC + &a2dp_lhdc_source, + &a2dp_lhdc_sink, +#endif #if ENABLE_LDAC &a2dp_ldac_source, # if HAVE_LDAC_DECODE diff --git a/src/bluealsa-config.c b/src/bluealsa-config.c index 30dfa68e0..9fc0ba227 100644 --- a/src/bluealsa-config.c +++ b/src/bluealsa-config.c @@ -18,6 +18,11 @@ # include #endif +#if ENABLE_LHDC +# include +# include +#endif + #include "codec-sbc.h" #include "hfp.h" @@ -133,6 +138,11 @@ struct ba_config config = { .ldac_eqmid = LDACBT_EQMID_SQ, #endif +#if ENABLE_LHDC + /* Use ABR as a reasonable default. */ + .lhdc_eqmid = LHDCBT_QUALITY_AUTO, +#endif + }; int bluealsa_config_init(void) { diff --git a/src/bluealsa-config.h b/src/bluealsa-config.h index 60b569b76..afef6e215 100644 --- a/src/bluealsa-config.h +++ b/src/bluealsa-config.h @@ -156,6 +156,10 @@ struct ba_config { uint8_t ldac_eqmid; #endif +#if ENABLE_LHDC + uint8_t lhdc_eqmid; + // TODO: LLAC/V3/V4, bit depth, sample frequency, LLAC bitrate +#endif }; /* Global BlueALSA configuration. */ diff --git a/src/bluez.c b/src/bluez.c index b462e2d81..0790aae24 100644 --- a/src/bluez.c +++ b/src/bluez.c @@ -316,6 +316,10 @@ static const char *bluez_get_media_endpoint_object_path( #if ENABLE_LDAC case A2DP_CODEC_VENDOR_LDAC: return "/A2DP/LDAC/source"; +#endif +#if ENABLE_LHDC + case A2DP_CODEC_VENDOR_LHDC_V3: + return "/A2DP/LHDC_V3/source"; #endif default: error("Unsupported A2DP codec: %#x", codec_id); @@ -352,6 +356,10 @@ static const char *bluez_get_media_endpoint_object_path( #if ENABLE_LDAC case A2DP_CODEC_VENDOR_LDAC: return "/A2DP/LDAC/sink"; +#endif +#if ENABLE_LHDC + case A2DP_CODEC_VENDOR_LHDC_V3: + return "/A2DP/LHDC_V3/sink"; #endif default: error("Unsupported A2DP codec: %#x", codec_id); diff --git a/src/main.c b/src/main.c index 6da13f13f..9328086c7 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,11 @@ # include #endif +#if ENABLE_LHDC +# include +# include +#endif + #include "a2dp.h" #include "a2dp-sbc.h" #include "audio.h" @@ -169,6 +174,10 @@ int main(int argc, char **argv) { { "aac-true-bps", no_argument, NULL, 18 }, { "aac-vbr", no_argument, NULL, 19 }, #endif +#if ENABLE_LHDC + // TODO: LLAC/V3/V4, bit depth, sample frequency, LLAC bitrate + { "lhdc-quality", required_argument, NULL, 22 }, +#endif #if ENABLE_LC3PLUS { "lc3plus-bitrate", required_argument, NULL, 20 }, #endif @@ -226,6 +235,9 @@ int main(int argc, char **argv) { " --ldac-abr\t\t\tenable LDAC adaptive bit rate\n" " --ldac-quality=MODE\t\tset LDAC encoder quality mode\n" #endif +#if ENABLE_LHDC + " --lhdc-quality=MODE\t\tset LHDC encoder quality mode\n" +#endif #if ENABLE_MP3LAME " --mp3-algorithm=TYPE\t\tselect LAME encoder algorithm type\n" " --mp3-vbr-quality=MODE\tset LAME encoder VBR quality mode\n" @@ -481,6 +493,33 @@ int main(int argc, char **argv) { } #endif +#if ENABLE_LHDC + case 22 /* --lhdc-quality=MODE */ : { + static const nv_entry_t values[] = { + { "low0", .v.ui = LHDCBT_QUALITY_LOW0 }, + { "low1", .v.ui = LHDCBT_QUALITY_LOW1 }, + { "low2", .v.ui = LHDCBT_QUALITY_LOW2 }, + { "low3", .v.ui = LHDCBT_QUALITY_LOW3 }, + { "low4", .v.ui = LHDCBT_QUALITY_LOW4 }, + { "low", .v.ui = LHDCBT_QUALITY_LOW }, + { "mid", .v.ui = LHDCBT_QUALITY_MID }, + { "high", .v.ui = LHDCBT_QUALITY_HIGH }, + { "auto", .v.ui = LHDCBT_QUALITY_AUTO }, + { 0 }, + }; + + const nv_entry_t *entry; + if ((entry = nv_find(values, optarg)) == NULL) { + error("Invalid LHDC encoder quality mode {%s}: %s", + nv_join_names(values), optarg); + return EXIT_FAILURE; + } + + config.lhdc_eqmid = entry->v.ui; + break; + } +#endif + #if ENABLE_MP3LAME case 12 /* --mp3-algorithm=TYPE */ : {