最近我突发奇想想做一个音频压缩算法。起初,我尝试将采样精度压缩到8位,但发现这样直接降低采样精度会带来明显的量化噪声。于是,我考虑保存音频的频域数据,做法是将音频转换为FFT频域表示,并将相位和幅值分别量化到8位,幅值还经过了对数处理。这样处理后,效果确实比直接在时域中压缩到8位好很多,文件大小也从原来的wav文件的38MB减半到19MB,符合预期结果。
但相位数据的处理较为复杂。因此,我在网上搜索了一下,找了一种叫DCT的算法,便将处理流程从FFT转为DCT,结果效果差别不大,文件大小仍保持在原来的一半。于是,我开始尝试去除部分不重要的DCT系数,我设置了一个阈值,将低于此阈值的系数设为0。通过调整不同的阈值,我找到了一个在音质和压缩率之间相对平衡的数值。然而,文件大小仍没有减少,因为这些零值依然占用相同的存储空间。
于是,我打开文件用十六进制编辑器查看,发现这些零值呈连续分布。所以,我决定采用游程编码来压缩零值序列,最后再套一层gzip以进一步压缩。效果非常显著,文件大小再次减半至9MB。之后,我注意到中频部分的音频信息被过度丢弃,因此我建立了一个表格,对不同频段设置不同的阈值,以尽量保留人耳更敏感的频段信息,这样文件最终压缩到7MB。
最后,我想到了之前玩FM立体声发射时的思路:FM立体声通过L+R和L-R信号传输,L-R信号含有的信息相对较少。因此,我改为分别处理L+R和L-R信号,并对L-R信号设置更高的阈值。最终,文件大小压缩到6MB。到此,我发现自己无意中发现了音频压缩的经典思想。
自从上次我鼓捣出一个音频压缩格式,命名为 QFP(Quantized Frequency Packing?其实就是随手取的)之后,心里总感觉不太踏实。虽然能压缩,虽然能放出来,但……说实话,那声音一听就知道有事。
首先,QFP 是基于 DCT 的,而DCT 的边缘效应让我听得头皮发麻:音频块和音频块之间的切口感太强,就像是剪辑没对齐的拼接视频,一听就出戏。整段音乐听起来像在翻页播放,咔哒咔哒的。其次,压缩效率也不尽如人意,就像一个节俭却舍不得扔瓶瓶罐罐的打包方式,空间没省多少,音质还受了伤。
于是我决定来个 QFP v2 大改造计划。
这次我换了一个更“滑”的编码方式 —— MDCT(修正型离散余弦变换)。这东西和 DCT 是远房亲戚,但最大优势是可以“块块重叠”,再配合我选用的 Vorbis 风窗函数,能把各块拼接得天衣无缝。
原来的 DCT 压缩听起来像马赛克音频,现在的 MDCT 则像用面粉和水揉了又揉的面团,压出来的面条——丝滑流畅,再无硬边。
不过,压缩高频的时候问题又来了。声音变得“空洞”了,像是空气被抽走了一样,塑料感特别强,听起来就像有人在水下讲电话,还拿个塑料袋套着话筒。
我思来想去,决定玩个“障眼法”:噪声填充。你说高频细节被压掉了,那我干脆自己造点细节出来!
具体做法也不复杂,我每隔一段(比如每64个 MDCT 系数)就记一下它们的强度,像在压缩包里夹一张“噪声强度说明书”。然后在解压时,把这些“指导手册”读出来,用它们生成差不多强度的高斯白噪声,再撒回原位。
结果效果还不错!不过一开始频谱图看着就像方块积木搭出来的,生硬得一批,听起来也有种“马赛克味儿”的奇怪感。于是我又整了个线性插值,把噪声分布“磨皮”一下,视觉上自然了,听感上也没那么刺耳。
现在这个 QFPv2,大概用 12kbps 左右就能比较准确地还原人声。为啥呢?因为人说话主要就在 0~4kHz 这段来回折腾,高频辅音(比如“s”“f”“sh”这些带刺儿的)本身就像沙子一样,用白噪声一糊,反而还原得更自然,听起来不但没毛病,反而比原声还“圆润”了一点。
当然,要是你拿来压音乐,白噪声多、打击乐重的曲子表现还可以,用 64~80kbps 就基本能搞定。但像钢琴、小提琴这类“谐波灵魂”型乐器就吃亏了,得提高到 120~160kbps 才不失真。
当然,QFPv2 目前还是个“科研玩具”级别的作品。没做心理声学建模,没有掩蔽效应那一套(也就是“大音量附近的我就懒得存”策略),量化也很原始——谁小就砍谁,完全不考虑比特预算。更糟的是,一旦少读一个字节,整个解码就崩,像走钢丝一样脆弱。不像 MP3 那种“久经沙场”的格式,有同步头、容错机制、纠错码,全都安排得明明白白。
这趟旅程让我有种“在车库里造压缩算法”的快乐感。虽然离真正能用还有很长的距离,但能在一次次“听感灾难”中总结经验,逐渐把水下说话调教回“人话”,也是挺有成就感的一件事!