語音信號處理中的“窗函數”

文章代碼倉庫:https://github.com/LXP-Never/window_fun

窗函數貫穿整個語音信號處理,語音信號是一個非平穩的時變信號,但“**短時間內可以認爲語音信號是平穩時不變的,一般 10~30ms**。

對連續的語音分幀做STFT處理,等價於截取一段時間信號,對其進行週期性延拓,從而變成無限長序列,並對該無限長序列做FFT變換,這一截斷並不符合傅里葉變換的定義。因此,會導致頻譜泄漏和混疊

  • 頻譜泄漏:如果不加窗,默認就是矩形窗,時域的乘積就是頻域的卷積,使得頻譜以實際頻率值爲中心, 以窗函數頻譜波形的形狀向兩側擴散,指某一頻點能量擴散到相鄰頻點的現象,會導致幅度較小的頻點淹沒在幅度較大的頻點泄漏分量中
  • 頻譜混疊:會在分段拼接處引入虛假的峯值,進而不能獲得準確的頻譜情況

加窗的目的讓一幀信號的幅度在兩端漸變到 0,漸變對傅里葉變換有好處,可以讓頻譜上的各個峯更細,不容易糊在一起,從而減輕頻譜泄漏和混疊的影響

加窗的代價一幀信號兩端的部分被削弱了,沒有像中央的部分那樣得到重視。彌補的辦法就是相互重疊。相鄰兩幀的起始位置的時間差叫做幀移,常見的取法是取爲幀長的一半

對於語音,窗函數常選漢寧窗(Hanning)、漢明窗(Hamming)、sqrthann及其改進窗,他們的時域波形和幅頻響應如下所示:

1、漢寧窗(Hann)

$$w(n) = 0.5 - 0.5 \cos\left(\frac{2\pi{n}}{M-1}\right) \qquad 0 \leq n \leq M-1$$

2、漢明窗(Hamming)

$$w(n) = 0.54 - 0.46 \cos\left(\frac{2\pi{n}}{M-1}\right) \qquad 0 \leq n \leq M-1$$

# -*- coding:utf-8 -*-
# Author:凌逆戰 | Never
# Date: 2023/1/1
"""
繪製 窗函數和對應的頻率響應
"""
import numpy as np
from numpy.fft import rfft
import matplotlib.pyplot as plt

window_len = 60


# frequency response
def frequency_response(window, window_len=window_len, NFFT=2048):
    A = rfft(window, NFFT) / (window_len / 2)  # (513,)
    mag = np.abs(A)
    freq = np.linspace(0, 0.5, len(A))
    # 忽略警告
    with np.errstate(divide='ignore', invalid='ignore'):
        response = 20 * np.log10(mag)
    response = np.clip(response, -150, 150)
    return freq, response


def Rectangle_windows(win_length):
    # 矩形窗
    return np.ones((win_length))


def Voibis_windows(win_length):
    """ Voibis_windows窗函數,RNNoise使用的是它,它滿足Princen-Bradley準則。
    :param x:
    :param win_length: 窗長
    :return:
    """
    x = np.arange(0, win_length)
    return np.sin((np.pi / 2) * np.sin((np.pi * x) / win_length) ** 2)


def sqrt_hanning_windows(win_length, mode="periodic"):
    # symmetric: 對稱窗,主要用於濾波器的設計
    # periodic: 週期窗,常用於頻譜分析
    if mode == "symmetric":
        haning_window = np.hanning(win_length)
        sqrt_haning_window = np.sqrt(haning_window)
    elif mode == "periodic":
        haning_window = np.hanning(win_length+1)
        sqrt_haning_window = np.sqrt(haning_window)
        sqrt_haning_window = sqrt_haning_window[0:-1].astype('float32')
    return sqrt_haning_window


Rectangle_windows = Rectangle_windows(window_len)
hanning_window = np.hanning(M=window_len)
print(np.argmax(hanning_window))
sqrt_hanning_windows = sqrt_hanning_windows(window_len)
hamming_window = np.hamming(M=window_len)
Voibis_windows = Voibis_windows(window_len)
blackman_window = np.blackman(M=window_len)
bartlett_window = np.bartlett(M=window_len)
kaiser_window = np.kaiser(M=window_len, beta=14)

