语音信号端点检测

语音信号的端点检测方法有很多种,简单的方法可以直接通过计算出声音的音量大小,找到音量大于某个阈值的部分,认为该部分为需要的语音信号,该部分与阈值的交点即为端点,其余部分认为非语音帧。

计算音量

计算音量的方法有两种,一种是以帧为单位(每一帧包含多个采样点),将该帧内的所有采样点的幅值的绝对值之后相加,作为该帧的音量值:

Vi = sum(|Wi|)

以采样率为 11025 Hz ,时长为 1s 的波形为例:该波形含有 11025 个采样点,若取帧长为 framesize = 256,帧间重叠大小为 overlap = 128,则计算出来的音量数组包含 frameNum = 11025 / (256 - 128) = 86.13,取整为 frameNum = 87。计算前 86 帧的音量代码(代码为 volume.pycalcNormalVolume):

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 :

  1. jupyter notebook
  2. volume 代码

参考

  1. thinkdsp-cn
  2. 语音信号处理之时域分析-音量及其Python实现
posted @ 2019-05-19 15:58  brifuture  阅读(4227)  评论(0编辑  收藏  举报