3. 音频信号基础:采样率、量化、傅里叶变换、梅尔频谱图、MFCC特征提取原理

各位同学,欢迎来到第三章。这一章咱们要啃一块硬骨头——音频信号的基础知识。说实话,很多做语音识别的同学,模型调得飞起,但一问到音频底层原理就懵了。我个人觉得,这恰恰是区分「调参侠」和「真专家」的分水岭。

好,咱们不整虚的,直接开干。

3.1 采样率:把连续的声音掰成离散的点

声音本质上是连续的空气振动。但计算机只认0和1。怎么办?

答案就是采样。每隔一小段时间,记录一次声音的「快照」。这个快照的频率,就是采样率。

核心公式:采样率 ≥ 2 × 信号最高频率(奈奎斯特定理)

举个例子。人耳能听到的最高频率大约是20kHz。所以CD音质的采样率是44.1kHz。为什么是这个数?因为44.1kHz > 2 × 20kHz = 40kHz,留了点余量。

我在项目中遇到过一个问题:有个同学录了一段语音,采样率设成了8kHz。结果做出来的语音识别模型,对「s」和「sh」这种高频辅音完全分不清。为什么?因为8kHz只能还原4kHz以下的信号,而「s」音的能量集中在8kHz以上。嗯,这就是典型的「采样率不够,高频全丢」。

实战建议:语音识别一般用16kHz就够了。音乐处理建议用44.1kHz或48kHz。别盲目追求高采样率,文件大小和计算量都会翻倍。

3.2 量化:给每个采样点一个精确的数字

采样解决了「什么时候记录」的问题。量化解决的是「记录成什么数值」的问题。

你想想看,每个采样点的振幅是连续的。但计算机只能用有限个数字来表示。比如16位量化,就是把振幅范围分成65536个等级。每个采样点取最接近的那个等级。

量化位数越高,精度越高,动态范围越大。但文件也越大。

量化位数 动态范围 常见用途
8位 48 dB 电话语音(早期)
16位 96 dB CD音质、语音识别
24位 144 dB 专业录音、音乐制作
32位浮点 理论上无限 音频处理中间格式

避坑指南:我曾经在做一个语音合成项目时,把16位音频直接转成8位来减小文件。结果合成出来的声音全是底噪,像收音机没调好频道。后来才发现,8位量化的量化噪声太大了,根本不适合做高质量合成。从此以后,我坚持用16位以上。

3.3 傅里叶变换:从时域到频域的魔法

原始音频信号是时域的——横轴是时间,纵轴是振幅。但人耳对频率更敏感。我们需要知道「某个时刻有哪些频率成分」。

傅里叶变换就是干这个的。它把时域信号分解成不同频率的正弦波叠加。

说白了,就是一句话:任何连续信号,都可以用不同频率、不同振幅的正弦波组合出来。

但在实际语音处理中,我们用的是短时傅里叶变换(STFT)。为什么?因为语音信号是非平稳的——它的频率成分随时间变化。你不可能用一个全局的傅里叶变换来描述一整段语音。

STFT的做法是:把信号切成一小段一小段(通常20-40ms),假设每小段内信号是平稳的,然后对每一小段做傅里叶变换。

# 伪代码示意
def stft(audio, frame_size=1024, hop_length=512):
    frames = []
    for start in range(0, len(audio) - frame_size, hop_length):
        frame = audio[start:start+frame_size]
        # 加窗(汉宁窗/汉明窗)
        windowed = frame * np.hanning(frame_size)
        # 傅里叶变换
        spectrum = np.fft.rfft(windowed)
        frames.append(np.abs(spectrum))
    return np.array(frames)  # 形状: [帧数, 频率点数]

这里有个细节要注意:加窗。直接切帧会在边界处产生不连续,导致频谱泄露。加窗就是让帧的两端平滑过渡到0。我个人习惯用汉宁窗,效果比较均衡。

3.4 梅尔频谱图:模拟人耳的听觉特性

好,现在我们有频谱了。但人耳对频率的感知不是线性的。人耳对低频更敏感,对高频分辨率较低。

