語音信號端點檢測

語音信號的端點檢測方法有不少種,簡單的方法能夠直接經過計算出聲音的音量大小,找到音量大於某個閾值的部分,認爲該部分爲須要的語音信號,該部分與閾值的交點即爲端點,其他部分認爲非語音幀。python

計算音量

計算音量的方法有兩種,一種是以幀爲單位(每一幀包含多個採樣點),將該幀內的全部採樣點的幅值的絕對值以後相加,做爲該幀的音量值:git

Vi = sum(|Wi|)github

以採樣率爲 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))

不論採樣點的數量是否可以被幀大小整除,最後一幀都須要單獨判斷,取波形長度和下一幀的波形長度較小的一個,並計算最後一幀的音量:dom

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) )3d

所以計算 volume[i] 的代碼須要修改一下:code

v = np.sum(np.power(curFrame, 2))

volume[i] = 10 * np.log10(v) if v > 0 else 0

通常不多出現平方和 v 的值爲 0 的狀況,不過爲了不這種狀況,計算時當 v = 0 時不須要通過 log 運算,直接給音量賦 0 值。orm

理論上講,當上面代碼中計算出 v 爲 0 的狀況,通過對數運算後獲得的值應該爲 負無窮 而不是 0。blog

根據閾值找到端點

計算出音量後,就獲得了一組離散的點,將其繪製在窗口上能夠獲得一個曲線, 閾值就是平行於橫軸的一條直線,這條直線與曲線的交點認爲是端點。判斷曲線是否與閾值相交的方法很簡單,(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 找出端點的序號 index一、index二、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實現
相關文章
相關標籤/搜索