plt.figure()
plt.plot(Rectangle_windows, label="Rectangle")
plt.plot(hanning_window, label="hanning")
plt.plot(sqrt_hanning_windows, label="sqrt_hanning")
plt.plot(hamming_window, label="hamming")
plt.plot(Voibis_windows, label="Voibis")
plt.plot(blackman_window, label="blackman")
plt.plot(bartlett_window, label="bartlett")
plt.plot(kaiser_window, label="kaiser")

plt.legend()
plt.tight_layout()
plt.show()

freq, Rectangle_FreqResp = frequency_response(Rectangle_windows, window_len)
freq, hanning_FreqResp = frequency_response(hanning_window, window_len)
freq, sqrt_hanning_FreqResp = frequency_response(sqrt_hanning_windows, window_len)
freq, hamming_FreqResp = frequency_response(hamming_window, window_len)
freq, Voibis_FreqResp = frequency_response(Voibis_windows, window_len)
freq, blackman_FreqResp = frequency_response(blackman_window, window_len)
freq, bartlett_FreqResp = frequency_response(bartlett_window, window_len)
freq, kaiser_FreqRespw = frequency_response(kaiser_window, window_len)

plt.figure()
plt.title("Frequency response")
plt.plot(freq, Rectangle_FreqResp, label="Rectangle")
plt.plot(freq, hanning_FreqResp, label="hanning")
plt.plot(freq, sqrt_hanning_FreqResp, label="sqrt_hanning")
plt.plot(freq, hamming_FreqResp, label="hamming")
plt.plot(freq, Voibis_FreqResp, label="Voibis")
plt.plot(freq, blackman_FreqResp, label="blackman")
plt.plot(freq, bartlett_FreqResp, label="bartlett")
plt.plot(freq, kaiser_FreqRespw, label="kaiser")
plt.ylabel("Magnitude [dB]")
plt.xlabel("Normalized frequency [cycles per sample]")
plt.legend()
plt.tight_layout()
plt.show()
繪製 窗函數和對應的頻率響應

1 如何選擇窗函數

  1. 窗函數頻譜的主瓣儘量窄,能量儘可能集中在主瓣內,在頻譜分析時能獲得較高的頻率分辨率
  2. 旁瓣增益小且隨衰減快,以減小頻譜分析時的泄漏失真

 但主瓣既窄,旁辨又小衰減又快的窗函數是不容易找到的,比如矩形窗的主瓣寬度最窄,但旁瓣很大,因此在分析處理對應數據時,需要做綜合考慮。

 下圖爲針對特定的一段語音信號,加矩形窗與漢寧窗的時域波形及頻譜圖,Fs=8kHz,窗長取256。可以看出,採用矩形窗時,基音諧波的各個峯都比較尖銳,且整個頻譜圖顯得比較破碎,這是因爲矩形窗的主瓣較窄,具有較高的頻率分辨率,但是其旁瓣增益較高,因而使基音的相鄰皆波之間的干擾比較嚴重。在相鄰諧波間隔內有時疊加,有時抵消,出現了一種隨機變化的現象,相鄰諧波之間發生頻率泄露和混疊,而相對來說,Hamming窗會好多。

2 週期窗和對稱窗

在 MATLAB 中,每一個窗函數都可以選擇 ‘symmetric’ 或 ‘periodic’ 類型。

  • symmetric’ 類型表示窗函數是對稱的,主要用於濾波器的設計
  • periodic’ 類型表示窗函數是週期性的,主要用於頻譜分析

下圖分別畫出了週期窗和對稱窗,藍色的是週期窗(periodic),紅色的是對稱窗(symmetric)。在圖形上最大的區別是 對稱窗有兩個最大值,週期窗的最大值在中間。注意如果做stft的時候使用對稱的窗函數是不能完美重建的,會有一個比較小的誤差。

下圖是8個點的頻率響應漏,從圖中可以看出, periodic擁有稍微窄一點的主瓣,稍微高一點的旁瓣,和稍微低一點的噪聲帶寬。

窗長的選擇

上面已經說過,幀長一般爲10~30ms之間,接下來就具體驗證幀長會產生什麼影響,爲了驗證該問題,我們人工造一段很簡單的數據進行觀察,假設overlap爲窗長一半,FFT點數與窗長一致,避免引入補零等情況,即爲:

通過上圖可以驗證:長窗具有較高的頻率分辨率,較低的時間分辨率。長窗起到了時間上的平均作用。窗寬的選擇需折中考慮。短窗具有較好的時間分辨率,能夠提取出語音信號中的短時變化(這常常是分析的目的),損失了頻率分辨率。

