語音信號端點檢測


語音信號的端點檢測方法有很多種,簡單的方法可以直接通過計算出聲音的音量大小,找到音量大於某個閾值的部分,認為該部分為需要的語音信號,該部分與閾值的交點即為端點,其余部分認為非語音幀。

計算音量

計算音量的方法有兩種,一種是以幀為單位(每一幀包含多個采樣點),將該幀內的所有采樣點的幅值的絕對值之后相加,作為該幀的音量值:

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實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM