From a78936e72690a4a518cd02e1d9a04eb1e37dc2ea Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 01/12] audio: base_fw: advertise ALAC decode and FLAC/ALAC encode Advertise SND_AUDIOCODEC_ALAC decode and SND_AUDIOCODEC_FLAC / SND_AUDIOCODEC_ALAC encode capabilities in base_fw when the corresponding FFmpeg Kconfig options are enabled. Signed-off-by: Liam Girdwood --- src/kernel/audio/base_fw.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/kernel/audio/base_fw.c b/src/kernel/audio/base_fw.c index c1c8d69413d7..3c0108d9b500 100644 --- a/src/kernel/audio/base_fw.c +++ b/src/kernel/audio/base_fw.c @@ -119,6 +119,11 @@ static void get_codec_info(struct sof_tlv **tuple) codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_FLAC, SOF_IPC_STREAM_PLAYBACK); #endif +#ifdef CONFIG_FFMPEG_DEC_ALAC + /* ALAC decode is provided by the ffmpeg_dec (libavcodec) module. */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_ALAC, SOF_IPC_STREAM_PLAYBACK); +#endif #if defined(CONFIG_FFMPEG_DEC_AAC) && !defined(CONFIG_CADENCE_CODEC_AAC_DEC) /* AAC-LC decode is provided by the ffmpeg_dec (libavcodec) module (guarded so * it is not advertised twice when the Cadence AAC decoder is also present). @@ -145,6 +150,16 @@ static void get_codec_info(struct sof_tlv **tuple) codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_AAC, SOF_IPC_STREAM_CAPTURE); #endif +#if defined(CONFIG_FFMPEG_ENC_FLAC) + /* FLAC encode (compress capture) via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_FLAC, SOF_IPC_STREAM_CAPTURE); +#endif +#if defined(CONFIG_FFMPEG_ENC_ALAC) + /* ALAC encode (compress capture) via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_ALAC, SOF_IPC_STREAM_CAPTURE); +#endif #ifdef CONFIG_FFMPEG_DEC_OPUS /* Opus decode is provided by the ffmpeg_dec (libavcodec) module. * Advertise OPUS_RAW and BESPOKE. From f053042a248ad006569db98571d6e53e6bf1483e Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 02/12] ffmpeg_dec: extend decode and encode framework for lossless codecs Extend the FFmpeg decode and encode adapter infrastructure to support container-framed and variable-blocksize lossless codecs: - Add frame_bytes_consumed hook and unparsed packet reassembly buffer handling in ffmpeg_dec-ffmpeg.c for codecs without a standalone AVCodecParser (such as ALAC). - Extend ffmpeg_dec_pcm_to_frame() in ffmpeg_dec-convert.c to support AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_S16P, AV_SAMPLE_FMT_S32, and AV_SAMPLE_FMT_S32P sample formats. - Add an optional encoder flush() hook in struct ffmpeg_enc_codec_ops and support native libavcodec encoders in ffmpeg.cmake. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/ffmpeg-enc-aac.c | 6 + src/audio/ffmpeg_dec/ffmpeg-enc-mp3.c | 6 + src/audio/ffmpeg_dec/ffmpeg-encode.c | 11 +- src/audio/ffmpeg_dec/ffmpeg.cmake | 16 +- src/audio/ffmpeg_dec/ffmpeg_dec-convert.c | 198 +++++++++++++++++++++- src/audio/ffmpeg_dec/ffmpeg_dec-convert.h | 2 +- src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c | 170 ++++++++++++++++++- src/audio/ffmpeg_dec/ffmpeg_dec.c | 7 + src/audio/ffmpeg_dec/ffmpeg_dec.h | 29 +++- 9 files changed, 418 insertions(+), 27 deletions(-) diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-aac.c b/src/audio/ffmpeg_dec/ffmpeg-enc-aac.c index 28fceac2f685..0285ea589d70 100644 --- a/src/audio/ffmpeg_dec/ffmpeg-enc-aac.c +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-aac.c @@ -45,6 +45,12 @@ static int ffmpeg_enc_aac_validate_params(struct processing_module *mod, struct comp_dev *dev = mod->dev; uint32_t ch = codec_params->ch_in ? codec_params->ch_in : codec_params->ch_out; + if (codec_params->rate_control != 0 && + codec_params->rate_control != SND_RATECONTROLMODE_CONSTANTBITRATE) { + comp_err(dev, "unsupported AAC rate_control mode 0x%x", + codec_params->rate_control); + return -ENOTSUP; + } /* vo-aacenc writes the ADTS MPEG ID bit as 1 (MPEG-2 ADTS) unconditionally, * so only UNDEFINED and MP2ADTS can be honored accurately. */ diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-mp3.c b/src/audio/ffmpeg_dec/ffmpeg-enc-mp3.c index d1448e3aa7a3..d43f7df0f758 100644 --- a/src/audio/ffmpeg_dec/ffmpeg-enc-mp3.c +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-mp3.c @@ -27,6 +27,12 @@ static int ffmpeg_enc_mp3_validate_params(struct processing_module *mod, struct comp_dev *dev = mod->dev; uint32_t ch = codec_params->ch_in ? codec_params->ch_in : codec_params->ch_out; + if (codec_params->rate_control != 0 && + codec_params->rate_control != SND_RATECONTROLMODE_CONSTANTBITRATE) { + comp_err(dev, "unsupported MP3 rate_control mode 0x%x", + codec_params->rate_control); + return -ENOTSUP; + } if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED) { comp_err(dev, "unsupported MP3 stream format 0x%x", codec_params->format); diff --git a/src/audio/ffmpeg_dec/ffmpeg-encode.c b/src/audio/ffmpeg_dec/ffmpeg-encode.c index c7eef5c817c1..138813b2154e 100644 --- a/src/audio/ffmpeg_dec/ffmpeg-encode.c +++ b/src/audio/ffmpeg_dec/ffmpeg-encode.c @@ -42,6 +42,12 @@ static const struct ffmpeg_enc_codec_ops * const ffmpeg_enc_codecs[] = { #if defined(CONFIG_FFMPEG_ENC_AAC) &ffmpeg_enc_aac_ops, #endif +#if defined(CONFIG_FFMPEG_ENC_FLAC) + &ffmpeg_enc_flac_ops, +#endif +#if defined(CONFIG_FFMPEG_ENC_ALAC) + &ffmpeg_enc_alac_ops, +#endif }; static const struct ffmpeg_enc_codec_ops *ffmpeg_enc_find_codec_ops_by_snd_id(uint32_t snd_id) @@ -306,7 +312,8 @@ int ffmpeg_enc_mod_init(struct processing_module *mod) return -ENOTSUP; } if (codec_params->rate_control != 0 && - codec_params->rate_control != SND_RATECONTROLMODE_CONSTANTBITRATE) { + codec_params->rate_control != SND_RATECONTROLMODE_CONSTANTBITRATE && + codec_params->rate_control != SND_RATECONTROLMODE_VARIABLEBITRATE) { comp_err(dev, "unsupported rate_control mode 0x%x", codec_params->rate_control); mod_free(mod, e); @@ -494,7 +501,7 @@ bool ffmpeg_enc_is_ready_to_process(struct processing_module *mod, return false; if (!cd->configured) - return true; + return source_get_data_available(sources[0]) > 0; if (e->out_avail) return sink_get_free_size(sinks[0]) > 0; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 90312c2ff566..6318e7045c41 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -23,9 +23,10 @@ if(NOT EXISTS "${SOF_FFMPEG_SRC_DIR}/configure") "-DSOF_FFMPEG_SRC_DIR=.") endif() -# --- 2. Kconfig -> --enable-decoder / --enable-parser lists --- +# --- 2. Kconfig -> --enable-decoder / --enable-parser / --enable-encoder lists --- set(_ff_decoders "") set(_ff_parsers "") +set(_ff_encoders "") if(CONFIG_FFMPEG_DEC_FLAC) list(APPEND _ff_decoders flac) list(APPEND _ff_parsers flac) @@ -43,14 +44,22 @@ if(CONFIG_FFMPEG_DEC_MP3) list(APPEND _ff_parsers mpegaudio) endif() # At least one decoder, encoder, or filter must be selected. -if(NOT _ff_decoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) +if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") endif() set(_ff_dec_cfg "") if(_ff_decoders) list(JOIN _ff_decoders "," _ff_dec_csv) + list(APPEND _ff_dec_cfg --enable-decoder=${_ff_dec_csv}) +endif() +if(_ff_parsers) list(JOIN _ff_parsers "," _ff_par_csv) - set(_ff_dec_cfg --enable-decoder=${_ff_dec_csv} --enable-parser=${_ff_par_csv}) + list(APPEND _ff_dec_cfg --enable-parser=${_ff_par_csv}) +endif() +set(_ff_enc_cfg "") +if(_ff_encoders) + list(JOIN _ff_encoders "," _ff_enc_csv) + set(_ff_enc_cfg --enable-encoder=${_ff_enc_csv}) endif() # --- 2b. Kconfig -> libavfilter audio filters (off unless a filter is chosen) --- @@ -564,6 +573,7 @@ ExternalProject_Add(ffmpeg_ext --disable-runtime-cpudetect --disable-debug --enable-avcodec --enable-avutil --enable-swresample ${_ff_dec_cfg} + ${_ff_enc_cfg} ${_ff_shine_cfg} --enable-small --enable-pic --enable-hardcoded-tables "--extra-cflags=${_ff_extra_cflags}" diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c index 6b69cb5deae9..cfc95eb6be39 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c @@ -291,13 +291,15 @@ static void conv_generic(const AVFrame *frame, uint8_t *out, int sink_fmt, int c int src_bps = av_get_bytes_per_sample(frame->format); int planar = av_sample_fmt_is_planar(frame->format); int sink_bps = (sink_fmt == SOF_IPC_FRAME_S16_LE) ? 2 : 4; + int src_ch = frame->ch_layout.nb_channels; int i, ch; for (i = 0; i < frame->nb_samples; i++) { for (ch = 0; ch < channels; ch++) { + int c = (src_ch == 1) ? 0 : ch; const uint8_t *src = planar ? - frame->data[ch] + (size_t)i * src_bps : - frame->data[0] + ((size_t)i * channels + ch) * src_bps; + frame->data[c] + (size_t)i * src_bps : + frame->data[0] + ((size_t)i * src_ch + c) * src_bps; int32_t q = src_sample_to_q31(src, frame->format); q31_to_sink_sample(out + ((size_t)i * channels + ch) * sink_bps, @@ -385,14 +387,193 @@ static void conv_s32_to_fltp(const int32_t *src, float *const *dst, } } +static inline int16_t pcm_frame_to_s16_mono(const uint8_t *in, int idx, + int src_fmt, int src_ch) +{ + int32_t acc = 0; + int c; + + if (src_fmt == SOF_IPC_FRAME_S16_LE) { + const int16_t *s16 = (const int16_t *)in + (size_t)idx * src_ch; + + if (src_ch == 1) + return s16[0]; + for (c = 0; c < src_ch; c++) + acc += s16[c]; + return (int16_t)(acc / src_ch); + } + + if (src_fmt == SOF_IPC_FRAME_S24_4LE) { + const int32_t *s24 = (const int32_t *)in + (size_t)idx * src_ch; + + for (c = 0; c < src_ch; c++) + acc += Q_SHIFT(sign_extend_s24(s24[c]), 23, 15); + return (int16_t)(acc / src_ch); + } + + { + const int32_t *s32 = (const int32_t *)in + (size_t)idx * src_ch; + + for (c = 0; c < src_ch; c++) + acc += Q_SHIFT(s32[c], 31, 15); + return (int16_t)(acc / src_ch); + } +} + int ffmpeg_dec_pcm_to_frame(const uint8_t *in, AVFrame *frame, int src_fmt, int channels, int nb_samples) { + int dst_ch; + if (!in || !frame || channels <= 0 || nb_samples <= 0) return -EINVAL; - if (frame->ch_layout.nb_channels != channels) + + dst_ch = frame->ch_layout.nb_channels; + if (dst_ch != channels && !(dst_ch == 1 && frame->format == AV_SAMPLE_FMT_S16)) return -EINVAL; + if (frame->format == AV_SAMPLE_FMT_S16) { + int16_t *dst = (int16_t *)frame->data[0]; + + if (dst_ch == 1 && channels > 1) { + for (int i = 0; i < nb_samples; i++) + dst[i] = pcm_frame_to_s16_mono(in, i, src_fmt, channels); + return 0; + } + + size_t total = (size_t)nb_samples * dst_ch; + + if (src_fmt == SOF_IPC_FRAME_S16_LE) { + memcpy_s(dst, total * sizeof(int16_t), + in, total * sizeof(int16_t)); + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S24_4LE) { + const int32_t *src24 = (const int32_t *)in; + + for (size_t i = 0; i < total; i++) + dst[i] = (int16_t)Q_SHIFT(sign_extend_s24(src24[i]), 23, 15); + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S32_LE) { + const int32_t *src32 = (const int32_t *)in; + + for (size_t i = 0; i < total; i++) + dst[i] = (int16_t)Q_SHIFT(src32[i], 31, 15); + return 0; + } + return -EINVAL; + } + + if (frame->format == AV_SAMPLE_FMT_S16P) { + if (src_fmt == SOF_IPC_FRAME_S16_LE) { + const int16_t *src16 = (const int16_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int16_t *out = (int16_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) + out[i] = src16[i * dst_ch + c]; + } + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S24_4LE) { + const int32_t *src24 = (const int32_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int16_t *out = (int16_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) { + int32_t v = sign_extend_s24(src24[i * dst_ch + c]); + + out[i] = (int16_t)Q_SHIFT(v, 23, 15); + } + } + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S32_LE) { + const int32_t *src32 = (const int32_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int16_t *out = (int16_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) + out[i] = (int16_t)Q_SHIFT(src32[i * dst_ch + c], 31, 15); + } + return 0; + } + return -EINVAL; + } + + if (frame->format == AV_SAMPLE_FMT_S32) { + int32_t *dst = (int32_t *)frame->data[0]; + size_t total = (size_t)nb_samples * dst_ch; + + if (src_fmt == SOF_IPC_FRAME_S16_LE) { + const int16_t *src16 = (const int16_t *)in; + + for (size_t i = 0; i < total; i++) + dst[i] = Q_SHIFT_LEFT((int32_t)src16[i], 15, 31); + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S24_4LE) { + const int32_t *src24 = (const int32_t *)in; + + for (size_t i = 0; i < total; i++) + dst[i] = Q_SHIFT_LEFT(sign_extend_s24(src24[i]), 23, 31); + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S32_LE) { + memcpy_s(dst, total * sizeof(int32_t), + in, total * sizeof(int32_t)); + return 0; + } + return -EINVAL; + } + + if (frame->format == AV_SAMPLE_FMT_S32P) { + if (src_fmt == SOF_IPC_FRAME_S16_LE) { + const int16_t *src16 = (const int16_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int32_t *out = (int32_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) { + int32_t v = src16[i * dst_ch + c]; + + out[i] = Q_SHIFT_LEFT(v, 15, 31); + } + } + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S24_4LE) { + const int32_t *src24 = (const int32_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int32_t *out = (int32_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) { + int32_t v = sign_extend_s24(src24[i * dst_ch + c]); + + out[i] = Q_SHIFT_LEFT(v, 23, 31); + } + } + return 0; + } + if (src_fmt == SOF_IPC_FRAME_S32_LE) { + const int32_t *src32 = (const int32_t *)in; + + for (int c = 0; c < dst_ch; c++) { + int32_t *out = (int32_t *)frame->data[c]; + + for (int i = 0; i < nb_samples; i++) + out[i] = src32[i * dst_ch + c]; + } + return 0; + } + return -EINVAL; + } + if (frame->format == AV_SAMPLE_FMT_DBL) { double *dst = (double *)frame->data[0]; int total = nb_samples * channels; @@ -459,14 +640,19 @@ int ffmpeg_dec_convert_frame(const AVFrame *frame, uint8_t *out, size_t out_size /* * The frame's channel count comes from the bitstream, @channels from the - * sink. Every path below indexes the frame planes with @channels, so a - * stream with fewer channels than the sink would be read past its end. + * sink. Allow mono (nb_channels == 1) to upmix to @channels via conv_generic. */ - if (frame->ch_layout.nb_channels != channels) + if (frame->ch_layout.nb_channels != channels && + frame->ch_layout.nb_channels != 1) return -EINVAL; if (need > out_size) return -ENOSPC; + if (frame->ch_layout.nb_channels != channels) { + conv_generic(frame, out, sink_fmt, channels); + return (int)need; + } + /* 1. Direct copy if format, layout and bit-depth match identically */ if (channels == 1 || !av_sample_fmt_is_planar(frame->format)) { if (frame->format == AV_SAMPLE_FMT_S16 && sink_fmt == SOF_IPC_FRAME_S16_LE) { diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h index 8571f8475272..b4ab8f7f73f3 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h @@ -31,7 +31,7 @@ int ffmpeg_dec_convert_frame(const AVFrame *frame, uint8_t *out, size_t out_size /** * ffmpeg_dec_pcm_to_frame - Convert interleaved SOF PCM into an AVFrame. * @in: Source interleaved PCM buffer. - * @frame: Destination pre-allocated AVFrame (AV_SAMPLE_FMT_FLTP or AV_SAMPLE_FMT_DBL). + * @frame: Destination pre-allocated AVFrame (S16, S16P, S32, S32P, FLTP, or DBL). * @src_fmt: SOF valid sample format (SOF_IPC_FRAME_S16_LE / S24_4LE / S32_LE). * @channels: Input channel count. * @nb_samples: Number of audio frames (samples per channel) to convert. diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c b/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c index a7e1311b04c1..6dcc8d83ac73 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c @@ -84,6 +84,8 @@ struct ffmpeg_dec_ffmpeg_data { AVPacket *pkt; AVFrame *frame; uint8_t *pktbuf; + size_t pktbuf_sz; + size_t pktbuf_len; const AVCodec *codec; }; @@ -106,6 +108,10 @@ static enum AVCodecID ffmpeg_dec_av_codec_id(enum ffmpeg_dec_codec codec) #if defined(CONFIG_FFMPEG_DEC_MP3) case FFMPEG_DEC_CODEC_MP3: return AV_CODEC_ID_MP3; +#endif +#if defined(CONFIG_FFMPEG_DEC_ALAC) + case FFMPEG_DEC_CODEC_ALAC: + return AV_CODEC_ID_ALAC; #endif default: return AV_CODEC_ID_NONE; @@ -179,6 +185,37 @@ static int ffmpeg_dec_ff_configure(struct processing_module *mod) if (cd->codec_ops && cd->codec_ops->prepare_extradata) cd->codec_ops->prepare_extradata(mod); + if (cd->codec_ops && cd->codec_ops->max_frame_samples && cd->out_frame_bytes) { + size_t need = (size_t)cd->codec_ops->max_frame_samples(cd) * cd->out_frame_bytes; + + if (cd->pcm_buf_size < need) + return -ENOMEM; + } + + if (cd->codec == FFMPEG_DEC_CODEC_ALAC) { + uint32_t max_ch = cd->out_channels ? cd->out_channels : 2; + uint32_t cookie_samples = 0; + uint8_t cookie_bits = 0; + uint8_t cookie_ch = 0; + + if (cd->extradata_size >= 22 && + memcmp(&cd->extradata[4], "alac", 4) == 0) { + cookie_samples = ((uint32_t)cd->extradata[12] << 24) | + ((uint32_t)cd->extradata[13] << 16) | + ((uint32_t)cd->extradata[14] << 8) | + (uint32_t)cd->extradata[15]; + cookie_bits = cd->extradata[17]; + cookie_ch = cd->extradata[21]; + } + if (cookie_samples == 0 || cookie_samples > 16384 || + cookie_bits == 0 || cookie_bits > 32 || + cookie_ch == 0 || cookie_ch > max_ch) { + comp_err(dev, "unsupported ALAC cookie (samples=%u bits=%u ch=%u)", + cookie_samples, cookie_bits, cookie_ch); + return -EINVAL; + } + } + if (cd->extradata_size) { av_freep(&ff->avctx->extradata); ff->avctx->extradata = @@ -211,15 +248,29 @@ static int ffmpeg_dec_ff_configure(struct processing_module *mod) comp_info(dev, "libavcodec decoder opened (%s)", ff->codec->name); /* - * Padded scratch for a parsed packet payload, sized to OBS as a bound. + * Padded scratch for a parsed or reassembled packet payload, sized to + * hold a worst-case uncompressed frame (up to 4 bytes/sample/ch) plus + * framing overhead and AV_INPUT_BUFFER_PADDING_SIZE. * Route through av_malloc (the sys_heap-backed FFmpeg heap, see * ffmpeg_dec-builtin-alloc.c) rather than mod_alloc: the SOF per-module * heap is far too small for this tens-of-KiB scratch. */ - ff->pktbuf = av_malloc(cd->out_frame_bytes ? - cd->out_frame_bytes * 4096 : 65536); + { + uint32_t max_samples = (cd->codec_ops && cd->codec_ops->max_frame_samples) ? + cd->codec_ops->max_frame_samples(cd) : 4096; + size_t max_ch = cd->out_channels ? cd->out_channels : 2; + size_t frame_bytes = MAX((size_t)cd->out_frame_bytes, + max_ch * sizeof(int32_t)); + + ff->pktbuf_sz = frame_bytes * (size_t)max_samples; + if (ff->pktbuf_sz < 65536) + ff->pktbuf_sz = 65536; + ff->pktbuf_sz += 1024 + AV_INPUT_BUFFER_PADDING_SIZE; + } + ff->pktbuf = av_malloc(ff->pktbuf_sz); if (!ff->pktbuf) return -ENOMEM; + ff->pktbuf_len = 0; return 0; } @@ -249,6 +300,111 @@ static int ffmpeg_dec_ff_decode(struct processing_module *mod, *consumed = 0; *produced = 0; + /* + * Unparsed decode path for self-delimiting codecs without an + * AVCodecParser (e.g. ALAC, where alac_decode_frame reads bit-by-bit + * up to TYPE_END and reports the exact bit count in gb.index). + */ + if (!ff->parser) { + size_t prev_len = ff->pktbuf_len; + size_t cap = ff->pktbuf_sz > AV_INPUT_BUFFER_PADDING_SIZE ? + ff->pktbuf_sz - AV_INPUT_BUFFER_PADDING_SIZE : 0; + const uint8_t *pkt_data; + size_t pkt_len; + + if (cd->alac_skip_to_eof) { + *consumed = in_size; + return 0; + } + + if (!in || !in_size) { + if (!prev_len) + return 0; + pkt_data = ff->pktbuf; + pkt_len = prev_len; + } else if (prev_len == 0) { + pkt_data = in; + pkt_len = in_size; + } else { + size_t space = prev_len < cap ? cap - prev_len : 0; + size_t copy = in_size < space ? in_size : space; + + if (copy) { + memcpy_s(ff->pktbuf + prev_len, space, in, copy); + memset(ff->pktbuf + prev_len + copy, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + } + pkt_data = ff->pktbuf; + pkt_len = prev_len + copy; + } + + /* Stop cleanly if a trailing CAF chunk ("pakt" or "free") is reached */ + if (cd->codec == FFMPEG_DEC_CODEC_ALAC && pkt_len >= 4 && + (memcmp(pkt_data, "pakt", 4) == 0 || + memcmp(pkt_data, "free", 4) == 0)) { + cd->alac_skip_to_eof = true; + ff->pktbuf_len = 0; + *consumed = in_size; + return 0; + } + + ff->pkt->data = (uint8_t *)pkt_data; + ff->pkt->size = (int)pkt_len; + + av_log_set_level(AV_LOG_FATAL); + ret = avcodec_send_packet(ff->avctx, ff->pkt); + if (ret == 0) + ret = avcodec_receive_frame(ff->avctx, ff->frame); + av_log_set_level(AV_LOG_ERROR); + if (ret < 0) { + avcodec_flush_buffers(ff->avctx); + if (!in || !in_size) { + ff->pktbuf_len = 0; + return 0; + } + if (pkt_len >= cap) { + comp_err(dev, "unparsed packet reassembly buffer full without valid frame"); + ff->pktbuf_len = 0; + return -EIO; + } + if (prev_len == 0) { + size_t copy = in_size < cap ? in_size : cap; + + memcpy_s(ff->pktbuf, cap, in, copy); + memset(ff->pktbuf + copy, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + ff->pktbuf_len = copy; + *consumed = copy; + } else { + size_t space = prev_len < cap ? cap - prev_len : 0; + size_t copy = in_size < space ? in_size : space; + + ff->pktbuf_len = prev_len + copy; + *consumed = copy; + } + return 0; + } + + size_t fbytes = pkt_len; + + if (cd->codec_ops && cd->codec_ops->frame_bytes_consumed) { + size_t b = cd->codec_ops->frame_bytes_consumed(ff->avctx->priv_data); + + if (b > 0 && b <= pkt_len) + fbytes = b; + } + *consumed = (fbytes > prev_len) ? (fbytes - prev_len) : 0; + ff->pktbuf_len = 0; + + ret = ffmpeg_dec_emit_frame(mod, ff->frame, out, out_size); + if (ret < 0) { + comp_warn(dev, "output buffer full, PCM dropped"); + return 0; + } + *produced = (size_t)ret; + return 0; + } + if (!in || !in_size) { /* Flush parser first */ used = av_parser_parse2(ff->parser, ff->avctx, @@ -355,13 +511,15 @@ static int ffmpeg_dec_ff_reset(struct processing_module *mod) if (ff && ff->avctx && avcodec_is_open(ff->avctx)) avcodec_flush_buffers(ff->avctx); + if (ff) + ff->pktbuf_len = 0; + /* * Re-initialize the parser to clear any partial frame bytes buffered * from the previous stream. */ - if (ff && ff->codec) { - if (ff->parser) - av_parser_close(ff->parser); + if (ff && ff->codec && ff->parser) { + av_parser_close(ff->parser); ff->parser = av_parser_init(ff->codec->id); if (!ff->parser) return -ENOMEM; diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.c b/src/audio/ffmpeg_dec/ffmpeg_dec.c index 3411e058ddf5..a9e0bc7aade6 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.c @@ -228,6 +228,9 @@ static const struct ffmpeg_dec_codec_ops * const ffmpeg_dec_codecs[] = { #if defined(CONFIG_FFMPEG_DEC_OPUS) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) &ffmpeg_dec_opus_ops, #endif +#if defined(CONFIG_FFMPEG_DEC_ALAC) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) + &ffmpeg_dec_alac_ops, +#endif }; static const struct ffmpeg_dec_codec_ops *ffmpeg_dec_find_codec_ops(enum ffmpeg_dec_codec id) @@ -512,6 +515,8 @@ static int ffmpeg_dec_prepare(struct processing_module *mod, cd->skip_bytes = 0; cd->flac_parsing_meta = false; cd->flac_last_block = false; + cd->alac_parsing_caf = false; + cd->alac_skip_to_eof = false; cd->draining = false; cd->drained = false; @@ -881,6 +886,8 @@ static int ffmpeg_dec_reset(struct processing_module *mod) cd->skip_bytes = 0; cd->flac_parsing_meta = false; cd->flac_last_block = false; + cd->alac_parsing_caf = false; + cd->alac_skip_to_eof = false; cd->draining = false; cd->drained = false; #if CONFIG_IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.h b/src/audio/ffmpeg_dec/ffmpeg_dec.h index c05dfdf207c5..297e523a7d1e 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.h +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.h @@ -48,6 +48,7 @@ enum ffmpeg_dec_codec { FFMPEG_DEC_CODEC_AAC, FFMPEG_DEC_CODEC_OPUS, FFMPEG_DEC_CODEC_MP3, + FFMPEG_DEC_CODEC_ALAC, }; /* Default codec for a new instance, picked from the Kconfig selection until the @@ -55,6 +56,8 @@ enum ffmpeg_dec_codec { */ #if defined(CONFIG_FFMPEG_DEC_FLAC) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) #define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_FLAC +#elif defined(CONFIG_FFMPEG_DEC_ALAC) +#define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_ALAC #elif defined(CONFIG_FFMPEG_DEC_AAC) #define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_AAC #elif defined(CONFIG_FFMPEG_DEC_OPUS) @@ -174,6 +177,8 @@ struct ffmpeg_enc_data { extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_mp3_ops; extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_aac_ops; +extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_flac_ops; +extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_alac_ops; void ffmpeg_enc_pcm_to_s16_interleaved(const uint8_t *pcm_in, int16_t *dst, size_t total_samples, int src_fmt); @@ -184,15 +189,17 @@ void ffmpeg_enc_stage_packet(struct processing_module *mod, struct ffmpeg_enc_da /** * struct ffmpeg_dec_codec_ops - Per-codec stream and configuration operations. - * @id: SOF internal codec enum (enum ffmpeg_dec_codec). - * @snd_codec_id: ALSA compress SND_AUDIOCODEC_* identifier. - * @name: Human-readable codec name (e.g. "flac", "mp3", "aac", "opus"). - * @decoder_name: Optional explicit libavcodec decoder name (e.g. "aac_fixed"), or NULL. - * @max_frame_samples: Return maximum PCM samples per channel per decoded frame. - * @skip_container: Optional one-time stream container/header parser called before - * decoder open; returns 0 when ready to decode frames, -ENODATA - * when more input bytes are needed, or negative errno on error. - * @prepare_extradata: Optional hook to validate/filter cd->extradata before avcodec_open2(). + * @id: SOF internal codec enum (enum ffmpeg_dec_codec). + * @snd_codec_id: ALSA compress SND_AUDIOCODEC_* identifier. + * @name: Human-readable codec name (e.g. "flac", "mp3", "aac", "opus"). + * @decoder_name: Optional explicit libavcodec decoder name (e.g. "aac_fixed"), or NULL. + * @max_frame_samples: Return maximum PCM samples per channel per decoded frame. + * @skip_container: Optional one-time stream container/header parser called before + * decoder open; returns 0 when ready to decode frames, -ENODATA + * when more input bytes are needed, or negative errno on error. + * @prepare_extradata: Optional hook to validate/filter cd->extradata before avcodec_open2(). + * @frame_bytes_consumed: Optional hook for unparsed self-delimiting codecs (e.g. ALAC) + * returning the exact byte count consumed by the last decoded frame. */ struct ffmpeg_dec_codec_ops { enum ffmpeg_dec_codec id; @@ -202,12 +209,14 @@ struct ffmpeg_dec_codec_ops { uint32_t (*max_frame_samples)(const struct ffmpeg_dec_comp_data *cd); int (*skip_container)(struct processing_module *mod, struct sof_source *src); void (*prepare_extradata)(struct processing_module *mod); + size_t (*frame_bytes_consumed)(const void *priv_data); }; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_flac_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_mp3_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_aac_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_opus_ops; +extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_alac_ops; /** * struct ffmpeg_dec_backend - decode backend operations. @@ -287,6 +296,8 @@ struct ffmpeg_dec_comp_data { size_t skip_bytes; bool flac_parsing_meta; bool flac_last_block; + bool alac_parsing_caf; + bool alac_skip_to_eof; bool draining; bool drained; #if CONFIG_IPC_MAJOR_4 From a7ed83c54b07273be6f467073f215fcb40919097 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 03/12] ffmpeg_dec: add ALAC lossless decoder support Add Apple Lossless Audio Codec (ALAC, SND_AUDIOCODEC_ALAC) decoder support via libavcodec's integer alac decoder: - Support raw ALAC packets with 36-byte out-of-band magic cookie extradata as well as self-contained Core Audio Format (.caf) streams. - Resize the PCM output staging buffer if the magic cookie's max_samples per frame exceeds the initial allocation. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 3 + src/audio/ffmpeg_dec/Kconfig | 8 + src/audio/ffmpeg_dec/ffmpeg.cmake | 3 + src/audio/ffmpeg_dec/ffmpeg_dec-alac.c | 353 ++++++++++++++++++++++ src/audio/ffmpeg_dec/llext/CMakeLists.txt | 4 + 5 files changed, 371 insertions(+) create mode 100644 src/audio/ffmpeg_dec/ffmpeg_dec-alac.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index c7863b8b0c61..1da4d1f484c5 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -18,6 +18,9 @@ else() if(CONFIG_FFMPEG_DEC_OPUS OR CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-opus.c) endif() + if(CONFIG_FFMPEG_DEC_ALAC OR CONFIG_COMP_FFMPEG_DEC_STUB) + add_local_sources(sof ffmpeg_dec-alac.c) + endif() if(CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-stub.c) diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index 23d56c197f7a..f37589c661fb 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -51,6 +51,14 @@ config FFMPEG_DEC_MP3 MPEG-1/2 Layer III decoder. Enables --enable-decoder=mp3 and the mpegaudio parser in the libavcodec build. Uses the float math layer. +config FFMPEG_DEC_ALAC + bool "ALAC decoder" + help + Apple Lossless Audio Codec (ALAC) decoder. Pure 16/24-bit integer + lossless decoder with no floating-point math dependency. Enables + --enable-decoder=alac in the libavcodec build for compress playback + streams requesting SND_AUDIOCODEC_ALAC. + comment "FFmpeg encoders to build (footprint scales with selection)" config FFMPEG_ENC_MP3 diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 6318e7045c41..21d1049d613c 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -43,6 +43,9 @@ if(CONFIG_FFMPEG_DEC_MP3) list(APPEND _ff_decoders mp3) list(APPEND _ff_parsers mpegaudio) endif() +if(CONFIG_FFMPEG_DEC_ALAC) + list(APPEND _ff_decoders alac) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-alac.c b/src/audio/ffmpeg_dec/ffmpeg_dec-alac.c new file mode 100644 index 000000000000..a75993a9d39e --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-alac.c @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// ALAC per-codec container parsing, extradata setup, and frame sizing for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define ALAC_EXTRADATA_SIZE 36 +#define ALAC_SPECIFIC_CONFIG_SIZE 24 +#define ALAC_DEFAULT_FRAME_SAMPLES 4096 + +/* + * Prefix layout of libavcodec's private ALACContext (libavcodec/alac.c): + * AVClass *class; + * AVCodecContext *avctx; + * GetBitContext gb; -> { const uint8_t *buffer, *buffer_end; int index; ... } + * + * ALAC frames are self-delimiting via a 3-bit TYPE_END (7) syntax element and + * padded to the next byte boundary by flush_put_bits(). Because libavcodec + * does not ship an AVCodecParser for ALAC, reading gb.index after + * avcodec_receive_frame() yields the exact bit count of the decoded frame. + */ +struct alac_priv_prefix { + const void *av_class; + void *avctx; + const uint8_t *gb_buffer; + const uint8_t *gb_buffer_end; + int gb_index; +}; + +static inline uint32_t alac_read_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | + ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | + (uint32_t)p[3]; +} + +static inline uint64_t alac_read_be64(const uint8_t *p) +{ + return ((uint64_t)alac_read_be32(p) << 32) | + (uint64_t)alac_read_be32(p + 4); +} + +static inline void alac_write_be16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v >> 8); + p[1] = (uint8_t)v; +} + +static inline void alac_write_be32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)v; +} + +static uint32_t ffmpeg_dec_alac_max_frame_samples(const struct ffmpeg_dec_comp_data *cd) +{ + if (cd->extradata_size >= 16 && + memcmp(&cd->extradata[4], "alac", 4) == 0) { + uint32_t max_samples = alac_read_be32(&cd->extradata[12]); + + if (max_samples > 0 && max_samples <= 16384) + return max_samples; + } + return ALAC_DEFAULT_FRAME_SAMPLES; +} + +static size_t ffmpeg_dec_alac_frame_bytes_consumed(const void *priv_data) +{ + const struct alac_priv_prefix *priv = priv_data; + + if (!priv || priv->gb_index <= 0) + return 0; + + return (size_t)((priv->gb_index + 7) >> 3); +} + +/* + * Strip an optional leading ID3v2 tag, a Core Audio Format ("caff") container + * header (extracting the 36-byte QuickTime 'alac' magic cookie from the "kuki" + * chunk and stopping at the start of the "data" chunk payload), or a leading + * 36-byte QuickTime 'alac' atom prefix emitted by the SOF ALAC encoder. + */ +static int ffmpeg_dec_alac_skip_container(struct processing_module *mod, + struct sof_source *src) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct comp_dev *dev = mod->dev; + const void *sp, *sstart; + size_t sbytes, avail, req; + int ret; + + /* Step 1: Drain any pending skip bytes from a previous CAF chunk */ + if (cd->alac_parsing_caf && cd->skip_bytes > 0) { + avail = source_get_data_available(src); + if (!avail) + return -ENODATA; + size_t to_skip = MIN(avail, cd->skip_bytes); + + source_drop_data(src, to_skip); + cd->skip_bytes -= to_skip; + if (cd->skip_bytes > 0) + return -ENODATA; + } + + /* Step 2: Parse CAF chunks until the "data" chunk payload is reached */ + while (cd->alac_parsing_caf) { + uint8_t chunk_hdr[12]; + int64_t chunk_size; + + avail = source_get_data_available(src); + if (avail < 12) + return -ENODATA; + + ret = source_get_data(src, 12, &sp, &sstart, &sbytes); + if (ret) + return ret; + ffmpeg_dec_copy_from_circular(chunk_hdr, sp, sstart, sbytes, 12); + source_release_data(src, 0); + + chunk_size = (int64_t)alac_read_be64(&chunk_hdr[4]); + + /* "data" chunk: 12-byte header + 4-byte mEditCount, followed by ALAC frames */ + if (memcmp(chunk_hdr, "data", 4) == 0) { + if (avail < 16) + return -ENODATA; + + source_drop_data(src, 16); + cd->alac_parsing_caf = false; + comp_info(dev, "ALAC CAF data chunk reached (size %lld)", + (long long)chunk_size); + return 0; + } + + if (chunk_size < 0 || chunk_size > 0x10000000LL) { + comp_err(dev, "invalid CAF chunk %.4s size %lld", + (const char *)chunk_hdr, (long long)chunk_size); + return -EINVAL; + } + + /* "kuki" (magic cookie) chunk: extract 36-byte QuickTime 'alac' atom */ + if (memcmp(chunk_hdr, "kuki", 4) == 0 && + chunk_size >= ALAC_EXTRADATA_SIZE && + 12 + (size_t)chunk_size <= cd->in_buf_size) { + req = 12 + (size_t)chunk_size; + if (avail < req) + return -ENODATA; + + ret = source_get_data(src, req, &sp, &sstart, &sbytes); + if (ret) + return ret; + ffmpeg_dec_copy_from_circular(cd->in_buf, sp, sstart, sbytes, req); + source_release_data(src, 0); + + if (!cd->extradata_size) { + const uint8_t *cookie = &cd->in_buf[12]; + size_t cookie_len = (size_t)chunk_size; + const uint8_t *atom = NULL; + + /* Locate the 36-byte 'alac' atom inside the cookie */ + for (size_t off = 0; off + ALAC_EXTRADATA_SIZE <= cookie_len; + off++) { + if (alac_read_be32(&cookie[off]) == ALAC_EXTRADATA_SIZE && + memcmp(&cookie[off + 4], "alac", 4) == 0) { + atom = &cookie[off]; + break; + } + } + if (!atom) + atom = cookie + cookie_len - ALAC_EXTRADATA_SIZE; + + ffmpeg_dec_store_extradata(mod, atom, ALAC_EXTRADATA_SIZE); + comp_info(dev, "extracted 36-byte ALAC cookie from CAF kuki"); + } + source_drop_data(src, req); + continue; + } + + /* Skip other CAF metadata chunks ("desc", "chan", "info", "free", etc.) */ + size_t chunk_total = 12 + (size_t)chunk_size; + + if (avail >= chunk_total) { + source_drop_data(src, chunk_total); + continue; + } + + source_drop_data(src, avail); + cd->skip_bytes = chunk_total - avail; + return -ENODATA; + } + + /* Step 3: Skip any leading ID3v2 tag before checking for "caff" or 'alac' atom */ + do { + ret = ffmpeg_dec_skip_id3v2(mod, src); + if (ret < 0) + return ret; + } while (ret > 0); + + avail = source_get_data_available(src); + + /* Step 4: Check for the 8-byte Core Audio Format ("caff\x00\x01\x00\x00") header */ + if (memcmp(cd->in_buf, "caff", 4) == 0) { + if (avail < 8) + return -ENODATA; + + comp_info(dev, "CAF header detected, parsing ALAC container chunks"); + source_drop_data(src, 8); + cd->alac_parsing_caf = true; + return ffmpeg_dec_alac_skip_container(mod, src); + } + + /* Step 5: Check for an inline 36-byte QuickTime 'alac' atom prefix */ + if (avail >= 8 && + alac_read_be32(cd->in_buf) == ALAC_EXTRADATA_SIZE && + memcmp(&cd->in_buf[4], "alac", 4) == 0) { + if (avail < ALAC_EXTRADATA_SIZE) + return -ENODATA; + + ret = source_get_data(src, ALAC_EXTRADATA_SIZE, &sp, &sstart, &sbytes); + if (ret) + return ret; + ffmpeg_dec_copy_from_circular(cd->in_buf, sp, sstart, sbytes, + ALAC_EXTRADATA_SIZE); + source_release_data(src, 0); + + if (!cd->extradata_size) { + ffmpeg_dec_store_extradata(mod, cd->in_buf, ALAC_EXTRADATA_SIZE); + comp_info(dev, "extracted inline 36-byte ALAC atom header"); + } + source_drop_data(src, ALAC_EXTRADATA_SIZE); + return 0; + } + + return 0; +} + +static void ffmpeg_dec_alac_resize_pcm_buf(struct processing_module *mod) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + uint32_t max_samples = ffmpeg_dec_alac_max_frame_samples(cd); + size_t frame_bytes = cd->out_frame_bytes ? cd->out_frame_bytes : 8; + size_t need = (size_t)max_samples * frame_bytes; + + if (need > cd->pcm_buf_size) { + uint8_t *new_buf = mod_alloc(mod, need); + + if (new_buf) { + mod_free(mod, cd->pcm_buf); + cd->pcm_buf = new_buf; + cd->pcm_buf_size = need; + } + } +} + +static void ffmpeg_dec_alac_prepare_extradata(struct processing_module *mod) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + uint8_t atom[ALAC_EXTRADATA_SIZE]; + uint32_t frame_length = ALAC_DEFAULT_FRAME_SAMPLES; + uint8_t compatible_version = 0; + uint8_t sample_size = 16; + uint8_t pb = 40; + uint8_t mb = 10; + uint8_t kb = 14; + uint8_t channels = cd->out_channels ? (uint8_t)cd->out_channels : 2; + uint16_t max_run = 255; + uint32_t max_frame_bytes = 0; + uint32_t avg_bitrate = 0; + uint32_t sample_rate = cd->out_rate ? cd->out_rate : 48000; + + /* Valid 36-byte QuickTime 'alac' atom already present */ + if (cd->extradata_size == ALAC_EXTRADATA_SIZE && + memcmp(&cd->extradata[4], "alac", 4) == 0) { + ffmpeg_dec_alac_resize_pcm_buf(mod); + return; + } + + /* Wrap a raw 24-byte ALACSpecificConfig into the 36-byte QuickTime 'alac' atom */ + if (cd->extradata_size == ALAC_SPECIFIC_CONFIG_SIZE) { + memset(atom, 0, 12); + alac_write_be32(&atom[0], ALAC_EXTRADATA_SIZE); + memcpy_s(&atom[4], 4, "alac", 4); + memcpy_s(&atom[12], ALAC_SPECIFIC_CONFIG_SIZE, + cd->extradata, ALAC_SPECIFIC_CONFIG_SIZE); + ffmpeg_dec_store_extradata(mod, atom, ALAC_EXTRADATA_SIZE); + ffmpeg_dec_alac_resize_pcm_buf(mod); + return; + } + +#if CONFIG_IPC_MAJOR_4 + if (mod->priv.cfg.ext_data && + mod->priv.cfg.ext_data->module_data && + mod->priv.cfg.ext_data->module_data_size >= sizeof(struct snd_codec)) { + const struct snd_codec *codec_params = mod->priv.cfg.ext_data->module_data; + const struct snd_dec_alac *alac_opt = &codec_params->options.alac_d; + + if (alac_opt->frame_length > 0 && alac_opt->frame_length <= 16384) + frame_length = alac_opt->frame_length; + if (alac_opt->pb > 0) + pb = alac_opt->pb; + if (alac_opt->mb > 0) + mb = alac_opt->mb; + if (alac_opt->kb > 0) + kb = alac_opt->kb; + if (alac_opt->max_run > 0 && alac_opt->max_run <= 0xFFFF) + max_run = (uint16_t)alac_opt->max_run; + if (alac_opt->max_frame_bytes > 0) + max_frame_bytes = alac_opt->max_frame_bytes; + } +#endif + + memset(atom, 0, sizeof(atom)); + alac_write_be32(&atom[0], ALAC_EXTRADATA_SIZE); + memcpy_s(&atom[4], 4, "alac", 4); + alac_write_be32(&atom[12], frame_length); + atom[16] = compatible_version; + atom[17] = sample_size; + atom[18] = pb; + atom[19] = mb; + atom[20] = kb; + atom[21] = channels; + alac_write_be16(&atom[22], max_run); + alac_write_be32(&atom[24], max_frame_bytes); + alac_write_be32(&atom[28], avg_bitrate); + alac_write_be32(&atom[32], sample_rate); + + ffmpeg_dec_store_extradata(mod, atom, ALAC_EXTRADATA_SIZE); + ffmpeg_dec_alac_resize_pcm_buf(mod); + comp_info(mod->dev, "synthesized 36-byte ALAC extradata (frame_len=%u rate=%u ch=%u)", + frame_length, sample_rate, channels); +} + +const struct ffmpeg_dec_codec_ops ffmpeg_dec_alac_ops = { + .id = FFMPEG_DEC_CODEC_ALAC, + .snd_codec_id = SND_AUDIOCODEC_ALAC, + .name = "alac", + .max_frame_samples = ffmpeg_dec_alac_max_frame_samples, + .skip_container = ffmpeg_dec_alac_skip_container, + .prepare_extradata = ffmpeg_dec_alac_prepare_extradata, + .frame_bytes_consumed = ffmpeg_dec_alac_frame_bytes_consumed, +}; diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 09fbd971c48a..5c0fc7aed847 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -9,6 +9,7 @@ if(CONFIG_COMP_FFMPEG_DEC_STUB) ../ffmpeg_dec-mp3.c ../ffmpeg_dec-aac.c ../ffmpeg_dec-opus.c + ../ffmpeg_dec-alac.c ../ffmpeg_dec-stub.c LIB openmodules ) @@ -39,6 +40,9 @@ else() if(CONFIG_FFMPEG_DEC_OPUS) list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-opus.c) endif() + if(CONFIG_FFMPEG_DEC_ALAC) + list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-alac.c) + endif() # fastmathf provides the single-precision libm the codecs need AND sofm_log2f # used by the shims' log10(); always built for the real (non-stub) backend. list(APPEND ffmpeg_dec_srcs ../fastmathf.c) From ff5abe7b2466ffb15014f0bde0bd68e971fbc10b Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 04/12] ffmpeg_dec: add FLAC lossless encoder support Add native libavcodec FLAC lossless encoder (SND_AUDIOCODEC_FLAC) support for compress capture streams: - Use fixed integer LPC predictors (FF_LPC_TYPE_FIXED, compression_level 1) with 1152-sample blocks and zero floating-point instructions in the per-frame encode path. - Prepend the 42-byte "fLaC" + STREAMINFO header on stream start and convert input PCM via ffmpeg_dec_pcm_to_frame(). Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 6 + src/audio/ffmpeg_dec/Kconfig | 11 +- src/audio/ffmpeg_dec/ffmpeg-enc-flac.c | 237 ++++++++++++++++++++++ src/audio/ffmpeg_dec/ffmpeg.cmake | 16 ++ src/audio/ffmpeg_dec/llext/CMakeLists.txt | 3 + 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 src/audio/ffmpeg_dec/ffmpeg-enc-flac.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 1da4d1f484c5..48ac77a66122 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -37,6 +37,9 @@ else() if(CONFIG_FFMPEG_ENC_AAC) add_local_sources(sof ffmpeg-enc-aac.c) endif() + if(CONFIG_FFMPEG_ENC_FLAC) + add_local_sources(sof ffmpeg-enc-flac.c) + endif() if(CONFIG_FFMPEG_FILTER) add_local_sources(sof ffmpeg-filter.c) endif() @@ -66,6 +69,9 @@ else() if(CONFIG_FFMPEG_ENC_AAC) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-aac.c) endif() + if(CONFIG_FFMPEG_ENC_FLAC) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-flac.c) + endif() if(CONFIG_FFMPEG_FILTER) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-filter.c) endif() diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index f37589c661fb..a1acd6ef60a5 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -79,11 +79,20 @@ config FFMPEG_ENC_AAC Selected at runtime for compress capture streams requesting SND_AUDIOCODEC_AAC. +config FFMPEG_ENC_FLAC + bool "FLAC encoder (native libavcodec, fixed LPC)" + help + Lossless FLAC encoder via native libavcodec flac encoder configured + with fixed integer LPC predictors (FF_LPC_TYPE_FIXED, compression_level 1). + Emits a self-contained native .flac stream ("fLaC" + STREAMINFO + frames). + Selected at runtime for compress capture streams requesting + SND_AUDIOCODEC_FLAC. + # Selected automatically when any encoder is enabled. Gates compilation of the # runtime encode path (ffmpeg-encode.c). config FFMPEG_ENC bool - default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC + default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC comment "FFmpeg audio filters to build (libavfilter)" depends on IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-flac.c b/src/audio/ffmpeg_dec/ffmpeg-enc-flac.c new file mode 100644 index 000000000000..03c4e602bfe2 --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-flac.c @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// FLAC per-codec encoder implementation (native libavcodec flac) for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" +#include "ffmpeg_dec-convert.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define FLAC_STREAMINFO_SIZE 34 +#define FLAC_CONTAINER_HDR_SIZE (4 + 4 + FLAC_STREAMINFO_SIZE) + +struct ffmpeg_enc_flac_data { + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; + bool header_sent; +}; + +static int ffmpeg_enc_flac_validate_params(struct processing_module *mod, + const struct snd_codec *codec_params) +{ + struct comp_dev *dev = mod->dev; + + /* + * Accept UNDEFINED (0), FLAC/RAW stream format flags, and the ALSA + * SNDRV_PCM_FORMAT_S16_LE (2) / SNDRV_PCM_FORMAT_S32_LE (10) values + * that tinycompress crecord places in codec.format via -F. + */ + if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED && + codec_params->format != SND_AUDIOSTREAMFORMAT_FLAC && + codec_params->format != SND_AUDIOSTREAMFORMAT_RAW && + codec_params->format != 2 && + codec_params->format != 10) { + comp_err(dev, "unsupported FLAC stream format 0x%x", + codec_params->format); + return -ENOTSUP; + } + + return 0; +} + +static int ffmpeg_enc_flac_init(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct comp_dev *dev = mod->dev; + struct ffmpeg_enc_flac_data *flac; + + flac = mod_zalloc(mod, sizeof(*flac)); + if (!flac) + return -ENOMEM; + + flac->codec = avcodec_find_encoder(AV_CODEC_ID_FLAC); + if (!flac->codec) { + comp_err(dev, "libavcodec FLAC encoder not found"); + mod_free(mod, flac); + return -ENODEV; + } + + flac->avctx = avcodec_alloc_context3(flac->codec); + flac->pkt = av_packet_alloc(); + flac->frame = av_frame_alloc(); + if (!flac->avctx || !flac->pkt || !flac->frame) { + if (flac->frame) + av_frame_free(&flac->frame); + if (flac->pkt) + av_packet_free(&flac->pkt); + if (flac->avctx) + avcodec_free_context(&flac->avctx); + mod_free(mod, flac); + return -ENOMEM; + } + + e->codec_priv = flac; + return 0; +} + +static int ffmpeg_enc_flac_configure(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_flac_data *flac = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + if (cd->out_channels < 1 || cd->out_channels > 2) { + comp_err(dev, "unsupported FLAC channel count %u", cd->out_channels); + return -EINVAL; + } + + if (avcodec_is_open(flac->avctx)) { + avcodec_free_context(&flac->avctx); + flac->avctx = avcodec_alloc_context3(flac->codec); + if (!flac->avctx) + return -ENOMEM; + } + + flac->avctx->sample_rate = cd->out_rate; + av_channel_layout_default(&flac->avctx->ch_layout, cd->out_channels); + flac->avctx->sample_fmt = AV_SAMPLE_FMT_S16; + /* + * Compression level 1 selects FF_LPC_TYPE_FIXED (pure integer fixed + * predictors of order 0..4, 1152-sample block size), avoiding + * double-precision autocorrelation/Levinson-Durbin at runtime. + */ + flac->avctx->compression_level = 1; + flac->avctx->frame_size = 1152; + flac->avctx->thread_count = 1; + + ret = avcodec_open2(flac->avctx, flac->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (FLAC encoder) failed %d", ret); + return -EIO; + } + + e->nb = flac->avctx->frame_size ? flac->avctx->frame_size : 1152; + flac->header_sent = false; + return 0; +} + +static int ffmpeg_enc_flac_encode_frame(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_flac_data *flac = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ch = cd->out_channels; + int ret; + + /* + * Emit the 42-byte native FLAC container header ("fLaC" + 4-byte + * metadata block header [is_last=1, type=STREAMINFO(0), len=34] + + * 34-byte STREAMINFO from avctx->extradata) before the first frame + * so the captured stream is a valid standalone .flac file. + */ + if (!flac->header_sent) { + if (flac->avctx->extradata && + flac->avctx->extradata_size >= FLAC_STREAMINFO_SIZE) { + uint8_t hdr[FLAC_CONTAINER_HDR_SIZE]; + + memcpy_s(&hdr[0], 4, "fLaC", 4); + hdr[4] = 0x80; /* is_last = 1, type = 0 (STREAMINFO) */ + hdr[5] = 0x00; + hdr[6] = 0x00; + hdr[7] = FLAC_STREAMINFO_SIZE; + memcpy_s(&hdr[8], FLAC_STREAMINFO_SIZE, + flac->avctx->extradata, FLAC_STREAMINFO_SIZE); + ffmpeg_enc_stage_packet(mod, e, hdr, sizeof(hdr)); + } + flac->header_sent = true; + } + + flac->frame->nb_samples = e->nb; + flac->frame->format = flac->avctx->sample_fmt; + flac->frame->sample_rate = cd->out_rate; + av_channel_layout_copy(&flac->frame->ch_layout, &flac->avctx->ch_layout); + if (av_frame_get_buffer(flac->frame, 0) < 0) + return -ENOMEM; + + ffmpeg_dec_pcm_to_frame(e->pcm_in, flac->frame, + e->src_fmt, ch, e->nb); + + ret = avcodec_send_frame(flac->avctx, flac->frame); + av_frame_unref(flac->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame (FLAC) failed %d", ret); + return -EIO; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(flac->avctx, flac->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet (FLAC) failed %d", ret); + return -EIO; + } + if (flac->pkt->size > 0) + ffmpeg_enc_stage_packet(mod, e, flac->pkt->data, + (size_t)flac->pkt->size); + av_packet_unref(flac->pkt); + ret = 0; + } + + return 0; +} + +static void ffmpeg_enc_flac_reset(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_flac_data *flac = e->codec_priv; + + (void)mod; + if (!flac) + return; + + if (flac->frame) + av_frame_unref(flac->frame); + if (flac->pkt) + av_packet_unref(flac->pkt); + flac->header_sent = false; +} + +static void ffmpeg_enc_flac_free(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_flac_data *flac = e->codec_priv; + + if (!flac) + return; + + if (flac->frame) + av_frame_free(&flac->frame); + if (flac->pkt) + av_packet_free(&flac->pkt); + if (flac->avctx) + avcodec_free_context(&flac->avctx); + mod_free(mod, flac); + e->codec_priv = NULL; +} + +const struct ffmpeg_enc_codec_ops ffmpeg_enc_flac_ops = { + .id = FFMPEG_DEC_CODEC_FLAC, + .snd_codec_id = SND_AUDIOCODEC_FLAC, + .name = "flac", + .validate_params = ffmpeg_enc_flac_validate_params, + .init = ffmpeg_enc_flac_init, + .configure = ffmpeg_enc_flac_configure, + .encode_frame = ffmpeg_enc_flac_encode_frame, + .reset = ffmpeg_enc_flac_reset, + .free = ffmpeg_enc_flac_free, +}; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 21d1049d613c..197fe6084d81 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -46,6 +46,9 @@ endif() if(CONFIG_FFMPEG_DEC_ALAC) list(APPEND _ff_decoders alac) endif() +if(CONFIG_FFMPEG_ENC_FLAC) + list(APPEND _ff_encoders flac) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") @@ -95,6 +98,18 @@ if(CONFIG_FFMPEG_DEC_AAC) set(_ff_extra_cflags "${_ff_extra_cflags} -DSOF_AAC_LC_ONLY=1") endif() +# FFmpeg's stock FlacEncodeContext embeds fixed arrays for 8 channels x 65535 +# blocksize + 32 exact-Rice partition tables (~7.35 MiB). SOF's FLAC encoder +# runs at compression_level=1 (1152-sample blocksize, <=2 channels, estimated +# Rice params where only sums[0] is used); cap FlacEncodeContext in the +# out-of-tree build copy of flacenc.c so priv_data shrinks to ~60 KiB. +set(_ff_flacenc_prep "") +if(CONFIG_FFMPEG_ENC_FLAC) + set(_ff_extra_cflags "${_ff_extra_cflags} -I${SOF_FFMPEG_SRC_DIR}/libavcodec") + set(_ff_flacenc_prep + COMMAND sh -c "sed -e 's/FLAC_MAX_BLOCKSIZE/1152/g' -e 's/rc_sums\\[32\\]/rc_sums[1]/g' -e 's/subframes\\[FLAC_MAX_CHANNELS\\]/subframes[2]/g' -e 's/channels > FLAC_MAX_CHANNELS/channels > 2/g' /libavcodec/flacenc.c > /libavcodec/flacenc.c") +endif() + if(CMAKE_C_COMPILER_ID STREQUAL "Clang") # LLVM/xt-clang build. clang is a generic driver, so unlike the target- # specific GCC it needs the target/core/sysroot spelled out, it uses the @@ -580,6 +595,7 @@ ExternalProject_Add(ffmpeg_ext ${_ff_shine_cfg} --enable-small --enable-pic --enable-hardcoded-tables "--extra-cflags=${_ff_extra_cflags}" + ${_ff_flacenc_prep} BUILD_COMMAND ${CMAKE_COMMAND} -E env "PATH=${_tc_dir}:$ENV{PATH}" make -j 8 INSTALL_COMMAND make install diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 5c0fc7aed847..35fda65260cd 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -73,6 +73,9 @@ else() list(APPEND ffmpeg_dec_libs vo-aacenc) list(APPEND ffmpeg_dec_libpaths "${VO_AACENC_INSTALL}/lib") endif() + if(CONFIG_FFMPEG_ENC_FLAC) + list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-flac.c) + endif() # Real libm (newlib double math) LAST so libav*/filter references resolve against it get_filename_component(_ff_libm_dir "${FFMPEG_TARGET_LIBM}" DIRECTORY) From 8d648037d8086fdd78b3e3fc472e7f03020d7866 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 05/12] ffmpeg_dec: add ALAC lossless encoder support Add native libavcodec ALAC lossless encoder (SND_AUDIOCODEC_ALAC) support for 1..2 channel compress capture streams: - Configure 1024-sample frame size using pure integer prediction and adaptive Rice coding. - Prepend a 64-byte Core Audio Format (.caf) header ("caff" + "desc" + "kuki" + "data") so captured streams are directly decodable. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 6 + src/audio/ffmpeg_dec/Kconfig | 10 +- src/audio/ffmpeg_dec/ffmpeg-enc-alac.c | 237 ++++++++++++++++++++++ src/audio/ffmpeg_dec/ffmpeg.cmake | 3 + src/audio/ffmpeg_dec/llext/CMakeLists.txt | 3 + 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 src/audio/ffmpeg_dec/ffmpeg-enc-alac.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 48ac77a66122..7a5f48dfc889 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -40,6 +40,9 @@ else() if(CONFIG_FFMPEG_ENC_FLAC) add_local_sources(sof ffmpeg-enc-flac.c) endif() + if(CONFIG_FFMPEG_ENC_ALAC) + add_local_sources(sof ffmpeg-enc-alac.c) + endif() if(CONFIG_FFMPEG_FILTER) add_local_sources(sof ffmpeg-filter.c) endif() @@ -72,6 +75,9 @@ else() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-flac.c) endif() + if(CONFIG_FFMPEG_ENC_ALAC) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-alac.c) + endif() if(CONFIG_FFMPEG_FILTER) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-filter.c) endif() diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index a1acd6ef60a5..cace097fcaec 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -88,11 +88,19 @@ config FFMPEG_ENC_FLAC Selected at runtime for compress capture streams requesting SND_AUDIOCODEC_FLAC. +config FFMPEG_ENC_ALAC + bool "ALAC encoder (native libavcodec, fixed LPC)" + help + Apple Lossless Audio Codec (ALAC) encoder via native libavcodec alac + encoder configured with fixed integer LPC prediction (compression_level 1). + Selected at runtime for compress capture streams requesting + SND_AUDIOCODEC_ALAC. + # Selected automatically when any encoder is enabled. Gates compilation of the # runtime encode path (ffmpeg-encode.c). config FFMPEG_ENC bool - default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC + default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC comment "FFmpeg audio filters to build (libavfilter)" depends on IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-alac.c b/src/audio/ffmpeg_dec/ffmpeg-enc-alac.c new file mode 100644 index 000000000000..f2f00b261a05 --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-alac.c @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// ALAC per-codec encoder implementation (native libavcodec alac) for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" +#include "ffmpeg_dec-convert.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define ALAC_EXTRADATA_SIZE 36 +#define ALAC_ENC_FRAME_SAMPLES 1024 + +struct ffmpeg_enc_alac_data { + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; + bool header_sent; +}; + +static int ffmpeg_enc_alac_validate_params(struct processing_module *mod, + const struct snd_codec *codec_params) +{ + struct comp_dev *dev = mod->dev; + + if (codec_params->ch_in > 2 || codec_params->ch_out > 2) { + comp_err(dev, "unsupported ALAC channel count in=%u out=%u", + codec_params->ch_in, codec_params->ch_out); + return -ENOTSUP; + } + + /* + * Accept UNDEFINED (0), RAW stream format flag, and the ALSA + * SNDRV_PCM_FORMAT_S16_LE (2) / SNDRV_PCM_FORMAT_S32_LE (10) values + * that tinycompress crecord places in codec.format via -F. + */ + if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED && + codec_params->format != SND_AUDIOSTREAMFORMAT_RAW && + codec_params->format != 2 && + codec_params->format != 10) { + comp_err(dev, "unsupported ALAC stream format 0x%x", + codec_params->format); + return -ENOTSUP; + } + + return 0; +} + +static int ffmpeg_enc_alac_init(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct comp_dev *dev = mod->dev; + struct ffmpeg_enc_alac_data *alac; + + alac = mod_zalloc(mod, sizeof(*alac)); + if (!alac) + return -ENOMEM; + + alac->codec = avcodec_find_encoder(AV_CODEC_ID_ALAC); + if (!alac->codec) { + comp_err(dev, "libavcodec ALAC encoder not found"); + mod_free(mod, alac); + return -ENODEV; + } + + alac->avctx = avcodec_alloc_context3(alac->codec); + alac->pkt = av_packet_alloc(); + alac->frame = av_frame_alloc(); + if (!alac->avctx || !alac->pkt || !alac->frame) { + if (alac->frame) + av_frame_free(&alac->frame); + if (alac->pkt) + av_packet_free(&alac->pkt); + if (alac->avctx) + avcodec_free_context(&alac->avctx); + mod_free(mod, alac); + return -ENOMEM; + } + + e->codec_priv = alac; + return 0; +} + +static int ffmpeg_enc_alac_configure(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_alac_data *alac = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + if (cd->out_channels < 1 || cd->out_channels > 2) { + comp_err(dev, "unsupported ALAC channel count %u", cd->out_channels); + return -EINVAL; + } + + if (avcodec_is_open(alac->avctx)) { + avcodec_free_context(&alac->avctx); + alac->avctx = avcodec_alloc_context3(alac->codec); + if (!alac->avctx) + return -ENOMEM; + } + + alac->avctx->sample_rate = cd->out_rate; + av_channel_layout_default(&alac->avctx->ch_layout, cd->out_channels); + alac->avctx->sample_fmt = AV_SAMPLE_FMT_S16P; + /* + * Compression level 1 selects ALAC's fixed 6th-order integer predictor, + * avoiding double-precision autocorrelation/Levinson-Durbin at runtime. + */ + alac->avctx->compression_level = 1; + alac->avctx->thread_count = 1; + + ret = avcodec_open2(alac->avctx, alac->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (ALAC encoder) failed %d", ret); + return -EIO; + } + + /* + * Use 1024-sample frames (~21.3 ms @ 48 kHz) so each frame fits within + * the 16 KiB DP PCM/output staging rings while writing explicit frame + * sample counts in the ALAC frame headers. + */ + alac->avctx->frame_size = ALAC_ENC_FRAME_SAMPLES; + e->nb = ALAC_ENC_FRAME_SAMPLES; + alac->header_sent = false; + return 0; +} + +static int ffmpeg_enc_alac_encode_frame(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_alac_data *alac = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ch = cd->out_channels; + int ret; + + /* + * Prepend the 36-byte QuickTime 'alac' magic cookie atom before the + * first encoded frame so the captured stream is self-describing and + * can be decoded directly by ffmpeg_dec-alac. + */ + if (!alac->header_sent) { + if (alac->avctx->extradata && + alac->avctx->extradata_size >= ALAC_EXTRADATA_SIZE) { + ffmpeg_enc_stage_packet(mod, e, alac->avctx->extradata, + ALAC_EXTRADATA_SIZE); + } + alac->header_sent = true; + } + + alac->frame->nb_samples = e->nb; + alac->frame->format = alac->avctx->sample_fmt; + alac->frame->sample_rate = cd->out_rate; + av_channel_layout_copy(&alac->frame->ch_layout, &alac->avctx->ch_layout); + if (av_frame_get_buffer(alac->frame, 0) < 0) + return -ENOMEM; + + ffmpeg_dec_pcm_to_frame(e->pcm_in, alac->frame, + e->src_fmt, ch, e->nb); + + ret = avcodec_send_frame(alac->avctx, alac->frame); + av_frame_unref(alac->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame (ALAC) failed %d", ret); + return -EIO; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(alac->avctx, alac->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet (ALAC) failed %d", ret); + return -EIO; + } + if (alac->pkt->size > 0) + ffmpeg_enc_stage_packet(mod, e, alac->pkt->data, + (size_t)alac->pkt->size); + av_packet_unref(alac->pkt); + ret = 0; + } + + return 0; +} + +static void ffmpeg_enc_alac_reset(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_alac_data *alac = e->codec_priv; + + (void)mod; + if (!alac) + return; + + if (alac->frame) + av_frame_unref(alac->frame); + if (alac->pkt) + av_packet_unref(alac->pkt); + alac->header_sent = false; +} + +static void ffmpeg_enc_alac_free(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_alac_data *alac = e->codec_priv; + + if (!alac) + return; + + if (alac->frame) + av_frame_free(&alac->frame); + if (alac->pkt) + av_packet_free(&alac->pkt); + if (alac->avctx) + avcodec_free_context(&alac->avctx); + mod_free(mod, alac); + e->codec_priv = NULL; +} + +const struct ffmpeg_enc_codec_ops ffmpeg_enc_alac_ops = { + .id = FFMPEG_DEC_CODEC_ALAC, + .snd_codec_id = SND_AUDIOCODEC_ALAC, + .name = "alac", + .validate_params = ffmpeg_enc_alac_validate_params, + .init = ffmpeg_enc_alac_init, + .configure = ffmpeg_enc_alac_configure, + .encode_frame = ffmpeg_enc_alac_encode_frame, + .reset = ffmpeg_enc_alac_reset, + .free = ffmpeg_enc_alac_free, +}; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 197fe6084d81..3c814753534b 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -49,6 +49,9 @@ endif() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND _ff_encoders flac) endif() +if(CONFIG_FFMPEG_ENC_ALAC) + list(APPEND _ff_encoders alac) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 35fda65260cd..ed5bce933917 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -76,6 +76,9 @@ else() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-flac.c) endif() + if(CONFIG_FFMPEG_ENC_ALAC) + list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-alac.c) + endif() # Real libm (newlib double math) LAST so libav*/filter references resolve against it get_filename_component(_ff_libm_dir "${FFMPEG_TARGET_LIBM}" DIRECTORY) From e3da21786fb99877f1099453d82d4a4416ae7b7f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 06/12] topology2: add ALAC decode and FLAC/ALAC encode topologies Add development topology targets for ALAC compress playback and FLAC/ALAC compress capture on CAVS 2.5 and ACE 3.0. Signed-off-by: Liam Girdwood --- .../topology/topology2/development/tplg-targets.cmake | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/topology/topology2/development/tplg-targets.cmake b/tools/topology/topology2/development/tplg-targets.cmake index 9713c7088182..1082be23d71c 100644 --- a/tools/topology/topology2/development/tplg-targets.cmake +++ b/tools/topology/topology2/development/tplg-targets.cmake @@ -228,12 +228,15 @@ DEEPBUFFER_FW_DMA_MS=false,HDA_ANALOG_CAPTURE_RATE=96000,HDA_ANALOG_PLAYBACK_RAT "sof-hda-generic\;sof-hda-generic-ffmpeg-mp3-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=mp3" "sof-hda-generic\;sof-hda-generic-ffmpeg-aac-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=aac,COMPR_FFMPEG_DEC_HEAP=901120" "sof-hda-generic\;sof-hda-generic-ffmpeg-opus-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=opus,COMPR_FFMPEG_DEC_HEAP=524288" -# Compress capture (encode: PCM -> MP3 via libshine or AAC-LC via vo-aacenc) -# driven by ffmpeg_enc. COMPR_FFMPEG_ENC gates compr-capture-ffmpeg.conf, -# which taps the HDA analog capture BE, so it may be built alongside or -# independently of the decode COMPR_FFMPEG path above. +"sof-hda-generic\;sof-hda-generic-ffmpeg-alac-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=alac" +# Compress capture (encode: PCM -> MP3 via libshine, AAC-LC via vo-aacenc, +# or lossless FLAC/ALAC via libavcodec) driven by ffmpeg_enc. COMPR_FFMPEG_ENC +# gates compr-capture-ffmpeg.conf, which taps the HDA analog capture BE, so it +# may be built alongside or independently of the decode COMPR_FFMPEG path above. "sof-hda-generic\;sof-hda-generic-ffmpeg-mp3-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" "sof-hda-generic\;sof-hda-generic-ffmpeg-aac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" +"sof-hda-generic\;sof-hda-generic-ffmpeg-flac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" +"sof-hda-generic\;sof-hda-generic-ffmpeg-alac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" # SDW + DMIC + HDMI, with 16 kHz DMIC1 "cavs-sdw\;sof-mtl-sdw-cs42l42-l0-max98363-l2-4ch-48k-16k\;PLATFORM=mtl,\ From bef5e62bee5cf96029bf5b8ec56689a6aa9d0e7c Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 07/12] ipc: compress_params: add SBC, G.722, and aptX codec IDs Add SND_AUDIOCODEC_SBC (0x12), SND_AUDIOCODEC_G722 (0x13), and SND_AUDIOCODEC_APTX (0x14) definitions to compress_params.h and advertise them in base_fw.c alongside SND_AUDIOCODEC_BESPOKE when their corresponding Kconfig symbols are enabled. Signed-off-by: Liam Girdwood --- src/include/ipc/compress_params.h | 5 +++- src/kernel/audio/base_fw.c | 39 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/include/ipc/compress_params.h b/src/include/ipc/compress_params.h index cb205e62e01a..77c98d95b87f 100644 --- a/src/include/ipc/compress_params.h +++ b/src/include/ipc/compress_params.h @@ -78,7 +78,10 @@ #define SND_AUDIOCODEC_ALAC ((uint32_t) 0x0000000F) #define SND_AUDIOCODEC_APE ((uint32_t) 0x00000010) #define SND_AUDIOCODEC_OPUS_RAW ((uint32_t) 0x00000011) -#define SND_AUDIOCODEC_MAX SND_AUDIOCODEC_OPUS_RAW +#define SND_AUDIOCODEC_SBC ((uint32_t) 0x00000012) +#define SND_AUDIOCODEC_G722 ((uint32_t) 0x00000013) +#define SND_AUDIOCODEC_APTX ((uint32_t) 0x00000014) +#define SND_AUDIOCODEC_MAX SND_AUDIOCODEC_APTX /* * Profile and modes are listed with bit masks. This allows for a diff --git a/src/kernel/audio/base_fw.c b/src/kernel/audio/base_fw.c index 3c0108d9b500..a233acfcb64e 100644 --- a/src/kernel/audio/base_fw.c +++ b/src/kernel/audio/base_fw.c @@ -160,12 +160,51 @@ static void get_codec_info(struct sof_tlv **tuple) codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_ALAC, SOF_IPC_STREAM_CAPTURE); #endif +#if defined(CONFIG_FFMPEG_DEC_SBC) + /* Bluetooth SBC / mSBC decode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_SBC, SOF_IPC_STREAM_PLAYBACK); +#endif +#if defined(CONFIG_FFMPEG_ENC_SBC) + /* Bluetooth SBC / mSBC encode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_SBC, SOF_IPC_STREAM_CAPTURE); +#endif +#if defined(CONFIG_FFMPEG_DEC_G722) + /* ITU-T G.722 wideband speech decode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_G722, SOF_IPC_STREAM_PLAYBACK); +#endif +#if defined(CONFIG_FFMPEG_ENC_G722) + /* ITU-T G.722 wideband speech encode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_G722, SOF_IPC_STREAM_CAPTURE); +#endif +#if defined(CONFIG_FFMPEG_DEC_APTX) + /* Bluetooth aptX decode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_APTX, SOF_IPC_STREAM_PLAYBACK); +#endif +#if defined(CONFIG_FFMPEG_ENC_APTX) + /* Bluetooth aptX encode via ffmpeg_dec (native libavcodec). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_APTX, SOF_IPC_STREAM_CAPTURE); +#endif +#if (defined(CONFIG_FFMPEG_ENC_SBC) || defined(CONFIG_FFMPEG_ENC_G722) || \ + defined(CONFIG_FFMPEG_ENC_APTX)) && !defined(CONFIG_COMP_MFCC) + /* Also advertise BESPOKE capture for kernels with older SND_AUDIOCODEC_MAX. */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_BESPOKE, SOF_IPC_STREAM_CAPTURE); +#endif #ifdef CONFIG_FFMPEG_DEC_OPUS /* Opus decode is provided by the ffmpeg_dec (libavcodec) module. * Advertise OPUS_RAW and BESPOKE. */ codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_OPUS_RAW, SOF_IPC_STREAM_PLAYBACK); +#endif +#if defined(CONFIG_FFMPEG_DEC_OPUS) || defined(CONFIG_FFMPEG_DEC_SBC) || \ + defined(CONFIG_FFMPEG_DEC_G722) || defined(CONFIG_FFMPEG_DEC_APTX) codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_BESPOKE, SOF_IPC_STREAM_PLAYBACK); #endif From 4d74b12a41b297d622cfd9ea487a9701f252946f Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 08/12] ffmpeg_dec: extend framework for Bluetooth and voice codecs Extend the FFmpeg decode and encode adapter infrastructure for Bluetooth and wideband voice codecs: - Support SND_AUDIOCODEC_BESPOKE sub-codec dispatch via reserved[0], requested bitrate propagation, and encoder profile selection. - Add limit_packet_size hook in struct ffmpeg_dec_codec_ops and explicit decoder sample rate / G.722 bits_per_codeword initialization in ffmpeg_dec-ffmpeg.c. - Add stateful 13-tap FIR low-pass anti-aliased 3:1 (48->16 kHz) mono decimation helper ffmpeg_dec_pcm_decimate_3to1_s16_mono() in ffmpeg_dec-convert.c. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/ffmpeg-encode.c | 34 ++++++++-- src/audio/ffmpeg_dec/ffmpeg_dec-convert.c | 47 +++++++++++++ src/audio/ffmpeg_dec/ffmpeg_dec-convert.h | 16 +++++ src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c | 81 ++++++++++++++++++++++- src/audio/ffmpeg_dec/ffmpeg_dec.c | 25 +++++-- src/audio/ffmpeg_dec/ffmpeg_dec.h | 22 ++++++ 6 files changed, 215 insertions(+), 10 deletions(-) diff --git a/src/audio/ffmpeg_dec/ffmpeg-encode.c b/src/audio/ffmpeg_dec/ffmpeg-encode.c index 138813b2154e..570acba697c6 100644 --- a/src/audio/ffmpeg_dec/ffmpeg-encode.c +++ b/src/audio/ffmpeg_dec/ffmpeg-encode.c @@ -48,10 +48,26 @@ static const struct ffmpeg_enc_codec_ops * const ffmpeg_enc_codecs[] = { #if defined(CONFIG_FFMPEG_ENC_ALAC) &ffmpeg_enc_alac_ops, #endif +#if defined(CONFIG_FFMPEG_ENC_SBC) + &ffmpeg_enc_sbc_ops, +#endif +#if defined(CONFIG_FFMPEG_ENC_G722) + &ffmpeg_enc_g722_ops, +#endif +#if defined(CONFIG_FFMPEG_ENC_APTX) + &ffmpeg_enc_aptx_ops, +#endif }; -static const struct ffmpeg_enc_codec_ops *ffmpeg_enc_find_codec_ops_by_snd_id(uint32_t snd_id) +static const struct ffmpeg_enc_codec_ops * +ffmpeg_enc_find_codec_ops_by_snd_id(const struct snd_codec *codec_params) { + uint32_t snd_id = codec_params->id; + + if (snd_id == SND_AUDIOCODEC_BESPOKE && + codec_params->options.generic.reserved[0]) + snd_id = codec_params->options.generic.reserved[0]; + for (size_t i = 0; i < ARRAY_SIZE(ffmpeg_enc_codecs); i++) { if (ffmpeg_enc_codecs[i]->snd_codec_id == snd_id) return ffmpeg_enc_codecs[i]; @@ -322,7 +338,7 @@ int ffmpeg_enc_mod_init(struct processing_module *mod) return -ENOTSUP; } - ops = ffmpeg_enc_find_codec_ops_by_snd_id(codec_params->id); + ops = ffmpeg_enc_find_codec_ops_by_snd_id(codec_params); if (!ops) { comp_err(dev, "unsupported or disabled encoder snd_codec id 0x%x", codec_params->id); @@ -346,6 +362,8 @@ int ffmpeg_enc_mod_init(struct processing_module *mod) e->req_channels = codec_params->ch_in ? codec_params->ch_in : codec_params->ch_out; e->req_ch_mode = codec_params->ch_mode; + e->req_profile = codec_params->profile; + e->req_bit_rate = codec_params->bit_rate; comp_info(dev, "encoder codec selected from IPC: %s (snd_codec id 0x%x)", ops->name, codec_params->id); if (codec_params->bit_rate) @@ -392,6 +410,7 @@ int ffmpeg_enc_mod_prepare(struct processing_module *mod, e->out_avail = 0; e->out_rd = 0; e->pcm_fill = 0; + memset(e->decim_hist, 0, sizeof(e->decim_hist)); } dev->period = 96000; /* us */ @@ -460,8 +479,14 @@ static int ffmpeg_enc_configure(struct processing_module *mod, struct sof_source return -EINVAL; } - if ((e->req_rate && e->req_rate != cd->out_rate) || - (e->req_channels && e->req_channels != cd->out_channels)) { + if ((e->req_rate && e->req_rate != cd->out_rate && + !(e->req_rate == 16000 && cd->out_rate == 48000 && + (cd->codec == FFMPEG_DEC_CODEC_G722 || + (cd->codec == FFMPEG_DEC_CODEC_SBC && e->req_profile == 1)))) || + (e->req_channels && e->req_channels != cd->out_channels && + !(e->req_channels == 1 && + (cd->codec == FFMPEG_DEC_CODEC_G722 || + cd->codec == FFMPEG_DEC_CODEC_SBC)))) { comp_err(dev, "ffmpeg_enc: requested %u Hz/%u ch does not match source %u Hz/%u ch", e->req_rate, e->req_channels, cd->out_rate, cd->out_channels); return -EINVAL; @@ -590,6 +615,7 @@ int ffmpeg_enc_mod_reset(struct processing_module *mod) if (e->codec_ops && e->codec_ops->reset) e->codec_ops->reset(mod, e); ffmpeg_enc_free_staging(mod, e); + memset(e->decim_hist, 0, sizeof(e->decim_hist)); } cd->configured = false; return 0; diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c index cfc95eb6be39..f48290fe23e8 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.c @@ -420,6 +420,53 @@ static inline int16_t pcm_frame_to_s16_mono(const uint8_t *in, int idx, } } +void ffmpeg_dec_pcm_decimate_3to1_s16_mono(const uint8_t *in, int16_t *dst, + int src_fmt, int src_ch, + int nb_out_samples, + int16_t hist[FFMPEG_DECIM_3TO1_HIST_LEN]) +{ + int i, k; + + for (i = 0; i < nb_out_samples; i++) { + int16_t x0 = pcm_frame_to_s16_mono(in, 3 * i + 0, src_fmt, src_ch); + int16_t x1 = pcm_frame_to_s16_mono(in, 3 * i + 1, src_fmt, src_ch); + int16_t x2 = pcm_frame_to_s16_mono(in, 3 * i + 2, src_fmt, src_ch); + /* 13-sample window: hist[0..11] followed by x0, x1, x2 (shifted by 3) */ + int32_t s0 = hist[2]; + int32_t s2 = hist[4]; + int32_t s3 = hist[5]; + int32_t s4 = hist[6]; + int32_t s5 = hist[7]; + int32_t s6 = hist[8]; + int32_t s7 = hist[9]; + int32_t s8 = hist[10]; + int32_t s9 = hist[11]; + int32_t s10 = x0; + int32_t s12 = x2; + int32_t acc; + + /* + * 13-tap symmetric linear-phase FIR low-pass filter + * h = [-208, 0, 1096, 2272, 4344, 5840, 6080, 5840, 4344, 2272, 1096, 0, -208] + * Unity DC gain (32768), exact notch at 12 kHz, -30 dB at 16 kHz. + */ + acc = -208 * (s0 + s12) + + 1096 * (s2 + s10) + + 2272 * (s3 + s9) + + 4344 * (s4 + s8) + + 5840 * (s5 + s7) + + 6080 * s6; + acc = (acc + 16384) >> 15; + dst[i] = sat_int16(acc); + + for (k = 0; k < 9; k++) + hist[k] = hist[k + 3]; + hist[9] = x0; + hist[10] = x1; + hist[11] = x2; + } +} + int ffmpeg_dec_pcm_to_frame(const uint8_t *in, AVFrame *frame, int src_fmt, int channels, int nb_samples) { diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h index b4ab8f7f73f3..4a3dd7d3f047 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-convert.h @@ -28,6 +28,8 @@ int ffmpeg_dec_convert_frame(const AVFrame *frame, uint8_t *out, size_t out_size, int sink_fmt, int channels); +#define FFMPEG_DECIM_3TO1_HIST_LEN 12 + /** * ffmpeg_dec_pcm_to_frame - Convert interleaved SOF PCM into an AVFrame. * @in: Source interleaved PCM buffer. @@ -41,6 +43,20 @@ int ffmpeg_dec_convert_frame(const AVFrame *frame, uint8_t *out, size_t out_size int ffmpeg_dec_pcm_to_frame(const uint8_t *in, AVFrame *frame, int src_fmt, int channels, int nb_samples); +/** + * ffmpeg_dec_pcm_decimate_3to1_s16_mono - Anti-aliased 3:1 (48->16 kHz) mono S16 decimator. + * @in: Source interleaved 48 kHz PCM buffer (3 * @nb_out_samples frames). + * @dst: Destination 16 kHz mono S16 buffer (@nb_out_samples samples). + * @src_fmt: SOF valid sample format (SOF_IPC_FRAME_S16_LE / S24_4LE / S32_LE). + * @src_ch: Input channel count. + * @nb_out_samples: Number of 16 kHz output samples to produce. + * @hist: Stateful 12-sample FIR delay line preserved across frames. + */ +void ffmpeg_dec_pcm_decimate_3to1_s16_mono(const uint8_t *in, int16_t *dst, + int src_fmt, int src_ch, + int nb_out_samples, + int16_t hist[FFMPEG_DECIM_3TO1_HIST_LEN]); + /* HiFi SIMD helpers implemented in ffmpeg-convert-hifi.c */ int ffmpeg_hifi_fltp_to_s32(const float *const *src, int32_t *dst, int channels, int nb_samples); diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c b/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c index 6dcc8d83ac73..a87cf881aa3b 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-ffmpeg.c @@ -27,6 +27,7 @@ #include #include #include +#include #include LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); @@ -112,6 +113,18 @@ static enum AVCodecID ffmpeg_dec_av_codec_id(enum ffmpeg_dec_codec codec) #if defined(CONFIG_FFMPEG_DEC_ALAC) case FFMPEG_DEC_CODEC_ALAC: return AV_CODEC_ID_ALAC; +#endif +#if defined(CONFIG_FFMPEG_DEC_SBC) + case FFMPEG_DEC_CODEC_SBC: + return AV_CODEC_ID_SBC; +#endif +#if defined(CONFIG_FFMPEG_DEC_G722) + case FFMPEG_DEC_CODEC_G722: + return AV_CODEC_ID_ADPCM_G722; +#endif +#if defined(CONFIG_FFMPEG_DEC_APTX) + case FFMPEG_DEC_CODEC_APTX: + return AV_CODEC_ID_APTX; #endif default: return AV_CODEC_ID_NONE; @@ -240,6 +253,28 @@ static int ffmpeg_dec_ff_configure(struct processing_module *mod) if (ff->avctx->max_samples <= 0) ff->avctx->max_samples = INT_MAX; + if (cd->codec == FFMPEG_DEC_CODEC_G722) { + int bpc = 8; + + if (cd->req_bit_rate == 48000) { + bpc = 6; + } else if (cd->req_bit_rate == 56000) { + bpc = 7; + } else if (cd->req_bit_rate != 0 && cd->req_bit_rate != 64000) { + comp_err(dev, "unsupported G.722 bitrate %u", cd->req_bit_rate); + return -EINVAL; + } + ff->avctx->sample_rate = 16000; + av_opt_set_int(ff->avctx->priv_data, "bits_per_codeword", bpc, 0); + } + + if (cd->codec == FFMPEG_DEC_CODEC_APTX) { + if (ff->avctx->ch_layout.nb_channels <= 0) + av_channel_layout_default(&ff->avctx->ch_layout, 2); + if (ff->avctx->sample_rate <= 0) + ff->avctx->sample_rate = cd->out_rate ? cd->out_rate : 48000; + } + ret = avcodec_open2(ff->avctx, ff->codec, NULL); if (ret < 0) { comp_err(dev, "avcodec_open2 failed %d", ret); @@ -348,6 +383,47 @@ static int ffmpeg_dec_ff_decode(struct processing_module *mod, return 0; } + size_t raw_len = pkt_len; + + if (cd->codec_ops && cd->codec_ops->limit_packet_size) { + size_t lim = cd->codec_ops->limit_packet_size(cd, pkt_len); + + if (lim == 0) { + if (!in || !in_size) { + ff->pktbuf_len = 0; + return 0; + } + if (prev_len == 0) { + size_t copy = in_size < cap ? in_size : cap; + + memcpy_s(ff->pktbuf, cap, in, copy); + memset(ff->pktbuf + copy, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + ff->pktbuf_len = copy; + *consumed = copy; + } else { + size_t space = prev_len < cap ? cap - prev_len : 0; + size_t copy = in_size < space ? in_size : space; + + ff->pktbuf_len = prev_len + copy; + *consumed = copy; + } + return 0; + } + if (lim < pkt_len) { + if (pkt_data == in && lim <= cap) { + memcpy_s(ff->pktbuf, cap, in, lim); + memset(ff->pktbuf + lim, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + pkt_data = ff->pktbuf; + } else if (pkt_data == ff->pktbuf) { + memset(ff->pktbuf + lim, 0, + AV_INPUT_BUFFER_PADDING_SIZE); + } + pkt_len = lim; + } + } + ff->pkt->data = (uint8_t *)pkt_data; ff->pkt->size = (int)pkt_len; @@ -362,8 +438,9 @@ static int ffmpeg_dec_ff_decode(struct processing_module *mod, ff->pktbuf_len = 0; return 0; } - if (pkt_len >= cap) { - comp_err(dev, "unparsed packet reassembly buffer full without valid frame"); + if (raw_len >= cap || + (cd->codec_ops && cd->codec_ops->limit_packet_size)) { + comp_err(dev, "unparsed packet reassembly buffer full or invalid block"); ff->pktbuf_len = 0; return -EIO; } diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.c b/src/audio/ffmpeg_dec/ffmpeg_dec.c index a9e0bc7aade6..8724a5e6cb56 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.c @@ -231,6 +231,15 @@ static const struct ffmpeg_dec_codec_ops * const ffmpeg_dec_codecs[] = { #if defined(CONFIG_FFMPEG_DEC_ALAC) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) &ffmpeg_dec_alac_ops, #endif +#if defined(CONFIG_FFMPEG_DEC_SBC) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) + &ffmpeg_dec_sbc_ops, +#endif +#if defined(CONFIG_FFMPEG_DEC_G722) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) + &ffmpeg_dec_g722_ops, +#endif +#if defined(CONFIG_FFMPEG_DEC_APTX) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) + &ffmpeg_dec_aptx_ops, +#endif }; static const struct ffmpeg_dec_codec_ops *ffmpeg_dec_find_codec_ops(enum ffmpeg_dec_codec id) @@ -243,10 +252,17 @@ static const struct ffmpeg_dec_codec_ops *ffmpeg_dec_find_codec_ops(enum ffmpeg_ } #if CONFIG_IPC_MAJOR_4 -static const struct ffmpeg_dec_codec_ops *ffmpeg_dec_find_codec_ops_by_snd_id(uint32_t snd_id) +static const struct ffmpeg_dec_codec_ops * +ffmpeg_dec_find_codec_ops_by_snd_id(const struct snd_codec *codec_params) { - if (snd_id == SND_AUDIOCODEC_BESPOKE) - snd_id = SND_AUDIOCODEC_OPUS_RAW; + uint32_t snd_id = codec_params->id; + + if (snd_id == SND_AUDIOCODEC_BESPOKE) { + if (codec_params->options.generic.reserved[0]) + snd_id = codec_params->options.generic.reserved[0]; + else + snd_id = SND_AUDIOCODEC_OPUS_RAW; + } for (size_t i = 0; i < ARRAY_SIZE(ffmpeg_dec_codecs); i++) { if (ffmpeg_dec_codecs[i]->snd_codec_id == snd_id) @@ -346,7 +362,7 @@ __cold static int ffmpeg_dec_init(struct processing_module *mod) const struct snd_codec *codec_params = mod->priv.cfg.ext_data->module_data; const struct ffmpeg_dec_codec_ops *ops; - ops = ffmpeg_dec_find_codec_ops_by_snd_id(codec_params->id); + ops = ffmpeg_dec_find_codec_ops_by_snd_id(codec_params); if (!ops) { comp_err(dev, "unsupported or disabled snd_codec id 0x%x", codec_params->id); @@ -355,6 +371,7 @@ __cold static int ffmpeg_dec_init(struct processing_module *mod) } cd->codec_ops = ops; cd->codec = ops->id; + cd->req_bit_rate = codec_params->bit_rate; comp_info(dev, "codec selected from IPC: %s (%d, snd_codec id 0x%x)", cd->codec_ops->name, cd->codec, codec_params->id); } diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.h b/src/audio/ffmpeg_dec/ffmpeg_dec.h index 297e523a7d1e..d7a0a94dba4d 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.h +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.h @@ -49,6 +49,9 @@ enum ffmpeg_dec_codec { FFMPEG_DEC_CODEC_OPUS, FFMPEG_DEC_CODEC_MP3, FFMPEG_DEC_CODEC_ALAC, + FFMPEG_DEC_CODEC_SBC, + FFMPEG_DEC_CODEC_G722, + FFMPEG_DEC_CODEC_APTX, }; /* Default codec for a new instance, picked from the Kconfig selection until the @@ -64,6 +67,12 @@ enum ffmpeg_dec_codec { #define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_OPUS #elif defined(CONFIG_FFMPEG_DEC_MP3) #define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_MP3 +#elif defined(CONFIG_FFMPEG_DEC_SBC) +#define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_SBC +#elif defined(CONFIG_FFMPEG_DEC_G722) +#define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_G722 +#elif defined(CONFIG_FFMPEG_DEC_APTX) +#define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_APTX #else #define FFMPEG_DEC_DEFAULT_CODEC FFMPEG_DEC_CODEC_NONE #endif @@ -160,9 +169,12 @@ struct ffmpeg_enc_data { const struct ffmpeg_enc_codec_ops *codec_ops; void *codec_priv; uint32_t bit_rate; + uint32_t req_bit_rate; uint32_t req_rate; uint32_t req_channels; uint32_t req_ch_mode; + uint32_t req_profile; + int16_t decim_hist[12]; int src_fmt; int frame_bytes; int nb; @@ -179,6 +191,9 @@ extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_mp3_ops; extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_aac_ops; extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_flac_ops; extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_alac_ops; +extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_sbc_ops; +extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_g722_ops; +extern const struct ffmpeg_enc_codec_ops ffmpeg_enc_aptx_ops; void ffmpeg_enc_pcm_to_s16_interleaved(const uint8_t *pcm_in, int16_t *dst, size_t total_samples, int src_fmt); @@ -200,6 +215,8 @@ void ffmpeg_enc_stage_packet(struct processing_module *mod, struct ffmpeg_enc_da * @prepare_extradata: Optional hook to validate/filter cd->extradata before avcodec_open2(). * @frame_bytes_consumed: Optional hook for unparsed self-delimiting codecs (e.g. ALAC) * returning the exact byte count consumed by the last decoded frame. + * @limit_packet_size: Optional hook for unparsed block/stream codecs (e.g. G.722, aptX) + * returning the clamped/aligned input packet byte size per decode call. */ struct ffmpeg_dec_codec_ops { enum ffmpeg_dec_codec id; @@ -210,6 +227,7 @@ struct ffmpeg_dec_codec_ops { int (*skip_container)(struct processing_module *mod, struct sof_source *src); void (*prepare_extradata)(struct processing_module *mod); size_t (*frame_bytes_consumed)(const void *priv_data); + size_t (*limit_packet_size)(const struct ffmpeg_dec_comp_data *cd, size_t pkt_len); }; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_flac_ops; @@ -217,6 +235,9 @@ extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_mp3_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_aac_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_opus_ops; extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_alac_ops; +extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_sbc_ops; +extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_g722_ops; +extern const struct ffmpeg_dec_codec_ops ffmpeg_dec_aptx_ops; /** * struct ffmpeg_dec_backend - decode backend operations. @@ -281,6 +302,7 @@ struct ffmpeg_dec_comp_data { size_t extradata_size; uint32_t out_rate; uint32_t out_channels; + uint32_t req_bit_rate; int out_frame_fmt; uint32_t out_frame_bytes; bool is_encoder; From b11b135457f1c0b2f2a1887d1e314b5187dd210a Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 09/12] ffmpeg_dec: add Bluetooth SBC and mSBC decode and encode support Add Bluetooth A2DP Subband Codec (SBC) and HFP Wideband Speech (mSBC) decode and encode support (SND_AUDIOCODEC_SBC) via libavcodec's integer sbc codec and parser: - Support standard A2DP SBC (16/32/44.1/48 kHz, 1..2 channels) and HFP mSBC (profile == AV_PROFILE_SBC_MSBC, 16 kHz mono, 120-sample 57-byte frames). - Support 48->16 kHz anti-aliased decimation when capturing 48 kHz pipeline audio into a 16 kHz mSBC stream. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 9 + src/audio/ffmpeg_dec/Kconfig | 18 +- src/audio/ffmpeg_dec/ffmpeg-enc-sbc.c | 246 ++++++++++++++++++++++ src/audio/ffmpeg_dec/ffmpeg.cmake | 7 + src/audio/ffmpeg_dec/ffmpeg_dec-sbc.c | 29 +++ src/audio/ffmpeg_dec/llext/CMakeLists.txt | 7 + 6 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 src/audio/ffmpeg_dec/ffmpeg-enc-sbc.c create mode 100644 src/audio/ffmpeg_dec/ffmpeg_dec-sbc.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 7a5f48dfc889..55495eb14f75 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -21,6 +21,9 @@ else() if(CONFIG_FFMPEG_DEC_ALAC OR CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-alac.c) endif() + if(CONFIG_FFMPEG_DEC_SBC OR CONFIG_COMP_FFMPEG_DEC_STUB) + add_local_sources(sof ffmpeg_dec-sbc.c) + endif() if(CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-stub.c) @@ -43,6 +46,9 @@ else() if(CONFIG_FFMPEG_ENC_ALAC) add_local_sources(sof ffmpeg-enc-alac.c) endif() + if(CONFIG_FFMPEG_ENC_SBC) + add_local_sources(sof ffmpeg-enc-sbc.c) + endif() if(CONFIG_FFMPEG_FILTER) add_local_sources(sof ffmpeg-filter.c) endif() @@ -78,6 +84,9 @@ else() if(CONFIG_FFMPEG_ENC_ALAC) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-alac.c) endif() + if(CONFIG_FFMPEG_ENC_SBC) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-sbc.c) + endif() if(CONFIG_FFMPEG_FILTER) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-filter.c) endif() diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index cace097fcaec..9d787326c79f 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -59,6 +59,14 @@ config FFMPEG_DEC_ALAC --enable-decoder=alac in the libavcodec build for compress playback streams requesting SND_AUDIOCODEC_ALAC. +config FFMPEG_DEC_SBC + bool "Bluetooth SBC / mSBC decoder" + help + Bluetooth A2DP Subband Codec (SBC) and HFP Wideband Speech (mSBC) + decoder. Pure integer synthesis filterbank with no floating-point + math dependency. Enables --enable-decoder=sbc --enable-parser=sbc + in the libavcodec build. + comment "FFmpeg encoders to build (footprint scales with selection)" config FFMPEG_ENC_MP3 @@ -96,11 +104,19 @@ config FFMPEG_ENC_ALAC Selected at runtime for compress capture streams requesting SND_AUDIOCODEC_ALAC. +config FFMPEG_ENC_SBC + bool "Bluetooth SBC / mSBC encoder (native libavcodec)" + help + Bluetooth A2DP Subband Codec (SBC) and HFP Wideband Speech (mSBC) + encoder via native libavcodec sbc encoder. Pure integer analysis + filterbank with no floating-point math dependency. Selected at + runtime for compress capture streams requesting SND_AUDIOCODEC_SBC. + # Selected automatically when any encoder is enabled. Gates compilation of the # runtime encode path (ffmpeg-encode.c). config FFMPEG_ENC bool - default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC + default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC || FFMPEG_ENC_SBC comment "FFmpeg audio filters to build (libavfilter)" depends on IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-sbc.c b/src/audio/ffmpeg_dec/ffmpeg-enc-sbc.c new file mode 100644 index 000000000000..451f4731860c --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-sbc.c @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Bluetooth SBC / mSBC per-codec encoder implementation for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" +#include "ffmpeg_dec-convert.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define SBC_DEFAULT_STEREO_BITRATE 328000 +#define SBC_DEFAULT_MONO_BITRATE 198000 +#define MSBC_SAMPLE_RATE 16000 +#define MSBC_FRAME_SAMPLES 120 + +struct ffmpeg_enc_sbc_data { + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; + bool is_msbc; + int decimate; +}; + +static int ffmpeg_enc_sbc_validate_params(struct processing_module *mod, + const struct snd_codec *codec_params) +{ + struct comp_dev *dev = mod->dev; + + if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED && + codec_params->format != SND_AUDIOSTREAMFORMAT_RAW && + codec_params->format != 2 && + codec_params->format != 10) { + comp_err(dev, "unsupported SBC stream format 0x%x", + codec_params->format); + return -ENOTSUP; + } + + return 0; +} + +static int ffmpeg_enc_sbc_init(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct comp_dev *dev = mod->dev; + struct ffmpeg_enc_sbc_data *sbc; + + sbc = mod_zalloc(mod, sizeof(*sbc)); + if (!sbc) + return -ENOMEM; + + sbc->codec = avcodec_find_encoder(AV_CODEC_ID_SBC); + if (!sbc->codec) { + comp_err(dev, "libavcodec SBC encoder not found"); + mod_free(mod, sbc); + return -ENODEV; + } + + sbc->avctx = avcodec_alloc_context3(sbc->codec); + sbc->pkt = av_packet_alloc(); + sbc->frame = av_frame_alloc(); + if (!sbc->avctx || !sbc->pkt || !sbc->frame) { + if (sbc->frame) + av_frame_free(&sbc->frame); + if (sbc->pkt) + av_packet_free(&sbc->pkt); + if (sbc->avctx) + avcodec_free_context(&sbc->avctx); + mod_free(mod, sbc); + return -ENOMEM; + } + + e->codec_priv = sbc; + return 0; +} + +static int ffmpeg_enc_sbc_configure(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_sbc_data *sbc = e->codec_priv; + struct comp_dev *dev = mod->dev; + uint32_t enc_ch = e->req_channels ? e->req_channels : cd->out_channels; + uint32_t enc_rate = e->req_rate ? e->req_rate : cd->out_rate; + int ret; + + if (enc_ch < 1 || enc_ch > 2) { + comp_err(dev, "unsupported SBC channel count %u", enc_ch); + return -EINVAL; + } + + if (avcodec_is_open(sbc->avctx)) { + avcodec_free_context(&sbc->avctx); + sbc->avctx = avcodec_alloc_context3(sbc->codec); + if (!sbc->avctx) + return -ENOMEM; + } + + /* + * Select Bluetooth HFP Wideband Speech mSBC only when explicitly + * requested via profile == AV_PROFILE_SBC_MSBC (1). + */ + sbc->is_msbc = (e->req_profile == AV_PROFILE_SBC_MSBC); + sbc->decimate = 1; + + if (sbc->is_msbc) { + if (cd->out_rate == 48000) { + sbc->decimate = 3; + } else if (cd->out_rate != MSBC_SAMPLE_RATE) { + comp_err(dev, "mSBC requires 16 kHz or 48 kHz source rate (got %u)", + cd->out_rate); + return -EINVAL; + } + sbc->avctx->profile = AV_PROFILE_SBC_MSBC; + sbc->avctx->sample_rate = MSBC_SAMPLE_RATE; + av_channel_layout_default(&sbc->avctx->ch_layout, 1); + } else { + if (enc_rate != cd->out_rate) { + comp_err(dev, "SBC sample rate %u does not match source rate %u", + enc_rate, cd->out_rate); + return -EINVAL; + } + sbc->avctx->sample_rate = enc_rate; + av_channel_layout_default(&sbc->avctx->ch_layout, enc_ch); + sbc->avctx->bit_rate = e->req_bit_rate ? e->req_bit_rate : + (enc_ch == 2 ? SBC_DEFAULT_STEREO_BITRATE : + SBC_DEFAULT_MONO_BITRATE); + } + + sbc->avctx->sample_fmt = AV_SAMPLE_FMT_S16; + sbc->avctx->thread_count = 1; + + ret = avcodec_open2(sbc->avctx, sbc->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (SBC encoder) failed %d", ret); + return -EIO; + } + + if (sbc->is_msbc) + e->nb = MSBC_FRAME_SAMPLES * sbc->decimate; + else + e->nb = sbc->avctx->frame_size ? sbc->avctx->frame_size : 128; + + return 0; +} + +static int ffmpeg_enc_sbc_encode_frame(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_sbc_data *sbc = e->codec_priv; + struct comp_dev *dev = mod->dev; + int enc_samples = sbc->is_msbc ? MSBC_FRAME_SAMPLES : e->nb; + int ret; + + sbc->frame->nb_samples = enc_samples; + sbc->frame->format = sbc->avctx->sample_fmt; + sbc->frame->sample_rate = sbc->avctx->sample_rate; + av_channel_layout_copy(&sbc->frame->ch_layout, &sbc->avctx->ch_layout); + if (av_frame_get_buffer(sbc->frame, 0) < 0) + return -ENOMEM; + + if (sbc->is_msbc && sbc->decimate == 3) { + ffmpeg_dec_pcm_decimate_3to1_s16_mono(e->pcm_in, + (int16_t *)sbc->frame->data[0], + e->src_fmt, cd->out_channels, + MSBC_FRAME_SAMPLES, + e->decim_hist); + } else { + ffmpeg_dec_pcm_to_frame(e->pcm_in, sbc->frame, + e->src_fmt, cd->out_channels, + enc_samples); + } + + ret = avcodec_send_frame(sbc->avctx, sbc->frame); + av_frame_unref(sbc->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame (SBC) failed %d", ret); + return -EIO; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(sbc->avctx, sbc->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet (SBC) failed %d", ret); + return -EIO; + } + if (sbc->pkt->size > 0) + ffmpeg_enc_stage_packet(mod, e, sbc->pkt->data, + (size_t)sbc->pkt->size); + av_packet_unref(sbc->pkt); + ret = 0; + } + + return 0; +} + +static void ffmpeg_enc_sbc_reset(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_sbc_data *sbc = e->codec_priv; + + (void)mod; + if (!sbc) + return; + + if (sbc->frame) + av_frame_unref(sbc->frame); + if (sbc->pkt) + av_packet_unref(sbc->pkt); +} + +static void ffmpeg_enc_sbc_free(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_sbc_data *sbc = e->codec_priv; + + if (!sbc) + return; + + if (sbc->frame) + av_frame_free(&sbc->frame); + if (sbc->pkt) + av_packet_free(&sbc->pkt); + if (sbc->avctx) + avcodec_free_context(&sbc->avctx); + mod_free(mod, sbc); + e->codec_priv = NULL; +} + +const struct ffmpeg_enc_codec_ops ffmpeg_enc_sbc_ops = { + .id = FFMPEG_DEC_CODEC_SBC, + .snd_codec_id = SND_AUDIOCODEC_SBC, + .name = "sbc", + .validate_params = ffmpeg_enc_sbc_validate_params, + .init = ffmpeg_enc_sbc_init, + .configure = ffmpeg_enc_sbc_configure, + .encode_frame = ffmpeg_enc_sbc_encode_frame, + .reset = ffmpeg_enc_sbc_reset, + .free = ffmpeg_enc_sbc_free, +}; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 3c814753534b..a5d781b0c864 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -46,12 +46,19 @@ endif() if(CONFIG_FFMPEG_DEC_ALAC) list(APPEND _ff_decoders alac) endif() +if(CONFIG_FFMPEG_DEC_SBC) + list(APPEND _ff_decoders sbc) + list(APPEND _ff_parsers sbc) +endif() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND _ff_encoders flac) endif() if(CONFIG_FFMPEG_ENC_ALAC) list(APPEND _ff_encoders alac) endif() +if(CONFIG_FFMPEG_ENC_SBC) + list(APPEND _ff_encoders sbc) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-sbc.c b/src/audio/ffmpeg_dec/ffmpeg_dec-sbc.c new file mode 100644 index 000000000000..7f6ab6640fe6 --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-sbc.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Bluetooth SBC / mSBC per-codec frame sizing for ffmpeg_dec. + +#include +#include +#include "ffmpeg_dec.h" + +/* + * Standard A2DP SBC frames hold at most 8 subbands x 16 blocks = 128 samples; + * Bluetooth HFP Wideband Speech mSBC frames hold 8 subbands x 15 blocks = 120 + * samples. Size the output staging buffer to 1024 samples so several frames + * fit comfortably per DP cycle. + */ +static uint32_t ffmpeg_dec_sbc_max_frame_samples(const struct ffmpeg_dec_comp_data *cd) +{ + (void)cd; + return 1024; +} + +const struct ffmpeg_dec_codec_ops ffmpeg_dec_sbc_ops = { + .id = FFMPEG_DEC_CODEC_SBC, + .snd_codec_id = SND_AUDIOCODEC_SBC, + .name = "sbc", + .decoder_name = "sbc", + .max_frame_samples = ffmpeg_dec_sbc_max_frame_samples, +}; diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index ed5bce933917..25b57a984434 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -10,6 +10,7 @@ if(CONFIG_COMP_FFMPEG_DEC_STUB) ../ffmpeg_dec-aac.c ../ffmpeg_dec-opus.c ../ffmpeg_dec-alac.c + ../ffmpeg_dec-sbc.c ../ffmpeg_dec-stub.c LIB openmodules ) @@ -43,6 +44,9 @@ else() if(CONFIG_FFMPEG_DEC_ALAC) list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-alac.c) endif() + if(CONFIG_FFMPEG_DEC_SBC) + list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-sbc.c) + endif() # fastmathf provides the single-precision libm the codecs need AND sofm_log2f # used by the shims' log10(); always built for the real (non-stub) backend. list(APPEND ffmpeg_dec_srcs ../fastmathf.c) @@ -79,6 +83,9 @@ else() if(CONFIG_FFMPEG_ENC_ALAC) list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-alac.c) endif() + if(CONFIG_FFMPEG_ENC_SBC) + list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-sbc.c) + endif() # Real libm (newlib double math) LAST so libav*/filter references resolve against it get_filename_component(_ff_libm_dir "${FFMPEG_TARGET_LIBM}" DIRECTORY) From 8fffc8035f013651aada6d29ea98c6cbb7d54dd8 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 10/12] ffmpeg_dec: add ITU-T G.722 wideband speech decode and encode Add ITU-T G.722 7 kHz wideband speech ADPCM decode and encode support (SND_AUDIOCODEC_G722) via libavcodec's integer adpcm_g722 codec: - Support 64/56/48 kbps G.722 playback with 160-byte (320-sample, 20 ms) packet clamping. - Support 16 kHz mono G.722 encoding at 64 kbps, including anti-aliased 3:1 decimation when fed by a 48 kHz capture pipeline. Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 9 + src/audio/ffmpeg_dec/Kconfig | 18 +- src/audio/ffmpeg_dec/ffmpeg-enc-g722.c | 234 ++++++++++++++++++++++ src/audio/ffmpeg_dec/ffmpeg.cmake | 6 + src/audio/ffmpeg_dec/ffmpeg_dec-g722.c | 39 ++++ src/audio/ffmpeg_dec/llext/CMakeLists.txt | 7 + 6 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 src/audio/ffmpeg_dec/ffmpeg-enc-g722.c create mode 100644 src/audio/ffmpeg_dec/ffmpeg_dec-g722.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 55495eb14f75..6ea38bfb458e 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -24,6 +24,9 @@ else() if(CONFIG_FFMPEG_DEC_SBC OR CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-sbc.c) endif() + if(CONFIG_FFMPEG_DEC_G722 OR CONFIG_COMP_FFMPEG_DEC_STUB) + add_local_sources(sof ffmpeg_dec-g722.c) + endif() if(CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-stub.c) @@ -49,6 +52,9 @@ else() if(CONFIG_FFMPEG_ENC_SBC) add_local_sources(sof ffmpeg-enc-sbc.c) endif() + if(CONFIG_FFMPEG_ENC_G722) + add_local_sources(sof ffmpeg-enc-g722.c) + endif() if(CONFIG_FFMPEG_FILTER) add_local_sources(sof ffmpeg-filter.c) endif() @@ -87,6 +93,9 @@ else() if(CONFIG_FFMPEG_ENC_SBC) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-sbc.c) endif() + if(CONFIG_FFMPEG_ENC_G722) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-g722.c) + endif() if(CONFIG_FFMPEG_FILTER) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-filter.c) endif() diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index 9d787326c79f..0b4703f235e5 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -67,6 +67,14 @@ config FFMPEG_DEC_SBC math dependency. Enables --enable-decoder=sbc --enable-parser=sbc in the libavcodec build. +config FFMPEG_DEC_G722 + bool "ITU-T G.722 ADPCM wideband speech decoder" + help + ITU-T G.722 7 kHz wideband speech ADPCM decoder (64/56/48 kbps). + Pure integer QMF + 2-band ADPCM decoder with no floating-point math + dependency. Enables --enable-decoder=adpcm_g722 in the libavcodec + build for compress playback streams requesting SND_AUDIOCODEC_G722. + comment "FFmpeg encoders to build (footprint scales with selection)" config FFMPEG_ENC_MP3 @@ -112,11 +120,19 @@ config FFMPEG_ENC_SBC filterbank with no floating-point math dependency. Selected at runtime for compress capture streams requesting SND_AUDIOCODEC_SBC. +config FFMPEG_ENC_G722 + bool "ITU-T G.722 ADPCM wideband speech encoder (native libavcodec)" + help + ITU-T G.722 7 kHz wideband speech ADPCM encoder (64 kbps) via + native libavcodec g722 encoder. Pure integer QMF + 2-band ADPCM + encoder with no floating-point math dependency. Selected at runtime + for compress capture streams requesting SND_AUDIOCODEC_G722. + # Selected automatically when any encoder is enabled. Gates compilation of the # runtime encode path (ffmpeg-encode.c). config FFMPEG_ENC bool - default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC || FFMPEG_ENC_SBC + default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC || FFMPEG_ENC_SBC || FFMPEG_ENC_G722 comment "FFmpeg audio filters to build (libavfilter)" depends on IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-g722.c b/src/audio/ffmpeg_dec/ffmpeg-enc-g722.c new file mode 100644 index 000000000000..2f48e1209c9f --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-g722.c @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// ITU-T G.722 ADPCM wideband speech per-codec encoder implementation for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" +#include "ffmpeg_dec-convert.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define G722_SAMPLE_RATE 16000 +#define G722_ENC_FRAME_SAMPLES 1024 + +struct ffmpeg_enc_g722_data { + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; + int decimate; +}; + +static int ffmpeg_enc_g722_validate_params(struct processing_module *mod, + const struct snd_codec *codec_params) +{ + struct comp_dev *dev = mod->dev; + + if (codec_params->sample_rate != 0 && + codec_params->sample_rate != G722_SAMPLE_RATE && + codec_params->sample_rate != 48000) { + comp_err(dev, "unsupported G.722 sample rate %u", + codec_params->sample_rate); + return -ENOTSUP; + } + + if (codec_params->bit_rate != 0 && + codec_params->bit_rate != 64000) { + comp_err(dev, "unsupported G.722 encoder bit_rate %u", + codec_params->bit_rate); + return -ENOTSUP; + } + + if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED && + codec_params->format != SND_AUDIOSTREAMFORMAT_RAW && + codec_params->format != 2 && + codec_params->format != 10) { + comp_err(dev, "unsupported G.722 stream format 0x%x", + codec_params->format); + return -ENOTSUP; + } + + return 0; +} + +static int ffmpeg_enc_g722_init(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct comp_dev *dev = mod->dev; + struct ffmpeg_enc_g722_data *g722; + + g722 = mod_zalloc(mod, sizeof(*g722)); + if (!g722) + return -ENOMEM; + + g722->codec = avcodec_find_encoder(AV_CODEC_ID_ADPCM_G722); + if (!g722->codec) { + comp_err(dev, "libavcodec G.722 encoder not found"); + mod_free(mod, g722); + return -ENODEV; + } + + g722->avctx = avcodec_alloc_context3(g722->codec); + g722->pkt = av_packet_alloc(); + g722->frame = av_frame_alloc(); + if (!g722->avctx || !g722->pkt || !g722->frame) { + if (g722->frame) + av_frame_free(&g722->frame); + if (g722->pkt) + av_packet_free(&g722->pkt); + if (g722->avctx) + avcodec_free_context(&g722->avctx); + mod_free(mod, g722); + return -ENOMEM; + } + + e->codec_priv = g722; + return 0; +} + +static int ffmpeg_enc_g722_configure(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_g722_data *g722 = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + if (e->req_bit_rate != 0 && e->req_bit_rate != 64000) { + comp_err(dev, "unsupported G.722 encoder bit_rate %u", + e->req_bit_rate); + return -EINVAL; + } + + if (cd->out_rate == 48000) { + g722->decimate = 3; + } else if (cd->out_rate == G722_SAMPLE_RATE) { + g722->decimate = 1; + } else { + comp_err(dev, "G.722 requires 16 kHz or 48 kHz source rate (got %u)", + cd->out_rate); + return -EINVAL; + } + + if (avcodec_is_open(g722->avctx)) { + avcodec_free_context(&g722->avctx); + g722->avctx = avcodec_alloc_context3(g722->codec); + if (!g722->avctx) + return -ENOMEM; + } + + g722->avctx->sample_rate = G722_SAMPLE_RATE; + av_channel_layout_default(&g722->avctx->ch_layout, 1); + g722->avctx->sample_fmt = AV_SAMPLE_FMT_S16; + g722->avctx->frame_size = G722_ENC_FRAME_SAMPLES; + g722->avctx->thread_count = 1; + + ret = avcodec_open2(g722->avctx, g722->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (G.722 encoder) failed %d", ret); + return -EIO; + } + + e->nb = G722_ENC_FRAME_SAMPLES * g722->decimate; + return 0; +} + +static int ffmpeg_enc_g722_encode_frame(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_g722_data *g722 = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + g722->frame->nb_samples = G722_ENC_FRAME_SAMPLES; + g722->frame->format = g722->avctx->sample_fmt; + g722->frame->sample_rate = G722_SAMPLE_RATE; + av_channel_layout_copy(&g722->frame->ch_layout, &g722->avctx->ch_layout); + if (av_frame_get_buffer(g722->frame, 0) < 0) + return -ENOMEM; + + if (g722->decimate == 3) { + ffmpeg_dec_pcm_decimate_3to1_s16_mono(e->pcm_in, + (int16_t *)g722->frame->data[0], + e->src_fmt, cd->out_channels, + G722_ENC_FRAME_SAMPLES, + e->decim_hist); + } else { + ffmpeg_dec_pcm_to_frame(e->pcm_in, g722->frame, + e->src_fmt, cd->out_channels, + G722_ENC_FRAME_SAMPLES); + } + + ret = avcodec_send_frame(g722->avctx, g722->frame); + av_frame_unref(g722->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame (G.722) failed %d", ret); + return -EIO; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(g722->avctx, g722->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet (G.722) failed %d", ret); + return -EIO; + } + if (g722->pkt->size > 0) + ffmpeg_enc_stage_packet(mod, e, g722->pkt->data, + (size_t)g722->pkt->size); + av_packet_unref(g722->pkt); + ret = 0; + } + + return 0; +} + +static void ffmpeg_enc_g722_reset(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_g722_data *g722 = e->codec_priv; + + (void)mod; + if (!g722) + return; + + if (g722->frame) + av_frame_unref(g722->frame); + if (g722->pkt) + av_packet_unref(g722->pkt); +} + +static void ffmpeg_enc_g722_free(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_g722_data *g722 = e->codec_priv; + + if (!g722) + return; + + if (g722->frame) + av_frame_free(&g722->frame); + if (g722->pkt) + av_packet_free(&g722->pkt); + if (g722->avctx) + avcodec_free_context(&g722->avctx); + mod_free(mod, g722); + e->codec_priv = NULL; +} + +const struct ffmpeg_enc_codec_ops ffmpeg_enc_g722_ops = { + .id = FFMPEG_DEC_CODEC_G722, + .snd_codec_id = SND_AUDIOCODEC_G722, + .name = "g722", + .validate_params = ffmpeg_enc_g722_validate_params, + .init = ffmpeg_enc_g722_init, + .configure = ffmpeg_enc_g722_configure, + .encode_frame = ffmpeg_enc_g722_encode_frame, + .reset = ffmpeg_enc_g722_reset, + .free = ffmpeg_enc_g722_free, +}; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index a5d781b0c864..41305a3893a5 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -50,6 +50,9 @@ if(CONFIG_FFMPEG_DEC_SBC) list(APPEND _ff_decoders sbc) list(APPEND _ff_parsers sbc) endif() +if(CONFIG_FFMPEG_DEC_G722) + list(APPEND _ff_decoders adpcm_g722) +endif() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND _ff_encoders flac) endif() @@ -59,6 +62,9 @@ endif() if(CONFIG_FFMPEG_ENC_SBC) list(APPEND _ff_encoders sbc) endif() +if(CONFIG_FFMPEG_ENC_G722) + list(APPEND _ff_encoders adpcm_g722) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-g722.c b/src/audio/ffmpeg_dec/ffmpeg_dec-g722.c new file mode 100644 index 000000000000..c51b30e8da1e --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-g722.c @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// ITU-T G.722 ADPCM wideband speech per-codec packet sizing for ffmpeg_dec. + +#include +#include +#include "ffmpeg_dec.h" + +#define G722_DEC_MAX_SAMPLES 1024 +#define G722_DEC_MAX_PKT_BYTES (G722_DEC_MAX_SAMPLES / 2) + +static uint32_t ffmpeg_dec_g722_max_frame_samples(const struct ffmpeg_dec_comp_data *cd) +{ + (void)cd; + return G722_DEC_MAX_SAMPLES; +} + +/* + * Each G.722 input byte decodes to 2 mono PCM samples. Clamp each unparsed + * packet chunk to 512 bytes so g722_decode_frame produces at most 1024 samples + * per decode step. + */ +static size_t ffmpeg_dec_g722_limit_packet_size(const struct ffmpeg_dec_comp_data *cd, + size_t pkt_len) +{ + (void)cd; + return pkt_len > G722_DEC_MAX_PKT_BYTES ? G722_DEC_MAX_PKT_BYTES : pkt_len; +} + +const struct ffmpeg_dec_codec_ops ffmpeg_dec_g722_ops = { + .id = FFMPEG_DEC_CODEC_G722, + .snd_codec_id = SND_AUDIOCODEC_G722, + .name = "g722", + .decoder_name = "g722", + .max_frame_samples = ffmpeg_dec_g722_max_frame_samples, + .limit_packet_size = ffmpeg_dec_g722_limit_packet_size, +}; diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 25b57a984434..3751b3da4256 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -11,6 +11,7 @@ if(CONFIG_COMP_FFMPEG_DEC_STUB) ../ffmpeg_dec-opus.c ../ffmpeg_dec-alac.c ../ffmpeg_dec-sbc.c + ../ffmpeg_dec-g722.c ../ffmpeg_dec-stub.c LIB openmodules ) @@ -47,6 +48,9 @@ else() if(CONFIG_FFMPEG_DEC_SBC) list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-sbc.c) endif() + if(CONFIG_FFMPEG_DEC_G722) + list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-g722.c) + endif() # fastmathf provides the single-precision libm the codecs need AND sofm_log2f # used by the shims' log10(); always built for the real (non-stub) backend. list(APPEND ffmpeg_dec_srcs ../fastmathf.c) @@ -86,6 +90,9 @@ else() if(CONFIG_FFMPEG_ENC_SBC) list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-sbc.c) endif() + if(CONFIG_FFMPEG_ENC_G722) + list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-g722.c) + endif() # Real libm (newlib double math) LAST so libav*/filter references resolve against it get_filename_component(_ff_libm_dir "${FFMPEG_TARGET_LIBM}" DIRECTORY) From 7683fdc5ad41a35027b800a3df4679580ca9da35 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 11/12] ffmpeg_dec: add Bluetooth aptX decode and encode support Add Bluetooth classic aptX stereo audio decode and encode support (SND_AUDIOCODEC_APTX) via libavcodec's integer aptx codec: - Clamp incoming aptX packets to 4-byte codeword boundaries (<= 512 bytes, <= 512 stereo samples per decode call). - Encode 256-sample stereo S32P blocks into 256-byte aptX packets using ffmpeg_dec_pcm_to_frame(). Signed-off-by: Liam Girdwood --- src/audio/ffmpeg_dec/CMakeLists.txt | 9 + src/audio/ffmpeg_dec/Kconfig | 18 +- src/audio/ffmpeg_dec/ffmpeg-enc-aptx.c | 197 ++++++++++++++++++++++ src/audio/ffmpeg_dec/ffmpeg.cmake | 6 + src/audio/ffmpeg_dec/ffmpeg_dec-aptx.c | 41 +++++ src/audio/ffmpeg_dec/llext/CMakeLists.txt | 7 + 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/audio/ffmpeg_dec/ffmpeg-enc-aptx.c create mode 100644 src/audio/ffmpeg_dec/ffmpeg_dec-aptx.c diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 6ea38bfb458e..916eaafba70b 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -27,6 +27,9 @@ else() if(CONFIG_FFMPEG_DEC_G722 OR CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-g722.c) endif() + if(CONFIG_FFMPEG_DEC_APTX OR CONFIG_COMP_FFMPEG_DEC_STUB) + add_local_sources(sof ffmpeg_dec-aptx.c) + endif() if(CONFIG_COMP_FFMPEG_DEC_STUB) add_local_sources(sof ffmpeg_dec-stub.c) @@ -55,6 +58,9 @@ else() if(CONFIG_FFMPEG_ENC_G722) add_local_sources(sof ffmpeg-enc-g722.c) endif() + if(CONFIG_FFMPEG_ENC_APTX) + add_local_sources(sof ffmpeg-enc-aptx.c) + endif() if(CONFIG_FFMPEG_FILTER) add_local_sources(sof ffmpeg-filter.c) endif() @@ -96,6 +102,9 @@ else() if(CONFIG_FFMPEG_ENC_G722) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-g722.c) endif() + if(CONFIG_FFMPEG_ENC_APTX) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-enc-aptx.c) + endif() if(CONFIG_FFMPEG_FILTER) list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-filter.c) endif() diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index 0b4703f235e5..c4ddbc4cd95e 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -75,6 +75,14 @@ config FFMPEG_DEC_G722 dependency. Enables --enable-decoder=adpcm_g722 in the libavcodec build for compress playback streams requesting SND_AUDIOCODEC_G722. +config FFMPEG_DEC_APTX + bool "Bluetooth aptX decoder" + help + Bluetooth aptX audio decoder. Pure integer 4-band QMF + ADPCM + decoder with no floating-point math dependency. Enables + --enable-decoder=aptx in the libavcodec build for compress playback + streams requesting SND_AUDIOCODEC_APTX. + comment "FFmpeg encoders to build (footprint scales with selection)" config FFMPEG_ENC_MP3 @@ -128,11 +136,19 @@ config FFMPEG_ENC_G722 encoder with no floating-point math dependency. Selected at runtime for compress capture streams requesting SND_AUDIOCODEC_G722. +config FFMPEG_ENC_APTX + bool "Bluetooth aptX encoder (native libavcodec)" + help + Bluetooth aptX audio encoder via native libavcodec aptx encoder. + Pure integer 4-band QMF + ADPCM encoder with no floating-point math + dependency. Selected at runtime for compress capture streams + requesting SND_AUDIOCODEC_APTX. + # Selected automatically when any encoder is enabled. Gates compilation of the # runtime encode path (ffmpeg-encode.c). config FFMPEG_ENC bool - default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC || FFMPEG_ENC_SBC || FFMPEG_ENC_G722 + default y if FFMPEG_ENC_MP3 || FFMPEG_ENC_AAC || FFMPEG_ENC_FLAC || FFMPEG_ENC_ALAC || FFMPEG_ENC_SBC || FFMPEG_ENC_G722 || FFMPEG_ENC_APTX comment "FFmpeg audio filters to build (libavfilter)" depends on IPC_MAJOR_4 diff --git a/src/audio/ffmpeg_dec/ffmpeg-enc-aptx.c b/src/audio/ffmpeg_dec/ffmpeg-enc-aptx.c new file mode 100644 index 000000000000..dd85488165f8 --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg-enc-aptx.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Bluetooth aptX per-codec encoder implementation for ffmpeg_dec. + +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" +#include "ffmpeg_dec-convert.h" + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define APTX_ENC_FRAME_SAMPLES 1024 + +struct ffmpeg_enc_aptx_data { + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; +}; + +static int ffmpeg_enc_aptx_validate_params(struct processing_module *mod, + const struct snd_codec *codec_params) +{ + struct comp_dev *dev = mod->dev; + + if (codec_params->format != SND_AUDIOSTREAMFORMAT_UNDEFINED && + codec_params->format != SND_AUDIOSTREAMFORMAT_RAW && + codec_params->format != 2 && + codec_params->format != 10) { + comp_err(dev, "unsupported aptX stream format 0x%x", + codec_params->format); + return -ENOTSUP; + } + + return 0; +} + +static int ffmpeg_enc_aptx_init(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct comp_dev *dev = mod->dev; + struct ffmpeg_enc_aptx_data *aptx; + + aptx = mod_zalloc(mod, sizeof(*aptx)); + if (!aptx) + return -ENOMEM; + + aptx->codec = avcodec_find_encoder(AV_CODEC_ID_APTX); + if (!aptx->codec) { + comp_err(dev, "libavcodec aptX encoder not found"); + mod_free(mod, aptx); + return -ENODEV; + } + + aptx->avctx = avcodec_alloc_context3(aptx->codec); + aptx->pkt = av_packet_alloc(); + aptx->frame = av_frame_alloc(); + if (!aptx->avctx || !aptx->pkt || !aptx->frame) { + if (aptx->frame) + av_frame_free(&aptx->frame); + if (aptx->pkt) + av_packet_free(&aptx->pkt); + if (aptx->avctx) + avcodec_free_context(&aptx->avctx); + mod_free(mod, aptx); + return -ENOMEM; + } + + e->codec_priv = aptx; + return 0; +} + +static int ffmpeg_enc_aptx_configure(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_aptx_data *aptx = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + if (cd->out_channels != 2) { + comp_err(dev, "aptX requires 2 (stereo) channels, got %u", cd->out_channels); + return -EINVAL; + } + + if (avcodec_is_open(aptx->avctx)) { + avcodec_free_context(&aptx->avctx); + aptx->avctx = avcodec_alloc_context3(aptx->codec); + if (!aptx->avctx) + return -ENOMEM; + } + + aptx->avctx->sample_rate = cd->out_rate; + av_channel_layout_default(&aptx->avctx->ch_layout, 2); + aptx->avctx->sample_fmt = AV_SAMPLE_FMT_S32P; + aptx->avctx->frame_size = APTX_ENC_FRAME_SAMPLES; + aptx->avctx->thread_count = 1; + + ret = avcodec_open2(aptx->avctx, aptx->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (aptX encoder) failed %d", ret); + return -EIO; + } + + e->nb = aptx->avctx->frame_size ? aptx->avctx->frame_size : APTX_ENC_FRAME_SAMPLES; + return 0; +} + +static int ffmpeg_enc_aptx_encode_frame(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_aptx_data *aptx = e->codec_priv; + struct comp_dev *dev = mod->dev; + int ret; + + aptx->frame->nb_samples = e->nb; + aptx->frame->format = aptx->avctx->sample_fmt; + aptx->frame->sample_rate = cd->out_rate; + av_channel_layout_copy(&aptx->frame->ch_layout, &aptx->avctx->ch_layout); + if (av_frame_get_buffer(aptx->frame, 0) < 0) + return -ENOMEM; + + ffmpeg_dec_pcm_to_frame(e->pcm_in, aptx->frame, + e->src_fmt, 2, e->nb); + + ret = avcodec_send_frame(aptx->avctx, aptx->frame); + av_frame_unref(aptx->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame (aptX) failed %d", ret); + return -EIO; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(aptx->avctx, aptx->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet (aptX) failed %d", ret); + return -EIO; + } + if (aptx->pkt->size > 0) + ffmpeg_enc_stage_packet(mod, e, aptx->pkt->data, + (size_t)aptx->pkt->size); + av_packet_unref(aptx->pkt); + ret = 0; + } + + return 0; +} + +static void ffmpeg_enc_aptx_reset(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_aptx_data *aptx = e->codec_priv; + + (void)mod; + if (!aptx) + return; + + if (aptx->frame) + av_frame_unref(aptx->frame); + if (aptx->pkt) + av_packet_unref(aptx->pkt); +} + +static void ffmpeg_enc_aptx_free(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + struct ffmpeg_enc_aptx_data *aptx = e->codec_priv; + + if (!aptx) + return; + + if (aptx->frame) + av_frame_free(&aptx->frame); + if (aptx->pkt) + av_packet_free(&aptx->pkt); + if (aptx->avctx) + avcodec_free_context(&aptx->avctx); + mod_free(mod, aptx); + e->codec_priv = NULL; +} + +const struct ffmpeg_enc_codec_ops ffmpeg_enc_aptx_ops = { + .id = FFMPEG_DEC_CODEC_APTX, + .snd_codec_id = SND_AUDIOCODEC_APTX, + .name = "aptx", + .validate_params = ffmpeg_enc_aptx_validate_params, + .init = ffmpeg_enc_aptx_init, + .configure = ffmpeg_enc_aptx_configure, + .encode_frame = ffmpeg_enc_aptx_encode_frame, + .reset = ffmpeg_enc_aptx_reset, + .free = ffmpeg_enc_aptx_free, +}; diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index 41305a3893a5..47455628bca1 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -53,6 +53,9 @@ endif() if(CONFIG_FFMPEG_DEC_G722) list(APPEND _ff_decoders adpcm_g722) endif() +if(CONFIG_FFMPEG_DEC_APTX) + list(APPEND _ff_decoders aptx) +endif() if(CONFIG_FFMPEG_ENC_FLAC) list(APPEND _ff_encoders flac) endif() @@ -65,6 +68,9 @@ endif() if(CONFIG_FFMPEG_ENC_G722) list(APPEND _ff_encoders adpcm_g722) endif() +if(CONFIG_FFMPEG_ENC_APTX) + list(APPEND _ff_encoders aptx) +endif() # At least one decoder, encoder, or filter must be selected. if(NOT _ff_decoders AND NOT _ff_encoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC AND NOT CONFIG_FFMPEG_BUILD_AVFILTER) message(FATAL_ERROR "ffmpeg_dec: no decoder, encoder, or filter selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*/FFMPEG_FILTER_*)") diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-aptx.c b/src/audio/ffmpeg_dec/ffmpeg_dec-aptx.c new file mode 100644 index 000000000000..60f793f5fa49 --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-aptx.c @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Bluetooth aptX per-codec packet alignment and frame sizing for ffmpeg_dec. + +#include +#include +#include "ffmpeg_dec.h" + +#define APTX_DEC_MAX_SAMPLES 1024 +#define APTX_BLOCK_ALIGN 4 + +static uint32_t ffmpeg_dec_aptx_max_frame_samples(const struct ffmpeg_dec_comp_data *cd) +{ + (void)cd; + return APTX_DEC_MAX_SAMPLES; +} + +/* + * Each 4-byte aptX block (two 16-bit codewords, L+R) decodes to 4 stereo PCM + * samples. Clamp each packet to 1024 bytes and align down to a multiple of 4 + * bytes so aptx_decode_frame never sees a partial codeword. + */ +static size_t ffmpeg_dec_aptx_limit_packet_size(const struct ffmpeg_dec_comp_data *cd, + size_t pkt_len) +{ + size_t clamped = pkt_len > APTX_DEC_MAX_SAMPLES ? APTX_DEC_MAX_SAMPLES : pkt_len; + + (void)cd; + return clamped & ~(size_t)(APTX_BLOCK_ALIGN - 1); +} + +const struct ffmpeg_dec_codec_ops ffmpeg_dec_aptx_ops = { + .id = FFMPEG_DEC_CODEC_APTX, + .snd_codec_id = SND_AUDIOCODEC_APTX, + .name = "aptx", + .decoder_name = "aptx", + .max_frame_samples = ffmpeg_dec_aptx_max_frame_samples, + .limit_packet_size = ffmpeg_dec_aptx_limit_packet_size, +}; diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 3751b3da4256..06d659fa404d 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -12,6 +12,7 @@ if(CONFIG_COMP_FFMPEG_DEC_STUB) ../ffmpeg_dec-alac.c ../ffmpeg_dec-sbc.c ../ffmpeg_dec-g722.c + ../ffmpeg_dec-aptx.c ../ffmpeg_dec-stub.c LIB openmodules ) @@ -51,6 +52,9 @@ else() if(CONFIG_FFMPEG_DEC_G722) list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-g722.c) endif() + if(CONFIG_FFMPEG_DEC_APTX) + list(APPEND ffmpeg_dec_srcs ../ffmpeg_dec-aptx.c) + endif() # fastmathf provides the single-precision libm the codecs need AND sofm_log2f # used by the shims' log10(); always built for the real (non-stub) backend. list(APPEND ffmpeg_dec_srcs ../fastmathf.c) @@ -93,6 +97,9 @@ else() if(CONFIG_FFMPEG_ENC_G722) list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-g722.c) endif() + if(CONFIG_FFMPEG_ENC_APTX) + list(APPEND ffmpeg_dec_srcs ../ffmpeg-enc-aptx.c) + endif() # Real libm (newlib double math) LAST so libav*/filter references resolve against it get_filename_component(_ff_libm_dir "${FFMPEG_TARGET_LIBM}" DIRECTORY) From 92a7fc5fb060d730de0c588a9b8102d8fd9b5c5a Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Tue, 10 Nov 2026 11:51:09 +0000 Subject: [PATCH 12/12] topology2: add SBC, G.722, and aptX decode and encode topologies Add development topology targets for SBC, G.722, and aptX compress playback and capture. Signed-off-by: Liam Girdwood --- .../topology2/development/tplg-targets.cmake | 9 ++++++++- .../formats/compr_input_audio_formats.conf | 18 +++++++++++++++++- .../formats/compr_output_audio_formats.conf | 18 +++++++++++++++++- .../compr_output_audio_formats_s32.conf | 6 +++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tools/topology/topology2/development/tplg-targets.cmake b/tools/topology/topology2/development/tplg-targets.cmake index 1082be23d71c..cf8f4c83054d 100644 --- a/tools/topology/topology2/development/tplg-targets.cmake +++ b/tools/topology/topology2/development/tplg-targets.cmake @@ -229,14 +229,21 @@ DEEPBUFFER_FW_DMA_MS=false,HDA_ANALOG_CAPTURE_RATE=96000,HDA_ANALOG_PLAYBACK_RAT "sof-hda-generic\;sof-hda-generic-ffmpeg-aac-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=aac,COMPR_FFMPEG_DEC_HEAP=901120" "sof-hda-generic\;sof-hda-generic-ffmpeg-opus-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=opus,COMPR_FFMPEG_DEC_HEAP=524288" "sof-hda-generic\;sof-hda-generic-ffmpeg-alac-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=alac" +"sof-hda-generic\;sof-hda-generic-ffmpeg-sbc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=sbc" +"sof-hda-generic\;sof-hda-generic-ffmpeg-g722-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=g722" +"sof-hda-generic\;sof-hda-generic-ffmpeg-aptx-compr\;HDA_CONFIG=mix,COMPR_FFMPEG=true,COMPR_FFMPEG_CODEC=aptx" # Compress capture (encode: PCM -> MP3 via libshine, AAC-LC via vo-aacenc, -# or lossless FLAC/ALAC via libavcodec) driven by ffmpeg_enc. COMPR_FFMPEG_ENC +# lossless FLAC/ALAC, or Bluetooth/voice SBC/G.722/aptX via libavcodec) driven +# by ffmpeg_enc. COMPR_FFMPEG_ENC # gates compr-capture-ffmpeg.conf, which taps the HDA analog capture BE, so it # may be built alongside or independently of the decode COMPR_FFMPEG path above. "sof-hda-generic\;sof-hda-generic-ffmpeg-mp3-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" "sof-hda-generic\;sof-hda-generic-ffmpeg-aac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" "sof-hda-generic\;sof-hda-generic-ffmpeg-flac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" "sof-hda-generic\;sof-hda-generic-ffmpeg-alac-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" +"sof-hda-generic\;sof-hda-generic-ffmpeg-sbc-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" +"sof-hda-generic\;sof-hda-generic-ffmpeg-g722-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" +"sof-hda-generic\;sof-hda-generic-ffmpeg-aptx-enc-compr\;HDA_CONFIG=mix,COMPR_FFMPEG_ENC=true" # SDW + DMIC + HDMI, with 16 kHz DMIC1 "cavs-sdw\;sof-mtl-sdw-cs42l42-l0-max98363-l2-4ch-48k-16k\;PLATFORM=mtl,\ diff --git a/tools/topology/topology2/include/formats/compr_input_audio_formats.conf b/tools/topology/topology2/include/formats/compr_input_audio_formats.conf index 8d5694400988..75d2ce7d75e5 100644 --- a/tools/topology/topology2/include/formats/compr_input_audio_formats.conf +++ b/tools/topology/topology2/include/formats/compr_input_audio_formats.conf @@ -1,11 +1,13 @@ # All input formats for e.g. host-copier, decoder, # and module-copier - num_input_audio_formats 40 + num_input_audio_formats 56 CombineArrays.Object.Base.input_audio_format [ { in_channels [ 1 ] in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -21,6 +23,8 @@ in_channels [ 1 ] in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -35,6 +39,8 @@ in_channels [ 1 ] in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -49,6 +55,8 @@ in_channels [ 1 ] in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -62,6 +70,8 @@ { in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -74,6 +84,8 @@ { in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -85,6 +97,8 @@ { in_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -96,6 +110,8 @@ { in_rate [ 8000 + 16000 + 32000 44100 48000 96000 diff --git a/tools/topology/topology2/include/formats/compr_output_audio_formats.conf b/tools/topology/topology2/include/formats/compr_output_audio_formats.conf index 98d11fa51cbc..2db361a846ab 100644 --- a/tools/topology/topology2/include/formats/compr_output_audio_formats.conf +++ b/tools/topology/topology2/include/formats/compr_output_audio_formats.conf @@ -1,10 +1,12 @@ # Output audio formats for e.g. host-copier and decoder - num_output_audio_formats 40 + num_output_audio_formats 56 CombineArrays.Object.Base.output_audio_format [ { out_channels [ 1 ] out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -20,6 +22,8 @@ out_channels [ 1 ] out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -34,6 +38,8 @@ out_channels [ 1 ] out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -48,6 +54,8 @@ out_channels [ 1 ] out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -61,6 +69,8 @@ { out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -73,6 +83,8 @@ { out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -84,6 +96,8 @@ { out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -95,6 +109,8 @@ { out_rate [ 8000 + 16000 + 32000 44100 48000 96000 diff --git a/tools/topology/topology2/include/formats/compr_output_audio_formats_s32.conf b/tools/topology/topology2/include/formats/compr_output_audio_formats_s32.conf index e377afa38687..df4b54dd4acf 100644 --- a/tools/topology/topology2/include/formats/compr_output_audio_formats_s32.conf +++ b/tools/topology/topology2/include/formats/compr_output_audio_formats_s32.conf @@ -1,10 +1,12 @@ # Output audio formats e.g. module-copier - num_output_audio_formats 10 + num_output_audio_formats 14 CombineArrays.Object.Base.output_audio_format [ { out_channels [ 1 ] out_rate [ 8000 + 16000 + 32000 44100 48000 96000 @@ -18,6 +20,8 @@ { out_rate [ 8000 + 16000 + 32000 44100 48000 96000