目录
1.双门限法原理2.双参数的双门限端点检测的实例3.python实现双门限法端点检测
端点检测是指从包含语音的一段信号中确定出语音的起始点和结束点位置.
在进行基于音频信号的深度学习中,模型训练前进行端点检测,将每一个有效的激励信号提取出来,不仅可以增加样本数量,而且能够减少网络训练过程中不必要的计算,提升模型训练的准确率.
1.双门限法原理
双门限法最初是基于短时平均能量和短时平均过零率而提出的,其原理是汉语的韵母中有元音,能量较大,所以可以从短时平均能量中找到韵母,而声母是辅音,它们的频率较高,相应的短时平均过零率较大,所以用这两个特点找到声母和韵母,等于找出完整的汉语音节.双门限法是使用二级判决来实现的,如图所示.
a是语音的波形,b是语音的短时平均能量,c是语音的短时平均过零率
进行判决的具体步骤如下:
1.第一级判决
①根据在语音短时能量包络线上选取的一个较高阈值(门限)T2进行一次粗判.
则高于该T2阈值肯定是语音(即在CD段之间肯定是语音),而语音起止点应位于该阈值与短时能量包络交点所对应的时间点之外(即在CD段之外).
②在平均能量上确定一个较低的阈值(门限)T1,并从C点往左,从D点往右搜索,分别找到短时能量包络与阈值T1相交的两个点B和E,于是BE段就是用双门限法根据短时能量所判定的语音段起止点位置.
2.第二级判决
以短时平均过零率为准,从B点往左和从E点往右搜索,找到短时平均过零率低于某个阈值T3的两点A和F,这便是语音段的起止点.
根据这两级判决,求出了语音的起始点位置A和结束点位置F.但考虑到语音发音时单词之间的静音区会有一个最小长度表示发音间的停顿,就是在小于阈值T3满足这样一个最小长度后才判断为该语音段结束,实际上相当于延长了语音尾音的长度,如图中在语音波形上标出语音的起止点分别为A和F+(从图中看出终止点位置为F,而实际处理中延长到
F+).
在端点检测的具体运行中,首先是对语音进行分帧,在分帧的基础上方能求出短时平均能量和短时平均过零率,然后逐帧地依阈值进行比较和判断.
2.双参数的双门限端点检测的实例
使用能量,自相关函数,用双门限判决来提取端点.
之前使用能量和过零率检测端点的双门限法,实际上就是一个双参数的双门限检测法,它涉及能量和过零率两个参数,同时给出三个或四个门限值T1,T2,T3(T4).其中,有一个参数是dst1,另一个参数是dst2.
以下进一步介绍二级判决说明
1.第一级判决
①根据在第一个参数dst1上选取的一个较高的阈值T2(或在第二个参数dst2上选取的一个较高阈值T4),进行一次粗判,就是高于该T2(或T4)阈值肯定是语音.
②在第一个参数dst1上确定一个较低的阈值T1,从①中的交汇点向两边扩展搜索,分别找到dst1与阈值T1相交的两个点,粗判定为语音段的起止点位置.
2.第二级判决
以dst2为准,从第一级判断得到的起止点位置向两端扩展搜索,找到dst2与某个阈值T3
相交的两点,这边是语音段的起止点.
按照这样的思路编制出相应的函数:
(1)vad_param2D
功能:按照dst1和dst2两个参数提取语音端点的位置
调用格式:[voiceseg,vsl,SF,NF]=vad_param2D(dst1,dst2,T1,T2,T3,T4);
实例:用双参数进行端点检测
①在输入参数中,x是语音信号序列,为了要分帧,设置帧长为wlen和帧移为inc,NIS是前导无话段的帧数.
在语音处理中为了能估算噪声的情况,往往在一段语音的前部有一段前导无话段,利用该段前导无话段来估算噪声的特性.
在实际中,有时可能不知道前导无话段的帧数,但从语音信号的波形图中可以估算出前导无话段的时长IS,有了IS就能计算出前导无话段的帧数NIS
NIS = fix(IS * fs - wlen) / inc + 1)
②在这里的讨论中都认为噪声是平稳的,所以估算出的噪声短时平均能量和自相关函数适用于整段语音.
③语音信号为x(n),加窗分帧后为xi(m),其中下标i表示为第i帧,帧长为N,总帧数为fn,而每帧的能量计算为:
A M P i = ∑ m = 1 N x i 2 ( m ) AMP_i\ = \displaystyle \sum^{N} _{m=1} x_i^{2}(m) AMPi =m=1∑Nxi2(m)
语音信号为x(n),加窗分帧后为xi(m),其中下标i表示为第i帧(i=1,2,3,……,M),M为总帧数.
每帧数据的短时自相关函数定义为:
R i ( k ) = ∑ m = 1 L − k x i ( m ) x i ( m + k ) R_i(k) = \displaystyle \sum^{L-k} _{m=1} x_i(m)x_i(m+k) Ri(k)=m=1∑L−kxi(m)xi(m+k)
L为语音分帧后的长度,k为延迟量
如果在相邻两帧之间计算相关函数,便是互相关函数,其表达式为:
R i ( k ) = ∑ m = 1 L − k x i − 1 ( m ) x i ( m + k ) R_i(k) = \displaystyle \sum^{L-k} _{m=1} x_{i-1}(m)x_i(m+k) Ri(k)=m=1∑L−kxi−1(m)xi(m+k)
因为语音信号是准稳态信号,它的变化较缓慢,根据噪声的情况设置两个阈值T3和T4,当相关函数最大值大于T4时,便判定为是语音;当相关函数最大值大于或小于T3时,则判定为语音信号的端点.
④先对前导无话段计算噪声短时平均能量和自相关函数:
etemp=mean(etemp(1:NIS));
thredth=max(Rum(2:NIS));
再在这两值的基础上设置能量的两个阈值(T1)和(T2),以及互相关函数的阈值(T3):
T2 = 4etemp; T1 = 2etemp;
T3=1.1*thredth;
在这里阈值不设为一个固定的值,将会随前导无话段计算噪声的情况动态地变动.有了阈值以后就能按双门限法进行检测了.
⑤在检测完成后的端点为x1和x2,还进一步设置了SF和NF这两个序列,它们都是1xfn的数组,
SF=1表示该帧为有话帧,SF=0表示该帧为无话帧;
NF与SF相反,NF=1表示该帧为无话帧,NF=0表示该帧为有话帧.
⑥同时又设置了一个voiceseg结构数据,它也给出了语音端点的信息.因为在分析的一组语音中可能中间有几次停顿,每一组有一个开始时间和结束时间,在voiceseg结构数据中就包含了每一组有话音段的开始时间,结束时间和这组有话段语音的长度,它们都是以帧为单位.
voiceseg的计算是通过以下两个语句完成的:
speechIndex=find(SF==1);
voiceseg=findSegment(speechIndex);
程序清单如下:
clear all; clc; close all;IS=0.25; % 设置前导无话段长度wlen=200; % 设置帧长为25msinc=80; % 求帧移SNR=10; % 设置信噪比fle = 'D:\audio\test1\51_674_740001.wav';[xx,fs]=wavread(fle); % 读入数据xx=xx-mean(xx); % 消除直流分量x=xx/max(abs(xx)); % 幅值归一化N=length(x); % 取信号长度time=(0:N-1)/fs; % 设置时间signal=Gnoisegen(x,SNR); % 叠加噪声wnd=hamming(wlen); % 设置窗函数overlap=wlen-inc; % 求重叠区长度NIS=fix((IS*fs-wlen)/inc +1); % 求前导无话段帧数y=enframe(signal,wnd,inc)'; % 分帧fn=size(y,2); % 求帧数frameTime=frame2time(fn, wlen, inc, fs);% 计算各帧对应的时间for k=2 : fn % 计算互相关函数 u1=y(:,k-1); u2=y(:,k); ru=xcorr(u1,u2); Ru(k)=max(ru);endRum=multimidfilter(Ru,1); % 平滑处理Rum=Rum/max(Rum); % 归一化%thredth=max(Rum(2:NIS)); % 计算阈值%T3=1.1*thredth;%T4=1.3*thredth;T3 = 0.065;etemp=sum(y.^2); % 求取短时平均能量etemp=etemp/max(etemp); % 能量幅值归一化fn=size(y,2); % 帧数% etemp = mean(etemp(1:NIS)); %计算初始无话段区间的能量平均值% T2 = 4*etemp; T1 = 2*etemp;%设置能量的阈值T1=0.2; % 设置阈值T2=0.15;[voiceseg,vsl,SF,NF]=vad_param2D(etemp,Rum,T1,T2,T3); % 双参数双门限端点检测% 作图subplot 311; plot(time,x,'k');title('纯语音波形');ylabel('幅值'); axis([0 max(time) -1 1]);subplot 312; plot(frameTime,etemp,'k')title('语音短时能量图');ylabel('幅值'); axis([0 max(time) 0 1]);xlabel('时间/s');line([0 max(time)],[T1 T1],'color','b','LineStyle','--');line([0 max(time)],[T2 T2],'color','r','LineStyle','--');subplot 313; plot(frameTime,Rum,'k');title('短时自相关函数'); axis([0 max(time) 0 1.2]);xlabel('时间/s'); ylabel('幅值'); line([0,frameTime(fn)], [T3 T3], 'color','b','LineStyle','--');for k=1 : vsl % 标出语音端点 nx1=voiceseg(k).begin; nx2=voiceseg(k).end; fprintf('%4d %4d %4d %4d\n',k,nx1,nx2,nx2-nx1); subplot 311; line([frameTime(nx1) frameTime(nx1)],[-1 1],'color','b','LineStyle','--'); line([frameTime(nx2) frameTime(nx2)],[-1 1],'color','r','LineStyle','--');end
端点检测结果图
3.vad_param2D函数说明及其他说明
名称:vad_param2D
功能:用短时平均能量和自相关函数最大值提取语音端点位置
调用格式:function [voiceseg,vsl,SF,NF]=vad_param2D(dst1,dst2,T1,T2,T3,T4)
说明:
①dst1和dst2可以是上述的能量和过零率,也可以是其他参数,如自相关函数等,
②dst1的两个阈值T1和T2,dst2的一个或两个阈值T3(和T4)都是在调用该函数之前先计算出来,这些阈值可以取决于dst1和dst2原始数据上的数值,也可以是取经过平滑处理的数值.
③对dst2可只带一个阈值(在函数调用时只写T3,不写T4),也可带两个阈值,在函数中会自动地判断是否带有T4,只有带了T4以后才会用T4进行判断.
⑦在vad_param2D函数中设置了一些参数:maxsilence表示一段语音结束时静音区的最小长度,minlin表示有话段的最小长度,它们都是固定的.
而对T1,T2,T3等参数虽是动态变化的,但它们的比例系数也是固定的.
实际上,在端点检测中想要正确的检测会受到很多因素的影响:噪声环境是一个主要因素,不同的噪声,不同的信噪比都会影响到检测的正确性,所以这些参数可以改变,可以按实际情况进行调整.
函数流程图如下
名称:findSegment
功能:根据SF中数值为1的地址组合出每一组有话音段的开始时间,结束时间和这组有话段语音的长度.
调用格式:function soundSegment=findSegment(express)
程序清单如下:
function soundSegment=findSegment(express)if express(1)==0 voicedIndex=find(express); % 寻找express中为1的位置else voicedIndex=express;endsoundSegment = [];k = 1;soundSegment(k).begin = voicedIndex(1); % 设置第一组有话段的起始位置for i=1:length(voicedIndex)-1,if voicedIndex(i+1)-voicedIndex(i)>1, % 本组有话段结束soundSegment(k).end = voicedIndex(i); % 设置本组有话段的结束位置soundSegment(k+1).begin = voicedIndex(i+1);% 设置下一组有话段的起始位置 k = k+1;endendsoundSegment(k).end = voicedIndex(end); % 最后一组有话段的结束位置% 计算每组有话段的长度for i=1 :k soundSegment(i).duration=soundSegment(i).end-soundSegment(i).begin+1;end
注:其实单参数(只用短时平均能量)的双门限法就可以对音频数据集进行端点检测.效果不错.
3.python实现双门限法端点检测
""" 本程序是进行端点检测 双门限法进行端点检测"""import numpy as npimport wavefrom scipy.io import wavfileimport soundfile as sfimport numpy as npimport matplotlib.pyplot as pltimport osimport refrom pydub import AudioSegmentplt.rcParams['font.family'] = ['sans-serif']plt.rcParams['font.sans-serif'] = ['SimHei']plt.rcParams['axes.unicode_minus'] = Falsedef STAc(x): """ 计算短时相关函数 :param x: :return: """ para = np.zeros(x.shape) fn = x.shape[1] for i in range(fn): R = np.correlate(x[:, i], x[:, i], 'valid') para[:, i] = R return paradef STEn(x, win, inc): """ 计算短时能量函数 :param x: :param win: :param inc: :return: """ X = enframe(x, win, inc) s = np.multiply(X, X) return np.sum(s, axis=1)def STZcr(x, win, inc, delta=0): """ 计算短时过零率 :param x: :param win: :param inc: :return: """ absx = np.abs(x) x = np.where(absx < delta, 0, x) X = enframe(x, win, inc) X1 = X[:, :-1] X2 = X[:, 1:] s = np.multiply(X1, X2) sgn = np.where(s < 0, 1, 0) return np.sum(sgn, axis=1)def FrameTimeC(frameNum, frameLen, inc, fs): ll = np.array([i for i in range(frameNum)]) return ((ll - 1) * inc + frameLen / 2) / fsdef enframe(x, win, inc=None): nx = len(x) if isinstance(win, list) or isinstance(win, np.ndarray): nwin = len(win) nlen = nwin # 帧长=窗长 elif isinstance(win, int): nwin = 1 nlen = win # 设置为帧长 if inc is None: inc = nlen nf = (nx - nlen + inc) // inc frameout = np.zeros((nf, nlen)) indf = np.multiply(inc, np.array([i for i in range(nf)])) for i in range(nf): frameout[i, :] = x[indf[i]:indf[i] + nlen] if isinstance(win, list) or isinstance(win, np.ndarray): frameout = np.multiply(frameout, np.array(win)) return frameoutdef findSegment(express): """ 分割成語音段 :param express: :return: """ if express[0] == 0: voiceIndex = np.where(express) else: voiceIndex = express d_voice = np.where(np.diff(voiceIndex) > 1)[0] voiceseg = {} if len(d_voice) > 0: for i in range(len(d_voice) + 1): seg = {} if i == 0: st = voiceIndex[0] en = voiceIndex[d_voice[i]] elif i == len(d_voice): st = voiceIndex[d_voice[i - 1]+1] en = voiceIndex[-1] else: st = voiceIndex[d_voice[i - 1]+1] en = voiceIndex[d_voice[i]] seg['start'] = st seg['end'] = en seg['duration'] = en - st + 1 voiceseg[i] = seg return voicesegdef vad_TwoThr1(x, wlen, inc, NIS): """ 使用门限法检测语音段 :param x: 语音信号 :param wlen: 分帧长度 :param inc: 帧移 :param NIS: :return: """ maxsilence = 15 minlen = 5 status = 0 y = enframe(x, wlen, inc) fn = y.shape[0] amp = STEn(x, wlen, inc) ampth = np.mean(amp[:NIS]) amp2 = 7.5 * ampth amp1 = 10 * ampth xn = 0 count = np.zeros(fn) silence = np.zeros(fn) x1 = np.zeros(fn) x2 = np.zeros(fn) for n in range(fn): if status == 0 or status == 1: if amp[n] > amp1: x1[xn] = max(1, n - count[xn] - 1) status = 2 silence[xn] = 0 count[xn] += 1 elif amp[n] > amp2 : status = 1 count[xn] += 1 else: status = 0 count[xn] = 0 x1[xn] = 0 x2[xn] = 0 elif status == 2: if amp[n] > amp2 : count[xn] += 1 else: silence[xn] += 1 if silence[xn] < maxsilence: count[xn] += 1 elif count[xn] < minlen: status = 0 silence[xn] = 0 count[xn] = 0 else: status = 3 x2[xn] = x1[xn] + count[xn] elif status == 3: status = 0 xn += 1 count[xn] = 0 silence[xn] = 0 x1[xn] = 0 x2[xn] = 0 el = len(x1[:xn]) if x1[el - 1] == 0: el -= 1 if x2[el - 1] == 0: print('Error: Not find endding point!\n') x2[el] = fn SF = np.zeros(fn) NF = np.ones(fn) for i in range(el): SF[int(x1[i]):int(x2[i])] = 1 NF[int(x1[i]):int(x2[i])] = 0 voiceseg = findSegment(np.where(SF == 1)[0]) vsl = len(voiceseg.keys()) return voiceseg, vsl, SF, NF, ampif __name__ == '__main__': wavepath = './vadtest/60_805.wav' f = wave.open(wavepath, 'rb') params = f.getparams() nchannels, sampwidth, fs, nframes = params[:4] # 声道数、量化位数、采样频率、采样点数 str_data = f.readframes(nframes) # 读取音频,字符串格式 f.close() wavedata = np.fromstring(str_data, dtype=np.short) # 将字符串转化为浮点型数据 data = wavedata * 1.0 / (max(abs(wavedata))) # wave幅值归一化 data /= np.max(data) N = len(data) wlen = 200 inc = 80 IS = 1 overlap = wlen - inc NIS = int((IS * fs - wlen) // inc + 1) fn = (N - wlen) // inc + 1 frameTime = FrameTimeC(fn, wlen, inc, fs) # 计算出对应的时间 time = [i / fs for i in range(N)] #voiceseg, vsl, SF, NF, amp, zcr = vad_TwoThr(data, wlen, inc, NIS) voiceseg, vsl, SF, NF, amp = vad_TwoThr1(data, wlen, inc, NIS) plt.subplot(2, 1, 1) plt.ylabel('幅值') plt.ylim((-1, 1)) # 设置y轴刻度范围为0~11 plt.title('纯音频波形图') # 设置绘图标题 plt.plot(time, data) plt.subplot(2, 1, 2) plt.plot(frameTime, amp) plt.ylabel('幅值') plt.xlabel('时间(s)') # 设置x、y轴标签 plt.title('音频短时能量图') # 设置绘图标题 for i in range(vsl): plt.subplot(2, 1, 1) plt.vlines(frameTime[voiceseg[i]['start']],-1,1,colors='r',linestyles='dashed') plt.vlines(frameTime[voiceseg[i]['end']], -1, 1, colors='g', linestyles='dashed') plt.show()
效果图
所涉及知识都是参考自—>matlab在语音信号分析与合成中的应用