語音信號的端點檢測方法有很多種,簡單的方法可以直接通過計算出聲音的音量大小,找到音量大於某個閾值的部分,認為該部分為需要的語音信號,該部分與閾值的交點即為端點,其余部分認為非語音幀。
計算音量
計算音量的方法有兩種,一種是以幀為單位(每一幀包含多個采樣點),將該幀內的所有采樣點的幅值的絕對值之后相加,作為該幀的音量值:
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 :