梅尔刻度就是模拟这种非线性感知的。公式如下:

Mel(f) = 2595 × log10(1 + f/700)

你看,低频段(比如0-1000Hz)的梅尔刻度变化很快,分辨率高。高频段(比如8000Hz以上)变化很慢,分辨率低。这和人耳的听觉特性完全吻合。

梅尔频谱图的生成步骤:

  1. 对音频做STFT,得到幅度谱
  2. 将频率轴映射到梅尔刻度上
  3. 用一组梅尔滤波器(三角滤波器)对频谱进行滤波
  4. 取对数(模拟人耳对音量的对数感知)

关键点:梅尔频谱图是语音识别中最常用的特征之一。它保留了时频信息,又模拟了人耳特性。很多端到端模型(如DeepSpeech、Wav2Vec)直接用它作为输入。

3.5 MFCC:梅尔频谱的进一步压缩

MFCC(梅尔频率倒谱系数)是梅尔频谱图的「升级版」。它做了两件事:

  • 取对数:已经做了
  • 离散余弦变换(DCT):对梅尔频谱做DCT,得到倒谱系数

为什么要做DCT?因为梅尔频谱中相邻频带之间有相关性。DCT可以去除这种相关性,把能量集中到前几个系数上。

通常我们取前13个MFCC系数。为什么是13?经验值。我试过8、13、20、40,13在大多数任务上效果最好。当然,具体任务要具体调。

# TensorFlow中提取MFCC
import tensorflow as tf

def extract_mfcc(waveform, sample_rate=16000):
    # 确保波形是float32
    waveform = tf.cast(waveform, tf.float32)
    
    # 计算梅尔频谱
    stfts = tf.signal.stft(waveform, 
                           frame_length=400,  # 25ms @ 16kHz
                           frame_step=160,    # 10ms @ 16kHz
                           fft_length=512)
    spectrograms = tf.abs(stfts)
    
    # 梅尔滤波器组
    num_spectrogram_bins = stfts.shape[-1]
    linear_to_mel_weight_matrix = tf.signal.linear_to_mel_weight_matrix(
        num_mel_bins=40,  # 40个梅尔频带
        num_spectrogram_bins=num_spectrogram_bins,
        sample_rate=sample_rate,
        lower_edge_hertz=80.0,
        upper_edge_hertz=7600.0)
    
    mel_spectrograms = tf.tensordot(spectrograms, 
                                     linear_to_mel_weight_matrix, 1)
    mel_spectrograms.set_shape(spectrograms.shape[:-1].concatenate(
        linear_to_mel_weight_matrix.shape[-1:]))
    
    # 取对数
    log_mel_spectrograms = tf.math.log(mel_spectrograms + 1e-6)
    
    # DCT得到MFCC
    mfccs = tf.signal.mfccs_from_log_mel_spectrograms(
        log_mel_spectrograms)[..., :13]  # 取前13个系数
    
    return mfccs

个人经验:MFCC在传统语音识别(GMM-HMM时代)是绝对的主角。但在深度学习时代,很多模型直接用梅尔频谱图甚至原始波形。不过MFCC仍然是一个很好的特征,尤其在数据量不大的时候,它能提供更紧凑的表示。

3.6 总结:这些特征怎么选?

好,咱们捋一捋:

  • 原始波形:信息最完整,但维度高、冗余大。适合数据量极大的端到端模型。
  • 频谱图:直观,但频率分辨率线性,不符合人耳特性。
  • 梅尔频谱图:模拟人耳,信息丰富。目前最常用的特征之一。
  • MFCC:更紧凑,去相关。适合传统方法和中小规模数据。

我个人建议:如果你刚开始做语音识别,先用梅尔频谱图。它足够好,而且可视化起来很直观。等你对任务有了感觉,再尝试MFCC或其他特征。

嗯,这一章的内容就到这儿。下一章咱们会动手用TensorFlow实现一个完整的语音识别流水线。到时候这些特征都会用上。

记住:理解原理比调参更重要。你把这些基础打牢了,后面遇到什么问题都能自己分析解决。