diff --git a/src/audio/ffmpeg_dec/CMakeLists.txt b/src/audio/ffmpeg_dec/CMakeLists.txt index 889c5dcd21d2..5b64c3caf3c8 100644 --- a/src/audio/ffmpeg_dec/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/CMakeLists.txt @@ -24,6 +24,9 @@ else() else() add_local_sources(sof ffmpeg_dec-ffmpeg.c) add_local_sources(sof ffmpeg_dec-convert.c) + if(CONFIG_FFMPEG_DEC_ENCODE_MODE) + add_local_sources(sof ffmpeg_dec-encode.c) + endif() # Scalar soft-float for the SOF glue that touches float. core-isa.h reports # XCHAL_HAVE_FP=1 so LLVM would lower the FLT/FLTP -> Q1.31 sample conversion # in ffmpeg_dec_emit_frame() to hardware FP0 ops (lsi/mul.s/trunc.s), which @@ -39,6 +42,9 @@ else() ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg_dec.c ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg_dec-ffmpeg.c ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg_dec-convert.c) + if(CONFIG_FFMPEG_DEC_ENCODE_MODE) + list(APPEND _ffmpeg_dec_sources ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg_dec-encode.c) + endif() if(CMAKE_C_COMPILER_ID MATCHES "Clang") set_source_files_properties( ${_ffmpeg_dec_sources} @@ -86,6 +92,21 @@ else() # calls libm at runtime, but the references still resolve to real kernels # now, and lossy decoders (AAC/Opus/Vorbis) need them to decode correctly. target_link_libraries(SOF INTERFACE avcodec swresample avutil) + # Encode mode: libshine MP3 encoder archive (built by ffmpeg.cmake + # section 3b when FFMPEG_ENC_MP3 is set). Linked AFTER avcodec, + # which references its shine_* symbols. + if(CONFIG_FFMPEG_ENC_MP3) + target_link_directories(SOF INTERFACE ${SHINE_INSTALL}/lib) + target_link_libraries(SOF INTERFACE shine) + add_dependencies(modules_sof shine_ext) + endif() + if(CONFIG_FFMPEG_ENC_AAC) + target_link_directories(SOF INTERFACE ${VO_AACENC_INSTALL}/lib) + target_link_libraries(SOF INTERFACE vo-aacenc) + target_include_directories(SOF INTERFACE ${VO_AACENC_INSTALL}/include) + zephyr_include_directories(${VO_AACENC_INSTALL}/include) + add_dependencies(modules_sof vo_aacenc_ext) + endif() # libm (newlib double math) LAST so libav*/libshine references # (modf/cos/sin/log/sqrt/exp2) resolve against it. target_link_libraries(SOF INTERFACE ${FFMPEG_TARGET_LIBM}) diff --git a/src/audio/ffmpeg_dec/Kconfig b/src/audio/ffmpeg_dec/Kconfig index f585d688e9eb..bc90fae2e21c 100644 --- a/src/audio/ffmpeg_dec/Kconfig +++ b/src/audio/ffmpeg_dec/Kconfig @@ -51,6 +51,42 @@ 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_ENC_MP3 + bool "MP3 encoder (libshine, fixed-point)" + help + MPEG-1 Layer III encoder via libshine, a small fixed-point encoder + (west-pinned; FFmpeg has no native MP3 encoder). Cross-builds libshine + and enables --enable-libshine --enable-encoder=libshine. Fixed-point, so + no float math dependency. The encoder is built into the archive; a module + encode path (PCM->MP3) is a separate build mode. + +config FFMPEG_ENC_AAC + bool "AAC-LC encoder (vo-aacenc, fixed-point)" + help + MPEG-4 AAC Low Complexity encoder via vo-aacenc, a lightweight 32-bit + fixed-point integer encoder (west-pinned). Pure integer fixed-point, no + float math dependency. Output is a standard ADTS AAC elementary stream. + +config FFMPEG_DEC_ENCODE_MODE + bool "Build as an audio encoder (PCM -> compressed) instead of a decoder [EXPERIMENTAL]" + help + Build the module as an encoder: PCM in, compressed elementary stream out + (AAC-LC via vo-aacenc or MP3 via libshine). + +choice + prompt "Default encoder for encode mode" + depends on FFMPEG_DEC_ENCODE_MODE + default FFMPEG_ENC_DEFAULT_AAC + +config FFMPEG_ENC_DEFAULT_AAC + bool "AAC-LC (vo-aacenc, fixed-point)" + select FFMPEG_ENC_AAC + +config FFMPEG_ENC_DEFAULT_MP3 + bool "MP3 (libshine, fixed-point)" + select FFMPEG_ENC_MP3 +endchoice + # Selected automatically when a decoder that needs IEEE float math is # built. Gates compilation of the fast single-precision libm (fastmathf.c). config FFMPEG_DEC_FLOAT_MATH diff --git a/src/audio/ffmpeg_dec/ffmpeg.cmake b/src/audio/ffmpeg_dec/ffmpeg.cmake index cbaabf743767..4053e8082c2e 100644 --- a/src/audio/ffmpeg_dec/ffmpeg.cmake +++ b/src/audio/ffmpeg_dec/ffmpeg.cmake @@ -42,9 +42,9 @@ if(CONFIG_FFMPEG_DEC_MP3) list(APPEND _ff_decoders mp3) list(APPEND _ff_parsers mpegaudio) endif() -# A decoder is required. -if(NOT _ff_decoders) - message(FATAL_ERROR "ffmpeg_dec: no decoder selected (Kconfig FFMPEG_DEC_*)") +# A decoder is required unless this is an encoder-only build. +if(NOT _ff_decoders AND NOT CONFIG_FFMPEG_ENC_MP3 AND NOT CONFIG_FFMPEG_ENC_AAC) + message(FATAL_ERROR "ffmpeg_dec: no decoder or encoder selected (Kconfig FFMPEG_DEC_*/FFMPEG_ENC_*)") endif() set(_ff_dec_cfg "") if(_ff_decoders) @@ -348,7 +348,150 @@ if(CONFIG_FFMPEG_DEC_COLD_SPLIT) ${FFMPEG_INSTALL_DIR}/lib/libswresample.a) endif() +# --- 3b. MP3 encoder via libshine (fixed-point; FFmpeg has no native one) --- set(_ff_cfg_env "PATH=${_tc_dir}:$ENV{PATH}") +set(_ff_shine_cfg "") +set(_ff_shine_dep "") +if(CONFIG_FFMPEG_ENC_MP3) + set(SHINE_SRC "${sof_top_dir}/../modules/audio/shine") + if(NOT EXISTS "${SHINE_SRC}/src/lib/layer3.h") + message(FATAL_ERROR "ffmpeg_dec: libshine source not at '${SHINE_SRC}'; run 'west update'") + endif() + set(SHINE_INSTALL "${CMAKE_CURRENT_BINARY_DIR}/shine-install") + file(MAKE_DIRECTORY "${SHINE_INSTALL}/lib/pkgconfig" "${SHINE_INSTALL}/include/shine") + # pkg-config file for FFmpeg's require_pkg_config libshine. + file(WRITE "${SHINE_INSTALL}/lib/pkgconfig/shine.pc" +"prefix=${SHINE_INSTALL} +libdir=\${prefix}/lib +includedir=\${prefix}/include +Name: shine +Description: Shine fixed-point MP3 encoder +Version: 3.1.1 +Libs: -L\${libdir} -lshine ${CMAKE_CURRENT_BINARY_DIR}/shine_cfgstub.o +Cflags: -I\${includedir} +") + # FFmpeg's -lshine link *test* pulls newlib malloc -> Zephyr runtime syms + # (z_errno_wrap, ...) that only exist at module load. Provide dummies so the + # configure test links; --extra-ldflags only affects tests, not the .a build. + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/shine_cfgstub.c" +"int z_errno_wrap(void){return 0;} +void *stdout; +int open(const char *p, int f, ...){return -1;} +void *sbrk(int i){return (void *)-1;} +") + # libshine's lib needs no config.h; build the objects directly and archive + # (its autotools CLI/shared link cannot work bare-metal). + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/shine-build.sh" +"#!/bin/sh +set -e +export PATH=${_tc_dir}:\$PATH +CC=\"${_ff_cc}\" +mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/shine-obj +for f in ${SHINE_SRC}/src/lib/*.c; do + \$CC -O2 -fPIC -mlongcalls -mtext-section-literals -DSHINE_HAVE_BSWAP_H \\ + -I${SHINE_SRC}/src/lib -c \"\$f\" \\ + -o ${CMAKE_CURRENT_BINARY_DIR}/shine-obj/\$(basename \"\$f\").o +done +${_ff_cross_prefix}ar rcs ${SHINE_INSTALL}/lib/libshine.a ${CMAKE_CURRENT_BINARY_DIR}/shine-obj/*.o +cp ${SHINE_SRC}/src/lib/layer3.h ${SHINE_INSTALL}/include/shine/layer3.h +\$CC -fPIC -c ${CMAKE_CURRENT_BINARY_DIR}/shine_cfgstub.c \\ + -o ${CMAKE_CURRENT_BINARY_DIR}/shine_cfgstub.o +") + add_custom_command( + OUTPUT "${SHINE_INSTALL}/lib/libshine.a" "${CMAKE_CURRENT_BINARY_DIR}/shine_cfgstub.o" + COMMAND sh "${CMAKE_CURRENT_BINARY_DIR}/shine-build.sh" + VERBATIM) + add_custom_target(shine_ext DEPENDS "${SHINE_INSTALL}/lib/libshine.a") + + set(_ff_cfg_env "PATH=${_tc_dir}:$ENV{PATH}" "PKG_CONFIG_PATH=${SHINE_INSTALL}/lib/pkgconfig") + # NOTE: the cfgstub is scoped to libshine's OWN detection via shine.pc + # Libs (as an object), NOT a global --extra-ldflags. A global stub makes + # FFmpeg's generic check_func tests (posix_memalign/strerror_r/iconv) + # link against sbrk and false-positive HAVE_*=1, emitting libc calls that + # do not exist in the =y newlib env -> final firmware link fails. Scoping + # keeps this build's FFmpeg config identical to the decoder builds. + # --disable-iconv guards the one live check (iconv, probed after libshine). + set(_ff_shine_cfg --enable-libshine --enable-encoder=libshine --pkg-config=pkg-config + --disable-iconv + # The precomputed shine init tables (shine_precomp.h) add ~52KB of + # .rodata to libshine. FFmpeg's bare-metal -lshine configure *probe* + # links against the default GNU linker script whose 'flash' region is + # only 64KB, so the probe overflows ("region flash overflowed"). Grow + # the probe's flash region via --defsym. This affects configure test + # links ONLY (--disable-programs builds .a archives, no real link) and + # provides no symbols, so unlike a global libc stub it cannot trigger + # false-positive HAVE_* detection. + "--extra-ldflags=-Wl,--defsym,__flash_size=0x400000") + set(_ff_shine_dep shine_ext) + message(STATUS "ffmpeg_dec: enabling MP3 encoder via libshine (${SHINE_SRC})") +endif() + +# --- 3c. AAC encoder via vo-aacenc (fixed-point integer AAC-LC) --- +set(_ff_vo_aacenc_dep "") +if(CONFIG_FFMPEG_ENC_AAC) + set(VO_AACENC_SRC "${sof_top_dir}/../modules/audio/vo-aacenc") + if(NOT EXISTS "${VO_AACENC_SRC}/common/include/voAAC.h") + message(FATAL_ERROR "ffmpeg_dec: vo-aacenc source not at '${VO_AACENC_SRC}'; run 'west update'") + endif() + set(VO_AACENC_INSTALL "${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-install") + file(MAKE_DIRECTORY "${VO_AACENC_INSTALL}/lib" "${VO_AACENC_INSTALL}/include/vo-aacenc") + + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-build.sh" +"#!/bin/sh +set -e +export PATH=${_tc_dir}:\$PATH +CC=\"${_ff_cc}\" +mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-obj +SRCS=\"\ +${VO_AACENC_SRC}/common/cmnMemory.c \ +${VO_AACENC_SRC}/aacenc/basic_op/basicop2.c \ +${VO_AACENC_SRC}/aacenc/basic_op/oper_32b.c \ +${VO_AACENC_SRC}/aacenc/src/aac_rom.c \ +${VO_AACENC_SRC}/aacenc/src/aacenc.c \ +${VO_AACENC_SRC}/aacenc/src/aacenc_core.c \ +${VO_AACENC_SRC}/aacenc/src/adj_thr.c \ +${VO_AACENC_SRC}/aacenc/src/band_nrg.c \ +${VO_AACENC_SRC}/aacenc/src/bit_cnt.c \ +${VO_AACENC_SRC}/aacenc/src/bitbuffer.c \ +${VO_AACENC_SRC}/aacenc/src/bitenc.c \ +${VO_AACENC_SRC}/aacenc/src/block_switch.c \ +${VO_AACENC_SRC}/aacenc/src/channel_map.c \ +${VO_AACENC_SRC}/aacenc/src/dyn_bits.c \ +${VO_AACENC_SRC}/aacenc/src/grp_data.c \ +${VO_AACENC_SRC}/aacenc/src/interface.c \ +${VO_AACENC_SRC}/aacenc/src/line_pe.c \ +${VO_AACENC_SRC}/aacenc/src/memalign.c \ +${VO_AACENC_SRC}/aacenc/src/ms_stereo.c \ +${VO_AACENC_SRC}/aacenc/src/pre_echo_control.c \ +${VO_AACENC_SRC}/aacenc/src/psy_configuration.c \ +${VO_AACENC_SRC}/aacenc/src/psy_main.c \ +${VO_AACENC_SRC}/aacenc/src/qc_main.c \ +${VO_AACENC_SRC}/aacenc/src/quantize.c \ +${VO_AACENC_SRC}/aacenc/src/sf_estim.c \ +${VO_AACENC_SRC}/aacenc/src/spreading.c \ +${VO_AACENC_SRC}/aacenc/src/stat_bits.c \ +${VO_AACENC_SRC}/aacenc/src/tns.c \ +${VO_AACENC_SRC}/aacenc/src/transform.c \ +\" +for f in \$SRCS; do + \$CC -O2 -DNDEBUG -fPIC -mlongcalls -mtext-section-literals -D__unused= \\ + -I${VO_AACENC_SRC}/aacenc/inc \\ + -I${VO_AACENC_SRC}/aacenc/basic_op \\ + -I${VO_AACENC_SRC}/common/include \\ + -c \"\$f\" \\ + -o ${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-obj/\$(basename \"\$f\").o +done +${_ff_cross_prefix}ar rcs ${VO_AACENC_INSTALL}/lib/libvo-aacenc.a ${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-obj/*.o +cp ${VO_AACENC_SRC}/common/include/*.h ${VO_AACENC_INSTALL}/include/vo-aacenc/ +") + add_custom_command( + OUTPUT "${VO_AACENC_INSTALL}/lib/libvo-aacenc.a" + COMMAND sh "${CMAKE_CURRENT_BINARY_DIR}/vo-aacenc-build.sh" + VERBATIM) + add_custom_target(vo_aacenc_ext DEPENDS "${VO_AACENC_INSTALL}/lib/libvo-aacenc.a") + set(_ff_vo_aacenc_dep vo_aacenc_ext) + message(STATUS "ffmpeg_dec: enabling AAC encoder via vo-aacenc (${VO_AACENC_SRC})") +endif() # --- 4. Configure + make + install (out-of-tree; source must be clean) --- set(_ff_byproducts @@ -357,6 +500,7 @@ set(_ff_byproducts ${FFMPEG_INSTALL_DIR}/lib/libswresample.a) ExternalProject_Add(ffmpeg_ext + DEPENDS ${_ff_shine_dep} ${_ff_vo_aacenc_dep} SOURCE_DIR "${SOF_FFMPEG_SRC_DIR}" BUILD_IN_SOURCE 0 CONFIGURE_COMMAND @@ -378,6 +522,7 @@ ExternalProject_Add(ffmpeg_ext --disable-runtime-cpudetect --disable-debug --enable-avcodec --enable-avutil --enable-swresample ${_ff_dec_cfg} + ${_ff_shine_cfg} --enable-small --enable-pic --enable-hardcoded-tables "--extra-cflags=${_ff_extra_cflags}" BUILD_COMMAND diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec-encode.c b/src/audio/ffmpeg_dec/ffmpeg_dec-encode.c new file mode 100644 index 000000000000..b7cde2d9a18f --- /dev/null +++ b/src/audio/ffmpeg_dec/ffmpeg_dec-encode.c @@ -0,0 +1,666 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// Encode backend for the ffmpeg_dec module (encode mode): PCM in, +// compressed elementary stream out (AAC-LC via vo-aacenc or MP3 via libshine). +// +// Like the decoder this is a sink/source (DP-domain) module: PCM arrives on the +// circular source buffer, compressed frames are written to the circular +// sink buffer. RAW_DATA processing is only prepared by module_adapter in the LL +// domain, so a DP-scheduled codec (needed to keep the heavy codec init and +// per-frame encode off the LL tick) must use the sink/source .process interface. +// +// SOF PCM is interleaved S32; the encoder is opened in whatever sample format it +// supports (S16 for both vo-aacenc and libshine), so S32 is converted to S16 per +// frame. +// - AAC-LC uses 1024-sample frames (21.33 ms @ 48 kHz). +// - MP3 uses 1152-sample frames (24.00 ms @ 48 kHz). +// One encoder frame worth of input is consumed per process cycle once enough +// source PCM has accumulated in pcm_in. + +#include +#include +#include +#include +#include +#include +#include "ffmpeg_dec.h" + +#if defined(CONFIG_FFMPEG_ENC_MP3) +#include +#include +#include +#endif + +#if defined(CONFIG_FFMPEG_ENC_AAC) +#include +#include +#endif + +LOG_MODULE_DECLARE(ffmpeg_dec, CONFIG_SOF_LOG_LEVEL); + +#define FFMPEG_ENC_DEFAULT_BITRATE 128000 + +/* Linear staging for compressed output, drained into the circular sink over as + * many cycles as needed. An AAC frame at 128 kbps is ~340 bytes; MP3 is ~384 bytes. + * 16 KiB holds dozens of frames comfortably. + */ +#define FFMPEG_ENC_STAGE_SIZE 16384 + +/* LL->DP input ring depth hint (bytes). bind allocates a ~3x ring; this must + * hold one whole worst-case encode of real-time capture PCM so a blocking + * frame encode does not overflow the input and overrun the DAI. + */ +#define FFMPEG_ENC_PCM_STAGE 16384 + +enum ffmpeg_enc_type { + FFMPEG_ENC_TYPE_NONE = 0, + FFMPEG_ENC_TYPE_MP3, + FFMPEG_ENC_TYPE_AAC, +}; + +struct ffmpeg_enc_data { + enum ffmpeg_enc_type type; + uint32_t bit_rate; /* target encoder bit rate in bps */ + int frame_bytes; /* SOF S32 input frame size (all channels) */ + int nb; /* encoder frame size in samples */ + size_t need; /* source bytes for one encoder frame (nb*frame_bytes) */ + uint8_t *pcm_in; /* linear S32 interleaved, one encoder frame */ + size_t pcm_fill; /* bytes accumulated in pcm_in so far (< need) */ + uint8_t *out_stage; /* linear compressed staging */ + size_t out_cap; /* capacity of out_stage */ + size_t out_avail; /* staged compressed bytes not yet drained to sink */ + size_t out_rd; /* read offset of next byte to drain from out_stage */ + +#if defined(CONFIG_FFMPEG_ENC_MP3) + const AVCodec *codec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFrame *frame; +#endif + +#if defined(CONFIG_FFMPEG_ENC_AAC) + VO_AUDIO_CODECAPI aac_api; + VO_HANDLE aac_handle; + VO_MEM_OPERATOR aac_memop; + int16_t *pcm_s16; +#endif +}; + +static inline size_t ffmpeg_enc_min(size_t a, size_t b) +{ + return a < b ? a : b; +} + +static void ffmpeg_enc_free_staging(struct processing_module *mod, struct ffmpeg_enc_data *e) +{ + if (!e) + return; + +#if defined(CONFIG_FFMPEG_ENC_AAC) + if (e->pcm_s16) { + mod_free(mod, e->pcm_s16); + e->pcm_s16 = NULL; + } +#endif + if (e->pcm_in) { + mod_free(mod, e->pcm_in); + e->pcm_in = NULL; + } + if (e->out_stage) { + mod_free(mod, e->out_stage); + e->out_stage = NULL; + } + e->out_cap = 0; + e->out_avail = 0; + e->out_rd = 0; + e->pcm_fill = 0; +} + +/* Drain as much staged compressed output as the sink can currently accept. */ +static void ffmpeg_enc_drain(struct ffmpeg_enc_data *e, struct sof_sink *sink) +{ + void *dst, *buf_start; + size_t buf_size, n; + int ret; + + n = ffmpeg_enc_min(e->out_avail, sink_get_free_size(sink)); + if (!n) + return; + + ret = sink_get_buffer(sink, n, &dst, &buf_start, &buf_size); + if (ret) + return; + + ffmpeg_dec_copy_to_circular(dst, buf_start, buf_size, + e->out_stage + e->out_rd, n); + sink_commit_buffer(sink, n); + e->out_rd += n; + e->out_avail -= n; + if (!e->out_avail) + e->out_rd = 0; +} + +int ffmpeg_enc_mod_init(struct processing_module *mod) +{ + struct module_data *md = &mod->priv; + struct comp_dev *dev = mod->dev; + struct ffmpeg_dec_comp_data *cd; + struct ffmpeg_enc_data *e; + + ffmpeg_dec_libc_bind(mod); + + /* Allocate the module private data and publish it (module_adapter does + * not allocate it for us; the decoder path does the same in its init). + */ + cd = mod_zalloc(mod, sizeof(*cd)); + if (!cd) + return -ENOMEM; + md->private = cd; + + /* Size the DP output ring so it holds several compressed frames + * (an AAC frame at 128 kbps is ~340 bytes; MP3 is ~384 bytes). + * bind sizes the ring between this DP module and downstream copier as 3x this. + */ + md->mpd.out_buff_size = FFMPEG_ENC_STAGE_SIZE; + + /* Size the LL->DP input ring deep enough to absorb one whole worst-case + * encode of real-time capture PCM. + */ + md->mpd.in_buff_size = FFMPEG_ENC_PCM_STAGE; + + e = mod_zalloc(mod, sizeof(*e)); + if (!e) { + mod_free(mod, cd); + return -ENOMEM; + } + +#if defined(CONFIG_FFMPEG_ENC_DEFAULT_AAC) || \ + (defined(CONFIG_FFMPEG_ENC_AAC) && !defined(CONFIG_FFMPEG_ENC_MP3)) + e->type = FFMPEG_ENC_TYPE_AAC; + cd->codec = FFMPEG_DEC_CODEC_AAC; +#elif defined(CONFIG_FFMPEG_ENC_MP3) + e->type = FFMPEG_ENC_TYPE_MP3; + cd->codec = FFMPEG_DEC_CODEC_MP3; +#else + e->type = FFMPEG_ENC_TYPE_NONE; + cd->codec = FFMPEG_DEC_CODEC_NONE; +#endif + e->bit_rate = FFMPEG_ENC_DEFAULT_BITRATE; + +#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; + + if (codec_params->id != 0) { + switch (codec_params->id) { +#if defined(CONFIG_FFMPEG_ENC_AAC) + case SND_AUDIOCODEC_AAC: + e->type = FFMPEG_ENC_TYPE_AAC; + cd->codec = FFMPEG_DEC_CODEC_AAC; + break; +#endif +#if defined(CONFIG_FFMPEG_ENC_MP3) + case SND_AUDIOCODEC_MP3: + e->type = FFMPEG_ENC_TYPE_MP3; + cd->codec = FFMPEG_DEC_CODEC_MP3; + break; +#endif + default: + comp_err(dev, "unsupported or disabled encoder snd_codec id 0x%x", + codec_params->id); + mod_free(mod, e); + mod_free(mod, cd); + return -ENOTSUP; + } + comp_info(dev, "encoder codec selected from IPC: snd_codec id 0x%x", + codec_params->id); + } + if (codec_params->bit_rate) + e->bit_rate = codec_params->bit_rate; + } +#endif + +#if defined(CONFIG_FFMPEG_ENC_AAC) + if (e->type == FFMPEG_ENC_TYPE_AAC) { + VO_CODEC_INIT_USERDATA user_data; + int ret; + + voGetAACEncAPI(&e->aac_api); + e->aac_memop.Alloc = cmnMemAlloc; + e->aac_memop.Copy = cmnMemCopy; + e->aac_memop.Free = cmnMemFree; + e->aac_memop.Set = cmnMemSet; + e->aac_memop.Check = cmnMemCheck; + user_data.memflag = VO_IMF_USERMEMOPERATOR; + user_data.memData = &e->aac_memop; + + ret = e->aac_api.Init(&e->aac_handle, VO_AUDIO_CodingAAC, &user_data); + if (ret != VO_ERR_NONE) { + comp_err(dev, "vo-aacenc Init failed %d", ret); + mod_free(mod, e); + mod_free(mod, cd); + return -EIO; + } + } +#endif + +#if defined(CONFIG_FFMPEG_ENC_MP3) + if (e->type == FFMPEG_ENC_TYPE_MP3) { + e->codec = avcodec_find_encoder_by_name("libshine"); + if (!e->codec) { + comp_err(dev, "libshine MP3 encoder not found"); + mod_free(mod, e); + mod_free(mod, cd); + return -ENODEV; + } + e->avctx = avcodec_alloc_context3(e->codec); + e->pkt = av_packet_alloc(); + e->frame = av_frame_alloc(); + if (!e->avctx || !e->pkt || !e->frame) { + if (e->frame) + av_frame_free(&e->frame); + if (e->pkt) + av_packet_free(&e->pkt); + if (e->avctx) + avcodec_free_context(&e->avctx); + mod_free(mod, e); + mod_free(mod, cd); + return -ENOMEM; + } + } +#endif + + cd->backend_data = e; + return 0; +} + +int ffmpeg_enc_mod_prepare(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + struct comp_dev *dev = mod->dev; + + if (num_of_sources != 1 || num_of_sinks != 1) { + comp_err(dev, "ffmpeg_enc: need 1 source/1 sink, got %d/%d", + num_of_sources, num_of_sinks); + return -EINVAL; + } + + ffmpeg_dec_libc_bind(mod); + + if (e) { + e->out_avail = 0; + e->out_rd = 0; + e->pcm_fill = 0; + } + + dev->period = 96000; /* us */ + + comp_info(dev, "ffmpeg_enc prepare: %d src / %d sink (open deferred to DP)", + num_of_sources, num_of_sinks); + + return 0; +} + +/* Open encoder once the source PCM format is known. Runs on the DP thread. */ +static int ffmpeg_enc_configure(struct processing_module *mod, struct sof_source *src) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + struct comp_dev *dev = mod->dev; + int ret; + + cd->out_channels = source_get_channels(src); + cd->out_rate = source_get_rate(src); + e->frame_bytes = source_get_frame_bytes(src); + + ffmpeg_enc_free_staging(mod, e); + +#if defined(CONFIG_FFMPEG_ENC_AAC) + if (e->type == FFMPEG_ENC_TYPE_AAC) { + AACENC_PARAM params = { 0 }; + + params.sampleRate = cd->out_rate; + params.bitRate = e->bit_rate; + params.nChannels = cd->out_channels; + params.adtsUsed = 1; + ret = e->aac_api.SetParam(e->aac_handle, VO_PID_AAC_ENCPARAM, ¶ms); + if (ret != VO_ERR_NONE) { + comp_err(dev, "vo-aacenc SetParam failed %d", ret); + return -EINVAL; + } + + e->nb = 1024; + e->need = (size_t)e->nb * e->frame_bytes; + e->pcm_in = mod_alloc(mod, e->need); + e->pcm_s16 = mod_alloc(mod, (size_t)e->nb * cd->out_channels * sizeof(int16_t)); + e->out_stage = mod_alloc(mod, FFMPEG_ENC_STAGE_SIZE); + if (!e->pcm_in || !e->pcm_s16 || !e->out_stage) { + comp_err(dev, "ffmpeg_enc: AAC staging alloc failed"); + ffmpeg_enc_free_staging(mod, e); + return -ENOMEM; + } + e->out_cap = FFMPEG_ENC_STAGE_SIZE; + + comp_info(dev, "ffmpeg_enc: vo-aacenc AAC open OK, rate %u ch %u frame_size %d need %zu", + cd->out_rate, cd->out_channels, e->nb, e->need); + return 0; + } +#endif + +#if defined(CONFIG_FFMPEG_ENC_MP3) + if (e->type == FFMPEG_ENC_TYPE_MP3) { + if (avcodec_is_open(e->avctx)) { + avcodec_free_context(&e->avctx); + e->avctx = avcodec_alloc_context3(e->codec); + if (!e->avctx) + return -ENOMEM; + } + + e->avctx->sample_rate = cd->out_rate; + av_channel_layout_default(&e->avctx->ch_layout, cd->out_channels); + e->avctx->sample_fmt = e->codec->sample_fmts ? + e->codec->sample_fmts[0] : AV_SAMPLE_FMT_S16; + e->avctx->bit_rate = e->bit_rate; + e->avctx->thread_count = 1; + + ret = avcodec_open2(e->avctx, e->codec, NULL); + if (ret < 0) { + comp_err(dev, "avcodec_open2 (encoder) failed %d", ret); + return -EIO; + } + + e->nb = e->avctx->frame_size ? e->avctx->frame_size : 1152; + e->need = (size_t)e->nb * e->frame_bytes; + + e->pcm_in = mod_alloc(mod, e->need); + e->out_stage = mod_alloc(mod, FFMPEG_ENC_STAGE_SIZE); + if (!e->pcm_in || !e->out_stage) { + comp_err(dev, "ffmpeg_enc: staging alloc failed"); + ffmpeg_enc_free_staging(mod, e); + return -ENOMEM; + } + e->out_cap = FFMPEG_ENC_STAGE_SIZE; + + comp_info(dev, "ffmpeg_enc: libshine MP3 open OK, rate %u ch %u frame_size %d need %zu", + cd->out_rate, cd->out_channels, e->nb, e->need); + return 0; + } +#endif + + return -EINVAL; +} + +bool ffmpeg_enc_is_ready_to_process(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + + if (num_of_sources < 1 || num_of_sinks < 1) + return false; + + if (!cd->configured) + return true; + + if (e->out_avail) + return sink_get_free_size(sinks[0]) > 0; + + return source_get_data_available(sources[0]) > 0; +} + +/* Convert one interleaved S32 frame block to the encoder AVFrame, then encode. */ +int ffmpeg_enc_mod_process(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + struct comp_dev *dev = mod->dev; + struct sof_source *src; + struct sof_sink *sink; + const void *sp, *sstart; + size_t sbytes; + int ch; + int ret; + + if (num_of_sources < 1 || num_of_sinks < 1) + return -EINVAL; + + src = sources[0]; + sink = sinks[0]; + + ffmpeg_dec_libc_bind(mod); + + /* One-time lazy open on the DP thread; encode on a later cycle. */ + if (!cd->configured) { + ret = ffmpeg_enc_configure(mod, src); + if (ret) + return ret; + cd->configured = true; + return 0; + } + ch = cd->out_channels; + + /* Drain leftover compressed output before consuming more input. */ + if (e->out_avail) { + ffmpeg_enc_drain(e, sink); + return 0; + } + + /* Accumulate source PCM into pcm_in until a full encoder frame is + * buffered. The LL->DP input ring holds only a few LL periods (far less + * than one encoder frame), so a frame must be gathered over + * several DP cycles rather than read in a single source_get_data() call. + */ + { + size_t avail = source_get_data_available(src); + size_t want = e->need - e->pcm_fill; + size_t n = ffmpeg_enc_min(want, avail); + + if (n) { + ret = source_get_data(src, n, &sp, &sstart, &sbytes); + if (ret) + return 0; + ffmpeg_dec_copy_from_circular(e->pcm_in + e->pcm_fill, sp, + sstart, sbytes, n); + source_release_data(src, n); + e->pcm_fill += n; + } + } + + /* Not a full encoder frame yet - wait for more input next cycle. */ + if (e->pcm_fill < e->need) + return 0; + e->pcm_fill = 0; + +#if defined(CONFIG_FFMPEG_ENC_AAC) + if (e->type == FFMPEG_ENC_TYPE_AAC) { + VO_CODECBUFFER input = { 0 }, output = { 0 }; + VO_AUDIO_OUTPUTINFO output_info = { 0 }; + uint8_t outbuf[2048]; + size_t total_samples = (size_t)e->nb * ch; + const int32_t *in = (const int32_t *)e->pcm_in; + size_t i; + + for (i = 0; i < total_samples; i++) + e->pcm_s16[i] = (int16_t)(in[i] >> 16); + + input.Buffer = (uint8_t *)e->pcm_s16; + input.Length = total_samples * sizeof(int16_t); + e->aac_api.SetInputData(e->aac_handle, &input); + + output.Buffer = outbuf; + output.Length = sizeof(outbuf); + ret = e->aac_api.GetOutputData(e->aac_handle, &output, &output_info); + if (ret != VO_ERR_NONE) { + comp_err(dev, "vo-aacenc GetOutputData failed %d", ret); + return -EIO; + } + + if (output.Length > 0) { + if (e->out_avail + output.Length <= e->out_cap) { + memcpy_s(e->out_stage + e->out_avail, + e->out_cap - e->out_avail, + outbuf, output.Length); + e->out_avail += output.Length; + } else { + comp_warn(dev, "AAC stage full, packet dropped"); + } + } + + ffmpeg_enc_drain(e, sink); + return 0; + } +#endif + +#if defined(CONFIG_FFMPEG_ENC_MP3) + if (e->type == FFMPEG_ENC_TYPE_MP3) { + const int32_t *in; + int planar, i, c; + + e->frame->nb_samples = e->nb; + e->frame->format = e->avctx->sample_fmt; + e->frame->sample_rate = cd->out_rate; + av_channel_layout_copy(&e->frame->ch_layout, &e->avctx->ch_layout); + if (av_frame_get_buffer(e->frame, 0) < 0) + return -ENOMEM; + + /* S32 interleaved -> S16 (planar or packed per the encoder format). */ + in = (const int32_t *)e->pcm_in; + planar = av_sample_fmt_is_planar(e->frame->format); + if (planar && ch == 2) { + int16_t *out0 = (int16_t *)e->frame->data[0]; + int16_t *out1 = (int16_t *)e->frame->data[1]; + + for (i = 0; i + 4 <= e->nb; i += 4) { + out0[i + 0] = (int16_t)(in[0] >> 16); + out1[i + 0] = (int16_t)(in[1] >> 16); + out0[i + 1] = (int16_t)(in[2] >> 16); + out1[i + 1] = (int16_t)(in[3] >> 16); + out0[i + 2] = (int16_t)(in[4] >> 16); + out1[i + 2] = (int16_t)(in[5] >> 16); + out0[i + 3] = (int16_t)(in[6] >> 16); + out1[i + 3] = (int16_t)(in[7] >> 16); + in += 8; + } + for (; i < e->nb; i++) { + out0[i] = (int16_t)(in[0] >> 16); + out1[i] = (int16_t)(in[1] >> 16); + in += 2; + } + } else if (planar) { + for (c = 0; c < ch; c++) { + int16_t *out = (int16_t *)e->frame->data[c]; + + for (i = 0; i < e->nb; i++) + out[i] = (int16_t)(in[i * ch + c] >> 16); + } + } else { + int16_t *out = (int16_t *)e->frame->data[0]; + int total = e->nb * ch; + + for (i = 0; i + 4 <= total; i += 4) { + out[i + 0] = (int16_t)(in[i + 0] >> 16); + out[i + 1] = (int16_t)(in[i + 1] >> 16); + out[i + 2] = (int16_t)(in[i + 2] >> 16); + out[i + 3] = (int16_t)(in[i + 3] >> 16); + } + for (; i < total; i++) + out[i] = (int16_t)(in[i] >> 16); + } + + ret = avcodec_send_frame(e->avctx, e->frame); + av_frame_unref(e->frame); + if (ret < 0) { + comp_err(dev, "avcodec_send_frame failed %d", ret); + return -EIO; + } + + /* Stage compressed packets; they are drained into the sink over the + * following cycles (starting with the one after this returns). + */ + while (ret >= 0) { + ret = avcodec_receive_packet(e->avctx, e->pkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + break; + if (ret < 0) { + comp_err(dev, "avcodec_receive_packet failed %d", ret); + return -EIO; + } + if (e->out_avail + e->pkt->size <= e->out_cap) { + memcpy_s(e->out_stage + e->out_avail, + e->out_cap - e->out_avail, + e->pkt->data, e->pkt->size); + e->out_avail += e->pkt->size; + } else { + comp_warn(dev, "mp3 stage full, packet dropped"); + } + av_packet_unref(e->pkt); + ret = 0; + } + + /* Drain what fits now; the rest is carried to the next cycle. */ + ffmpeg_enc_drain(e, sink); + return 0; + } +#endif + + return 0; +} + +int ffmpeg_enc_mod_reset(struct processing_module *mod) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + + ffmpeg_dec_libc_bind(mod); + ffmpeg_enc_free_staging(mod, e); + cd->configured = false; + return 0; +} + +int ffmpeg_enc_mod_free(struct processing_module *mod) +{ + struct ffmpeg_dec_comp_data *cd = module_get_private_data(mod); + struct ffmpeg_enc_data *e = cd->backend_data; + + ffmpeg_dec_libc_bind(mod); + if (!e) { + mod_free(mod, cd); + mod->priv.private = NULL; + return 0; + } + +#if defined(CONFIG_FFMPEG_ENC_AAC) + if (e->type == FFMPEG_ENC_TYPE_AAC) { + if (e->aac_handle) + e->aac_api.Uninit(e->aac_handle); + } +#endif + +#if defined(CONFIG_FFMPEG_ENC_MP3) + if (e->type == FFMPEG_ENC_TYPE_MP3) { + if (e->frame) + av_frame_free(&e->frame); + if (e->pkt) + av_packet_free(&e->pkt); + if (e->avctx) + avcodec_free_context(&e->avctx); + } +#endif + + ffmpeg_enc_free_staging(mod, e); + mod_free(mod, e); + cd->backend_data = NULL; + mod_free(mod, cd); + mod->priv.private = NULL; + return 0; +} diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.c b/src/audio/ffmpeg_dec/ffmpeg_dec.c index 7612bc340f03..3aa055e9d1d3 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.c +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.c @@ -104,6 +104,119 @@ int ffmpeg_dec_store_extradata(struct processing_module *mod, return 0; } +static inline size_t ffmpeg_dec_min(size_t a, size_t b) +{ + return a < b ? a : b; +} + +/* Copy @bytes out of a circular source buffer (starting at @src) into the + * linear @dst, wrapping at the buffer end if necessary. + */ +void ffmpeg_dec_copy_from_circular(void *dst, const void *src, + const void *buf_start, size_t buf_size, + size_t bytes) +{ + size_t to_end = (size_t)((const uint8_t *)buf_start + buf_size - + (const uint8_t *)src); + + if (to_end >= bytes) { + memcpy_s(dst, bytes, src, bytes); + return; + } + memcpy_s(dst, to_end, src, to_end); + memcpy_s((uint8_t *)dst + to_end, bytes - to_end, + buf_start, bytes - to_end); +} + +/* Copy @bytes from the linear @src into a circular sink buffer (starting at + * @dst), wrapping at the buffer end if necessary. + */ +void ffmpeg_dec_copy_to_circular(void *dst, void *buf_start, + size_t buf_size, const void *src, + size_t bytes) +{ + size_t to_end = (size_t)((uint8_t *)buf_start + buf_size - + (uint8_t *)dst); + + if (to_end >= bytes) { + memcpy_s(dst, bytes, src, bytes); + return; + } + memcpy_s(dst, to_end, src, to_end); + memcpy_s(buf_start, bytes - to_end, + (const uint8_t *)src + to_end, bytes - to_end); +} + +/* + * Peek at the start of the stream and skip a leading ID3v2 tag if present. + * Returns: + * 1 if an ID3v2 tag was completely skipped (caller should re-check), + * 0 if the stream does not start with an ID3v2 tag (cd->in_buf holds the + * peeked header bytes), + * -ENODATA while waiting for enough bytes to inspect or finish skipping. + */ +int ffmpeg_dec_skip_id3v2(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; + + /* Drain any pending skip bytes from a previous chunk */ + if (cd->skip_bytes > 0) { + avail = source_get_data_available(src); + if (!avail) + return -ENODATA; + size_t to_skip = ffmpeg_dec_min(avail, cd->skip_bytes); + + source_drop_data(src, to_skip); + cd->skip_bytes -= to_skip; + if (cd->skip_bytes > 0) + return -ENODATA; + } + + avail = source_get_data_available(src); + if (avail < 4) + return -ENODATA; + + req = ffmpeg_dec_min(avail, cd->in_buf_size); + 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); + + /* ID3v2 tag: 10-byte header with 28-bit syncsafe length */ + if (memcmp(cd->in_buf, "ID3", 3) == 0) { + uint32_t tag; + size_t total_tag; + + if (req < 10) + return -ENODATA; + + tag = ((uint32_t)(cd->in_buf[6] & 0x7f) << 21) | + ((uint32_t)(cd->in_buf[7] & 0x7f) << 14) | + ((uint32_t)(cd->in_buf[8] & 0x7f) << 7) | + (uint32_t)(cd->in_buf[9] & 0x7f); + total_tag = 10 + tag + ((cd->in_buf[5] & 0x10) ? 10 : 0); + comp_info(dev, "ID3v2 tag detected: %zu bytes", total_tag); + + if (avail >= total_tag) { + source_drop_data(src, total_tag); + return 1; + } + + source_drop_data(src, avail); + cd->skip_bytes = total_tag - avail; + return -ENODATA; + } + + return 0; +} + +#if !CONFIG_FFMPEG_DEC_ENCODE_MODE + static const struct ffmpeg_dec_codec_ops * const ffmpeg_dec_codecs[] = { #if defined(CONFIG_FFMPEG_DEC_FLAC) || defined(CONFIG_COMP_FFMPEG_DEC_STUB) &ffmpeg_dec_flac_ops, @@ -250,11 +363,6 @@ __cold static int ffmpeg_dec_init(struct processing_module *mod) return 0; } -static inline size_t ffmpeg_dec_min(size_t a, size_t b) -{ - return a < b ? a : b; -} - static inline size_t ffmpeg_dec_max(size_t a, size_t b) { return a > b ? a : b; @@ -351,44 +459,6 @@ static int ffmpeg_dec_prepare(struct processing_module *mod, return 0; } -/* Copy @bytes out of a circular source buffer (starting at @src) into the - * linear @dst, wrapping at the buffer end if necessary. - */ -void ffmpeg_dec_copy_from_circular(void *dst, const void *src, - const void *buf_start, size_t buf_size, - size_t bytes) -{ - size_t to_end = (size_t)((const uint8_t *)buf_start + buf_size - - (const uint8_t *)src); - - if (to_end >= bytes) { - memcpy_s(dst, bytes, src, bytes); - return; - } - memcpy_s(dst, to_end, src, to_end); - memcpy_s((uint8_t *)dst + to_end, bytes - to_end, - buf_start, bytes - to_end); -} - -/* Copy @bytes from the linear @src into a circular sink buffer (starting at - * @dst), wrapping at the buffer end if necessary. - */ -static void ffmpeg_dec_copy_to_circular(void *dst, void *buf_start, - size_t buf_size, const void *src, - size_t bytes) -{ - size_t to_end = (size_t)((uint8_t *)buf_start + buf_size - - (uint8_t *)dst); - - if (to_end >= bytes) { - memcpy_s(dst, bytes, src, bytes); - return; - } - memcpy_s(dst, to_end, src, to_end); - memcpy_s(buf_start, bytes - to_end, - (const uint8_t *)src + to_end, bytes - to_end); -} - /* Drain as much staged PCM as the sink can currently accept. */ static void ffmpeg_dec_drain_pcm(struct ffmpeg_dec_comp_data *cd, struct sof_sink *sink) @@ -412,74 +482,6 @@ static void ffmpeg_dec_drain_pcm(struct ffmpeg_dec_comp_data *cd, cd->pcm_avail -= n; } -/* - * Peek at the start of the stream and skip a leading ID3v2 tag if present. - * Returns: - * 1 if an ID3v2 tag was completely skipped (caller should re-check), - * 0 if the stream does not start with an ID3v2 tag (cd->in_buf holds the - * peeked header bytes), - * -ENODATA while waiting for enough bytes to inspect or finish skipping. - */ -int ffmpeg_dec_skip_id3v2(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; - - /* Drain any pending skip bytes from a previous chunk */ - if (cd->skip_bytes > 0) { - avail = source_get_data_available(src); - if (!avail) - return -ENODATA; - size_t to_skip = ffmpeg_dec_min(avail, cd->skip_bytes); - - source_drop_data(src, to_skip); - cd->skip_bytes -= to_skip; - if (cd->skip_bytes > 0) - return -ENODATA; - } - - avail = source_get_data_available(src); - if (avail < 4) - return -ENODATA; - - req = ffmpeg_dec_min(avail, cd->in_buf_size); - 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); - - /* ID3v2 tag: 10-byte header with 28-bit syncsafe length */ - if (memcmp(cd->in_buf, "ID3", 3) == 0) { - uint32_t tag; - size_t total_tag; - - if (req < 10) - return -ENODATA; - - tag = ((uint32_t)(cd->in_buf[6] & 0x7f) << 21) | - ((uint32_t)(cd->in_buf[7] & 0x7f) << 14) | - ((uint32_t)(cd->in_buf[8] & 0x7f) << 7) | - (uint32_t)(cd->in_buf[9] & 0x7f); - total_tag = 10 + tag + ((cd->in_buf[5] & 0x10) ? 10 : 0); - comp_info(dev, "ID3v2 tag detected: %zu bytes", total_tag); - - if (avail >= total_tag) { - source_drop_data(src, total_tag); - return 1; - } - - source_drop_data(src, avail); - cd->skip_bytes = total_tag - avail; - return -ENODATA; - } - - return 0; -} - /** * ffmpeg_dec_is_ready_to_process() - Gate the DP process() call. * @@ -822,7 +824,19 @@ __cold static int ffmpeg_dec_free(struct processing_module *mod) return 0; } +#endif /* !CONFIG_FFMPEG_DEC_ENCODE_MODE */ + /* This defines the module operations */ +#if CONFIG_FFMPEG_DEC_ENCODE_MODE +static const struct module_interface ffmpeg_dec_interface = { + .init = ffmpeg_enc_mod_init, + .prepare = ffmpeg_enc_mod_prepare, + .process = ffmpeg_enc_mod_process, + .is_ready_to_process = ffmpeg_enc_is_ready_to_process, + .reset = ffmpeg_enc_mod_reset, + .free = ffmpeg_enc_mod_free, +}; +#else static const struct module_interface ffmpeg_dec_interface = { .init = ffmpeg_dec_init, .prepare = ffmpeg_dec_prepare, @@ -832,6 +846,7 @@ static const struct module_interface ffmpeg_dec_interface = { .reset = ffmpeg_dec_reset, .free = ffmpeg_dec_free }; +#endif /* If COMP_FFMPEG_DEC is =m in Kconfig this is built as a loadable LLEXT module. */ #if CONFIG_COMP_FFMPEG_DEC_MODULE diff --git a/src/audio/ffmpeg_dec/ffmpeg_dec.h b/src/audio/ffmpeg_dec/ffmpeg_dec.h index 9dd15d4fb669..3ff967b9d1d1 100644 --- a/src/audio/ffmpeg_dec/ffmpeg_dec.h +++ b/src/audio/ffmpeg_dec/ffmpeg_dec.h @@ -68,6 +68,23 @@ enum ffmpeg_dec_codec { struct ffmpeg_dec_comp_data; struct ipc_msg; struct sof_source; +struct sof_sink; + +/* Module-interface entry points for encode mode (CONFIG_FFMPEG_DEC_ENCODE_MODE): + * PCM in -> compressed elementary stream out (ffmpeg_dec-encode.c). + */ +int ffmpeg_enc_mod_init(struct processing_module *mod); +int ffmpeg_enc_mod_prepare(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks); +int ffmpeg_enc_mod_process(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks); +bool ffmpeg_enc_is_ready_to_process(struct processing_module *mod, + struct sof_source **sources, int num_of_sources, + struct sof_sink **sinks, int num_of_sinks); +int ffmpeg_enc_mod_reset(struct processing_module *mod); +int ffmpeg_enc_mod_free(struct processing_module *mod); /** * struct ffmpeg_dec_codec_ops - Per-codec stream and configuration operations. @@ -201,6 +218,10 @@ void ffmpeg_dec_copy_from_circular(void *dst, const void *src, const void *buf_start, size_t buf_size, size_t bytes); +void ffmpeg_dec_copy_to_circular(void *dst, void *buf_start, + size_t buf_size, const void *src, + size_t bytes); + int ffmpeg_dec_skip_id3v2(struct processing_module *mod, struct sof_source *src); /** diff --git a/src/audio/ffmpeg_dec/llext/CMakeLists.txt b/src/audio/ffmpeg_dec/llext/CMakeLists.txt index 630b375980b2..aa9780c52a96 100644 --- a/src/audio/ffmpeg_dec/llext/CMakeLists.txt +++ b/src/audio/ffmpeg_dec/llext/CMakeLists.txt @@ -91,8 +91,8 @@ else() ) # The archives must be built before the module compiles/links against them. - add_dependencies(ffmpeg_dec_llext_lib ffmpeg_ext newlib_m) - add_dependencies(ffmpeg_dec ffmpeg_ext newlib_m) + add_dependencies(ffmpeg_dec_llext_lib ffmpeg_ext newlib_m ${_ff_shine_dep} ${_ff_vo_aacenc_dep}) + add_dependencies(ffmpeg_dec ffmpeg_ext newlib_m ${_ff_shine_dep} ${_ff_vo_aacenc_dep}) # Cold split: place the (renamed) FFmpeg .cold code so the loader relocates # it to DRAM. See ffmpeg_cold.ld for why INSERT AFTER .bss. diff --git a/src/kernel/audio/base_fw.c b/src/kernel/audio/base_fw.c index a34176238d48..8addf4cab3e9 100644 --- a/src/kernel/audio/base_fw.c +++ b/src/kernel/audio/base_fw.c @@ -133,6 +133,18 @@ static void get_codec_info(struct sof_tlv **tuple) codec_info.items[codec_info.count++] = SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_MP3, SOF_IPC_STREAM_PLAYBACK); #endif +#if defined(CONFIG_FFMPEG_ENC_MP3) && !defined(CONFIG_CADENCE_CODEC_MP3_ENC) + /* MP3 encode (compress capture) via ffmpeg_dec (libshine), guarded so it is + * not advertised twice when the Cadence MP3 encoder is also present. + */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_MP3, SOF_IPC_STREAM_CAPTURE); +#endif +#if defined(CONFIG_FFMPEG_ENC_AAC) + /* AAC-LC encode (compress capture) via ffmpeg_dec (vo-aacenc). */ + codec_info.items[codec_info.count++] = + SET_CODEC_INFO_ITEM(SND_AUDIOCODEC_AAC, 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. diff --git a/tools/topology/topology2/cavs-mixin-mixout-hda.conf b/tools/topology/topology2/cavs-mixin-mixout-hda.conf index 50a4fc555bb1..f6ca8673b848 100644 --- a/tools/topology/topology2/cavs-mixin-mixout-hda.conf +++ b/tools/topology/topology2/cavs-mixin-mixout-hda.conf @@ -21,6 +21,17 @@ Define { EFX_IIR_PARAMS 'passthrough' EFX_DRC_PARAMS 'passthrough' HDA_MIC_ENHANCED_CAPTURE 'false' + + # Compress capture (encode) via the FFmpeg encoder (ffmpeg_enc). The PCM + # name overrides the generic compr-default.conf default. COMPR_ENC_SOURCE + # is the capture-BE fan-out tap: when COMPR_FFMPEG_ENC is enabled the + # capture path is rebuilt around a mixin (mixin.4.1) that fans out - via the + # proven mixer path - to both the normal analog capture FE (mixout.3.1) and + # the encoder FE (mixout.93.1). A copier's 2nd output pin does not deliver + # data across a pipeline boundary, which starved the encoder; mixin->mixout + # does. + COMPR_ENC_PCM_NAME 'Compress Capture HDA Analog' + COMPR_ENC_SOURCE 'mixin.4.1' } # include deep buffer config if buffer size is in 1 - 1000 ms. @@ -36,6 +47,12 @@ IncludeByKey.COMPR_FFMPEG { "true" "platform/intel/compr-ffmpeg.conf" } +IncludeByKey.COMPR_FFMPEG_ENC { + "true" { + + } +} + Object.Dai.HDA [ { name $HDA_ANALOG_DAI_NAME @@ -178,114 +195,164 @@ Object.Pipeline { } } ] +} - host-gateway-capture [ - { - index 3 - Object.Widget.host-copier.1 { - stream_name $ANALOG_CAPTURE_PCM - pcm_id 0 - num_input_audio_formats 1 - num_output_audio_formats 3 - Object.Base.input_audio_format [ - { - in_rate $HDA_ANALOG_CAPTURE_RATE - in_bit_depth 32 - in_valid_bit_depth 32 - } - ] - Object.Base.output_audio_format [ - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 16 - out_valid_bit_depth 16 - } - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 32 - out_valid_bit_depth 24 - } - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 32 - out_valid_bit_depth 32 - } - ] +# Capture path. Default (COMPR_FFMPEG_ENC=false) keeps the stock +# dai-copier -> eqiir -> module-copier -> host-copier.0 chain. When the FFmpeg +# encoder FE is enabled the capture is rebuilt around the mixer so a single +# mixin (mixin.4.1) fans out to both the normal capture FE (mixout.3.1) and the +# encoder FE (mixout.93.1): a module-copier 2nd output pin does not deliver data +# across a pipeline boundary, whereas mixin->mixout (the playback mix path) does. +IncludeByKey.COMPR_FFMPEG_ENC { +"false" { + Object.Pipeline { + host-gateway-capture [ + { + index 3 + Object.Widget.host-copier.1 { + stream_name $ANALOG_CAPTURE_PCM + pcm_id 0 + num_input_audio_formats 1 + num_output_audio_formats 3 + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_CAPTURE_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 16 + out_valid_bit_depth 16 + } + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 24 + } + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } } - } - ] + ] - dai-copier-eqiir-module-copier-capture [ - { - index 4 + dai-copier-eqiir-module-copier-capture [ + { + index 4 - Object.Widget.dai-copier."1" { - dai_type "HDA" - type "dai_out" - copier_type "HDA" - stream_name $HDA_ANALOG_DAI_NAME - node_type $HDA_LINK_INPUT_CLASS - num_output_pins 1 - num_input_audio_formats 1 - num_output_audio_formats 1 - Object.Base.input_audio_format [ - { - in_rate $HDA_ANALOG_CAPTURE_RATE - in_bit_depth 32 - in_valid_bit_depth 32 + Object.Widget.dai-copier."1" { + dai_type "HDA" + type "dai_out" + copier_type "HDA" + stream_name $HDA_ANALOG_DAI_NAME + node_type $HDA_LINK_INPUT_CLASS + num_output_pins 1 + num_input_audio_formats 1 + num_output_audio_formats 1 + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_CAPTURE_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + Object.Widget.eqiir.1 { + Object.Control.bytes."1" { + name '$ANALOG_CAPTURE_PCM IIR Eq' + } - ] - Object.Base.output_audio_format [ - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 32 - out_valid_bit_depth 32 + num_input_audio_formats 1 + num_output_audio_formats 1 + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_CAPTURE_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + Object.Widget.module-copier."2" { + stream_name $HDA_ANALOG_DAI_NAME + num_input_audio_formats 1 + num_output_audio_formats 1 + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_CAPTURE_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + } + ] + } +} +"true" { + # BE: HDA analog capture -> gain -> mixin.4.1 (the fan-out point). + # FE1 (normal analog capture, pcm0c): mixout.3.1 -> gain -> host-copier.0. + # FE2 (encoder, pcm51): mixin.4.1 -> mixout.93.1 -> ffmpeg_enc -> host-copier.51, + # declared in platform/intel/compr-capture-ffmpeg.conf. + Object.Pipeline { + mixout-gain-host-copier-capture [ + { + index 3 + Object.Widget.host-copier.1 { + stream_name $ANALOG_CAPTURE_PCM + pcm_id 0 + } + Object.Widget.gain.1 { + Object.Control.mixer.1 { + name 'Analog Capture Post Mixer Volume' } - ] + } } - Object.Widget.eqiir.1 { - Object.Control.bytes."1" { - name '$ANALOG_CAPTURE_PCM IIR Eq' - - } - num_input_audio_formats 1 - num_output_audio_formats 1 - Object.Base.input_audio_format [ - { - in_rate $HDA_ANALOG_CAPTURE_RATE - in_bit_depth 32 - in_valid_bit_depth 32 - } - ] - Object.Base.output_audio_format [ - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 32 - out_valid_bit_depth 32 - } - ] - } - Object.Widget.module-copier."2" { - stream_name $HDA_ANALOG_DAI_NAME - num_input_audio_formats 1 - num_output_audio_formats 1 - Object.Base.input_audio_format [ - { - in_rate $HDA_ANALOG_CAPTURE_RATE - in_bit_depth 32 - in_valid_bit_depth 32 - } - ] - Object.Base.output_audio_format [ - { - out_rate $HDA_ANALOG_CAPTURE_RATE - out_bit_depth 32 - out_valid_bit_depth 32 - } - ] - } - } - ] + ] + + dai-copier-gain-mixin-capture [ + { + index 4 + Object.Widget.dai-copier.1 { + dai_type "HDA" + copier_type "HDA" + stream_name $HDA_ANALOG_DAI_NAME + } + Object.Widget.gain.1 { + Object.Control.mixer.1 { + name 'Analog Capture Pre Mixer Volume' + } + } + } + ] + } +} } IncludeByKey.HDA_MIC_ENHANCED_CAPTURE { @@ -407,39 +474,70 @@ Object.Base.route [ source 'mixin.1.1' sink 'mixout.2.1' } - { - source 'dai-copier.HDA.$HDA_ANALOG_DAI_NAME.capture' - sink 'eqiir.4.1' - } { source 'host-copier.0.playback' sink 'gain.1.1' } ] -IncludeByKey.HDA_MIC_ENHANCED_CAPTURE { - "true" { - Object.Base.route [ - { - source 'module-copier.4.2' - sink 'tdfb.3.1' - } - { - source 'tdfb.3.1' - sink 'drc.3.1' - } - { - source 'drc.3.1' - sink 'host-copier.0.capture' - } - ] - } - "false" { - Object.Base.route [ - { - source 'module-copier.4.2' - sink 'host-copier.0.capture' - } - ] - } +IncludeByKey.COMPR_FFMPEG_ENC { +"false" { + # stock capture BE processing chain feeding the normal analog capture FE + Object.Base.route [ + { + source 'dai-copier.HDA.$HDA_ANALOG_DAI_NAME.capture' + sink 'eqiir.4.1' + } + ] + IncludeByKey.HDA_MIC_ENHANCED_CAPTURE { + "true" { + Object.Base.route [ + { + source 'module-copier.4.2' + sink 'tdfb.3.1' + } + { + source 'tdfb.3.1' + sink 'drc.3.1' + } + { + source 'drc.3.1' + sink 'host-copier.0.capture' + } + ] + } + "false" { + Object.Base.route [ + { + source 'module-copier.4.2' + sink 'host-copier.0.capture' + } + ] + } + } +} +"true" { + # BE dai -> gain -> mixin.4.1; the mixin fans out to mixout.3.1 (normal FE) + # and to mixout.93.1 (encoder FE - that tap is in compr-capture-ffmpeg.conf). + # The gain->mixin (BE) and mixout->gain->host-copier.0 (FE) hops are provided + # by the dai-copier-gain-mixin-capture / mixout-gain-host-copier-capture classes. + Object.Base.route [ + { + source 'dai-copier.HDA.$HDA_ANALOG_DAI_NAME.capture' + sink 'gain.4.1' + } + { + source 'mixin.4.1' + sink 'mixout.3.1' + } + { + source 'mixout.3.1' + sink 'gain.3.1' + } + { + source 'gain.3.1' + sink 'host-copier.0.capture' + } + ] +} } diff --git a/tools/topology/topology2/development/tplg-targets.cmake b/tools/topology/topology2/development/tplg-targets.cmake index 48dfc1c7934e..9713c7088182 100644 --- a/tools/topology/topology2/development/tplg-targets.cmake +++ b/tools/topology/topology2/development/tplg-targets.cmake @@ -228,6 +228,12 @@ 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-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" # 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/components/ffmpeg_enc.conf b/tools/topology/topology2/include/components/ffmpeg_enc.conf new file mode 100644 index 000000000000..c04307e3b7b5 --- /dev/null +++ b/tools/topology/topology2/include/components/ffmpeg_enc.conf @@ -0,0 +1,109 @@ +# +# Common widget ffmpeg_enc +# +# A generic FFmpeg (libavcodec) encoder widget. It wraps the SAME firmware +# module as ffmpeg_dec (identical UUID) but carries the DAPM widget type +# "encoder" instead of "decoder". All attributes defined herein are namespaced +# by alsatplg to "Object.Widget.ffmpeg_enc.N.attribute_name". +# +# Usage: this component can be used by instantiating it in the parent object. i.e. +# +# Object.Widget.ffmpeg_enc."N" { +# index 1 +# type "encoder" +# } +# +# Where N is the unique instance number for the ffmpeg_enc object within the +# same alsaconf node. +# +# The module encodes interleaved PCM to a compressed elementary stream (MP3 via +# libshine or AAC-LC via vo-aacenc). Both streams are self-describing (MPEG +# audio frame headers / ADTS headers), so no codec-setup (extradata) control is +# needed on the widget. +# + +Class.Widget."ffmpeg_enc" { + # + # Pipeline ID for the ffmpeg_dec widget object + # + DefineAttribute."index" { + type "integer" + } + + # + # Unique instance for ffmpeg_dec widget + # + DefineAttribute."instance" { + type "integer" + } + + #include common component definition + + + DefineAttribute."cpc" { + # Token set reference name and type + token_ref "comp.word" + } + + # NOTE: this is a 1-in/1-out module-adapter with the DAPM widget type + # "encoder" (snd_soc_dapm_encoder). The compress kernel path + # (sof_ipc4_compr_get_module) only recognises the codec module in a + # compress pipeline when its DAPM id is decoder/encoder, so the type must + # be "encoder" (not "effect") for the module to be found and configured. + # MP3 and ADTS AAC are self-describing, so unlike a decoder no codec-setup + # (extradata) bytes control is needed here. + + # Attribute categories + attributes { + # + # The ffmpeg_dec widget name is constructed from the index and instance. + # + !constructor [ + "index" + "instance" + ] + + # + # mandatory attributes that must be provided when the class is instantiated + # + !mandatory [ + "no_pm" + "uuid" + "num_input_audio_formats" + "num_output_audio_formats" + ] + + # + # immutable attributes cannot be modified in the object instance + # + !immutable [ + "type" + ] + + # + # deprecated attributes should not be added in the object instance + # + !deprecated [ + "preload_count" + ] + + # + # ffmpeg_dec widget objects instantiated within the same alsaconf node + # must have a unique instance attribute + # + unique "instance" + } + + # Default attribute values for the ffmpeg_dec widget. + # + # UUID (registry name "ffmpeg_dec") in SOF topology byte order. Must match the + # UUID the firmware registers for this module. Can be overridden in the + # top-level topology file. + uuid "73:af:07:12:c0:f9:65:4f:92:b4:9d:45:17:8f:c9:a8" + type "encoder" + no_pm "true" + core_id 0 + cpc 5000 + num_input_pins 1 + num_output_pins 1 +} diff --git a/tools/topology/topology2/include/pipelines/cavs/compr-capture-ffmpeg.conf b/tools/topology/topology2/include/pipelines/cavs/compr-capture-ffmpeg.conf new file mode 100644 index 000000000000..c5cfa7922f07 --- /dev/null +++ b/tools/topology/topology2/include/pipelines/cavs/compr-capture-ffmpeg.conf @@ -0,0 +1,85 @@ +# +# FE capture pipeline: compr-capture-ffmpeg +# +# Mirror of compr-playback-ffmpeg, but for the ENCODE direction: interleaved PCM +# arrives from a capture BE (the HDA analog capture DAI) and the FFmpeg +# (libavcodec) encoder widget "ffmpeg_enc" compresses it to an MP3 or AAC-LC +# elementary stream that the aif_out host-copier DMAs to the host (read via +# crecord). +# +# ffmpeg_enc is the same firmware module as ffmpeg_dec built in encode mode; it +# carries the DAPM type "encoder" so the compress kernel path +# (sof_ipc4_compr_get_module) finds and configures it. Although libshine and +# vo-aacenc are lightweight fixed-point encoders, the widget runs on the DP +# (data-processing) domain like the decoder: MP3 needs a full 1152-sample +# (24 ms) frame and AAC-LC needs a full 1024-sample (21.33 ms) frame of input +# before emitting a packet, and the DP ring can be sized to buffer that many +# samples ahead, which a 2 ms low-latency buffer cannot. core_id / stack sizing +# are set on the platform instance. +# +# This pipeline holds only the DP encoder and the aif_out host-copier. Its PCM +# input is delivered by an EXTERNAL mixout that lives in its own timer-scheduled +# mixout-capture pipeline (see platform/intel/compr-capture-ffmpeg.conf): the +# capture BE fans PCM out via a single mixin (mixin.4.1) to two mixouts - one in +# the normal analog capture FE and one that feeds this encoder - using the +# proven mixer fan-out (the same mix path playback uses). A module-copier's 2nd +# output pin does NOT deliver data across a pipeline boundary, which starved the +# encoder in the earlier copier-fan-out design; mixin->mixout does deliver. +# +# The tap mixout is isolated in its own LL pipeline because a DP module breaks +# the LL copy walk: a mixout sharing this pipeline would sit on the far side of +# the break from the pipeline's sink scheduler and never run. Standalone, the +# mixout self-schedules on the timer and its output crosses the boundary into +# this DP module's input ring, mirroring how the decode pipeline's LL +# host-copier feeds ffmpeg_dec. +# +# The mixin->mixout, mixout->ffmpeg_enc and ffmpeg_enc->host-copier routes are +# declared in the platform conf (platform/intel/compr-capture-ffmpeg.conf) +# because they depend on the capture BE fan-out tap (COMPR_ENC_SOURCE) and the +# compress PCM id. +# +# All attributes defined herein are namespaced by alsatplg to +# "Object.Pipeline.compr-capture-ffmpeg.N.attribute_name". +# + + + + + + + +Class.Pipeline."compr-capture-ffmpeg" { + + + + attributes { + !constructor [ + "index" + ] + + unique "instance" + } + + Object.Widget { + ffmpeg_enc."1" { + scheduler_domain "DP" + no_pm "true" + } + + host-copier."1" { + type "aif_out" + node_type $HDA_HOST_INPUT_CLASS + num_input_pins 1 + num_output_pins 1 + } + + pipeline."1" { + priority 0 + lp_mode 0 + } + } + + direction "capture" + dynamic_pipeline 1 + time_domain "timer" +} diff --git a/tools/topology/topology2/include/pipelines/cavs/mixout-capture.conf b/tools/topology/topology2/include/pipelines/cavs/mixout-capture.conf new file mode 100644 index 000000000000..faeaf51c4c27 --- /dev/null +++ b/tools/topology/topology2/include/pipelines/cavs/mixout-capture.conf @@ -0,0 +1,50 @@ +# +# Capture pipeline: mixout-capture. +# +# A single mixout that is the sole (source == sink) component of its own +# timer-scheduled pipeline. It exists to give the mixer capture fan-out tap an +# LL scheduling anchor: the pipeline's scheduling component falls back to its +# sink (this mixout), so the LL scheduler runs mixout_process every period, +# draining the feeding mixin into the mixout's sink buffer. +# +# This is needed when the mixout feeds a DP (data-processing) module in a +# downstream pipeline (e.g. the FFmpeg encoder): a DP module breaks the LL copy +# walk, so a mixout placed in the SAME pipeline as the DP module - on the far +# side of the break from the pipeline's DMA-driven scheduling component - is +# never scheduled and never drains its mixin. Isolating the mixout in its own +# timer-scheduled pipeline (the standard mixer pattern) makes it self-schedule; +# its output then crosses the pipeline boundary into the DP module's input ring. +# +# All attributes defined herein are namespaced by alsatplg to +# "Object.Pipeline.mixout-capture.N.attribute_name". +# + + + + +Class.Pipeline."mixout-capture" { + + + + attributes { + !constructor [ + "index" + ] + + unique "instance" + } + + Object.Widget { + mixout."1" {} + + pipeline."1" { + priority 0 + lp_mode 0 + direction "capture" + } + } + + direction "capture" + dynamic_pipeline 1 + time_domain "timer" +} diff --git a/tools/topology/topology2/include/pipelines/cavs/mixout-gain-host-copier-capture.conf b/tools/topology/topology2/include/pipelines/cavs/mixout-gain-host-copier-capture.conf index ab18e5b48fda..c0843f1cdf38 100644 --- a/tools/topology/topology2/include/pipelines/cavs/mixout-gain-host-copier-capture.conf +++ b/tools/topology/topology2/include/pipelines/cavs/mixout-gain-host-copier-capture.conf @@ -107,13 +107,6 @@ Class.Pipeline."mixout-gain-host-copier-capture" { } } - Object.Base.route [ - { - source gain.$index.1 - sink copier.host.$index.1 - } - ] - direction "capture" dynamic_pipeline 1 time_domain "timer" diff --git a/tools/topology/topology2/platform/intel/compr-capture-ffmpeg.conf b/tools/topology/topology2/platform/intel/compr-capture-ffmpeg.conf new file mode 100644 index 000000000000..f6801e6b4c9d --- /dev/null +++ b/tools/topology/topology2/platform/intel/compr-capture-ffmpeg.conf @@ -0,0 +1,117 @@ +# +# Compress *capture* (encode) instance driven by the FFmpeg (libavcodec) +# encoder widget "ffmpeg_enc". It is the mirror of platform/intel/compr-ffmpeg.conf +# (compress playback / decode), but in the opposite direction: +# +# HDA analog capture BE --> mixin.4.1 --> mixout (own LL pipeline) +# --> ffmpeg_enc (DP, PCM -> MP3 / AAC-LC) +# --> aif_out host-copier --> host +# +# PCM arrives already converted to S32 48 kHz 2ch at the capture BE fan-out tap. +# In cavs-mixin-mixout-hda.conf the capture BE (when COMPR_FFMPEG_ENC is set) is +# rebuilt around a single mixin (mixin.4.1) that fans out to two mixouts via the +# proven mixer path: mixout.3.1 in the normal analog capture FE, and the encoder +# tap mixout (COMPR_ENC_SOURCE -> mixout.$COMPR_ENC_MIXOUT_PIPELINE_ID.1). That +# tap mixout lives in its OWN timer-scheduled pipeline (mixout-capture, id +# COMPR_ENC_MIXOUT_PIPELINE_ID) rather than in the encoder pipeline: a DP module +# breaks the LL copy walk, so a mixout sharing the encoder pipeline would sit on +# the far side of the break from that pipeline's sink scheduler and never run. +# The standalone mixout self-schedules on the timer, drains the mixin, and its +# output crosses the pipeline boundary into the DP encoder's input ring. The +# encoder compresses to an MP3 or ADTS AAC-LC elementary stream that the aif_out +# host-copier DMAs to the host (read with crecord). Both streams are +# self-describing, so no codec-setup / extradata control is needed here. +# +# Why a mixin fan-out and not a copier 2nd output pin: a module-copier's second +# output pin does not deliver data across a pipeline boundary (the encoder DP +# module starved on an empty input ring), whereas mixin->mixout - the same node +# playback uses to mix/route across pipelines - does deliver. +# +# Gated on its own IncludeByKey.COMPR_FFMPEG_ENC so the stock capture path and the +# decode compr-ffmpeg path are unaffected. Uses its own COMPR_ENC_* defines +# (pipeline id 91, pcm id 51) so it can coexist with the decode compress PCM. +# +# COMPR_ENC_SOURCE is the capture-BE mixin fan-out tap; it is platform specific +# and set in cavs-mixin-mixout-hda.conf, where the capture BE is rebuilt around +# the mixer when COMPR_FFMPEG_ENC is enabled. +# + +IncludeByKey.COMPR_FFMPEG_ENC { +"true" { + # Standalone timer-scheduled mixout that taps the capture-BE mixin + # fan-out and feeds the DP encoder pipeline across a pipeline boundary. + # It must be its own pipeline: a DP module (ffmpeg_enc) breaks the LL + # copy walk, so a mixout sharing the encoder pipeline would sit on the + # far side of the break from that pipeline's sink scheduler and never + # run. Alone, the mixout is its pipeline's sink (== scheduling comp) and + # self-schedules on the timer, draining the mixin every period. + Object.Pipeline.mixout-capture [ + { + index $COMPR_ENC_MIXOUT_PIPELINE_ID + Object.Widget.mixout.1 {} + } + ] + + Object.Pipeline.compr-capture-ffmpeg [ + { + index $COMPR_ENC_PIPELINE_ID + + Object.Widget.host-copier.1 { + stream_name $COMPR_ENC_PCM_NAME + pcm_id $COMPR_ENC_PCM_ID + + + } + + Object.Widget.ffmpeg_enc.1 { + # Encoder runs on the DP thread, core $COMPR_ENCODER_CORE_ID. + # MP3 (1152 samples, 24 ms) and AAC-LC (1024 samples, + # 21.33 ms) must accumulate a full frame before emitting a + # packet, so the DP ring supplies headroom a 2 ms LL buffer + # lacks. + core_id $COMPR_ENCODER_CORE_ID + stack_bytes_requirement $COMPR_FFMPEG_ENC_STACK + heap_bytes_requirement $COMPR_FFMPEG_ENC_HEAP + + + } + } + ] + + Object.PCM.pcm [ + { + name $COMPR_ENC_PCM_NAME + id $COMPR_ENC_PCM_ID + direction capture + compress "true" + + Object.Base.fe_dai.1 { + name "$COMPR_ENC_PCM_NAME" + } + + Object.PCM.pcm_caps.1 { + name $COMPR_ENC_PCM_NAME + formats 'S32_LE' + rates '8000' + } + } + ] + + Object.Base.route [ + { + # capture-BE mixin fan-out tap -> the standalone LL mixout + source $COMPR_ENC_SOURCE + sink mixout.$COMPR_ENC_MIXOUT_PIPELINE_ID.1 + } + { + # LL mixout -> DP encoder, crossing the pipeline boundary + source mixout.$COMPR_ENC_MIXOUT_PIPELINE_ID.1 + sink ffmpeg_enc.$COMPR_ENC_PIPELINE_ID.1 + } + { + source ffmpeg_enc.$COMPR_ENC_PIPELINE_ID.1 + sink host-copier.$COMPR_ENC_PCM_ID.capture + } + ] +} +} diff --git a/tools/topology/topology2/platform/intel/compr-default.conf b/tools/topology/topology2/platform/intel/compr-default.conf index 6de5c185ffb0..0bd48e76034e 100644 --- a/tools/topology/topology2/platform/intel/compr-default.conf +++ b/tools/topology/topology2/platform/intel/compr-default.conf @@ -28,6 +28,28 @@ Define { # ~541 KiB). COMPR_FFMPEG_DEC_HEAP 524288 + # When true, adds a compress *capture* PCM driven by the FFmpeg encoder + # (ffmpeg_enc, PCM -> MP3 via libshine or AAC-LC via vo-aacenc). + # Independent of the decode COMPR_FFMPEG path above; uses its own + # pcm/pipeline ids so both may be built into one topology. The + # platform-specific capture-BE tap source (COMPR_ENC_SOURCE) and the + # HDA-specific PCM name are set where the BE is defined + # (cavs-mixin-mixout-hda.conf). + COMPR_FFMPEG_ENC false + COMPR_ENC_PCM_NAME 'Compress Capture' + COMPR_ENC_PCM_ID 51 + COMPR_ENC_PIPELINE_ID 91 + # LL mixout that taps the capture-BE mixin fan-out and feeds the DP + # encoder pipeline (COMPR_ENC_PIPELINE_ID) across a pipeline boundary. + COMPR_ENC_MIXOUT_PIPELINE_ID 93 + COMPR_ENCODER_CORE_ID 0 + COMPR_FFMPEG_ENC_STACK 65536 + # Modular heap requirement (mod_alloc): 48 KiB DP output ring (3x 16 KiB) + + # 48 KiB LL->DP input ring (3x 16 KiB) + ~32 KiB staging (pcm_in, out_stage, + # pcm_s16 / AVFrame) + ~48 KiB libshine / vo-aacenc state fits comfortably + # within 256 KiB. + COMPR_FFMPEG_ENC_HEAP 262144 + COMPRESSED_2 false COMPR_2_PCM_NAME 'Compress Playback 2' diff --git a/tools/topology/topology2/sof-hda-generic.conf b/tools/topology/topology2/sof-hda-generic.conf index c915ffd86a5c..fddf1c76f7b6 100644 --- a/tools/topology/topology2/sof-hda-generic.conf +++ b/tools/topology/topology2/sof-hda-generic.conf @@ -23,11 +23,14 @@ + + + diff --git a/west.yml b/west.yml index b927597e9333..3200fc166e1c 100644 --- a/west.yml +++ b/west.yml @@ -69,6 +69,22 @@ manifest: path: modules/audio/ffmpeg revision: 08ab4baa3400d55be75be1280f6119e210ce354e + # libshine: fixed-point MP3 encoder used by ffmpeg_dec (FFMPEG_ENC_MP3; + # FFmpeg has no native MP3 encoder). Cross-built by ffmpeg.cmake. + # SOF fork branch sof-hifi (precomputed init tables + Xtensa HiFi multipliers). + - name: shine + repo-path: shine + path: modules/audio/shine + revision: ab594a06915cfd10349765f73435592e65cbf746 + + # vo-aacenc: VisualOn fixed-point AAC-LC encoder used by ffmpeg_dec + # (FFMPEG_ENC_AAC). Cross-built by ffmpeg.cmake. + # SOF fork branch sof-hifi (Xtensa mulsh/nsa/clamps fixed-point optimizations). + - name: vo-aacenc + repo-path: vo-aacenc + path: modules/audio/vo-aacenc + revision: c823d30ac87a09ab22903b716b22c87de5bfdf9b + - name: zephyr repo-path: zephyr revision: 76f604468505b30dfafa2c5a80d8b1c665ed6633