概述
在之前的文章中,比较简单的介绍了几个时域特征,其实时域特征在现有的音频应用中基本不用。而使用较多的是频域特征,特别是在与深度学习有关的应用中,目前使用到的频域特征主要包括
-
语谱图
-
MFCC
-
Fbank
-
PLP
-
CQCC
下图很好了描述了上述几个特征的提取过程。

图片来自https://www.zhihu.com/question/310006797/answer/705053762
接下来的文章将会按照上述流程逐个介绍这5个特征(如果日后发现了其它使用较多的特征,将会续上)。其实现有的一些开源框架库如librosa,python_speech_feature都有提供现成的函数供开发者调用。而我之所以采用自己逐个实现的方式一方面是想加强自己的代码能力,另一方面算是补充自己的基础吧。(也是逐步提高自己的文字组织能力,因为一直被标哥吐槽写得烂)
语谱图对于从事语音信号处理的人来说应该是一个再熟悉不过的东西,所以在这里只做一个简单的介绍,就不贴公式了。因为语音信号整体上是个非平稳过程,但是却又是短时平稳的(10ms~30ms内认为是个准平稳过程)。所以对于语音信号的分析就绕不开短时分析技术--短时傅里叶变换(STFT)(加了窗的傅里叶变换)。接下来进入正题,提取语谱图主要得步骤如下
预加重
因为语音信号的功率谱随频率的增加而减小,因此导致语音的大部分能量都集中在低频部分,从而导致高频部分的信噪比很低。因此一般使用一阶高通滤波器去提升信号在高频部分的信噪比。
def preemphasis(signal, coeff=0.95):return np.append(signal[1], signal[1:] - coeff * signal[:-1])
分帧加窗
对语音进行完预加重后,然后就是分帧加窗操作,对于一段语音,以10ms~30ms为一帧,为了保证帧与帧之间平滑过渡保持连续性,帧与帧之间会有重叠。下图是分帧加窗的示例




首先对分帧后的信号进行傅里叶变换,每一帧信号都会得到一个频谱,然后用灰度值表示频谱幅度,幅度越大,颜色越深。然后将所有帧的频谱按如图所示的形式连起来,就形成了语谱。横轴是时间,纵轴是频谱。分帧加窗的代码如下
def frame_sig(sig, frame_len, frame_step, win_func):''':param sig: 输入的语音信号:param frame_len: 帧长:param frame_step: 帧移:param win_func: 窗函数:return: array of frames, num_frame * frame_len'''slen = len(sig)if slen <= frame_len:num_frames = 1else:# np.ceil(), 向上取整num_frames = 1 + int(np.ceil((slen - frame_len) / frame_step))padlen = int( (num_frames - 1) * frame_step + frame_len)# 将信号补长,使得(slen - frame_len) /frame_step整除zeros = np.zeros((padlen - slen,))padSig = np.concatenate((sig, zeros))indices = np.tile(np.arange(0, frame_len), (num_frames, 1)) + np.tile(np.arange(0, num_frames*frame_step, frame_step), (frame_len, 1)).Tindices = np.array(indices, dtype=np.int32)frames = padSig[indices]win = np.tile(win_func(frame_len), (num_frames, 1))return frames * win
FFT
对提取出来的帧信号进行傅里叶变换。
complex_spec = np.fft.rfft(frames, NFFT)
幅值平方
对得到的频谱幅度求平方
np.square(np.abs(complex_spec))
对数功率
librosa.power_to_db(feature.T)
经过上述变换后,就能得到漂亮的语谱图了

完整代码如下:
import matplotlib.pyplot as pltimport librosaimport numpy as npimport soundfile as sfimport python_speech_features as psfimport librosaimport librosa.display# Spectrogram步骤,# Step 1: 预加重# Step 2: 分帧# Step 3: 加窗# Step 4: FFT# Step 5: 幅值平方# Step 6: 对数功率def preemphasis(signal, coeff=0.95):return np.append(signal[1], signal[1:] - coeff * signal[:-1])def pow_spec(frames, NFFT):complex_spec = np.fft.rfft(frames, NFFT)return 1 / NFFT * np.square(np.abs(complex_spec))def frame_sig(sig, frame_len, frame_step, win_func):''':param sig: 输入的语音信号:param frame_len: 帧长:param frame_step: 帧移:param win_func: 窗函数:return: array of frames, num_frame * frame_len'''slen = len(sig)if slen <= frame_len:num_frames = 1else:# np.ceil(), 向上取整num_frames = 1 + int(np.ceil((slen - frame_len) / frame_step))padlen = int( (num_frames - 1) * frame_step + frame_len)# 将信号补长,使得(slen - frame_len) /frame_step整除zeros = np.zeros((padlen - slen,))padSig = np.concatenate((sig, zeros))indices = np.tile(np.arange(0, frame_len), (num_frames, 1)) + np.tile(np.arange(0, num_frames*frame_step, frame_step), (frame_len, 1)).Tindices = np.array(indices, dtype=np.int32)frames = padSig[indices]win = np.tile(win_func(frame_len), (num_frames, 1))return frames * winy, sr = sf.read('q1.wav')# 预加重y = preemphasis(y, coeff=0.98)# 分帧加窗frames = frame_sig(y, frame_len=2048, frame_step=512, win_func=np.hanning)# FFT及幅值平方feature = pow_spec(frames, NFFT=2048)# 对数功率及绘图.librosa.display.specshow(librosa.power_to_db(feature.T),sr=sr, x_axis='time', y_axis='linear')plt.title('Spectrogram')plt.colorbar(format='%+2.0f dB')plt.tight_layout()plt.show()
https://mp.weixin.qq.com/s/PKBZgFXicNHghb39iyPfow
















