把 AAC 的 MDCT 变换分解为较小 FFT
FFmpeg 的 AAC 编码器通过 av_tx_init 取得 MDCT 函数。通用实现用折叠、旋转与半长 FFT 完成变换,FFT 再分解为较小子变换。
工程问题#
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。
处理流程#
- AAC 编码器初始化两种 MDCT 上下文;av_tx_init 返回匹配的上下文和函数指针
- 对长窗执行一次 1024 系数变换,或对八个短窗分别执行 128 系数变换
- 通用 MDCT 路径折叠实数采样、按映射排列并预旋转,准备较小的复数 FFT
- FFT 按可用 codelet 分解并组合子变换,MDCT 再后旋转得到输出系数
- 变换上下文与预计算表可供后续音频帧继续使用,销毁时统一释放
源码片段#
固定版本 · 原文摘录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;初始化 1024 和 128 个输出系数的正向浮点 MDCT。len 是帧/系数长度,输入窗为其两倍;函数指针由变换库返回。
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}音频加窗后,长窗调用一次 mdct1024_fn,八短窗则逐块调用 mdct128_fn。这是编码器实际消费变换接口的位置。
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);通用 MDCT 申请 len/2 点复数 FFT,并先尝试适合预重排的原地实现;若不匹配再尝试一般 FFT。后续还会准备映射与旋转因子。
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));把两倍窗长的实数数据按对称关系折叠、预旋转并写入复数布局,再调用初始化时选出的子 FFT;s->len 是输出系数数。
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}对 FFT 结果后旋转并按 stride 写出 MDCT 系数。它说明这里不是把 FFT 输出直接当作 MDCT 结果。
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)该宏生成二次幂 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 分解,不把它当成所有架构的唯一执行路径。
参考来源#
源码、官方文档与原始论文libavcodec/aacenc.c · dsp_init / apply_window_and_mdct
libavutil/tx_template.c · ff_tx_mdct_init / ff_tx_mdct_fwd
libavutil/tx_template.c · DECL_SR_CODELET — split-radix FFT