소프트웨어 오디오 코덱 low-level document
1. 개요
1.1 오디오 부분의 동작
본 화상회의 애플리케이션에서 오디오 부분의 절차는 다음과 같다.
1) 오디오 출력 디바이스(Audio Output Device)를 연다.
세션(Session)에 참가하기 전에 이미 세션에 참가중인 다른 사람들의 음성 데이터를 받아 이를 디코딩하기 위해 오디오 출력 디바이스(Audio Output Device)가 열려있어야 한다.
2) 오디오 입력 디바이스(Audio Input Device)를 연다.
자신이 마이크로폰으로 말하는 음성을 인코딩하기 위한 오디오 입력 디바이스(Audio Input Device)가 열려있어야 할 것이다.
3) 세션에 참가
세션에서 음성의 송신과 수신을 위해 할당된 멀티캐스트 주소와 포트로 데이터그램(datagram) 소켓을 바인드(Bind)해 놓는다.
4) 디코딩
1-3의 초기화 과정을 마치면 그 소켓을 통해 참가한 세션에서 현재 말하고 있는 사람의 음성 데이터를 받게 되고, 이를 오디오 출력 디바이스를 통해 디코딩하여 음성을 듣게 된다.
5) 인코딩
1-3의 초기화 과정을
마친후 세션 제어에 의해 자신에게 발언권이 주어지게 되면 이제 내가 마이크로폰으로 말하는 음성이 오디오 입력 디바이스를 통해
샘플링 되고 이 음성 데이터를 위에서 말한 음성을 위한 소켓을 통해 네트워크로 전송한다.
이러한 절차들에 대해선 2절 오디오 코덱 구현에서 자세히 다루도록 한다.
1.2 오디오 데이터 형식 :
본 애플리케이션에는 사용하는 오디오 데이터 형식으로 8-bit PCM 방식을 사용하며 이 형식의 특징은 다음과 같다.
1) 초당 8000번을 샘플링
2) 각 샘플을 8bit로 나타냄
3) 이 형식을 사용하는 데이터는 0에서 255(0xFF)사이의 값을 갖게 된다.
4)
64Kbps 이상의 네트워크 대역폭(Bandwidth)을 요구 (초당 8000번을 샘플링하고, 한 샘플을 한 byte로
나타내므로 초당 8kbyte의 오디오 데이터가 생성된다고 할 수 있다. 따라서 초당 8kbyte, 즉 64kbit를 전송해야
하므로 헤더 부분을 고려하 면 64Kbps 이상의 네트워크 대역폭(Bandwidth)을 사용한다고 할 수 있다.)
1.3 오디오 데이터 교환 방식
본 애플리케이션에는 세션의 참가자의 수에 따라 오디오 데이터의 교환 방식이 full-duplex 방식에서 half-duplex 방식으로, 또는 역으로 전환된다.
1) Full-duplex 방식 : 2명만이 세션에 있게 될 때는 서로의 음성을 full-duplex방식으로 서로 주고받을 수 있다.
2) Half-duplex 방식 : 세션에 3명 이상이 있게 될 때는 세션 제어에 의해 한 사람에게만 발언권이 주어지는 Half-duplex 방식으로 전환된다.
1.4 오디오 서비스를 제공하는 window 95의 멀티미디어 API
Windows 95는 본 화상회의 애플리케이션에서 다루어야 하는 파형
오디오(Waveform Audio)를 사용하기 위한 여러 다양한 기능들을 포함한 멀티미디어와 관련된 기능들을 멀티미디어 API로
제공한다. Windows 95의 멀티미디어 API에서는 오디오 서비스의 여러 가지 타입과 레벨들을 제공한다. 오디오 서비스의
다른 타입들은 각기 다른 포맷과 다른 기술을 필요로 하는데, 제공하는 오디오 서비스의 타입들에는 파형(Waveform) 오디오
서비스, MIDI 오디오 서비스, 컴팩트 디스크 오디오 서비스가 있다. 본 화상회의 애플리케이션에서 다루는 파형 오디오 서비스는
디지털 오디오 하드웨어를 위한 디코딩과 인코딩을 지원한다. 오디오 서비스의 레벨에는 하이-레벨과 로우-레벨이 있다. 하이-레벨
오디오 서비스는 프로그래밍하기가 편하다는 장점이 있고, 로우-레벨 오디오 서비스는 오디오 디바이스 드라이버를 직접 억세스 하므로
복잡한 프로그래밍을 요구하지만 오디오 디코딩과 인코딩에 대해 미세한 제어까지 할 수 있는 장점이 있다. 오디오 서비스를 제공하는
window 95의 멀티미디어 API를 이용하는 애플리케이션의 구조는 <그림 2-1>과 같다.
2. 오디오 코덱 구현
2.1 구현 환경과 요구 사항
Windows 95 상에서 Visual C++ 4.0과 MFC, 그리고
로우-레벨(Low Level) 오디오 API를 이용하여 구현하였으며 팬트엄 166 이상의 프로세서를 가진 PC에서만
테스트해보았다. 요구 사항으로는 8-bit PCM 샘플링과 full-duplex를 지원하는 사운드 카드를 요구한다.
2.2 Low-Level 오디오 API를 이용하여 오디오를 인코딩/디코딩하는 절차
로우-레벨(Low Level) 오디오 API는 애플리케이션으로 하여금 직접 오디오
디바이스 드라이버와 통하게 함으로써 로우-레벨 오디오 서비스를 제공한다. 로우-레벨(Low Level) 오디오 API를 이용하여
파형 오디오(Waveform Audio)를 인코딩/디코딩하기 위한 절차를 간단히 기술하면 다음과 같다.
1) 원하는 오디오 데이터 형식이 사용가능한지를 아닌지를 검사한다.
2) 사용이 가능하다면 원하는 오디오 입출력 디바이스(Audio Input/Output Device)를 연다.
3)
디바이스로부터 파형 오디오(Waveform Audio) 샘플 데이터를 넘겨주고 받기 위해서 하나 이상의 데이터 버퍼 블록을
할당(Allocation)한다. 할당한 데이터 버퍼 블록은 처리를 기다리는 디바이스 큐(queue)로 보내져야 한다.
4) 디바이스가 데이터 버퍼 블록을 처리하면, 블록은 다른 처리(디코딩, 또는 전송) 또는 재사용을 위해서 애플리케이션에게 리턴 된다.
2.3 구현 내용
가. 초기화
1) 원하는 오디오 데이터 형식이 사용가능한지를 테스트
본 애플리케이션에서 사용하는 오디오 데이터 형식인 8-bit PCM 방식이 audio
device를 통해 사용가능한지를 waveInOpen, waveOutOpen에 다음과 같은 parameter들로 호출하여
리턴되는 결과 값으로 알아낸다. (waveInOpen, waveOutOpen 대한 자세한 설명은 Visual C++의 Help를
참조) 관련된 코드는 다음과 같다.
< decl.h >
#define BLKS_PER_READ 4
#define READ_AHEAD 8
#define BLKS_PER_WRITE 4
#define WRITE_AHEAD 4
// 8-bit PCM format을 나타내는 WAVEFORMATEX 구조체
static const WAVEFORMATEX lin8fmt = {
WAVE_FORMAT_PCM,
1, // 1 is mono, 2 is streo
8000,
8000,
1,
8,
0
};
< audioview.cpp의 audioview() 함수안에 있는 부분 >
int sts;
// 먼저 input audio device에 대해 알아본다.
sts = waveInOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0, WAVE_FORMAT_QUERY);
if ( sts ) // 지원되지 않음을 알린다.
AfxMessageBox("not support 8bit 8kHz PCM audio input");
else { // 지원되면 audio input device에서 사용되어질 버퍼를 new를 써 // 서 Heap memory에 할당한다.
iformat_ = &lin8fmt; // input format을 8-bit PCM으로 지정
iblen_ = len;
len *= READ_AHEAD;
ibufStart_ = new u_char[len]; // input buffer start를 지정
ibufEnd_ = ibufStart_ + len; // input buffer end를 지정
tmp = new u_char[len];
}
// 다음 output audio device에 대해 알아본다.
sts = waveOutOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0, WAVE_FORMAT_QUERY);
if ( sts ) // 지원되지 않음을 알린다.
AfxMessageBox("not support 8bit 8kHz PCM audio output");
else { // 지원되면 audio output device에서 사용되어질 버퍼를 new를 써 // 서 Heap memory에 할당한다.
oformat_ = &lin8fmt; // output format을 8-bit PCM으로 지정
len = blksize * BLKS_PER_WRITE;
oblen_ = len;
len *= WRITE_AHEAD;
obufStart_ = new u_char[len]; // output buffer start를 지정
obufEnd_ = obufStart_ + len; // output buffer end를 지정
}
2) 파형 오디오 입출력 디바이스 열기
오디오 디바이스를 여는 함수(waveInOpen, waveOutOpen)들은 디바이스
식별자, 메모리 위치에 대한 포인터, 각 디바이스 타입에 독특한 파라미터들을 지정한다. 지정된 메모리 위치에는 디바이스 핸들
값이 들어가게 된다. 다른 로우-레벨 오디오 API들(waveInStart, waveInStop, waveOutWrite 등)을
호출할 때 열려진 오디오 디바이스를 식별하기 위해서 이 디바이스 핸들을 사용하여야 한다. ( 자세한 설명은 Visual C++의
Help를 참조)
디코딩을 위해서는 waveOutOpen함수를 사용하여 파형 오디오 출력 디바이스를
열고, 인코딩을 위해서는 waveInOpen함수를 사용하여 파형 오디오 입력 디바이스를 연다. 이 함수들은 파라미터로 주어지는
디바이스 식별자와 관계된 디바이스를 열고 그 디바이스에 대한 핸들을 파라미터로 지정된 메모리 위치에 써놓음으로써 리턴 한다.
관련된 코드는 다음과 같다. 파형 오디오 입력 디바이스를 여는 부분은 CAudioView::OpenIn() 함수에 있으며 파형 오디오 출력 디바이스를 여는 부분은 CAudioView::OpenOut() 함수에 있다.
// audio input device를 8-bit PCM format으로 open
void CAudioView::OpenIn()
{
// error 가 났으면 어떤 error인지를 알아낸다.
if ( error == MMSYSERR_ALLOCATED)
AfxMessageBox("Specified resource is already allocated.");
else if ( error == MMSYSERR_BADDEVICEID)
AfxMessageBox("Specified device identifier is out of range.");
else if ( error == MMSYSERR_NODRIVER)
AfxMessageBox("No device driver is present.");
else if ( error == MMSYSERR_NOMEM)
AfxMessageBox("Unable to allocate or lock memory.");
else if ( error == WAVERR_BADFORMAT)
AfxMessageBox("Attempted to open with an unsupported waveform-audio format.");
........................................
// 오디오 입력 장치를 여는데 성공한 후에는 오디오 입력 장치를 통해 인코딩 될
// 데이터가 저장될 버퍼를 ( 지원한지를 검사한 후 위에서 미리 할당된)
// waveInPrepareHeader를 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있
// 게끔 준비하고 waveInAddBuffer를 이용해 버퍼를 디바이스 드라이버에 보낸다.
// waveInStart로 인코딩을 시작하기 전에는 반드시 위와 같은 방식대로 드라이버에
// 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게 된다.
// 위의 일을 하는 코드가 이 부분에 위치하며 이는 뒤의 파형 오디오 데이터 인코
// 딩 부분에서 설명된다.
........................................
return;
}
// audio output device를 8-bit PCM format으로 open
void CAudioView::OpenOut()
{
int error = 0; // waveOutOpen()의 return 값을 저장if (out_ == 0) // in_ : decl.h안에 HWAVEIN in_로 선언되어 있다.
error = waveOutOpen(&out_, WAVE_MAPPER, iformat_,(DWORD)(VOID*)waveInProc, (long)GetSafeHwnd(),CALLBACK_FUNCTION);
........................................// waveOutWrite함수에 보낼 파형 오디오 데이터 블록을 waveOutPrepareHeader함
// 수를 사용해 준비한다. 이러한 일과 관계된 코드가 이 부분에 위치하며 이는 뒤
// 의 파형 오디오 데이터 디코딩 부분에서 설명된다.
........................................
return ;
}
3) 파형 오디오 입출력 데이터 타입
파형 오디오 입출력 함수들을 위해서 Windows 95의 멀티미디어 API에서 정의한
파형 오디오 데이터 타입들 중에서 본 애플리케이션에서 이용한 것들은 <표 2-1>과 같다. (자세한 사항은
VIsual C++의 Help를 참고)
<표 2- 1> 본 애플리케이션 구현에서 사용한 파형 오디오 데이터 타입
타입 |
설명 |
HWAVEOUT |
열려진 파형 오디오 출력 디바이스에 대한 핸들 |
HWAVEIN |
열려진 파형 오디오 입력 디바이스에 대한 핸들 |
WAVEHDR |
파형 오디오 입력 또는 출력 데이터 블록에 대한 헤더로 쓰이는 구조체 |
WAVEFORMATEX |
특정한 파형 오디오 입력 또는 출력 장치에 의해 지원되는 데이터 포맷을 명시하는 구조체 |
본 애플리케이션의 구현에서 사용되는 파형 오디오 데이터 타입들로 선언된 변수들의 정의에 관련된 코드는 다음과 같으며 decl.h에 정의되어 있다.
< decl.h >
const WAVEFORMATEX* iformat_; // input audio format
const WAVEFORMATEX* oformat_; // output audio format
HWAVEOUT out_; // 오디오 출력 장치에 대한 핸들
HWAVEIN in_; // 오디오 입력 장치에 대한 핸들
WAVEHDR iwhdr_[READ_AHEAD];
// 오디오 입력 데이터 블록에 대한 헤더에 대한 정의
WAVEHDR owhdr_[BLKS_PER_WRITE * WRITE_AHEAD];
// 오디오 출력 데이터 블록에 대한 헤더에 대한 정의
4) 파형 오디오 데이터 포맷 지정
waveOutOpen 또는 waveInOpen을 사용해 오디오 입출력 디바이스를 열 때
사용할 파형 오디오 데이터 포맷을 담은 WAVEFORMATEX 구조체에 대한 포인터를 파라미터로 전달한다.
WAVEFORMATEX 구조체의 정의는 <표 2-2>와 같다. (자세한 사항은 VIsual C++의 Help를 참고)
<표 2- 2> WAVEFORMATEX 구조체
멤버 변수 |
설명 |
wFormatTag |
파형 오디오 형식. PCM이면 WAVE_FORMAT_PCM |
nChannels |
파형 오디오 데이터의 채널의 개수. 1은 모노, 2는 스테레오를 의미한다. |
nSamplesPerSec |
샘플링율(Sampling rate). 파형 오디오 형식이 PCM이면 이 값은 8.0kHz, 11.025kHz, 22.05kHz, 44.1kHz 중의 하나여야 한다 |
nAvgBytesPerSec |
요구되는 average data-transfer rate. 파형 오디오 형식이 PCM이면 이 값은 샘플링율과 block alignment의 곱과 같아야 한다. |
nBlockAlign |
block alignment. 파형 오디오 형식이 PCM이면 채널의 개수와 샘플당 비트 수를 8(bits per byte)로 나눈 값의 곱과 같아야 한다. |
wBitsPerSample |
Bits per sample. 파형 오디오 형식이 PCM이면 이 값은 8(모노 일 때) 또는 16(스테레오 일 때)이어야 한다. |
cbSize |
WAVEFORMATEX 구조체의 끝에 덧붙여질 추가 형식 정보의 크기 |
본
화상회의 애플리케이션에서는 음성의 샘플링(Sampling) 방식으로 PCM을 사용하였고, 초당 8000번을 샘플링하고, 각
샘플을 8비트로 나타내는 8-bit PCM 방식의 파형 오디오 데이터 형식을 사용하므로 본 애플리케이션에서
WAVEFORMATEX 구조체의 멤버 변수의 값은 <표 2-3>과 같다.
<표 2- 3> 본 애플리케이션에서 WAVEFORMATEX 구조체의 멤버 변수의 값
멤버 변수 |
사용한 값 |
wFormatTag |
WAVE_FORMAT_PCM |
nChannels |
1(모노) |
SamplesPerSec |
8000(8kHz의 샘플링) |
nAvgBytesPerSec |
8000 |
nBlockAlign |
1 |
wBitsPerSample |
8(한 샘플을 8bit로 표현) |
cbSize |
0(추가 정보 없음) |
나. 파형 오디오 데이터 인코딩과 디코딩
1) 파형 오디오 데이터 디코딩
파형 오디오 출력 디바이스를 열었으면 이제 waveOutWrite함수를 사용하여 음성
데이터 블록을 오디오 출력 디바이스로 보내면 오디오 출력 디바이스는 이를 디코딩한다. waveOutWrite함수에 보낼 파형
오디오 데이터 블록을 지정하기 위해서는 WAVEHDR 구조체를 사용한다. 이 구조체는 데이터 블록에 대한 포인터와 데이터 블록의
크기와 몇 가지의 플래그를 포함한다. ( 자세한 설명은 Visual C++의 Help를 참조) 이 데이터 블록은 사용 전에 파형
오디오 출력 디바이스가 사용 할 수 있게끔 waveOutPrepareHeader함수를 사용해 준비되어져야 한다.
waveOutPrepareHeader함수를 사용하여 미리 할당해 둔 데이터 블록을
오디오 출력 장치가 사용할 수 있도록 준비하는 코드는 아래와 같으며 이 부분은 CAudioView::OpenOut() 함수 안에
위치하며 오디오 출력 장치를 성공적으로 연 경우에만 수행된다.
// 파형 오디오 출력 디바이스를 열었으면 이제 waveOutWrite함수를 사용하여
// 음성 데이터 블록을 오디오 출력 디바이스로 보내면 오디오 출력 디바이스는
// 이를 디코딩한다. waveOutWrite함수에 보낼 파형 오디오 데이터
// 블록을 지정하기 위해서는 WAVEHDR 구조체를 사용한다. 이 구조체는
// 데이터 블록에 대한 포인터와 데이터 블록의 크기와 몇 가지의 플래그를 포
// 함한다. 이 데이터 블록은 사용 전에 파형 오디오 출력 디바이스가 사용 할
// 수 있게끔 waveOutPrepareHeader함수를 사용해 준비되어져야 한다.
if ( error == MMSYSERR_NOERROR ) {
}
데이터 블록을 waveOutWrite함수를 사용해 오디오 출력 디바이스에 보낸 후, 디바이스
드라이버가 그 데이터 블록에 대한 처리를 끝내고 나면 이 데이터 블록을 해제시킬 수 있다. WAVEHDR 구조의 멤버 변수인
lpData는 파형 오디오 데이터 샘플들에 대한 포인터이다.
본 애플리케이션의 구현에서는 오디오 데이터 블록의 디코딩은 세션에 참가 할 때 음성을
위해 바인드해놓은 소켓으로 데이터를 받을 때마다 수행한다. 더 자세한 설명은 뒤의 오디오 데이터의 전송 부분에서 다룬다.
디코딩을 마친 데이터 블록을 특별히 해제시키지 않고 이후의 받은 데이터에 의해 덮어 쓰여지도록 하였다. 좀 더 자세한 설명은
파형 오디오 데이터의 수신에서 언급한다. 오디오 데이터 블록의 디코딩에 해당하는 부분은 소켓으로 데이터를 받을 때마다 호출되는
ProcessPendingRead()에 있으며 다음과 같다.
void CAudioView::ProcessPendingRead()
{
WAVEHDR* cp = &owhdr_[(i + 1) * BLKS_PER_WRITE - 1];
// 현재 네트웍으로부터 받은 오디오 데이터 블록을 waveOutWrite함수를 사용
// 하여 디코딩하기 위해서는 WAVEHDR 구조체를 사용하여 그 오디오 데이터
// 블록을 지정해서 waveOutWrite함수의 파라미터로 사용해야 하므로 여기서
// OpenIn()에서 waveOutPrepareHeader함수를 사용해 오디오 출력 장치가 사
// 용할 수 있도록 준비해 둔 WAVEHDR 구조체 타입의 변수 array인 owhdr중
// 의 하나를 지정한다.
i = ( i + 1 ) % WRITE_AHEAD ;
// 다음 오디오 데어터 블록을 위해서는 owhdr array의 index를 하나 증가한다.int nBytes = m_pSocket->ReceiveFrom(Revpkt, PDU_SIZE, (CString&)m_strRevIP, (UINT&) m_Revport);
if ( nBytes == -1 ) {
// error를 처리하는 루틴
}
// 현재 받은 packet의 id 필드가 현재 발언권을 얻은 사람의 id와 같은지를
// 검사한다. 또한 자신의 id와 같은지를 검사한다. 발언권을 얻지 않은 사람의
// packet이거나 자신이 보낸 packet의 경우에는 디코딩을 하지 않는다.
if ( (strcmp(Revpkt->id , m_CurrentSayingID ) != 0) || ( strcmp(Revpkt->id , m_ID) == 0) )
return;// packet의 구조체의 정의는 decl.h에 다음과 같이 정의되어 있다.
// typedef struct {
// char id[9];
// u_char data[8192];
// } Packet;
// 그러므로 순수한 오디오 데이터의 size는 packet size에서 id의 size 9를
// 뺀 값이므로 이 값을 오디어 데이터 블록을 지정하는 WAVEHDR의 변수인
// cp의 dwBufferLength에 넣는다.
cp->dwBufferLength = nBytes - 9;
// packet의 data를 오디어 데이터 블록을 지정하는 WAVEHDR의 변수인 cp의
// lpData에 copy한다.
memcpy(cp->lpData ,Revpkt->data ,cp->dwBufferLength);
// ---> data copying problem : protocol overhead
cp->dwFlags = WHDR_DONE ;
// 네트웍으로부터 받은 packet의 오디오 데이터를 위와 같이 WAVEHDR의 변
// 수인 cp로 변환한 후 waveOutWrite를 사용해 디코딩을 한다.
waveOutWrite(out_, cp, sizeof(*cp));
}
2) 파형 오디오 디코딩을 관리하기 위한 Windows Message의 사용
파형 오디오 디코딩을 관리하도록 window procedure 함수 또는 애플리케이션에
의해 제공되는 파형 오디오 출력 장치에 대한 콜백 함수에는 <표 2-4>에서와 같은 메시지들이 올 수 있다. (괄호
안은 콜백 함수에게 전달되는 메시지임.)
<표 2- 4> 파형 오디오 디코딩 관리를 위한 Window Message들
message |
설명 |
MM_WOM_CLOSE
(WOM_CLOSE) |
waveOutClose함수를 사용하여 디바이스를 닫았을 때 |
MM_WOM_DONW
(WOM_DONE) |
디바이스 드라이버가 waveOutWrite함수를 사용하여 보내진 데이터 블록에 대한 처리를 마쳤을 때 |
MM_WOM_OPEN
(WOM_OPEN) |
waveOutOpen함수를 사용하여 디바이스를 열었을 때 |
이
메시지들에 대한 wParam은 항상 열려진 파형 오디오 디바이스에 대한 핸들 값이다. MM_WOM_DONE(WOM_DONE)의
경우에는 이 메시지의 파라미터로 전달받는 lParam의 값은 디코딩이 끝난 데이터 블록에 대한 WAVEHDR 구조체에 대한
포인터 값이다. 이를 가지고 디코딩이 끝난 데이터 블록을 해제(free)시킬 수 있다.
본 구현에서는 파형 오디오의 디코딩 관리를 특별히 하지 않고 디코딩이 끝난 데이터 블록은 이후의 소켓을 통해 받은 데이터로 overwrite되도록 하였다.
3) 파형 오디오 인코딩
파형 오디오 입력 디바이스를 연후에는 파형 오디오 데이터를 인코딩 할 수 있다. 파형
오디오 데이터는 애플리케이션에서 WAVEHDR 구조체로 지정해 놓은 버퍼에 인코딩 되어진다. 이 데이터 블록들은 미리
waveInPrepareHeader를 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있게끔 준비되어져 있어야 한다.
멀티미디어 API에서 파형 오디오 녹음(waveform-audio recording)을 처리하기 위해 제공하는 함수들은 <표 2-5>와 같다.
<표 2- 5> 파형 오디오 인코딩을 처리하기 위해 제공하는 함수들
함수 |
설명 |
waveInAddBuffer |
디바이스 드라이버에 버퍼를 보낸다. 그러면 이 버퍼에는 디바이스 드라이버에 의해 인코딩된 파형 오디오 데이터가 채워질 수 있게 된다. |
waveInStart |
파형 오디오 인코딩을 시작한다. |
waveInStop |
파형 오디오 인코딩을 멈춘다. |
waveInAddBuffer
를 통해 디바이스 드라이버에 보내진 버퍼가 인코딩된 파형 오디오 데이터로 채워지면 애플리케이션은 콜백 메시지를 받아서 이를 알게
된다. waveInStart로 인코딩을 시작하기 전에는 반드시 드라이버에 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게
된다.
waveInPrepareHeader함수를 사용하여 미리 할당해 둔 데이터 블록을 오디오 출력
장치가 사용할 수 있도록 준비하는 코드와 waveInAddBuffer를 사용해 버퍼를 디바이스 드라이버에 보내는 코드는 아래와
같으며 이 부분은 CAudioView::OpenIn() 함수 안에 위치하며 오디오 입력 장치를 성공적으로 연 경우에만 수행된다.
// waveform audio data는 애플리케이션에서 WAVEHDR 구조체로 지정해 놓은
// 버퍼에 인코딩 되어진다. 이 데이터 블록들은 미리 waveInPrepareHeader를
// 사용하여 파형 오디오 입력 디바이스가 사용 할 수 있게끔 준비되어져 있어
// 야 한다. 다음은 waveInAddBuffer를 이용해 버퍼를 디바이스 드라이버에 버
// 낸다. waveInStart로 인코딩을 시작하기 전에는 반드시 위와 같은 방식대로
// 드라이버에 버퍼를 보내야 한다. 그렇지 않으면 데이터를 잃어버리게 된다.
/* (re-)initialize the input buffer descriptors */
memset(iwhdr_, 0, sizeof(iwhdr_));
// iwhdr_ : decl.h안에 WAVEHDR iwhdr_[READ_AHEAD]로 선언된어 있다.
ibindx_ = 0;
rbuf_ = rbufEnd_;
u_char* bp = ibufStart_;
u_int len = iblen_;
memset(bp, 0, len * READ_AHEAD);
// 사용되는 버퍼를 모두 초기화한다.
for (int i = 0; i < READ_AHEAD; ++i) {
WAVEHDR* whp = &iwhdr_[i];
whp->dwFlags = 0;
whp->dwBufferLength = len;
whp->lpData = (char*)bp;
bp += len;
waveInPrepareHeader(in_, whp, sizeof(*whp));
if(MMSYSERR_NOERROR!=waveInAddBuffer(in_,whp,sizeof(*whp))) {
AfxMessageBox("waveInAddBuffer error");
return ;
}
} // end of for loop
4) 파형 오디오 인코딩을 관리하기 위한 Windows Message의 사용
파형 오디오 인코딩을 관리하도록 window procedure 함수 또는 애플리케이션에
의해 제공되는 파형 오디오 출력 장치에 대한 콜백 함수에는 <표 2-4>에서와 같은 메시지들이 올 수 있다. (괄호
안은 콜백 함수에게 전달되는 메시지임. 자세한 사항은 Visual C++의 Help를 참조)
<표 2- 6> 파형 오디오 인코딩 관리를 위한 Window Message들
message |
설명 |
MM_WIM_CLOSE
(WIM_CLOSE) |
waveInClose함수를 사용하여 디바이스를 닫았을 때 |
MM_WIM_DATA
(WIM_DATA) |
디바이스 드라이버가 waveInAddBuffer함수를 사용하여 보내어진 데이터 블록을 인코딩된 오디오 데이터로 채웠을 때 |
MM_WIM_OPEN
(WIM_OPEN) |
waveInOpen함수를 사용하여 디바이스를 열었을 때 |
MM_WIM_DATA(WIM_DATA)
의 파라미터로 전달되는 lParam은 버퍼를 식별하는 WAVEHDR 구조체에 대한 포인터의 값을 가진다. 이 버퍼는 파형 오디오
데이터로 완전히 채워지지 않았을 수도 있다. 버퍼가 채워지기 전에 인코딩이 정지될 수도 있기 때문이다. 따라서 버퍼의 유용한
데이터의 양을 알아내기 위해 WAVEHDR 구조체의 멤버 변수인 dwBytesRecorded를 사용한다. lParam을 가지고
애플리케이션이 그 데이터 블록에 대한 사용(본 화상회의 애플리케이션의 경우 네트워크로 전송)을 끝마쳤을 때 그 데이터 블록을
해제할 수 있다.
본 애플리케이션의 구현에서 오디오 인코딩 관리를 위한 코드는 VOID CALLBACK waveInProc()에 있으며 다음과 같다.
VOID CALLBACK waveInProc(HWAVEIN hwi, UINT uMsg, long dwInstance, DWORD dwParam1, DWORD dwParam2 )
{
switch (uMsg)
{
case WIM_DATA:
// 디바이스 드라이버가 waveInAddBuffer함수를 사용하여 보내어진 데
// 이터 블록을 인코딩된 오디오 데이터로 채웠을 때
{
// MM_WIM_DATA(WIM_DATA)의 파라미터로 전달되는 lParam은 버퍼
// 를 식별하는 WAVEHDR 구조체에 대한 포인터의 값을 가진다.
WAVEHDR *whp = (LPWAVEHDR)dwParam1;
WAVEHDR* cp = &tmphdr_;// 이미 선언해 놓은 WAVEHDR 구조체에 대한 포인터인 tmphdr_로
// 인코딩된 오디오 데이터 블록에 대한 WAVEHDR을 가리키는
// dwParam1 ( WAVEHDR *whp = (LPWAVEHDR)dwParam1 ) 의 값
// 들을 copy한다.
cp->dwFlags = whp->dwFlags;
cp->dwBufferLength = whp->dwBufferLength ;
dwParam1 = (DWORD) cp;
memcpy(cp->lpData,whp->lpData , cp->dwBufferLength );
// copy가 끝난 WAVEHDR의 오디오 데이터 블록을 null로 만든다.
memset(whp->lpData, 0, whp->dwBufferLength);
// MY_WIM_DATA 메시지를 발생시킨다. MY_WIM_DATA 메시지를 처
// 리하는 루틴에서는 역시 파라미터로 오는 WAVEHDR의 오디오 데이
// 터 블록 부분을 네트웍으로 전송한다.
if ( PostMessage ((HWND) dwInstance , MY_WIM_DATA ,0 , dwParam1 ) == FALSE)
AfxMessageBox("PostMsg error");
// 사용이 끝난 WAVEHDR을 다시 waveInPrepareHeader을 사용해 오
// 디오 입력 장치가 사용할 수 있도록 준비한다.
waveInPrepareHeader(hwi, whp, sizeof(*whp));// 다음 waveInAddBuffer를 사용해 디바이스 드라이버에 해당 버퍼를
// 보낸다.
if(MMSYSERR_NOERROR!=waveInAddBuffer(hwi,whp,sizeof(*whp))) {
AfxMessageBox("waveInAddBuffer error");
return ;
}
break;
}
} // end of switch
}
다. 파형 오디오 데이터의 전송과 수신
1) 파형 오디오 데이터 블록의 크기
본 화상회의 애플리케이션에서는 WAVEHDR의 멤버 변수인 lpData가 가리키는
데이터 블록의 크기를 8Kbyte로 하였다. 본 화상회의 애플리케이션에서는 음성의 인코딩 방식으로 PCM을 사용하므로 1초당
8000번을 샘플링하고, 한 샘플을 1byte로 나타낸다. 따라서 한 데이터 블록의 크기를 8Kbyte로 하였으므로 한 데이터
블록은 1초 동안의 음성 데이터를 나타낸다. 또한 한 데이터 블록의 크기를 8kbyte이므로 애플리케이션은 데이터 블록에 오디오
데이터를 채웠다는 MM_WIM_DATA(WIM_DATA) 메시지를 운영 체제로부터 1초에 한번씩 받게 된다.
본 애플리케이션의 구현에서는 MM_WIM_DATA(WIM_DATA) 메시지를 처리하는
루틴에서 음성 데이터 블록을 음성의 전송을 위해 만들어둔 소켓을 통해 네트워크로 내보낸다. 애플리케이션은 1초마다 한번씩
MM_WIM_DATA(WIM_DATA) 메시지를 받을 것이고, 이때 8Kbyte의 음성 데이터를 전송하므로 1초의 네트워크
지연이 추가되어진다고 볼 수 있다. 물론 데이터 블록의 크기를 1Kbyte로 조정하면 추가되는 네트워크 지연을 1초에서
0.125초로 줄일 수 있지만, 테스트 해본 결과 이때는 너무 잦은 MM_WIM_DATA(WIM_DATA)의 처리에 대한
오버헤드와 받는 쪽에서도 음성 전송을 위한 소켓을 통해 데이터가 왔음을 알리는 window message의 잦은 처리에 대한
오버헤드로 시스템이 부하를 견디지 못하고 다운되는 현상이 종종 있었다. 이를 해결하고 시스템을 안정적으로 동작하게 만들기 위해서
데이터 블록의 크기를 8Kbyte로 정하여 사용하였다. 또한, 초당 8Kbyte, 즉 64kbit를 전송해야 하므로 헤더 부분을
고려하면 64Kbps 이상의 네트워크 대역폭을 사용한다고 할 수 있다
2) 파형 오디오 데이터의 전송
본 애플리케이션에서는 세션 제어에 의해서 자신이 발언권을 얻으면
CAudioView::TriggerPlay()를 호출함으로써 파형 오디오 데이터를 인코딩하기 시작한다. 발언권이 세션의 다른
참가자에게 주어지면 CAudioView::TriggerStop()를 호출함으로써 파형 오디오 데이터의 인코딩을 중단한다. 관련된
코드는 다음과 같다.
void CAudioView::TriggerPlay()
{
// PlayFlag는 현재 인코딩 중이면 TRUE, 아니면 FALSE로 setting한다.
if ( PlayFlag == FALSE ) {
waveInStart(in_);
PlayFlag = TRUE;
.......
}
}
void CAudioView::TriggerStop()
{
if ( PlayFlag == TRUE ) {
waveInStop(in_);
PlayFlag = FALSE;
.......
}
}
4) 파형 오디오 인코딩을 관리하기 위한 Windows Message의 사용에서 설명한데로 한
데이터 블록에 대한 처리가 끝났음을 애플리케이션에게 알리는 MM_WIM_DATA(WIM_DATA)를 받으면 애플리케이션에서
MY_WIM_DATA라고 정의해 놓은 메시지를 포스트하고 다시 이 메시지를 애플리케이션이 받으면 MY_WIM_DATA를 처리하는
프로시저로 메시지 맵핑(Message Mapping) 해놓은 OnWimData가 수행된다. OnWimData에서는 파라미터로
전달받은 lParam이 가리키는 WAVEHDR 구조체의 멤버 변수인 lpData가 가리키는 파형 오디오 데이터 블록과 다른
오디오 스트림과 구별되기 위한 식별자를 포함한 패킷을 구성하고 이를 음성 전송과 수신을 위한 소켓을 통해 네트워크로 보낸다.
<그림 2-2>
패킷 구조체의 정의는 다음과 같으며 decl.h에 선언되어 있다.
typedef struct {
char id[9];
u_char data[8192];//2048];
} Packet;
음성 데이터의 전송과 관련된 코드는 CAudioView::OnWimData() 함수안에 있으며 다음과 같다.
long CAudioView::OnWimData(UINT wParam , LONG lParam)
{
// 파라미터로 전달되는 lParam은 버퍼를 식별하는
// WAVEHDR 구조체에 대한 포인터의 값을 가진다.
WAVEHDR *whp = (LPWAVEHDR)lParam;struct sockaddr_in my_addr;
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(m_port);
my_addr.sin_addr.s_addr = inet_addr(m_strPeerIP);
// packet에 자신의 id를 집어 넣는다.
strncpy ( Sndpkt->id , m_ID , 9 );
// packet의 data 필드에 오디오 데이터 블록을 copy한다.
memcpy(Sndpkt->data , whp->lpData, whp->dwBufferLength);
// 사용이 끝난 WAVEHDR 구조체의 lpData를 null로 만든다.
memset(whp->lpData, 0, whp->dwBufferLength);
// packet을 전송한다.
m_pSocket->SendTo(Sndpkt, PDU_SIZE, (LPSOCKADDR)&my_addr, sizeof(my_addr));
return 0L;
}
3) 파형 오디오 데이터의 수신
음성 전송과 수신을 위한 소켓을 통해서 데이터를 받으면 일단 패킷 중에서 식별자 필드를
보고 이 패킷이 세션 제어에 의해서 발언권을 얻은 사람의 음성 데이터인지 아닌지를 판단한다. 판단한 결과 발언권을 얻은 사람의
것이면 이 데이터를 이미 선언해 놓은 WAVEHDR의 lpData로 복사하고 WAVEHDR의 다른 멤버 변수들의 값을 적절히
세팅한 후, waveOutWrite의 파라미터로 이 WAVEHDR을 넘겨주고 호출하면 받은 음성 데이터가 디코딩 되게된다.
<그림 2-2>
해당하는 부분의 코드는 소켓으로 데이터를 받을 때마다 호출되는 ProcessPendingRead()에 있으며 이는 1) 파형 오디오 데이터 디코딩 부분에 함께 나와 있다.