FFmpeg/AAC 编码器 · libavutil 变换库

把 AAC 的 MDCT 变换分解为较小 FFT

FFmpeg 的 AAC 编码器通过 av_tx_init 取得 MDCT 函数。通用实现用折叠、旋转与半长 FFT 完成变换,FFT 再分解为较小子变换。

n7.1音视频处理核验 2026-10-11

工程问题#

AAC 编码需要反复把加窗音频转换为频域系数。直接逐项求和的 MDCT 成本随块长平方增长;同时,不同 CPU、数据类型与变换长度需要不同实现,编码器不宜直接绑定某个 FFT 内核。

实际实现#

编码器初始化 1024 和 128 点输出的浮点 MDCT,并在长窗或短窗路径调用相应函数。av_tx_init 根据类型、长度、标志与 CPU 能力挑选 codelet。以 ff_tx_mdct_fwd 这一通用 C 快速路径为例,它把 2N 个实数采样折叠并预旋转成 N/2 个复数,执行 N/2 点 FFT,再后旋转生成 N 个系数;对 M 点 FFT,split-radix codelet 再分成 M/2、M/4、M/4 点子变换并组合;这里首层 M = N/2。

处理流程#

  1. AAC 编码器初始化两种 MDCT 上下文;av_tx_init 返回匹配的上下文和函数指针
  2. 对长窗执行一次 1024 系数变换,或对八个短窗分别执行 128 系数变换
  3. 通用 MDCT 路径折叠实数采样、按映射排列并预旋转,准备较小的复数 FFT
  4. FFT 按可用 codelet 分解并组合子变换,MDCT 再后旋转得到输出系数
  5. 变换上下文与预计算表可供后续音频帧继续使用,销毁时统一释放

源码片段#

固定版本 · 原文摘录
libavcodec/aacenc.cn7.1 · L1212–L1217
查看原文件
1212    if ((ret = av_tx_init(&s->mdct1024, &s->mdct1024_fn, AV_TX_FLOAT_MDCT, 0,1213                          1024, &scale, 0)) < 0)1214        return ret;1215    if ((ret = av_tx_init(&s->mdct128, &s->mdct128_fn,   AV_TX_FLOAT_MDCT, 0,1216                          128, &scale, 0)) < 0)1217        return ret;
dsp_init — MDCT 初始化

初始化 1024 和 128 个输出系数的正向浮点 MDCT。len 是帧/系数长度,输入窗为其两倍;函数指针由变换库返回。

libavcodec/aacenc.cn7.1 · L475–L490
查看原文件
475static void apply_window_and_mdct(AACEncContext *s, SingleChannelElement *sce,476                                  float *audio)477{478    int i;479    float *output = sce->ret_buf;480481    apply_window[sce->ics.window_sequence[0]](s->fdsp, sce, audio);482483    if (sce->ics.window_sequence[0] != EIGHT_SHORT_SEQUENCE)484        s->mdct1024_fn(s->mdct1024, sce->coeffs, output, sizeof(float));485    else486        for (i = 0; i < 1024; i += 128)487            s->mdct128_fn(s->mdct128, &sce->coeffs[i], output + i*2, sizeof(float));488    memcpy(audio, audio + 1024, sizeof(audio[0]) * 1024);489    memcpy(sce->pcoeffs, sce->coeffs, sizeof(sce->pcoeffs));490}
apply_window_and_mdct

音频加窗后,长窗调用一次 mdct1024_fn,八短窗则逐块调用 mdct128_fn。这是编码器实际消费变换接口的位置。

libavutil/tx_template.cn7.1 · L1243–L1260
查看原文件
1243    s->scale_d = *((SCALE_TYPE *)scale);1244    s->scale_f = s->scale_d;12451246    flags &= ~FF_TX_OUT_OF_PLACE; /* We want the subtransform to be */1247    flags |=  AV_TX_INPLACE;      /* in-place */1248    flags |=  FF_TX_PRESHUFFLE;   /* First try with an in-place transform */12491250    if ((ret = ff_tx_init_subtx(s, TX_TYPE(FFT), flags, &sub_opts, len >> 1,1251                                inv, scale))) {1252        flags &= ~FF_TX_PRESHUFFLE; /* Now try with a generic FFT */1253        if ((ret = ff_tx_init_subtx(s, TX_TYPE(FFT), flags, &sub_opts, len >> 1,1254                                    inv, scale)))1255            return ret;1256    }12571258    s->map = av_malloc((len >> 1)*sizeof(*s->map));1259    if (!s->map)1260        return AVERROR(ENOMEM);
ff_tx_mdct_init — 子 FFT

通用 MDCT 申请 len/2 点复数 FFT,并先尝试适合预重排的原地实现;若不匹配再尝试一般 FFT。后续还会准备映射与旋转因子。

libavutil/tx_template.cn7.1 · L1293–L1306
查看原文件
1293    for (int i = 0; i < len2; i++) { /* Folding and pre-reindexing */1294        const int k = 2*i;1295        const int idx = sub_map[i];1296        if (k < len2) {1297            tmp.re = FOLD(-src[ len2 + k],  src[1*len2 - 1 - k]);1298            tmp.im = FOLD(-src[ len3 + k], -src[1*len3 - 1 - k]);1299        } else {1300            tmp.re = FOLD(-src[ len2 + k], -src[5*len2 - 1 - k]);1301            tmp.im = FOLD( src[-len2 + k], -src[1*len3 - 1 - k]);1302        }1303        CMUL(z[idx].im, z[idx].re, tmp.re, tmp.im, exp[i].re, exp[i].im);1304    }13051306    s->fn[0](&s->sub[0], z, z, sizeof(TXComplex));
ff_tx_mdct_fwd — 折叠和 FFT