在python中有很多庫都可以創建窗函數,我們一起來探索一下他們是對稱窗還是週期窗(非對稱)

  • numpy的hanning函數是對稱的
  • scipy有hanning函數有sym參數設置,默認是對稱的
  • torch的hanning函數有periodic參數設置,默認是非對稱的
# -*- coding:utf-8 -*-  
# Author:凌逆戰 | Never# Date: 2024/3/8  
"""  
對比不同庫中hann窗函數的實現  
如果對稱(sym=True)的話,有兩個最大值,如果不對稱(sym=False)的話,有一個最大值  
  
- numpy的hanning函數是對稱的  - scipy有hanning函數有sym參數設置,默認是對稱的  
- torch的hanning函數有periodic參數設置,默認是非對稱的  
"""  
import numpy as np  
  
import torch  
import scipy.signal as signal  
  
window_len = 512  
  
  
def hann_sym(window_len):  
    """對稱hann窗"""  
    win = np.zeros(window_len)  
    for i in range(window_len):  
        win[i] = 0.5 - 0.5 * np.cos(2 * np.pi * i / (window_len - 1))  
    return win  
  
  
def hann_asym(window_len):  
    """非對稱hann"""  
    p_win = np.zeros(window_len)  
    for i in range(window_len):  
        p_win[i] = np.sin(np.pi * i / window_len)  
        p_win[i] = p_win[i] * p_win[i]  
    return p_win  
  
  
def my_hann_aysm(win_len):  
    haning_window = np.hanning(win_len + 1)  # 對稱的hann窗  
    out = haning_window[0:-1].astype('float32')  # 捨棄最後一個元素  
    return out  
  
  
scipy_sym = signal.windows.hann(window_len, sym=True)  # 對稱的hann窗  
scipy_Asym = signal.windows.hann(window_len, sym=False)  # 非對稱的hann窗  
hann_sym_c = hann_sym(window_len)  
hann_asym_c = hann_asym(window_len)  
my_hann= my_hann_aysm(window_len)  
  
print(np.allclose(scipy_sym, hann_sym_c))  # True  
print(np.allclose(scipy_Asym, hann_asym_c))  # True  
print(np.allclose(my_hann, hann_asym_c))  # True  
  
numpy_window = np.hanning(window_len)  # 說明numpy的hanning函數是對稱的  
print(np.allclose(numpy_window, scipy_sym))  # True  
  
torch_window = torch.hann_window(window_len)  # 非對稱  
torch_window_periodic = torch.hann_window(window_len, periodic=False)  # 非週期=對稱  
# print(torch.argmax(window_torch))  
  
# 判斷兩個窗函數是否相等  
print(np.allclose(scipy_Asym, torch_window.numpy(), rtol=1e-3))  # True  
print(np.allclose(scipy_sym, torch_window_periodic.numpy(), rtol=1e-3))  # True
對比不同庫中hann窗函數的實現

3 低延遲非對稱窗

這裏講的低延遲非對稱窗並不是上文的非對稱窗(週期窗),而是真正圖形上的非對稱窗。

在STFT中,通常會使用重疊的窗來處理信號,以提高頻譜分辨率和減少頻譜泄漏。重疊的窗會導致相鄰窗之間存在重疊部分,這就需要使用OLA技術來將這些重疊部分合並起來,以恢復原始信號。

在進行重疊相加的過程中,會引入一定的延遲,這是因爲在重疊部分的處理過程中,需要考慮到前一個窗口和後一個窗口之間的重疊,以確保信號能夠完美重建。因此,延遲的產生主要是由於重疊窗口的處理過程中所引入的時間偏移。因此延遲產生的主要因素就有窗長、重疊比例、以及窗的形狀。

算法處理延遲一般是由於OLA決定的,比如一個窗長爲512,幀移爲256的hann窗,一般在做OLA的時候,在256個點之後,第一個完美重建的點纔會出來,因此延遲等於幀移。如果我們想要將算法延遲壓縮到32個點(2ms),第一種方法是使用窗長爲64,幀移爲32個點的窗,這樣我們NFFT=64,會導致頻率分辨率很低。第二種方法就是使用低延遲非對稱窗。在助聽器研究中常使用非對稱窗函數。

下面舉個例子,sqrthann非對稱窗,窗長爲512

圖2:具有高時間(窗口1)和高頻譜分辨率(窗口2)的分析和合成窗,用於窗長爲K = 512,M = 64和d=64

延遲等於2M-hop_size,如果M=hop_size,如果延遲等於hop_size。

