From f06f0db31b36d9bd61b5487e3497740848265328 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 26 Apr 2026 14:01:31 +0300 Subject: [PATCH] bluez5: add AAC-ELD vendor codec used by Airpods Add AAC-ELD vendor codec. This is based on the documentation by Sa Xiao: Link: https://github.com/wasdwasd0105/PicoW-usb2bt-audio/blob/main/aac-eld-apple.md --- spa/include/spa/param/bluetooth/audio.h | 1 + spa/include/spa/param/bluetooth/type-info.h | 1 + spa/plugins/bluez5/a2dp-codec-aac-eld-a.c | 671 ++++++++++++++++++++ spa/plugins/bluez5/a2dp-codec-caps.h | 27 + spa/plugins/bluez5/a2dp-codec-lc3plus.c | 1 + spa/plugins/bluez5/aac-bits.h | 190 ++++++ spa/plugins/bluez5/codec-loader.c | 2 + spa/plugins/bluez5/meson.build | 8 + 8 files changed, 901 insertions(+) create mode 100644 spa/plugins/bluez5/a2dp-codec-aac-eld-a.c create mode 100644 spa/plugins/bluez5/aac-bits.h diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h index 34b45e297..3a9698959 100644 --- a/spa/include/spa/param/bluetooth/audio.h +++ b/spa/include/spa/param/bluetooth/audio.h @@ -36,6 +36,7 @@ enum spa_bluetooth_audio_codec { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD_A, /* HFP */ SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100, diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h index 7bf1da52c..12e792cba 100644 --- a/spa/include/spa/param/bluetooth/type-info.h +++ b/spa/include/spa/param/bluetooth/type-info.h @@ -40,6 +40,7 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = { { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_duplex", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_pro", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_g", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD_A, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aac_eld_a", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL }, diff --git a/spa/plugins/bluez5/a2dp-codec-aac-eld-a.c b/spa/plugins/bluez5/a2dp-codec-aac-eld-a.c new file mode 100644 index 000000000..dbb45e6d9 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-aac-eld-a.c @@ -0,0 +1,671 @@ +/* Spa A2DP AAC-ELD-A(irpods) codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2026 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "rtp.h" +#include "media-codecs.h" +#include "aac-bits.h" + +static struct spa_log *global_log; + +#define DEFAULT_AAC_BITRATE 256000 + +#define FRAMES_PER_PACKET 3 +#define FRAME_SAMPLES 480 +#define SAMPLE_SIZE 2 /* S16 */ + +struct frame_header { + uint8_t data[4]; +} __attribute__((packed)); + +struct impl { + HANDLE_AACENCODER enc; + HANDLE_AACDECODER dec; + + int codesize; + + uint32_t enc_delay; + uint32_t dec_delay; + + uint16_t enc_seq; +}; + +static bool parse_frame_header(const struct frame_header *hdr, uint16_t *seq, uint16_t *size) +{ + const uint8_t *buf = hdr->data; + + if ((buf[0] & 0xf0) != 0xb0) + return false; + *seq = (buf[0] & 0x0f) << 8 | buf[1]; + + if ((buf[2] & 0xf0) != 0x10) + return false; + *size = (buf[2] & 0x0f) << 8 | buf[3]; + + return true; +} + +static void write_frame_header(struct frame_header *hdr, uint16_t seq, uint16_t size) +{ + uint8_t *buf = hdr->data; + + buf[0] = 0xb0 | (seq & 0xf00) >> 8; + buf[1] = seq & 0xff; + buf[2] = 0x10 | (size & 0xf00) >> 8; + buf[3] = size & 0xff; +} + +static bool eld_supported(void) +{ + static bool supported = false, checked = false; + HANDLE_AACENCODER enc = NULL; + + if (checked) + return supported; + + if (aacEncOpen(&enc, 0, 2) != AACENC_OK) + goto done; + if (aacEncoder_SetParam(enc, AACENC_AOT, AOT_ER_AAC_ELD) != AACENC_OK) + goto done; + if (aacEncoder_SetParam(enc, AACENC_SBR_MODE, 1) != AACENC_OK) + goto done; + + supported = true; + +done: + if (enc) + aacEncClose(&enc); + checked = true; + spa_log_debug(global_log, "FDK-AAC AAC-ELD-A support:%d", (int)supported); + return supported; +} + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + const a2dp_aac_eld_a_t a2dp_aac = { + .info = codec->vendor, + AAC_ELD_A_INIT_AOT(AAC_ELD_A_AOT_AAC_ELD), + AAC_ELD_A_INIT_FREQ_CH(AAC_ELD_A_FREQ_48000, AAC_ELD_A_CH_MONO | AAC_ELD_A_CH_STEREO), + AAC_ELD_A_INIT_FLAGS_BITRATE(AAC_ELD_A_FLAG_VBR, DEFAULT_AAC_BITRATE), + }; + + if (!eld_supported()) + return -ENOTSUP; + + memcpy(caps, &a2dp_aac, sizeof(a2dp_aac)); + return sizeof(a2dp_aac); +} + +static int codec_select_config(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], + void **config_data) +{ + a2dp_aac_eld_a_t conf; + int aot, freq, ch, cflags, bitrate; + + if (!eld_supported()) + return -ENOTSUP; + + 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 (AAC_ELD_A_GET_AOT(conf) & AAC_ELD_A_AOT_AAC_ELD) + aot = AAC_ELD_A_AOT_AAC_ELD; + else + return -EINVAL; + + if (AAC_ELD_A_GET_FREQ(conf) & AAC_ELD_A_FREQ_48000) + freq = AAC_ELD_A_FREQ_48000; + else + return -EINVAL; + + if (AAC_ELD_A_GET_CH(conf) & AAC_ELD_A_CH_STEREO) + ch = AAC_ELD_A_CH_STEREO; + else if (AAC_ELD_A_GET_CH(conf) & AAC_ELD_A_CH_MONO) + ch = AAC_ELD_A_CH_MONO; + else + return -EINVAL; + + if (AAC_ELD_A_GET_FLAGS(conf) & AAC_ELD_A_FLAG_VBR) + cflags = AAC_ELD_A_FLAG_VBR; + else + cflags = 0; + + bitrate = AAC_ELD_A_GET_BITRATE(conf); + + conf = (a2dp_aac_eld_a_t) { + .info = conf.info, + AAC_ELD_A_INIT_AOT(aot), + AAC_ELD_A_INIT_FREQ_CH(freq, ch), + AAC_ELD_A_INIT_FLAGS_BITRATE(cflags, bitrate), + }; + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + a2dp_aac_eld_a_t conf; + + if (caps == NULL || caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16; + + if (AAC_ELD_A_GET_AOT(conf) != AAC_ELD_A_AOT_AAC_ELD) + return -EINVAL; + + switch (AAC_ELD_A_GET_FREQ(conf)) { + case AAC_ELD_A_FREQ_48000: + info->info.raw.rate = 48000; + break; + default: + return -EINVAL; + } + + switch (AAC_ELD_A_GET_CH(conf)) { + case AAC_ELD_A_CH_STEREO: + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + break; + case AAC_ELD_A_CH_MONO: + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int codec_enum_config(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) +{ + struct spa_audio_info info; + struct spa_pod_frame f[1]; + int res; + + if ((res = codec_validate_config(codec, flags, caps, caps_size, &info)) < 0) + return res; + + 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(info.media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info.media_subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(info.info.raw.format), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info.info.raw.rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info.info.raw.channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, info.info.raw.channels, info.info.raw.position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static void *codec_init(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 *this = NULL; + a2dp_aac_eld_a_t conf; + struct spa_audio_info config_info; + uint8_t asc_buf[8]; + UCHAR *asc[1] = { (void *)asc_buf }; + UINT asc_len[1]; + UINT bitratemode; + int bitrate, max_bitrate; + int res; + size_t rate, channels; + + if (config_len < sizeof(conf)) { + res = -EINVAL; + goto error; + } + memcpy(&conf, config, sizeof(conf)); + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16) { + res = -EINVAL; + goto error; + } + + if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0) + goto error; + if (config_info.info.raw.channels != info->info.raw.channels || + config_info.info.raw.rate != info->info.raw.rate) { + res = -EINVAL; + goto error; + } + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) { + res = -errno; + goto error; + } + + mtu = mtu; + rate = config_info.info.raw.rate; + channels = config_info.info.raw.channels; + + /* No fragmentation: make sure it fits into MTU */ + if (mtu <= sizeof(struct rtp_header) + sizeof(struct frame_header) * FRAMES_PER_PACKET) + goto error; + + max_bitrate = (mtu - sizeof(struct rtp_header) - sizeof(struct frame_header) * FRAMES_PER_PACKET) + * 8 * rate / FRAME_SAMPLES / FRAMES_PER_PACKET; + bitrate = SPA_MIN(max_bitrate, AAC_ELD_A_GET_BITRATE(conf)); + + if (AAC_ELD_A_GET_FLAGS(conf) & AAC_ELD_A_FLAG_VBR) + bitratemode = 5; + else + bitratemode = 0; + + /* + * Encoder + */ + res = aacEncOpen(&this->enc, 0, channels); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_AOT, AOT_ER_AAC_ELD); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_SBR_RATIO, 1); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_SBR_MODE, 1); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_CHANNELMODE, channels); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_SAMPLERATE, rate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_GRANULE_LENGTH, FRAME_SAMPLES); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_PEAK_BITRATE, max_bitrate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_BITRATE, bitrate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_BITRATEMODE, bitratemode); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_TRANSMUX, TT_MP4_RAW); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->enc, AACENC_AFTERBURNER, 1); + if (res != AACENC_OK) + goto error; + + res = aacEncEncode(this->enc, NULL, NULL, NULL, NULL); + if (res != AACENC_OK) + goto error; + + AACENC_InfoStruct enc_info = {}; + res = aacEncInfo(this->enc, &enc_info); + if (res != AACENC_OK) + goto error; + + this->enc_delay = enc_info.nDelay; + + if (enc_info.frameLength != FRAME_SAMPLES) + goto error; + + this->codesize = channels * FRAME_SAMPLES * SAMPLE_SIZE * FRAMES_PER_PACKET; + + /* + * Decoder + */ + this->dec = aacDecoder_Open(TT_MP4_RAW, 1); + if (!this->dec) { + res = -EINVAL; + goto error; + } +#ifdef AACDECODER_LIB_VL0 + res = aacDecoder_SetParam(this->dec, AAC_PCM_MIN_OUTPUT_CHANNELS, channels); + if (res != AAC_DEC_OK) + goto error; + res = aacDecoder_SetParam(this->dec, AAC_PCM_MAX_OUTPUT_CHANNELS, channels); + if (res != AAC_DEC_OK) + goto error; +#else + res = aacDecoder_SetParam(this->dec, AAC_PCM_OUTPUT_CHANNELS, channels); + if (res != AAC_DEC_OK) + goto error; +#endif + res = aac_make_asc(asc_buf, sizeof(asc_buf), AAC_AOT_ER_AAC_ELD, + rate, rate, channels, true); + if (res < 0) + goto error; + asc_len[0] = res; + res = aacDecoder_ConfigRaw(this->dec, asc, asc_len); + if (res != AAC_DEC_OK) + goto error; + + this->dec_delay = 0; + + return this; + +error: + if (this && this->enc) + aacEncClose(&this->enc); + if (this && this->dec) + aacDecoder_Close(this->dec); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + if (this->enc) + aacEncClose(&this->enc); + if (this->dec) + aacDecoder_Close(this->dec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct rtp_header *header; + + if (dst_size < sizeof(struct rtp_header)) + return -ENOSPC; + + header = (struct rtp_header *)dst; + memset(header, 0, sizeof(struct rtp_header)); + + header->v = 2; + header->pt = 96; + header->sequence_number = htons(seqnum); + header->timestamp = htonl(timestamp); + header->ssrc = htonl(1); + + return sizeof(struct rtp_header); +} + +static int encode_frame(struct impl *this, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct frame_header *hdr = dst; + int res; + + if (dst_size < sizeof(*hdr)) + return -ENOSPC; + + dst_size -= sizeof(*hdr); + dst = SPA_PTROFF(dst, sizeof(*hdr), void); + + void *in_bufs[] = {(void *) src}; + int in_buf_ids[] = {IN_AUDIO_DATA}; + int in_buf_sizes[] = {src_size}; + int in_buf_el_sizes[] = {SAMPLE_SIZE}; + AACENC_BufDesc in_buf_desc = { + .numBufs = 1, + .bufs = in_bufs, + .bufferIdentifiers = in_buf_ids, + .bufSizes = in_buf_sizes, + .bufElSizes = in_buf_el_sizes, + }; + AACENC_InArgs in_args = { + .numInSamples = src_size / SAMPLE_SIZE, + }; + + void *out_bufs[] = {dst}; + int out_buf_ids[] = {OUT_BITSTREAM_DATA}; + int out_buf_sizes[] = {dst_size}; + int out_buf_el_sizes[] = {SAMPLE_SIZE}; + AACENC_BufDesc out_buf_desc = { + .numBufs = 1, + .bufs = out_bufs, + .bufferIdentifiers = out_buf_ids, + .bufSizes = out_buf_sizes, + .bufElSizes = out_buf_el_sizes, + }; + AACENC_OutArgs out_args = {}; + + res = aacEncEncode(this->enc, &in_buf_desc, &out_buf_desc, &in_args, &out_args); + if (res != AACENC_OK) + return -EINVAL; + + write_frame_header(hdr, this->enc_seq++, out_args.numOutBytes); + + *dst_out = out_args.numOutBytes + sizeof(*hdr); + + return out_args.numInSamples * SAMPLE_SIZE; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int consumed = 0; + int i, res; + + *dst_out = 0; + + for (i = 0; i < FRAMES_PER_PACKET; ++i) { + size_t out; + + res = encode_frame(this, src, src_size, dst, dst_size, &out); + if (res < 0) + return res; + + src = SPA_PTROFF(src, res, void); + src_size -= res; + consumed += res; + + dst = SPA_PTROFF(dst, out, void); + dst_size -= out; + *dst_out += out; + } + + *need_flush = NEED_FLUSH_ALL; + + return consumed; +} + +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; + + if (src_size <= sizeof(struct rtp_header)) + return -EINVAL; + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + + return sizeof(struct rtp_header); +} + +static int decode_frame(struct impl *this, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + uint16_t seq, size; + const struct frame_header *hdr = src; + + if (src_size < sizeof(*hdr)) + return -EINVAL; + + if (!parse_frame_header(hdr, &seq, &size)) + return -EINVAL; + + src_size -= sizeof(*hdr); + src = SPA_PTROFF(src, sizeof(*hdr), void); + + uint data_size = SPA_MIN(src_size, size); + uint bytes_valid = data_size; + CStreamInfo *aacinf; + int res; + + res = aacDecoder_Fill(this->dec, (UCHAR **)&src, &data_size, &bytes_valid); + if (res != AAC_DEC_OK) { + spa_log_debug(global_log, "AAC buffer fill error: 0x%04X", res); + return -EINVAL; + } + + res = aacDecoder_DecodeFrame(this->dec, dst, dst_size, 0); + if (res != AAC_DEC_OK) { + spa_log_debug(global_log, "AAC decode frame error: 0x%04X", res); + return -EINVAL; + } + + aacinf = aacDecoder_GetStreamInfo(this->dec); + if (!aacinf) { + spa_log_debug(global_log, "AAC get stream info failed"); + return -EINVAL; + } + *dst_out = aacinf->frameSize * aacinf->numChannels * SAMPLE_SIZE; + + spa_assert_se(data_size >= bytes_valid); + + return data_size - bytes_valid + sizeof(*hdr); +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int consumed = 0; + int res; + + *dst_out = 0; + + while (src_size) { + size_t out; + + res = decode_frame(this, src, src_size, dst, dst_size, &out); + if (res < 0) + return res; + + src = SPA_PTROFF(src, res, void); + src_size -= res; + consumed += res; + + dst = SPA_PTROFF(dst, out, void); + dst_size -= out; + *dst_out += out; + } + + return consumed; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) + *encoder = this->enc_delay; + + if (decoder) { + CStreamInfo *info = aacDecoder_GetStreamInfo(this->dec); + if (info) + this->dec_delay = info->outputDelay; + *decoder = this->dec_delay; + } +} + +static void codec_set_log(struct spa_log *log_) +{ + global_log = log_; + spa_log_topic_init(global_log, &codec_plugin_log_topic); +} + +const struct media_codec a2dp_codec_aac_eld_a = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD_A, + .kind = MEDIA_CODEC_A2DP, + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = AAC_ELD_A_VENDOR_ID, + .codec_id = AAC_ELD_A_CODEC_ID }, + .name = "aac_eld_a", + .description = "AAC-ELD-A", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .set_log = codec_set_log, + .get_delay = codec_get_delay, +}; + +MEDIA_CODEC_EXPORT_DEF( + "aac-eld-a", + &a2dp_codec_aac_eld_a +); diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h index b58e33624..6594ba3ec 100644 --- a/spa/plugins/bluez5/a2dp-codec-caps.h +++ b/spa/plugins/bluez5/a2dp-codec-caps.h @@ -307,6 +307,25 @@ (a).data = ((freq) & OPUS_G_FREQUENCY_MASK) | ((dur) & OPUS_G_DURATION_MASK) | ((ch) & OPUS_G_CHANNELS_MASK) +#define AAC_ELD_A_VENDOR_ID 0x0000004C +#define AAC_ELD_A_CODEC_ID 0x8001 + +#define AAC_ELD_A_GET_AOT(a) ((a).aot[0] << 8 | (a).aot[1]) +#define AAC_ELD_A_GET_FREQ(a) (((a).freq[0] << 8 | (a).freq[1]) >> 4) +#define AAC_ELD_A_GET_CH(a) ((a).freq[1] & 0x0f) +#define AAC_ELD_A_GET_FLAGS(a) ((a).bitrate[0] & 0x80) +#define AAC_ELD_A_GET_BITRATE(a) (((a).bitrate[0] & 0x7f) << 16 | (a).bitrate[1] << 8 | (a).bitrate[0]) + +#define AAC_ELD_A_INIT_AOT(b) .aot = { ((b) >> 8), (b) & 0xff } +#define AAC_ELD_A_INIT_FREQ_CH(f, ch) .freq = { ((f) >> 4), (((f) << 4) & 0xf0) | ((ch) & 0x0f) } +#define AAC_ELD_A_INIT_FLAGS_BITRATE(f, br) .bitrate = { ((f) & 0x80) | (((br) >> 16) & 0x7f), ((br) >> 8) & 0xff, ((br) & 0xff) } + +#define AAC_ELD_A_AOT_AAC_ELD 0x0080 +#define AAC_ELD_A_FREQ_48000 0x008 +#define AAC_ELD_A_CH_MONO 0x8 +#define AAC_ELD_A_CH_STEREO 0x4 +#define AAC_ELD_A_FLAG_VBR 0x80 + typedef struct { uint32_t vendor_id; uint16_t codec_id; @@ -486,6 +505,14 @@ typedef struct { uint8_t data; } __attribute__ ((packed)) a2dp_opus_g_t; +typedef struct { + a2dp_vendor_codec_t info; + uint8_t aot[2]; + uint8_t freq[2]; + uint8_t reserved; + uint8_t bitrate[3]; +} __attribute__ ((packed)) a2dp_aac_eld_a_t; + #define ASHA_CODEC_G722 0x63 #endif diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c index da6260926..1059a33cd 100644 --- a/spa/plugins/bluez5/a2dp-codec-lc3plus.c +++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/spa/plugins/bluez5/aac-bits.h b/spa/plugins/bluez5/aac-bits.h new file mode 100644 index 000000000..4c5e64f28 --- /dev/null +++ b/spa/plugins/bluez5/aac-bits.h @@ -0,0 +1,190 @@ +/* Spa AAC bits */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_AAC_BITS_H +#define SPA_BLUEZ5_AAC_BITS_H + +#include + +struct bits_out { + uint8_t *buf; + size_t size; + size_t pos; +}; + +#define BITS_OUT_INIT(buf, size) ((struct bits_out) { (buf), (size), 0 }) + +static inline void bits_push(struct bits_out *b, uint8_t nbits, uint8_t value) +{ + size_t pos = b->pos; + + /* Maximally simple, doesn't need to be fast... */ + + spa_assert(nbits <= 8); + + value = ((uint16_t)value) << (8 - nbits); + b->pos += nbits; + + while (nbits) { + size_t n = pos / 8; + size_t bit = pos % 8; + + if (n >= b->size) + break; + + if (bit == 0) + b->buf[n] = 0; + if (value & 0x80) + b->buf[n] |= 1 << (7 - bit); + + pos++; + nbits--; + value = ((uint16_t)value) << 1; + } +} + +enum aac_aot_type { + AAC_AOT_AAC_LC = 2, + AAC_AOT_ER_AAC_ELD = 39, +}; + +static inline int aac_frequency_index(int frequency) +{ + switch (frequency) { + case 96000: return 0x0; + case 88200: return 0x1; + case 64000: return 0x2; + case 48000: return 0x3; + case 44100: return 0x4; + case 32000: return 0x5; + case 24000: return 0x6; + case 22050: return 0x7; + case 16000: return 0x8; + case 12000: return 0x9; + case 11025: return 0xa; + case 8000: return 0xb; + case 7350: return 0xc; + default: + spa_assert_not_reached(); + return -1; + } +} + +static inline int aac_channel_index(int channels) +{ + switch (channels) { + case 1: return 0x1; + case 2: return 0x2; + default: + spa_assert_not_reached(); + return -1; + } +} + +/** Write AudioSpecificConfig to given buffer */ +static inline int aac_make_asc(void *buf, size_t buf_size, enum aac_aot_type aot, + int frequency, int downscale_frequency, int channels, bool sbr) +{ + int freq, down_freq, chan; + struct bits_out b = BITS_OUT_INIT(buf, buf_size); + + if ((freq = aac_frequency_index(frequency)) < 0) + return -1; + if ((down_freq = aac_frequency_index(downscale_frequency)) < 0) + return -1; + if ((chan = aac_channel_index(channels)) < 0) + return -1; + + switch (aot) { + case AAC_AOT_AAC_LC: + case AAC_AOT_ER_AAC_ELD: + break; + default: + spa_assert_not_reached(); + return -EINVAL; + } + + if (aot <= 31) { + bits_push(&b, 5, aot); + } else { + bits_push(&b, 5, 31); + bits_push(&b, 6, aot - 32); + } + + bits_push(&b, 4, freq); /* frequency index */ + bits_push(&b, 4, chan); /* channel configuration */ + + switch (aot) { + case AAC_AOT_AAC_LC: + /* GASpecificConfig */ + bits_push(&b, 1, 0x0); /* frame length flag (1024 length) */ + bits_push(&b, 1, 0x0); /* depends on core coder */ + bits_push(&b, 1, 0x0); /* extension flag */ + break; + case AAC_AOT_ER_AAC_ELD: + /* ELDSpecificConfig */ + bits_push(&b, 1, 0x1); /* frame length flag (480 length) */ + bits_push(&b, 1, 0x0); /* SectionDataResilience? */ + bits_push(&b, 1, 0x0); /* ScalefactorDataResilience? */ + bits_push(&b, 1, 0x0); /* SpectralDataResilience? */ + bits_push(&b, 1, sbr ? 0x1 : 0x0); /* SBR */ + + if (sbr) { + bits_push(&b, 1, 0x0); /* ldSbrSamplingRate */ + bits_push(&b, 1, 0x0); /* ldSbrCrcFlag */ + + /* ld_sbr_header */ + if (channels != 1 && channels != 2) + return -EINVAL; + + /* sbr_header: + * These are just the FDK-AAC default values for 48000/48000... + */ + if (freq != down_freq) + return -EINVAL; + + bits_push(&b, 1, 1); /* bs_amp_res */ + bits_push(&b, 4, 13); /* bs_start_freq */ + bits_push(&b, 4, 7); /* bs_stop_freq */ + bits_push(&b, 3, 0); /* bs_xover_band */ + + bits_push(&b, 2, 0x0); /* bs_reserved */ + bits_push(&b, 1, 0x1); /* bs_header_extra_1 */ + bits_push(&b, 1, 0x0); /* bs_header_extra_2 */ + + /* bs_header_extra_1 */ + bits_push(&b, 2, 0x1); /* bs_freq_scale */ + bits_push(&b, 1, 0x1); /* bs_alter_scale */ + bits_push(&b, 2, 0x3); /* bs_noise_bands */ + + } + + if (freq != down_freq) { + bits_push(&b, 4, 0x3); /* ELDEXT_DOWNSCALEINFO */ + bits_push(&b, 4, 0x1); + bits_push(&b, 4, down_freq); + bits_push(&b, 4, 0x0); + } + + bits_push(&b, 4, 0x0); /* ELDEXT_TERM */ + + /* epConfig */ + bits_push(&b, 2, 0x0); + break; + } + + /* byte align */ + if (b.pos % 8) + bits_push(&b, 8 - (b.pos % 8), 0x0); + spa_assert(b.pos % 8 == 0); + + if (b.pos / 8 >= buf_size) { + spa_assert_not_reached(); + return -EINVAL; + } + + return b.pos / 8; +} + +#endif diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c index 68e6af756..5761425a1 100644 --- a/spa/plugins/bluez5/codec-loader.c +++ b/spa/plugins/bluez5/codec-loader.c @@ -51,6 +51,7 @@ static int codec_order(const struct media_codec *c) SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD, + SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD_A, SPA_BLUETOOTH_AUDIO_CODEC_G722, SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127, @@ -181,6 +182,7 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo #define MEDIA_CODEC_FACTORY_LIB(basename) \ { MEDIA_CODEC_FACTORY_NAME(basename), MEDIA_CODEC_LIB_BASE basename } MEDIA_CODEC_FACTORY_LIB("aac"), + MEDIA_CODEC_FACTORY_LIB("aac-eld-a"), MEDIA_CODEC_FACTORY_LIB("aptx"), MEDIA_CODEC_FACTORY_LIB("faststream"), MEDIA_CODEC_FACTORY_LIB("ldac"), diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build index 01c5f3ac1..311737f51 100644 --- a/spa/plugins/bluez5/meson.build +++ b/spa/plugins/bluez5/meson.build @@ -122,6 +122,14 @@ if fdk_aac_dep.found() dependencies : [ spa_dep, fdk_aac_dep ], install : true, install_dir : spa_plugindir / 'bluez5') + + bluez_codec_aac_eld_a = shared_library('spa-codec-bluez5-aac-eld-a', + [ 'a2dp-codec-aac-eld-a.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, fdk_aac_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') endif if aptx_dep.found()