【微科普】离散余弦变换在音频压缩里怎么用?MDCT、AAC、MP3的变换编码逻辑,一文吃透

📅 2026/8/26 18:29:41
【微科普】离散余弦变换在音频压缩里怎么用?MDCT、AAC、MP3的变换编码逻辑,一文吃透
音频压缩和图像压缩底层都离不开变换编码但音频领域用的 DCT 变体和 JPEG 里那个标准 DCT-II不是一回事。音频压缩真正的主角是MDCTModified Discrete Cosine Transform改进离散余弦变换。下面从为什么音频压缩不能直接用标准 DCT 讲起再到 MDCT 的时频分块、重叠相加、量化编码最后给出一个 Python 示例帮你直观理解 MDCT 在音频压缩中的位置。文章目录1. 音频压缩为什么要做变换从波形编码到变换编码1.1 音频信号的特点时域上几乎没有冗余可以直接丢弃1.2 标准 DCT 在音频里的局限块效应与预回声2. MDCT为音频压缩定制的重叠变换2.1 重叠分块的核心思想块与块之间不是独立拼接2.2 MDCT 的公式与正反变换对2.3 窗函数选择为什么音频压缩里窗的形状很重要3. 从 MDCT 系数到压缩码流完整的音频变换编码链路3.1 量化不是均匀的心理声学模型驱动的噪声分配3.2 短窗处理瞬态预回声的工程对策3.3 熵编码与边信息哈夫曼编码怎么与量化系数配合4. 一个 Python 示例用 MDCT 做简单的音频压缩模拟5. 工程避坑自己动手做音频压缩最容易踩的 3 个坑5.1 窗函数不满足重叠条件导致重建失败5.2 量化太粗糙导致预回声放大5.3 忽略比例因子造成低频失真1. 音频压缩为什么要做变换从波形编码到变换编码1.1 音频信号的特点时域上几乎没有冗余可以直接丢弃音频信号和图像有个本质区别图像在空间上有大量平坦区域和相似纹理直接做像素预测或分块压缩效果就很好而音频在时域上几乎是持续快速振荡的波形相邻采样点之间的相关性虽然存在但不像图像像素那样直观可利用。如果直接在时域对 PCM 采样值做量化量化噪声会均匀分布在整个频谱上。人耳对不同频段的噪声敏感度差异巨大均匀量化会导致部分噪声被明显听到。这就是为什么音频压缩不能简单地把采样值幅度缩小、量化而必须在变换域中做文章。1.2 标准 DCT 在音频里的局限块效应与预回声JPEG 里用的二维 DCT-II 对图像8 × 8 8\times88×8块效果很好但如果把一维 DCT-II 直接用在音频分块上会出现严重问题。标准 DCT 对每块独立变换、独立量化。块与块之间没有处理边界连续性量化误差在块交界处会产生不连续跳变在图像上表现为块效应在音频上会表现为周期性“咔嗒”声。更要命的是预回声。音频里一个瞬态信号比如鼓点或敲击声发生前本应非常安静。如果这个瞬态被分到某个变换块里量化误差会扩散到整块的时间范围导致在鼓点之前的静音段里出现可听见的噪声。图像块效应只是难看音频预回声直接破坏听感。因此音频压缩需要一种能在频域集中能量、同时又在时间域上平滑衔接的变换MDCT 就是为此设计的。2. MDCT为音频压缩定制的重叠变换2.1 重叠分块的核心思想块与块之间不是独立拼接MDCT 最大的设计特点是重叠分块。标准 DCT 把信号切成互不重叠的块块长N NN而 MDCT 的相邻块之间有50 % 50\%50%的重叠每个块长度是2 N 2N2N但每次向前推进只有N NN个样本。设第m mm个输入块包含样本x m N , x m N 1 , … , x m N 2 N − 1 x_{mN},x_{mN1},\dots,x_{mN2N-1}xmN​,xmN1​,…,xmN2N−1​共2 N 2N2N个点。相邻块共享N NN个采样点。这种重叠初看会带来数据量翻倍但 MDCT 的输出系数只有N NN个也就是说2 N 2N2N个输入样本经过变换后压缩成N NN个频域系数采样数没有增加。重叠的意义在于后一个块的变换起点在前一个块中间任何量化误差在时间域上的扩散边界被后一个块的窗口“兜住”不会产生硬边界跳变。配合合适的窗函数块交界处是平滑过渡的。2.2 MDCT 的公式与正反变换对2 N 2N2N点 MDCT 正向变换定义为X k ∑ n 0 2 N − 1 x n w n cos ⁡ [ π N ( n 1 2 N 2 ) ( k 1 2 ) ] , k 0 , 1 , … , N − 1 X_k\sum_{n0}^{2N-1} x_n w_n \cos\left[\frac{\pi}{N}\left(n\frac{1}{2}\frac{N}{2}\right)\left(k\frac{1}{2}\right)\right],\quad k0,1,\dots,N-1Xk​n0∑2N−1​xn​wn​cos[Nπ​(n21​2N​)(k21​)],k0,1,…,N−1其中w n w_nwn​是分析窗函数长度为2 N 2N2N。输出X k X_kXk​只有N NN个频域系数。反向变换 IMCT 的公式是y n 1 N w n ∑ k 0 N − 1 X k cos ⁡ [ π N ( n 1 2 N 2 ) ( k 1 2 ) ] , n 0 , 1 , … , 2 N − 1 y_n\frac{1}{N}w_n\sum_{k0}^{N-1} X_k \cos\left[\frac{\pi}{N}\left(n\frac{1}{2}\frac{N}{2}\right)\left(k\frac{1}{2}\right)\right],\quad n0,1,\dots,2N-1yn​N1​wn​k0∑N−1​Xk​cos[Nπ​(n21​2N​)(k21​)],n0,1,…,2N−1注意正向变换和反向变换都带有窗函数w n w_nwn​。在解码端相邻的两个 IMCT 块重叠N NN个样本通过重叠相加恢复原始信号。MDCT 有一个重要性质如果直接对单块做 IMCT 再 IDCT恢复出的不是原始信号的精确副本而是带有时间混叠的版本。这种时域混叠必须在重叠相加后才能消除。这就是 MDCT 名字里 MModified的含义——它牺牲了单块完美重建换来了全局完美重建和更好的频域特性。2.3 窗函数选择为什么音频压缩里窗的形状很重要MDCT 要求窗函数满足两个条件w n 2 w n N 2 1 w_n^2w_{nN}^21wn2​wnN2​1w n w 2 N − 1 − n w_nw_{2N-1-n}wn​w2N−1−n​第一个条件叫Princen-Bradley 条件保证重叠相加后幅度平铺第二个条件是偶数对称配合 MDCT 的混叠消除。工程上最常用的是正弦窗w n sin ⁡ [ π 2 N ( n 1 2 ) ] , n 0 , … , 2 N − 1 w_n\sin\left[\frac{\pi}{2N}\left(n\frac{1}{2}\right)\right],\quad n0,\dots,2N-1wn​sin[2Nπ​(n21​)],n0,…,2N−1正弦窗优点是主瓣较窄频率选择性好适合稳态信号。AAC 还使用KBD 窗Kaiser-Bessel Derived它的旁瓣更低适合瞬态信号能更好地抑制预回声。这里引出一个关键工程实践音频编码器不会固定用同一种窗。对稳态信号用长窗正弦窗频率分辨率高对瞬态信号切换到短窗 KBD 窗时间分辨率高。窗切换是 MP3、AAC 里抑制预回声的重要手段。3. 从 MDCT 系数到压缩码流完整的音频变换编码链路3.1 量化不是均匀的心理声学模型驱动的噪声分配MDCT 本身不压缩数据它只是把时域样本换成频域系数。真正的压缩来自量化。音频编码器里有一整套心理声学模型模拟人耳的听觉特性核心是两个效应频域掩蔽一个强的单音会抬高相邻频段的听觉阈值落在阈值以下的噪声听不见。时域掩蔽一个强信号发生后的一小段时间内听音者无法听见较弱的信号。心理声学模型根据当前音频块计算每个频段的掩蔽阈值然后在量化时对不同频段分配不同比特数。掩蔽阈值高的频段允许更多量化噪声用更少比特人耳敏感的频段分配更多比特。这就是为什么 MP3 和 AAC 在低码率下仍然能保持相对好的听感量化噪声没有被消除而是被藏到了听不见的地方。3.2 短窗处理瞬态预回声的工程对策预回声的本质是量化误差在时间上扩散过长。对于稳态信号长窗提供了良好的频率分辨率量化误差在整段内呈噪声状不易察觉。但遇到鼓点这类瞬态信号长窗的量化噪声可能泄漏到瞬态发生前的静音区。对策就是窗切换。编码器检测到瞬态时把当前帧的长窗替换为一组短窗每个短窗长度只有长窗的1 / 8 1/81/8时间分辨率提高8 88倍量化误差的时间扩散被限制在很短范围内预回声大幅降低。MP3 和 AAC 都使用这种长窗/短窗自适应机制。这也是为什么低码率下一些瞬态丰富的音乐片段会出现短暂的“炸音”或“糊音”往往与窗切换决策有关。3.3 熵编码与边信息哈夫曼编码怎么与量化系数配合量化后的 MDCT 系数还需要进一步无损压缩。AAC 和 MP3 使用哈夫曼编码对量化系数进行熵编码。量化后的频域系数通常大量为零或小整数哈夫曼编码利用这种统计分布不均匀来减少平均码长。MP3 中使用了多组预先设计的哈夫曼码表不同频段可以选不同码表AAC 的哈夫曼编码更灵活支持不同码段组合。除了系数本身码流里还要携带边信息量化步长、窗类型、码表选择、比例因子等。比例因子在 AAC 里尤为重要它相当于每个频段的增益控制决定该频段系数在量化前的缩放级别。比例因子分辨率越高噪声整形越精细但边信息开销也越大。4. 一个 Python 示例用 MDCT 做简单的音频压缩模拟下面用 Python 模拟 MDCT 压缩的核心流程分块 → 加窗 → MDCT → 阈值量化 → IMCT → 重叠相加。为了直观省略心理声学模型直接对频域系数做保留大系数的量化。importnumpyasnpimportscipy.io.wavfileaswavdefmdct_analysis(x_block,win):MDCT 正变换2N 个时域样本 - N 个频域系数Nlen(x_block)//2Mlen(x_block)xwx_block*win Xnp.zeros(N)forkinrange(N):cos_termnp.cos(np.pi/N*(np.arange(M)0.5N/2)*(k0.5))X[k]np.sum(xw*cos_term)returnXdefmdct_synthesis(X,win):MDCT 逆变换N 个频域系数 - 2N 个时域样本Nlen(X)M2*N ynp.zeros(M)forninrange(M):cos_termnp.cos(np.pi/N*(n0.5N/2)*(np.arange(N)0.5))y[n]np.sum(X*cos_term)*win[n]/Nreturnydefsine_window(M):正弦窗returnnp.sin(np.pi/M*(np.arange(M)0.5))# 模拟一个简单音频信号多个正弦波叠加fs44100tnp.arange(fs)/fs signal(0.3*np.sin(2*np.pi*440*t)0.2*np.sin(2*np.pi*1000*t)0.1*np.sin(2*np.pi*3000*t))frame_size512# N每帧输出系数数block_size1024# 2Nhopframe_size winsine_window(block_size)reconstructednp.zeros_like(signal)num_frameslen(signal)//hop-1forminrange(num_frames):startm*hop blocksignal[start:startblock_size]# 正向 MDCTXmdct_analysis(block,win)# 模拟量化只保留绝对值最大的 50 个系数其余置零thresholdnp.sort(np.abs(X))[-50]X_quantX.copy()X_quant[np.abs(X_quant)threshold]0# 逆向 MDCT 并重叠相加y_blockmdct_synthesis(X_quant,win)reconstructed[start:startblock_size]y_block# 计算重建信噪比noisesignal[:num_frames*hopblock_size-hop]-\ reconstructed[:num_frames*hopblock_size-hop]snr10*np.log10(np.sum(signal[:len(noise)]**2)/np.sum(noise**2))print(f保留每帧最大 50 个系数时的重建信噪比:{snr:.2f}dB)运行结果大致在15 1515到25 2525dB 之间具体取决于信号内容和保留系数数量。这个示例里每帧1024 10241024个时域样本被压缩成50 5050个非零频域系数相当于压缩比约20 : 1 20:120:1但重建波形仍然保留了主要频率成分。实际音频编码器不会简单丢弃小系数而是用心理声学模型指导的量化策略。但核心链路——分块、加窗、MDCT、量化、逆变换、重叠相加——和上述代码完全一致。5. 工程避坑自己动手做音频压缩最容易踩的 3 个坑5.1 窗函数不满足重叠条件导致重建失败MDCT 的重建依赖重叠相加窗函数必须满足w n 2 w n N 2 1 w_n^2w_{nN}^21wn2​wnN2​1。如果随便用 Hamming 窗或 Hann 窗而不做修正重叠相加后的幅度会起伏产生周期性音量波动。动手实现时务必使用正弦窗或满足 PB 条件的窗。5.2 量化太粗糙导致预回声放大为了追求压缩比直接对 MDCT 系数做粗量化稳态信号也许还能接受但遇到瞬态信号预回声会非常明显。工程上对瞬态帧要切换到短窗而不是继续用长窗硬压。检测瞬态可以用时域能量变化率或频域平坦度指标。5.3 忽略比例因子造成低频失真直接对全频段均匀量化会让人耳最敏感的低频段分配不足比特导致低频失真。实际系统中比例因子是逐频段调整的低频段用较小的量化步长高频段用较大步长。即使没有完整心理声学模型也至少应该对不同频段做不同精度的量化。MDCT 是理解现代音频压缩的钥匙。从 MP3 到 AAC再到 Opus底层都离不开重叠变换加窗、心理声学驱动的噪声分配和熵编码这三板斧。你在音频编解码项目中遇到过预回声、窗切换或比特分配的问题吗欢迎留言讨论。