把两倍窗长的实数数据按对称关系折叠、预旋转并写入复数布局,再调用初始化时选出的子 FFT;s->len 是输出系数数。

libavutil/tx_template.cn7.1 · L1308–L1318
查看原文件
1308    for (int i = 0; i < len4; i++) {1309        const int i0 = len4 + i, i1 = len4 - i - 1;1310        TXComplex src1 = { z[i1].re, z[i1].im };1311        TXComplex src0 = { z[i0].re, z[i0].im };13121313        CMUL(dst[2*i1*stride + stride], dst[2*i0*stride], src0.re, src0.im,1314             exp[i0].im, exp[i0].re);1315        CMUL(dst[2*i0*stride + stride], dst[2*i1*stride], src1.re, src1.im,1316             exp[i1].im, exp[i1].re);1317    }1318}
ff_tx_mdct_fwd — 后旋转

对 FFT 结果后旋转并按 stride 写出 MDCT 系数。它说明这里不是把 FFT 输出直接当作 MDCT 结果。

libavutil/tx_template.cn7.1 · L619–L633
查看原文件
619#define DECL_SR_CODELET(n, n2, n4)                                    \620static void TX_NAME(ff_tx_fft##n##_ns)(AVTXContext *s, void *_dst,    \621                                        void *_src, ptrdiff_t stride) \622{                                                                     \623    TXComplex *src = _src;                                            \624    TXComplex *dst = _dst;                                            \625    const TXSample *cos = TX_TAB(ff_tx_tab_##n);                      \626                                                                      \627    TX_NAME(ff_tx_fft##n2##_ns)(s, dst,        src,        stride);   \628    TX_NAME(ff_tx_fft##n4##_ns)(s, dst + n4*2, src + n4*2, stride);   \629    TX_NAME(ff_tx_fft##n4##_ns)(s, dst + n4*3, src + n4*3, stride);   \630    TX_NAME(ff_tx_fft_sr_combine)(dst, cos, n4 >> 1);                 \631}                                                                     \632                                                                      \633DECL_SR_CODELET_DEF(n)
DECL_SR_CODELET — FFT 分治

该宏生成二次幂 FFT codelet:调用一个半长和两个四分之一长子变换,再组合结果。它是通用 C split-radix 实现,架构专用 codelet 可能采用不同布局。

六段按编码器初始化、音频调用、子 FFT 初始化、折叠与执行、后旋转、FFT 分治展开。tx_float.c 通过模板实例化浮点代码;宏生成的 FFT 函数保留原文,未改写成示意伪码。CPU 匹配、codelet 优先级和失败回退可在 tx.c 的固定版本链接中核对。

收益与取舍#

工程分析

带来的收益

  • 对这里的二次幂长度,快速分解把单次变换从直接求和的 O(N²) 降为 O(N log N),折叠与旋转另需 O(N);这是算法分析,不是编码器整体性能测量
  • 编码器只依赖统一变换接口,长度和硬件相关的实现选择留在变换库
  • 初始化生成的映射和旋转因子可跨音频帧复用

付出的代价

  • 变换上下文、重排映射和旋转因子占用额外内存;初始化成本不能混入单次稳态变换成本后忽略
  • 具体 codelet 取决于构建、CPU、长度和标志;这里的通用 C 路径不代表所有机器都会执行同一函数
  • MDCT 与普通 FFT 不是同一个变换。MDCT 的窗口长度是输出系数数的两倍,完整重建还依赖匹配窗、逆变换和重叠相加

可比较的选择

直接 MDCT 求和定义直观,适合校验或很小规模;一般长度需要 O(N²) 运算。FFmpeg 同文件保留 naive codelet,不能因此把整个库的所有调用都标成 O(N log N)。

架构专用 FFT / MDCT codelet可以利用 SIMD、特定排列和寄存器组织,但依赖指令集与长度条件;调度器按支持条件和优先级选择,源码本身不证明某种 CPU 上的固定加速倍数。

版本与适用范围#

固定 FFmpeg n7.1 的 AAC 编码调用和 libavutil 变换实现。N 表示 MDCT 输出系数数;对应输入窗为 2N,通用路径内部 FFT 为 N/2 复数点。函数由 av_tx_init 动态选择,本文展示可被选择的通用 C 分解,不把它当成所有架构的唯一执行路径。

参考来源#

源码、官方文档与原始论文
01
ffmpeg · dsp_init / apply_window_and_mdct

libavcodec/aacenc.c · dsp_init / apply_window_and_mdct

源码n7.1
02
ffmpeg · AV_TX_FLOAT_MDCT 接口契约

libavutil/tx.h · AV_TX_FLOAT_MDCT 接口契约

源码n7.1
03
ffmpeg · ff_tx_init_subtx — 匹配与选择

libavutil/tx.c · ff_tx_init_subtx — 匹配与选择

源码n7.1
04
ffmpeg · ff_tx_mdct_init / ff_tx_mdct_fwd

libavutil/tx_template.c · ff_tx_mdct_init / ff_tx_mdct_fwd

源码n7.1
05
ffmpeg · DECL_SR_CODELET — split-radix FFT

libavutil/tx_template.c · DECL_SR_CODELET — split-radix FFT

源码n7.1
2 个匹配案例