3. 麦克风与阵列基础

做麦克风阵列,说白了就是在跟「波」打交道。声波在时域里看起来就是一条上下抖动的曲线,但真正有价值的信息往往藏在频率里。我刚开始接触阵列信号处理时,总觉得时域波形挺直观的,直到有一次做声源定位,发现两个不同方向的说话人在时域上完全混在一起,根本分不开。后来转到频域一看,嘿,清清楚楚。

这一章我们就来啃下时频分析这块硬骨头。别怕,我会把傅里叶变换、STFT、MFCC这些概念掰开揉碎了讲,再配上Python代码,让你能直接上手跑起来。

3.1 傅里叶变换:从时域到频域的桥梁

傅里叶变换的核心思想其实很简单:任何连续的时间信号,都可以分解成一系列不同频率的正弦波之和。你想想看,一个复杂的声音,比如钢琴和小提琴同时演奏,在时域里波形乱成一团,但到了频域,每个乐器的基频和泛音就各自站好了位置。

连续傅里叶变换的公式长这样:

X(f) = ∫ x(t) · e^(-j2πft) dt

但在实际工程中,我们处理的是离散采样信号。所以用的是离散傅里叶变换(DFT):

X[k] = Σ x[n] · e^(-j2πkn/N),  n=0 to N-1

嗯,这里要注意,DFT的计算量是O(N²),如果采样点一多,计算时间会让人崩溃。好在有快速傅里叶变换(FFT),把复杂度降到了O(N log N)。

我的习惯:在Python里做FFT,我几乎只用numpy.fft或scipy.fft。别自己手写FFT,除非你在做学术研究。实际项目中,np.fft.rfft更常用——它只计算正频率部分,速度更快,内存更省。

3.2 频谱分析实战:Python实现

光说不练假把式。我们直接写一段代码,生成一个混合信号,然后看看它的频谱长什么样。

import numpy as np
import matplotlib.pyplot as plt

# 参数设置
fs = 16000          # 采样率 16kHz
t = np.arange(0, 1, 1/fs)  # 1秒时长

# 生成信号:1000Hz正弦波 + 3000Hz正弦波 + 噪声
f1, f2 = 1000, 3000
x = 0.5 * np.sin(2 * np.pi * f1 * t) + 0.3 * np.sin(2 * np.pi * f2 * t)
x += 0.1 * np.random.randn(len(t))

# 做FFT
N = len(x)
X = np.fft.rfft(x)          # 只取正频率
freqs = np.fft.rfftfreq(N, 1/fs)
mag = np.abs(X) / N * 2     # 幅度归一化

# 绘制频谱
plt.figure(figsize=(10, 4))
plt.plot(freqs, mag)
plt.xlim(0, 5000)
plt.xlabel('频率 (Hz)')
plt.ylabel('幅度')
plt.title('信号频谱')
plt.grid(True)
plt.show()

跑完这段代码,你会看到在1000Hz和3000Hz处有两个明显的尖峰。这就是频域的魅力——把混在一起的信号拆得明明白白。

我曾经踩过的坑:做FFT之前一定要检查信号长度是不是2的幂次。如果不是,FFT计算时会自动补零,导致频谱泄露。我习惯用np.fft.fft(x, n=2**int(np.ceil(np.log2(len(x)))))来强制对齐长度。

3.3 短时傅里叶变换(STFT):时频分析的利器

傅里叶变换有个硬伤:它假设信号在整个时间轴上是平稳的。但语音信号是时变的——同一个人的发音,不同时刻的频率成分完全不一样。你想想看,一个「啊」字从开始到结束,频率一直在变。

STFT的思路很直接:把信号切成一小段一小段,假设每段内信号是平稳的,然后对每一段做FFT。这样我们就得到了一个「时间-频率」二维矩阵,也就是语谱图。

STFT的数学表达:

X(m, k) = Σ x[n] · w[n - mH] · e^(-j2πkn/N)

其中w[n]是窗函数,H是帧移,m是帧索引。

Python里用scipy.signal.stft就能搞定:

from scipy.signal import stft

f, t, Zxx = stft(x, fs=16000, window='hann', nperseg=1024, noverlap=512)

# 绘制语谱图
plt.pcolormesh(t, f, np.abs(Zxx), shading='gouraud')
plt.ylabel('频率 [Hz]')
plt.xlabel('时间 [sec]')
plt.title('STFT 语谱图')
plt.colorbar()
plt.show()
关键参数选择:
  • 窗长(nperseg):决定了频率分辨率。窗越长,频率分辨率越高,但时间分辨率越低。我一般用1024点(64ms @16kHz)。
  • 帧移(noverlap):通常设为窗长的50%~75%。重叠越多,时间轴越平滑,但计算量也越大。
  • 窗函数:汉宁窗(hann)是语音处理的标准选择,旁瓣抑制好。

3.4 梅尔频率倒谱系数(MFCC):人耳感知的数学建模

MFCC是语音识别和声学处理中最常用的特征之一。为什么?因为人耳对频率的感知不是线性的——低频区域我们分辨得很细,高频区域则比较粗糙。梅尔刻度就是模拟这种感知特性。

梅尔频率与线性频率的转换关系:

mel = 2595 * log10(1 + f/700)

MFCC的提取流程,我用一张SVG图来展示:

MFCC 提取流程 输入语音信号 预加重 分帧 + 加窗 FFT → 频谱 梅尔滤波器组 log + DCT 说明: 1. 预加重:提升高频分量,补偿口唇辐射损失 2. 梅尔滤波器组:将线性频率映射到梅尔刻度 3. DCT:去相关,将能量集中到前几个系数

Python实现MFCC提取,我推荐用librosa库,一行代码搞定:

import librosa

# 加载音频
y, sr = librosa.load('speech.wav', sr=16000)

# 提取MFCC,默认13维
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, n_fft=1024, hop_length=512)

print(mfcc.shape)  # (13, 帧数)
我的经验:MFCC的维度不是越多越好。13维是经典配置,覆盖了大部分语音信息。我在做说话人识别时试过20维、30维,结果发现13维的效果反而最好。另外,别忘了加上一阶差分(delta)和二阶差分(delta-delta),它们能捕捉动态变化,对识别率提升很明显。

3.5 本章小结

这一章我们走完了从时域到频域,再到时频分析,最后到MFCC的完整路径。核心就三件事:

  • FFT:把信号从时域搬到频域,看清频率成分
  • STFT:在时间轴上滑动窗口做FFT,得到时频图
  • MFCC:模拟人耳感知,提取紧凑的声学特征