http://zheart.egloos.com/tb/2697324 <---출처
Endpoint Detection (끝점검출)
음성검출이란 ?
말 그대로 음성이 존재하는 부분을 찾는 것 이다.
그럼, 실시간 음성 검출이란 ?
음 성이 입력되는 순간부터 실시간에 음성을 잡아내고 음성이 종료되는 것까지 실시간에 처리하는 것을 의미한다. 연속음 인식에서는 음성 검출이 많이 중요하지는 않지만 고립단어나 연결단어 인식에서는 상당히 중요하다. 특히, DTW를 사용하는 화자 종속 인식기에서는 그 성능의 상당 부분을 이 음성 검출부가 좌우 한다고 알려져 있다.
일반적으로 음성 검출은 영어로 endpoint detection 즉, 끝점 검출이라는 용어로 사용된다.음성의 시작과 끝을 찾는다는 뜻이다. 그럼, 이게 왜 어려울까? 사람의 음성 중 유성음과 같이 에너지도 크고 주기적인 신호는 잡음과 잘 구분되지만 파열음이나 마찰음 같은 음성은 잡음과 비슷한 성질을 갖고 또한 에너지도 작아서 잘 구분하기 쉽지 안다. 특히, 주변 잡음의 레벨이 높으면 더더욱 큰 문제가됩니다. 잘 알려진 끝점 검출 알고리듬으로 Rabiner와 Sambur의 알고리듬이 있는데 이는 영교차율(ZCR; zero-crossing rate)과 구간 에너지 (frame energy)를 사용하는 방법으로 시간 영역에서 간단하고 빠르게 음성을 검출 할 수 있다. 그러나, 이 방법은 실시간 검출에는 부적합한 off-line 검출 알고리듬입니다. 따라서, 대부분의 경우 이를 실시간 검출에 맞게 변형해서 사용한다. 참고로 주파수 영역의 특징을 이용해서 검출하는 방법도 있고 이 방법이 잡음에 대해서 강인하다고 알려져 있지만 계산량도 많고 구현도 복잡해서 실제로는 잘 사용하지 않는다. 음성을 고정된 길이로 나누는 것을 보통 framing한다고 하고 그 고정된 길이 안의 샘플들을 하나의 frame 이라고 부른다. 또한, 일반적으로 resolution을 높이기 위해서 각 frame의 일정 샘플들은 중첩(overlap)이 된다. 예를 들어서, 16 kHz로 sampling된 음성의 경우 초당 16000 샘플이 얻어지는데 이 경우 한 frame은 보통 20-40 msec 정도의 샘플이 된다. 즉, 320-640 샘플 정도가 되는 것이다. 중첩은 frame 길이의 2/3, 1/2 정도 사용된다. frame = 320 샘플, 중첩 1/2 인 경우 160 샘플씩 중첩 시키면 된다. 이렇게 얻어진 각 frame에 대해서 ZCR과 에너지를 계산한다. ZCR은 말 그대로 그 frame내에서 부호가 바뀌는 (영점을 교차하는) 경우가 몇번이나 생기는가를 나타내고, 에너지는 각 샘플의 제곱의 합으로 나타낸다. 또한, 한 frame의 샘플 중 일부는 다음 frame과 겹치니깐 그 부분은 미리 계산된 ZCR과 에너지를 사용하면 계산을 더 줄일 수 있다.
그런데, ZCR의 경우 잡음에 상당히 민감하다. 따라서, 많은 경우 에너지를 사용하게 된다. 그래서, 대부분의 인식기에서 마이크 볼륨을 되도록 크게 해서 사용하길 권장한다. 또한, 대부분의 사운드 카드에는 dc bias가 존재한다. 즉, 아무런 음성이 없는 경우에도 신호의 레벨이 0이 아닌 어떤 값을 갖는다는 거다. 따라서, 그 값을미리 계산해서 제거해 주는 것이 필요하다. 또한, 경우에 따라서는 high-pass나 low-pass filter를 SW적으로 구현해 줘야할 필요도 있다.
음 성은 짧은 구간으로 나누어서 분석하고 인식에 사용한다고 했다. 예를 들어서, 초당 16000 샘플로 샘플링한다면 (Fs=16000)한 구간(frame)은 대략 N=320 샘플 (20 msec) 정도 잡으면 된다. 그리고, 음성이 변하는 구간을 잘 잡기 위해서 연속적으로 M=160샘플을 중첩해서 구간을 잡아 나가면 된다. 그럼, 1초 간의음성에서 얻을 수 있는 frame의 개수는 몇 개 일까 ?
---> 99 = Fs/M-1 맞나 ? 각각 계산하기
각 frame에서 p 차원의 특징을 추출한다면 1초의 음성은 99 개의 p차원 벡터열이 된다. 이게 인식에 사용되는 것이다.
마 찬가지로 음성의 검출도 frame단위의 계산에 의해서 수행될 수 있다. 이 경우의 frame 크기는 특징 추출시 보다 더 적을 수도 있지만 실시간 구현시 음성 검출과 특징 추출을 동시에 수행하려면 크기가 같은게 좋다. 이제 각 frame에서 energy와 ZCR을 계산한다. 그런데, 지난 번에 말했지만 대부분의 사운드 카드는 DC bias가 존재합니다. 따라서, 이 bias를 제거해야한다. 이건 처음 어느 정도 시간 동안 (1초-2초) 음성이 없다고 가정하고 채취한 샘플들의 평균을 계산해서 빼는 것으로 간단히 구현이 될 수 있다. 아니면 사전에 알고 있어도 된다. 이렇게 bias가 제거된 샘플들에 대해서 energy를 계산한다. 어떻게 하나면 각 샘플의 제곱을 모두 더하고 샘플수로 나누면 된다. 그런데, 위에서 한 절반 정도씩 중첩한다고 했었다. 그러니까, 한 frame의 절반에 대해서만 계산하고 나머지는 그 전에 계산한 걸 더하면 계산량이 줄어든다.
ZCR은 본래 샘플의 부호가 바뀌는 횟수를 세면 되는데 이게 잡음에 상당히 민감한다. 잡음이 상당한 경우 ZCR이 급격히 커질 수 있다. 따라서 평균적인 잡음의 레벨을 추정하고 이를 기준으로 ZCR을 계산한다. 무슨 얘기냐 하면 실제로 0을 지나는 샘플들을 세는 대신에 +- n 사이를 지나는 샘플의 수를 센다는 것이다. 당연히, n은 평균적인 잡음의 레벨이다. 그래도, 여전히 ZCR은 조금 불안한 기준이다. 차라리 마이크 볼륨을 키우고 목소리를 조금 크게 하는게 낮다. ZCR도 그 전 frame의 것을 절반 정도 사용할 수 있다.
각 frame에서 energy와 ZCR을 구한 경우 이를 이와 관련된 threshold 값들과 비교해서 음성이 개시되었는지 판정을 한다. 일단 음성이 개시된 걸로 판정되면 그 후 연속적으로 5-6 frame이 계속 음성으로 판정되는지 보고 음성으로 판정되면 음성이 개시되었다고 판정한다. 이때 처음 음성으로 판정된 frame의 몇 frame 앞에서 음성이 개시되었다고 판정하는 것이 안전할 수 있다. (마찰음이나 파열음 등 때문에...) 하여간 일단 음성이 시작되고 처음 음성이 아닌 것으로 판정된 frame이 나타나면 계속 저장을 하면서 그 후로 한 20 frame 내외에서 연속해서 5-6 frame이 음성이 아닌 것으로 판정되면 음성이 끝난 것으로 판정한다. 아니면 음성이 계속되고 있다고 보고 계속 끝점 검출을 반복한다. 마찬가지로 끝으로 판정되면 그 판정된 frame의 몇 frame뒤에서 끝난 것으로 간주할 수도 있다. 일단, 이렇게 음성이 검출되면 전체 frame 수를 보고 너무 짧으면 잡음으로 간주한다. 또한, 일정 수준 이상의 에너지를 갖는 frame들이 최소한 K 개 이산되지 않으면 역시 잡음으로 간주한다. 전체 평균적인 ZCR에 의해서도 이러한 판단을 할 수 있다. 이러한 후처리를 통해서 잡음과 음성을 잘 구별하는 것이 중요하다. 각각의 threshold 값이나 필요한 frame 수 등은 상당히 경험적으로 얻어질 수 밖에 없다. 또한, 사용하는 사운드 카드에 따라서도 달라진다.
적당한 low-pass나 high-pass filter가 없는 경우엔 이를 sw적으로 구현해 줘야 한다. 디지털 신호 처리 책을 참고하면 간단한 필터들을 구현할 수 있다.
음성 끝점 검출프로그램 소스
int CSoundIn::getendpoint(short *samples)
{
float tmp;
// zero-crossing 함수 호출
zcross(samples);
// 음성의 peak에너지 처리
if( peakreturn > maxpeak )
{
maxpeak = peakreturn;
if((tmp = maxpeak/endfact) > endthresh)
endthresh = tmp;
}
// 음성의 현 상태 구분
switch(epstate)
{
// 음성 입력이 처음 처리되는 부분 , 무음의 시작...
case 0:
ave += peakreturn;
// 음성의 단구간 에너지를 구한다.
if( ++scnt <= 3)
{
if(scnt == 1)
setnoise();
else
averagenoise();
// 잡음의 평균이 minstartsilence 보다 작으면 무음으로 간주
if( Endpointnoise < minstartsilence)
{
startsilenceok = 1 ;
ave += peakreturn;
avescnt++;
}
// 무음 구간으로 return
number = 0;
return number;
}
// 무음이 아니라면
if( startsilenceok==0)
{
epstate = 2;
// 음성의 시작으로 return
number =8;
return(number);
}
// 무음의 잡음 평균을 계속 구한다.
ave /= avescnt;
noise = ave;
begthresh = noise + begfact;
endthresh = begthresh;
mnbe = noise * energyfact;
// 무음의 계속
epstate = 1;
number = 2;
return(number);
// 계속적인 무음 인지 아닌지를 평가
case 1:
// 프레임의 평균
ave = (float)(((3.0 * ave) + peakreturn) / 4.0);
// 음성 신호가 들어 왔을 경우
if(peakreturn > begthresh || zero1 > zcthresh)
{
// 잠음 차감법 사용
energy += peakreturn - noise ;
// zero-crossing 이 zcthresh을 넘을 경우
if(zero1 > zcthresh)
zccnt++;
// 단구간 에너지가 begthresh을 넘을 경우
if(peakreturn > begthresh)
voicecount++;
// 음성의 시작...
if(++vcnt > minrise)
{
scnt =0;
epstate = 2;
}
// 음성이 있다고 return
number = 3;
return(number);
}
else // 계속적으로 무 음성이 있을 경우
{
energy = 0.0;
// 무음 시작의 잡음 평균이 계속적인 무음 구간보다 크다면 잡음 평균을 작게한다.
if( ave < noise)
{
// 시작과 끝을 다시 정의
noise = ave;
begthresh = noise + begfact;
endthresh = begthresh;
mnbe = noise * energyfact;
}
// 이전 프레임이 유성음 이었다면
if(vcnt > 0)
{
// 무성음이 없었다면
if(++bscnt > startblip || zccnt == vcnt)
{
// 새로 초기화 시킨다.
noise = ave;
begthresh = noise * begfact;
endthresh = begthresh;
mnbe = noise * energyfact;
vcnt =0;
zccnt =0;
bscnt =0;
voicecount =0;
number = 1;
return(number);
}
// 신호이 음성으로 return
number =3;
return(number);
}
// 무음으로 return
zccnt =0;
number = 2;
return(number);
}
// 음성의 시작
case 2:
// 유성음의 시작이라면
if(peakreturn > begthresh || zero1 > zcthresh)
{
// a++;
// 잡음 차감법 사용
energy += peakreturn - noise;
if(zero1 > zcthresh)
zccnt++;
if(peakreturn > begthresh)
voicecount++;
//이전 프레임이 유성음이었다.
vcnt += scnt +1;
// 다시 초기화
scnt =0;
// 유성음이라면
if(energy > mnbe || zccnt > minfriclng)
{
// 계속적인 신호 return
epstate = 3;
number =4;
return(number);
}
else
{
number= 3;
return(number);
}
}
else
if(++scnt > maxpause)
{
// 신호가 다시 낮아져서 음성 신호의 실패라면
vcnt = zccnt = voicecount = 0;
energy = 0.0;
epstate = 1;
ave=(float)(((3.0 * ave) + peakreturn) / 4.0);
if(ave < noise + begfact)
{ // noise보다 더 낮으면
noise = ave;
begthresh = noise + begfact;
endthresh = begthresh;
mnbe = noise * energyfact;
}
number=1;
return(number);
}
else
{
number=3;
return(number);
}
// 계속적인 음성 입력
case 3:
if(peakreturn > endthresh || zero1 > zcthresh)
{ // 여전히 신호 입력이라면
if(peakreturn > endthresh)
voicecount++;
vcnt++;
scnt =0;
number=3;
return(number);
}
else
{
//endthresh 에 속하면 , 끝일지도 모른다.
scnt++;
epstate = 4;
number = 5;
return(number);
}
// 음성의 끝 발언일 경우를 체크한다.
case 4:
// 신호가 끝이 아닐지도 모른다.
if(peakreturn > endthresh || zero1 > zcthresh)
{
if(peakreturn > endthresh)
voicecount++;
if(++Endvcnt > endblip)
{
// 음성신호가 다시 들어온다면
vcnt += scnt + 1;
Endvcnt =0;
scnt =0;
epstate = 3;
number = 7;
return(number);
}
else
{
number = 3;
return(number);
}
}
else
if(++scnt > maxipause)
{
// 침묵이 inter-word pause를 초과했다면
if(vcnt > minuttlng && voicecount > minvoicelng)
{
// 음성발언의 끝
number = 6;
return(number);
}
else
{
// 음성 신호가 너무 짧으면
scnt = vcnt = voicecount =0;
epstate = 1;
// 발성의 실패
number = 1;
return(number);
}
}
else
{
// inter-word pause 이라먄
if(peakreturn ==0)
{
// 음성의 끝
number = 6;
return(number);
}
Endvcnt =0;
// 여전히 신호가 있다고 가정
number = 3;
return(number);
}
}
number = 2;
return(number);
}
///////////////////////////////////////////////////////////////////
// zero-crossing 함수
//////////////////////////////////////////////////////////////////
void CSoundIn::zcross(short *samples1)
{
zero1 =0;
float trigger=0.0 , sum = 0.0 ;
short *smp,*smp1;
smp = samples1;
smp1 = samples1;
// 단구간 에너지를 구한다.
for(int xx1=0; xx1< FrameSize ; xx1++)
{
sum += (float)(*smp * *smp);
smp++;
}
peakreturn = (float)(sqrt(sum/FrameSize));
lastEndnoise[0] = peakreturn;
// 초기값
if( numcount ==0 )
Endpointnoise = peakreturn;
trigger = Endpointnoise * triggerfact;
for(int kki =0; kki< FrameSize; kki++)
{
if(low == 1) // down cross 를 찾는다.
{
if( *smp1 > trigger)
{
zero1++;
low = 0;
}
}
if(low == 0) // up cross를 찾는다.
{
if( *smp1 < -trigger)
{
zero1++;
low = 1;
}
}
smp1++;
}
}