目前非對稱窗窗形狀有:Orka窗、Tukey 窗、Asqrt hann 窗

def Orka_forward_window(N1=64, N2=448, hop_size=64, NFFT=512):  
    analysisWindow = np.zeros(NFFT)  
    for n in range(NFFT):  
        if n < N1:  
            analysisWindow[n] = np.sin(n * np.pi / (2 * N1)) ** 2  
        elif N1 <= n <= N2:  
            analysisWindow[n] = 1  
        elif N2 < n <= N2 + hop_size:  
            analysisWindow[n] = np.sin(np.pi * (N2 + hop_size - n) / (2 * hop_size))  
  
    return analysisWindow  
  
  
def Orka_backward_window(N1=64, N2=448, hop_size=64, NFFT=512):  
    synthesisWindow = np.zeros(NFFT)  
    for n in range(NFFT):  
        if n < N2 - hop_size:  
            synthesisWindow[n] = 0  
        elif N2 - hop_size <= n <= N2:  
            synthesisWindow[n] = np.cos(np.pi * (n - N2) / (2 * hop_size)) ** 2  
        elif N2 < n <= N2 + hop_size:  
            synthesisWindow[n] = np.sin(np.pi * (N2 + hop_size - n) / (2 * hop_size))  
    return synthesisWindow
Orka窗
def TukeyAW(n, N, alpha):  
    # assert n >= 0  
    if n < alpha * N:  
        return 0.5 * (1 - np.cos(np.pi * n / (alpha * N)))  
    elif n <= N - alpha * N:  
        return 1  
    elif n <= N:  
        return 0.5 * (1 - np.cos(np.pi * (N - n) / (alpha * N)))  
  
  
def getTukeyAnalysisWindow(filter_length, alpha):  
    analysisWindow = np.zeros(filter_length)  
    for i in range(filter_length):  
        analysisWindow[i] = TukeyAW(i, filter_length, alpha)  
    return analysisWindow  
  
  
def getTukeySynthesisWindow(N, A, B, alpha):  
    synthesisWindow = np.zeros(A)  
    for i in range(A):  
        x = N - A + i  
        numerator = TukeyAW(x, N, alpha)  
        denonminator = 0  
        for k in range(int(A / B)):  
            y = N - A + i % B + k * B  
            denonminator += TukeyAW(y, N, alpha) ** 2  
        synthesisWindow[i] = numerator / denonminator  
  
    synthesisWindow = np.pad(synthesisWindow, (N - A, 0), 'constant', constant_values=0)  
    return synthesisWindow  
Tukey窗
def getAsqrtAnalysisWindow(N, M, d):  
    # filter_length, hop_length, d  
    risingSqrtHann = np.sqrt(np.hanning(2 * (N - M - d) + 1)[:(N - M - d)])  
    fallingSqrtHann = np.sqrt(np.hanning(2 * M + 1)[:2 * M])  # 下降  
  
    window = np.zeros(N)  
    window[:d] = 0  
    window[d:N - M] = risingSqrtHann[:N - M - d]  
    window[N - M:] = fallingSqrtHann[-M:]  
  
    return window  
  
  
def getAsqrtSynthesisWindow(N, M, d):  
    risingSqrtHannAnalysis = np.sqrt(np.hanning(2 * (N - M - d) + 1)[:(N - M - d)])  
    fallingSqrtHann = np.sqrt(np.hanning(2 * M + 1)[:2 * M])  
    risingNoramlizedHann = np.hanning(2 * M + 1)[:M] / risingSqrtHannAnalysis[N - 2 * M - d:N - M - d]  
  
    window = np.zeros(N)  
    window[:-2 * M] = 0  
    window[-2 * M:-M] = risingNoramlizedHann  
    window[-M:] = fallingSqrtHann[-M:]  
  
    return window
Asqrthann窗

通過OLA過程發現,使用非對稱窗確實是在hop_size處信號完美重建,代碼見倉庫。

 

參考

【論文】CEC2 E008 Technical Pape
【論文】Wang Z Q, Wichern G, Watanabe S, et al. STFT-domain neural speech enhancement with very low algorithmic latency[J]. IEEE/ACM Transactions on Audio, Speech, and Language Processing, 2022, 31: 397-410.
【論文】Mauler D, Martin R. A low delay, variable resolution, perfect reconstruction spectral analysis-synthesis system for speech enhancement[C]//2007 15th European Signal Processing Conference. IEEE, 2007: 222-226.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章