音频处理
音频处理
音频处理是编程界的几大天坑之一。
容器
类似 7z,音频文件大多数情况下只是容器,一般由元数据和各种编码的音频数据组成,而编码才是音频文件的本质。
有的容器只支持一种编码格式(容器名就是编码名),例如 MP3 和 FLAC。而有的容器支持多种,例如 OGG 支持 Vorbis、Opus、FLAC。
编码
最原始的 raw 音频也就是一串 f32 的序列(根据计算方式也有可能是 i32, i24 等),编码就是将这串序列进行有损/无损压缩后塞到容器里的过程。
最简单的编码就是 WAV 容器使用的 “PCM 编码”。实际上 PCM 本身是脉冲编码调制(Pulse Code Modulation),目的是将模拟信号转换为数字信号,这其中经过了采样、量化、编码的步骤。而 WAV 就直接将 PCM 后的数字信号塞到容器里,不做任何压缩,所以 WAV 一般都挺大的。因为容易处理,编码器好写,经常作为音频处理教学使用。
生活中最常见的 MP3 本身就是一种有损编码方式,MP3 容器只是对该编码的包装。它主要做了两件事,(1)舍弃 PCM 音频中人耳不敏感部分(FFT 到频域处理)(2)通过 Huffman 编码、动态比特率等各种手段减小音频数据大小。MP3 具有相当大的技术限制,并且放在今天 MP3 编码算法也算不上多先进,因此还是尽量少用。
Vorbis 编码被称为 Open-source alternative to MP3,是 OGG 容器中占有最主要地位的编码,常用于游戏中。我在 galgame 音频处理时跟 Vorbis 打了很多交道。Vorbis 基于 MDCT 和熵编码,能达到比 MP3 更高的压缩率。
opus 是新一代音频编码,具有高压缩率、低延迟、高音质、完全开放的特点。我非常喜欢,我希望能把所有音频格式都统一成 opus,并且我自己的音乐已经在进行这一步骤。
MP3 和 Vorbis 都是有损编码,而在高品质音乐中最常用的无损编码是 FLAC。它在无损界基本是唯一选择,因为它有许多很棒的特性,例如快速解码、流式、可定位、可调压缩等级(ref)等。
响度与均衡
峰值电平 并不严格等同于 响度:响度是人耳对声音强度的主观感知,会受到多种因素影响。
ITU-R BS.1770-4 (2015) 定义了 LUFS 作为响度单位。LUFS 综合考虑了电信号强度与人类感知,已经得到了广泛的使用。
EBU R.128
EBU R.128 是 EBU (European Broadcasting Union) 提出的关于响度标准化的建议和一套测量方法。其一般实现支持测量音频信息片段或音频流的 LUFS。
EBU R.128 基于三种不同长度区间内的响度进行截尾综合决策:Momentary loudness(400ms),Short-Term loudness(3s)和 Integrated loudness(长期)。更具体的可以阅读这篇文章。
EBU R.128 本身并不具备响度标准化的能力;一般的响度标准化会用 EBU R.128 测出 LUFS 后,为音频叠加一种增益模式使其达到 target LUFS。这一点将在后文展开。
增益
增益是放大或衰减音频信号强度的过程,也就是调整响度。
最简单的增益模式就是全局增益。然而经过我的一些原型验证,这样的处理具有较为显著的缺点:
- Loudness-Normalization-tauri-app 是流式场景下的响度标准化应用。在人声占据主导地位的场合下,人声的第一个字会爆音,因为基于之前的非人声音频流计算出的增益过高。
- audio-loudness-batch-normalize 是音频片段下的响度标准化应用。在片段上应用时不会出现爆音问题,在一定程度上可以改善用户体验;但是由于简单的全局增益不会改变动态范围(即高音量与低音量之间的差值),这并不能使人声更加清晰可辨,没有达到我做响度均衡的期望。
对于长音频片段来说,分段增益可能可以改善音频片段的响度标准化体验。分段增益的切分点较为关键,应该尽可能避免在高音量时切分导致响度割裂。继续改进,我们可以将分段的增益进行平滑处理,使其成为连续的增益曲线。
实例
Bilibili 在 2024 年实装了 音量动态均衡 功能,具体算法未公开,猜测是基于 EBU R.128 的检测,全局增益或分段增益均有可能。不过,文章 ASMR 类的 UP 主和听众, 可以帮我一起向客服反馈下音量均衡爆音的问题吗? 表明,正常音频的 target LUFS 并不适合 ASMR 的 target LUFS。
Youtube 会对高于 -14 LUFS 的音频进行负增益调至 -14 LUFS,对小于 -14 LUFS 的音频不作处理(来源请求)。不过也有一些帖子认为 Youtube 的增益算法有一些问题,其调整并不严格。
APU Dynamics Optimizer 是一款专业的高级调音软件,它可以让用户预设一条“动态范围曲线”,曲线规定了音频中不同 LUFS 的时长“占比”,超出该比例的音频片段则会被 APU Loudness Compressor 进行压缩/扩展。这种方式可以看成一种特殊的、更加自由的分段增益,具体效果取决于 APU Loudness Compressor 的检测与压缩方式:貌似其结合了 EBU R.128 与 RMS 进行连续的 LUFS 检测,而 Limiter 的压缩较为复杂,原理暂时未知。(描述可能存在错误,欢迎指出) 根据 APU Dynamics Optimizer 首页给出的视频演示,这个软件在高动态范围的纯音乐上效果非常好。可惜软件本身是付费的。
变速与变调
变速与变调是相辅相成的。我们有仅变速、仅变调、变速变调的算法,将它们自由组合起来可以玩出很多花样。
仅变速
行业泛用的是 WSOLA (Waveform Similarity and Overlap Add),例如 soundtouch 就用的这个(严格来说是 WSOLA-like time-stretching routines)。除此之外还有 PLOSA (Time-Domain Pitch-Synchronous Overlap and Add),及其变体 TD-PSOLA 等。这一类的最大特点是需要找峰值,并保留峰值。
Rust 的现成 crates 里,wsola 是个脑残占名字的没有内容,而 tdpsola 有一个可用实现。把仓库拉下来,example 里带了 wav 支持,不需要手动转 raw。虽然只支持单声道 wav,我还需要手动转一次,但是没有什么难度。并且作者在 README 里给出了一个 documentation,里面的视频把 TD-PSOLA 原理讲得非常透彻。
仅变调
我试过使用 pitch_shift (bugfix fork) 进行声调变换。这玩意是个经典 Phase Vocoder(相位声码器),大致流程如下:
- 分帧(Framing):把音频流切成重叠片段。
- 加窗(Windowing):Hanning Window,给每个片段边缘做淡入淡出。
- FFT:转为频域信号。
- 变调处理(Pitch Shifting):移动频率的位置,并修正相位。
- IFFT:把变后的频率信号变回时间信号。
- 重叠相加:把处理好的片段重新拼起来。
做 FFT/IFFT 的是 rustfft,realfft 这俩 crate,它们好像都有 simd 优化,性能还是很不错的。
不过这种相位声码器的音质就有点不尽人意,至少离我对人声音频的期望质量还是差了一点,有金属感。
至于更高级的仅变调算法,还没研究过,不过大概率也是纯时域算法效果要好些。
变速变调
变速变调其实算是生活中最常见到的类型。例如我有单声道 88200 个 sample 需要在 1 秒之内在 44100Hz 的声卡上播放完;由于声卡播放的音频 sample 数量是一定不会改变的 44100,此时就会触发音频 dll 的自动降采样。最终播放出的声音会带上 “Chipmunk Effect”,也就是听起来更尖。
升采样和降采样总称为重采样(SRC),都是改变 sample 个数的操作。虽然现代 dll 已经帮我们做了重采样,但其中也是有算法在的。
如果我们要将音频降采样到 0.5 倍,每隔 2 个数据点就扔掉一个是不行的:根据离散信号系统,原信号的频谱会被拉伸为 2 倍,而大于 2π 的频谱会被折叠回 [0-2π] 区间发生混叠,进而导致音频质量下降。(我曾在把《信号与系统》全部还给大学老师以后尝试过一次这样的降采样,然后就记住了)
人声判定
有一些需求是区分人声和背景音乐,也就是 input = 一段音频数据,output: bool = 是否为人声。
人声的特征还是比较明显的。
- 时域分析:
- 人声具有明显的音节结构,能量变化大,低能量帧较多。
- 频域分析:
- 人声的主要能量集中在 300Hz - 4000Hz。
- 人声的基频不稳定;而音乐由于十二平均律的存在,基频相对稳定。
通过这些指标可以大致作为人声和音乐的判据。根据应用场景,有时还会有一些额外判据,例如在 galgame 场景下,时长也是一个重要判据。
不过如果不只是区分人声和背景音乐,而是有更高精度要求、更多背景干扰的场景,或者需求变为“提取人声”,那么这些简单的判断可能会造成较大的误差。此时就需要更高级的分析方法了。
比较常用的是 MFCC(Mel-Frequency Cepstral Coefficients),它主要基于人耳对频率感知的对数关系,在低频处使用更多滤波器进行特征提取,因此在人声领域应用广泛。内部具体实现暂先不论,总之 MFCC 每一帧音频的输出都是一个向量,具体取多少维度可以根据实际情况考虑,常用的是取 13 维。一段音频的输出是一个 MFCC 矩阵,可以直接转换为图像,称为梅尔频谱图,如果再过一次 DCT 就是梅尔倒频谱。
至于后续分析基本都是机器学习的活了。MFCC 矩阵包含的信息非常丰富,通过一些模型,可以直接还原为人声音频信号,可以解析出这段音频对应的句子。如果只是进行人声判定,其实只需要简单训练一个判别器,将 MFCC 矩阵的一些统计特征、传统分析方法里的指标全部扔进去即可。
当然对于这些高级分析方法,我也只是稍作了解,并没有做这种复杂机器学习的打算;在我的场景下用传统判据就好了。
