语音信号端点检测
语音信号的端点检测方法有很多种,简单的方法可以直接通过计算出声音的音量大小,找到音量大于某个阈值的部分,认为该部分为需要的语音信号,该部分与阈值的交点即为端点,其余部分认为非语音帧。
计算音量
计算音量的方法有两种,一种是以帧为单位(每一帧包含多个采样点),将该帧内的所有采样点的幅值的绝对值之后相加,作为该帧的音量值:
Vi = sum(|Wi|)
以采样率为 11025 Hz ,时长为 1s 的波形为例:该波形含有 11025 个采样点,若取帧长为 framesize = 256
,帧间重叠大小为 overlap = 128
,则计算出来的音量数组包含 frameNum = 11025 / (256 - 128) = 86.13,取整为 frameNum = 87
。计算前 86 帧的音量代码(代码为 volume.py
的 calcNormalVolume
):
for i in range(frameNum - 1):
# 获取第 i 帧数据
curFrame = wave[i*step: i*step + framesize]
curFrame = curFrame - np.mean(curFrame)
# 公式: v = sum(|w|)
volume[i] = np.sum(np.abs(curFrame))
不论采样点的数量是否能够被帧大小整除,最后一帧都需要单独判断,取波形长度和下一帧的波形长度较小的一个,并计算最后一帧的音量:
curFrame = wave[(frameNum - 1)*step: min((frameNum - 1)*step + framesize, wlen)]
curFrame = curFrame - np.mean(curFrame)
volume[(frameNum - 1)] = np.sum(np.abs(curFrame))
另一种方法是计算分贝音量,与上面的代码差别在于计算 volume
时,使用的公式不同:
Vi = 10 * log10( sum(|Wi| ^ 2) )
因此计算 volume[i]
的代码需要修改一下:
v = np.sum(np.power(curFrame, 2))
volume[i] = 10 * np.log10(v) if v > 0 else 0
一般很少出现平方和 v 的值为 0 的情况,不过为了避免这种情况,计算时当 v = 0 时不需要经过 log
运算,直接给音量赋 0 值。
理论上讲,当上面代码中计算出 v 为 0 的情况,经过对数运算后得到的值应该为
负无穷
而不是 0。
根据阈值找到端点
计算出音量后,就得到了一组离散的点,将其绘制在窗口上可以得到一个曲线, 阈值就是平行于横轴的一条直线,这条直线与曲线的交点认为是端点。判断曲线是否与阈值相交的方法很简单,(ys[i] - threshold) * (ys[i+1] - threshold) < 0
。这个方法的缺点在于,当 ys[i] 或者 ys[i+1] 恰好等于 threshold 时,可能会遗漏端点。
def simpleEndPointDetection(vol, wave: vp.Wave, thresholds):
"""一种简单的端点检测方法,首先计算出声波信号的音量(能量),分别以
音量最大值的10%和音量最小值的10倍为阈值,最后以前两种阈值的一半作为阈值。
分别找到三个阈值与波形的交点并绘制图形,在查找交点时,各个阈值之间没有相互联系
figure 1:绘制出声波信号的波形,并分别用 red green blue 三种颜色的竖直线段
画出检测到的语音信号的端点
figure 2: 绘制声波音量的波形。并分别用 red green blue 三种颜色的音量阈值横线
表示出三种不同的阈值。
"""
# 给出三个固定的阈值
threshold1 = thresholds[0]
threshold2 = thresholds[1]
threshold3 = thresholds[2]
deltatime = wave.deltatime
frame = np.arange(0, len(vol)) * deltatime
# 分别找出三个不同的阈值
index1 = vp.findIndex(vol, threshold1) * deltatime
index2 = vp.findIndex(vol, threshold2) * deltatime
index3 = vp.findIndex(vol, threshold3) * deltatime
end = len(wave.ws) * (1.0 / wave.framerate)
plt.subplot(211)
plt.plot(wave.ts,wave.ws,color="black")
if len(index1) > 0:
plt.plot([index1,index1],[-1,1],'-r')
if len(index2) > 0:
plt.plot([index2,index2],[-1,1],'-g')
if len(index3) > 0:
plt.plot([index3,index3],[-1,1],'-b')
plt.ylabel('Amplitude')
plt.subplot(212)
plt.plot(frame, vol, color="black")
if len(index1) > 0:
plt.plot([0,end],[threshold1,threshold1],'-r', label="threshold 1")
if len(index2) > 0:
plt.plot([0,end],[threshold2,threshold2],'-g', label="threshold 2")
if len(index3) > 0:
plt.plot([0,end],[threshold3,threshold3],'-b', label="threshold 3")
plt.legend()
plt.ylabel('Volume(absSum)')
plt.xlabel('time(seconds)')
plt.show()
上述代码通过 findIndex 找出端点的序号 index1、index2、index3,然后计算出这几个端点在时间轴上的数值(乘以 deltatime)。最后使用 plt 进行绘制。
另一种比较复杂的是在给定一个阈值的基础上,再通过一个新的 threshold 计算出更大的语音部分,详见 volume.py
中的 findIndexWithPreIndex
。这种方法于上面的类似,但是只用到了两组端点索引。
calcNormalVolume 和 calcDbVolume 计算得到的曲线不同,最终得到的端点也不一样。以 one.wav 为语音样本,通过 DbVolume 计算得到的端点效果不如 normalVolume 得到的端点。
根据过零率找到端点
计算过零率也是以帧为单位,判断每两个相邻的采样值是否异号,代码和 findIndex
类似,这样可以得到每一帧中越过 0 的采样点的个数:
zcr[i] = sum(curFrame[:-1]*curFrame[1:] < 0) / framesize
与音量曲线类似,给定阈值之后就可以找到端点。绘制出过零率后可以看到,语音部分的过零率比非语音部分的过零率要低很多。
小结
不论是通过音量还是过零率,实际上都是通过固定的阈值找到端点,当信噪比较大情况下,很容易找到端点(one.wav 中的信噪比较大),但当信噪比较小时,固定的阈值就不一定能够找到端点了。并且固定的阈值(本文中用到的是占音量区间一定比例作为阈值)并不适应不同的语音信号,难以找出端点。
其他常见的还有 MFCC 系数、自相关函数等等方法可以找到语音信号的端点。
代码
github :
参考
本博客由 BriFuture 原创,并在个人博客(WordPress构建) BriFuture's Blog 上发布。欢迎访问。
欢迎遵照 CC-BY-NC-SA 协议规定转载,请在正文中标注并保留本人信息。