写在前面:仅作为个人学习笔记,便于以后查阅,如对你有帮助,荣幸之至,如有错误,欢迎评论指正
编程软件:matlab R2018b
系统: win10
将.m4a的录音文件转化为.wav格式的语言文件后,对语音文件中接收到的语音信号进行消除趋势项和滤波处理。
消除多项式趋势:即减弱硬件设备对语音信号的影响。在采集语音信号数据的过程中,由于测试系统的某些原因在时间序列中会产生一个线性的或者慢变的趋势误差,例如放大器随温度变化产生的零漂移等,总之使语音信号的零线偏离基线,甚至偏离基线的大小随时间变化。零线随时间偏离基线被称为信号的趋势项。趋势项误差的存在,会使相关函数、功率谱函数在处理计算中出现变形,甚至可能使低频段的谱估计完全失去真实性和正确性,所以应去除语音信号的趋势项。常用的消除多项式趋势项的方法是用多项式最小二乘法。具体代码如下:
polydetrend.m
:
function [y,xtrend]=polydetrend(x, fs, m)
x=x(:); % 把语音信号x转换为列数据
N=length(x); % 求出x的长度
t= (0: N-1)'/fs; % 按x的长度和采样频率设置时间序列
a=polyfit(t, x, m); % 用最小二乘法拟合语音信号x的多项式系数a
xtrend=polyval(a, t); % 用系数a和时间序列t构成趋势项
y=x-xtrend; % 从语音信号x中清除趋势项
使用方法如下:
[xPoly, xtrend] = polydetrend(x, fs, 3);%%% wave filtering——Clear polynomial trend
% xPoly为去除多项式趋势后的语音信号,xtrend为得到的多项式趋势项,x为语音信号,fs为信号采样率,3为多项式级数
滤波(Wave filtering):是将信号中特定波段频率滤除的操作,是抑制和防止干扰的一项重要措施。
设计带通滤波器:本案例参考了网上教程,使用matlab自带的滤波器对语音信号进行滤波,具体操作流程如下所示:
filterDesigner
Design Filter
,设计出的滤波器频域如下所示:设计好的滤波器代码如下,本案例将matlab自动生成的滤波器保存为bPFilter_18_20.m
:
function Hd = bPFilter_16_18
%BPFILTER_16_18 Returns a discrete-time filter object.
% MATLAB Code
% Generated by MATLAB(R) 9.5 and Signal Processing Toolbox 8.1.
% Generated on: 08-Dec-2020 14:51:28
% FIR Window Bandpass filter designed using the FIR1 function.
% All frequency values are in Hz.
Fs = 48000; % Sampling Frequency
Fstop1 = 15000; % First Stopband Frequency
Fpass1 = 16000; % First Passband Frequency
Fpass2 = 18000; % Second Passband Frequency
Fstop2 = 19000; % Second Stopband Frequency
Dstop1 = 1e-05; % First Stopband Attenuation
Dpass = 0.057501127785; % Passband Ripple
Dstop2 = 1e-05; % Second Stopband Attenuation
flag = 'scale'; % Sampling Flag
% Calculate the order from the parameters using KAISERORD.
[N,Wn,BETA,TYPE] = kaiserord([Fstop1 Fpass1 Fpass2 Fstop2]/(Fs/2), [0 ...
1 0], [Dstop1 Dpass Dstop2]);
% Calculate the coefficients using the FIR1 function.
b = fir1(N, Wn, TYPE, kaiser(N+1, BETA), flag);
Hd = dfilt.dffir(b);
% [EOF]
滤波器调用方法:y = filter(bPFilter_18_20, x);
,其中x
为待滤波的语音信号,y
为滤波后的语音信号。
加窗和分帧都是语音信号提取特征的预处理阶段。
分帧:简单来说,一段语音信号整体上看不是平稳的,但在局部上可以看作是平稳的。在后期的语音处理中需要输入的是平稳信号,所以要对整段语音信号分帧,也就是切分成很多段。在10-30ms范围内都可以认为信号是稳定的,一般以不少于20ms为一帧,1/2左右时长为帧移分帧。帧移是相邻两帧间的重叠区域,是为了避免相邻两帧的变化过大。
加窗:按上述方法加窗后,每一帧的起始段和末尾端会出现不连续的地方,所以分帧越多与原始信号的误差也就越大。加窗就是为了解决这个问题,使分帧后的信号变得连续,每一帧就会表现出周期函数的特征。在语音信号处理中一般加汉明窗。
可使用maltab内置的分帧函数enframe进行分帧,可根据实际需求确定窗长、帧移、窗口类型三个参数,本案例使用的窗函数为汉明窗,具体代码如下所示:
%%% frame the record signal data
wlen = round(0.002 * fs); %set the windows length %窗口长度
inc = round(0.4 * wlen); %set the windows steps %帧移
win = hamming(wlen); %use hamming window %窗口类型
Y = enframe(x, win, inc)'; %framing the audio signal %使用分帧函数enframe,x为待分帧信号
fn = size(Y, 2); %get the number of frames %计算分帧数目
所谓端点检测,就是区分采集(接收)到的信号中的语音部分和非语音部分。本文先对采集到的语音信号进行分帧,再使用短时平均能量和平均过零率的方法进行端点检测。(端点检测原理查看参考文献4内容,此处篇幅限制不赘述)
具体代码如下:
endpoint.m代码及其使用的函数如下:
%% endpoint detection
fileName = 'chirpEcho_16_18_0.wav'; % set the audio file name
[x, fs] = audioread(fileName); % read the audio file
time = (0 : length(x) - 1) / fs;
plot(time, x);
axis([0 max(time) min(x) max(x)]);
title('录音信号的端点检测');
ylabel('幅值');
xlabel('时间/s');
grid;
%%% 对信号进行分帧,短时能量信号和短时平均过零率的前提条件
wlen = round(0.002 * fs); %set the windows length
inc = round(0.4 * wlen); %set the windows steps
win = hamming(wlen); %use hamming window
Y = enframe(x, win, inc)'; %framing the audio signal
fn = size(Y, 2); %get the number of frames
%%% 端点检测
IS = 0.1; % 设置静默段参考时长,单位s,可自行调整
overlap = wlen - inc; % 计算窗口重复长度
NIS = fix((IS * fs - wlen) / inc + 1); % 计算NIS
frameTime = frame2time(fn, wlen, inc, fs);% 计算每帧对应的时间
[voiceseg, vsl, SF, NF] = de_vad_ezm(Y, NIS); % 端点检测
for k=1 : vsl % 画出起止点位置
nx1 = voiceseg(k).begin;
nx2 = voiceseg(k).end;
nxl = voiceseg(k).duration;
fprintf('%4d %4d %4d %4d\n', k, nx1, nx2, nxl);
line([frameTime(nx1) frameTime(nx1)], [-1 1], 'color', 'k', 'LineStyle', '-');
% line([frameTime(nx2) frameTime(nx2)],[-1.5 1.5],'color','k','LineStyle','--');
end
hold on;
frame2time.m:
function frameTime=frame2time(frameNum,framelen,inc,fs)
% ================= 计算分帧后每一帧对应的时间=====================
% ================= 输 入 ===================================
%frameNum : 总帧数
%framelen : 帧长
%inc : 帧移
%fs : 采样频率
%================== 输 出 ====================================
%frametime : 每帧的时间,即取这一帧数据中间位置的时间
frameTime=(((1:frameNum)-1)*inc+framelen/2)/fs;
de_vad_ezm.m:
% function [voiceseg,vsl,SF,NF]=vad_ezm1(x,wlen,inc,NIS) %x为原始录音语音信号
function [voiceseg,vsl,SF,NF]=de_vad_ezm(y,NIS) %y为分帧后的语音信号
% x=x(:); % 把x转换成列数组
maxsilence = 15; % 初始化
minlen = 5;
status = 0;
count = 0;
silence = 0;
% y=enframe(x,wlen,inc)'; % 分帧
fn=size(y,2); % 帧数
amp=sum(y.^2); % 求取短时平均能量
zcr=zc2(y,fn); % 计算短时平均过零率
ampth=mean(amp(1:NIS)); % 计算初始无话段区间能量和过零率的平均值
zcrth=mean(zcr(1:NIS));
amp2=512*ampth; amp1=512*ampth; % 设置能量和过零率的阈值
zcr2=256*zcrth;
%开始端点检测
xn=1;
for n=1:fn
switch status
case {0,1} % 0 = 静音, 1 = 可能开始
if amp(n) > amp1 % 确信进入语音段
x1(xn) = max(n-count(xn)-1,1);
status = 2;
silence(xn) = 0;
count(xn) = count(xn) + 1;
elseif amp(n) > amp2 || zcr(n) > zcr2 % 可能处于语音段
status = 1;
count(xn) = count(xn) + 1;
else % 静音状态
status = 0;
count(xn) = 0;
x1(xn)=0;
x2(xn)=0;
end
case 2 % 2 = 语音段
if amp(n) > amp2 && zcr(n) > zcr2 % 保持在语音段
count(xn) = count(xn) + 1;
silence(xn) = 0;
else % 语音将结束
silence(xn) = silence(xn)+1;
if silence(xn) < maxsilence % 静音还不够长,语音尚未结束
count(xn) = count(xn) + 1;
elseif count(xn) < minlen % 语音长度太短,认为是静音或噪声
status = 0;
silence(xn) = 0;
count(xn) = 0;
else % 语音结束
status = 3;
x2(xn)=x1(xn)+count(xn);
end
end
case 3 % 语音结束,为下一个语音准备
status = 0;
xn=xn+1;
count(xn) = 0;
silence(xn)=0;
x1(xn)=0;
x2(xn)=0;
end
end
el=length(x1);
if x1(el)==0, el=el-1; end % 获得x1的实际长度
if x2(el)==0 % 如果x2最后一个值为0,对它设置为fn
fprintf('Error: Not find endding point!\n');
x2(el)=fn;
end
SF=zeros(1,fn); % 按x1和x2,对SF和NF赋值
NF=ones(1,fn);
for i=1 : el
SF(x1(i):x2(i))=1;
NF(x1(i):x2(i))=0;
end
speechIndex=find(SF==1); % 计算voiceseg
voiceseg=findSegment(speechIndex);
vsl=length(voiceseg);
zc2.m:
function zcr=zc2(y,fn)
if size(y,2)~=fn, y=y'; end
wlen=size(y,1); % 设置帧长
zcr=zeros(1,fn); % 初始化
delta=0.01; % 设置一个很小的阈值
for i=1:fn
yn=y(:,i); % 取来一帧
for k=1 : wlen % 中心截幅处理
if yn(k)>=delta
ym(k)=yn(k)-delta;
elseif yn(k)<-delta
ym(k)=yn(k)+delta;
else
ym(k)=0;
end
end
zcr(i)=sum(ym(1:end-1).*ym(2:end)<0); % 取得处理后的一帧数据寻找过零率
end
findSegment.m:
function soundSegment=findSegment(express)
if express(1)==0
voicedIndex=find(express); % 寻找express中为1的位置
else
voicedIndex=express;
end
soundSegment = [];
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;
end
end
soundSegment(k).end = voicedIndex(end); % 最后一组有话段的结束位置
% 计算每组有话段的长度
for i=1 :k
soundSegment(i).duration=soundSegment(i).end-soundSegment(i).begin+1;
end