이 글은 조만간 출판될 Game Programming Gems 3 한국어판(류광 역, 정보문화사)의 일부입니다. 이 글에 대한 모든 권리는 정보문화사가 가지고 있으며, 사전 허락 없이는 웹 사이트 게시를 비롯한 어떠한 형태의 재배포도 금지되어 있습니다.

이 글은 최종 교정 전의 상태이므로 오타나 오역이 있을 수 있습니다. 또한 웹 페이지의 한계 상, 실제로 종이에 인쇄된 형태와는 다를 수 있습니다. 실제 책에서는 표나 수식이 좀 더 정확한 형태로 표시될 것이며 그림/도표 안의 영문도 적절히 한글화될 것입니다.

Game Programming Gem 3 한국어판에 대한 정보는 GPG 스터디(http://www.gpgstudy.com/)에서 얻을 수 있습니다.

6.1 Ogg Vorbis를 이용한 오디오 압축

Jack Moffitt, Xiph.org Foundation
jack_at_xiph.org

컴퓨터가 소리를 낼 수 있게 된 이후, 오디오는 어떤 형태로든 게임의 일부가 되어 왔다. 초창기에는 삑삑거리는 소리가 전부였으나, 이후 MIDI가 도입되면서 작은 용량에 많은 음악적 정보를 담는 것이 가능해졌다. 사운드 카드가 널리 보급됨에 따라, 샘플링된 사운드로 시퀀싱을 할 수 있으며 약간의 효과도 가미할 수 있는 MOD 류의 파일 형식들이 인기를 끌게 되었다. CD-ROM이 게임의 표준 매체로 등장하면서, 레드북 오디오(일반 CD 오디오)가 대중화되었으며, 그 결과 게임에도 고품질의 음악이 쓰이게 되었다.

그러나 음질이 향상됨에 따라 음악 파일의 크기도 커졌다. CD에는 한 시간 가량의 고품질 오디오를 담을 수 있으나, 게임 CD에는 오디오뿐만 아니라 게임 코드와 데이터도 들어가야 한다. 상용 게임들은 음질을 유지하면서도 음악 파일의 크기를 줄이기 위해 ADPCM 같은 압축 표준을 이용한다. 그러나 ADPCM으로 줄일 수 있는 용량은 그리 크지 않다. 요즘 게임에서는 고품질의 음악이 필수적으로 요구되나, 항상 극단까지 밀어붙이고자 노력하는 게임 개발자들에게 있어 현재의 음악 파일은 아직 너무나 크다.

여기서 우리는 음향인지학적 압축에 주목하게 된다.

음향인지학적 압축

음향인지학적 압축(psychoacoustic compression, 音響認知學-)은 사람의 귀가 듣지 못하거나 중요하지 않다고 느낄 만한 부분을 제거함으로써 음성 데이터를 압축하는 기법이다. 다른 형태의 음성 압축 기법들과 달리, 음향인지학적 압축은 유손실 압축이다. 원래의 신호는 비가역적으로 변경되나, 대부분의 사람들은 그 차이를 느끼지 못한다.

음향인지학적 압축의 압축률은 대략 10:1에서 20:1 정도이다. 이는 다른 음성 압축 기술들에 비해 훨씬 나은 수준이다.

작동 방식

음향인지학에 관련된 몇 가지 기본 원칙들을 살펴보자. 우선, 청각의 절대 임계치(absolute threshold of hearing)라는 것이 있다. 그림 6.1.1의 그래프가 청각의 절대 임계치를 표현한 것이다.

그림 6.1.1 청각의 절대 임계치. x 축은 주파수, y 축은 음량이다.

청각의 절대 임계치는 주어진 주파수 범위에서 하나의 음이 얼마나 커야 사람의 귀에 인식될 수 있는지를 나타낸다. 그래프에서, 곡선 아래의 음은 사람의 귀에 들리지 않는다. 곡선 아래의 모든 음들을 원래의 음에서 제거해도 결과로 생긴 음은 원래의 음과 동일하게 들린다.

또 다른 기본적인 개념은 마스킹(masking)이다. 배경의 밝은 광원이 전경의 물체들을 가리듯이, 커다란 음은 더 작은 음을 가리게 된다. 강한 음색은 약한 음색의 높은 주파수대와 낮은 주파수대를 가린다. 그리고 강한 음색이 사라져도 여전히 음의 일부가 가려지는데, 이는 시간적인 마스킹 때문이다. 음향인지학적 압축은 그러한 가려진 음들도 제거하며, 임계치에서와 마찬가지로 사람의 귀는 그 차이를 인식하지 못한다.

다른 음성 압축 기법들과의 비교

음향인지학적 압축의 기본적인 작동 방식을 살펴보았다. 그럼 다른 형태의 오디오 압축 기법들과 음향인지학적 압축을 비교해보자. 오디오는 보통 가공되지 않은 PCM 표본(sample)들의 형태로 전송된다. 이 데이터는 압축되지 않은 것이며, 따라서 스테레오 16비트 표본 하나는 4 바이트를 차지한다. CD 음질의 오디오를 위해서는 매 초마다 44,100개의 표본들이 필요하며, 이는 1분에 약 10MB의 데이터를 의미한다.

오디오 데이터의 크기를 줄일 때 흔히 다운믹싱이나 리샘플링을 사용한다. 다운믹싱이란 스테레오 채널을 모노 채널로 줄이는 것으로, 크기는 1/2로 줄어든다. 리샘플링은 초 당 표본 수를 줄이는 것이다. 일반적으로 쓰이는 리샘플링 비율은 22,050Hz나 11,025Hz이다. 두 방법 모두 파일 크기는 크게 줄지만, 스테레오를 포기해야 하며 음질도 많이 나빠진다.

ADPCM은 이전 표본과의 차이를 인코딩하는 형식으로, 압축률은 4:1이다. 다운믹싱이나 리샘플링에 비해 음질 저하가 낮기 때문에 게임에서도 종종 쓰인다. 그러나 ADPCM 역시 유손실 압축이며, 음질 저하를 피할 수 없다. 파일 크기도 1/4만 줄일 수 있으므로 한 시간의 음을 저장하려면 여전히 200MB 정도의 용량이 필요하다.

무손실 압축 기법들도 존재한다. 그런 기법들에서는 음질 저하가 나타나지 않으나, 대신 압축률이 별로 좋지 않다. 일반적인 무손실 압축들은 대략 1.5:1에서 2:1 사이의 압축률을 제공한다. 속도가 느리다는 점뿐만 아니라, 무엇보다도 압축률이 낮다는 점 때문에 게임에 사용하기에는 별로 적합하지 않다.

Ogg Vorbis의 장점

여기까지 읽은 독자라면 게임 음악에 음향인지학적 압축을 사용하는 것이 당연하다고 느낄 것이다. 음향인지학적 압축은 높은 압축률과 낮은 음질 저하라는 두 가지 요구를 모두 만족한다. 그런데 음향인지학적 압축을 제공하는 코덱들은 여러 가지가 있다. 그렇다면 어떤 코덱을 사용할 것인가?

Ogg Vorbis[OggVorbis02]는 게임이나 기타 응용에 완벽하게 적합하다는 점에서 다른 음향인지학 코덱들보다 우월하다. 우선 Ogg Vorbis는 로열티가 없으므로 다른 코덱들보다 비용이 훨씬 싸다. 또한 오픈소스 형태의 구현들이 존재하므로 개발자가 자신의 요구에 맞게 개선 또는 수정할 수 있다. 게다가 Ogg Vorbis는 다양한 플랫폼들을 지원한다. Windows, Macintosh(클래식 및 OS X), 리눅스, BeOS, 그리고 콘솔 플랫폼들에서도 동일한 코드를 사용할 수 있다.

이러한 장점들 이외에도, Ogg는 게임 개발자에게 특히 매력적인 몇 가지 독특한 특징들을 가지고 있다. Ogg는 오디오 데이터와 다른 종류의 정보를 합치는 기능을 지원한다. 오디오 장치의 동기적인 제어를 위해 CD 음질의 오디오와 MIDI 데이터를 섞는 기능이 이미 구현되어 있다. 또한 다중 채널을 지원하는데, 이는 배경음 안에서 다른 음을 인코딩 및 재생하는 것이 가능하다는 뜻이다. 그보다 더 거창한 일도 가능할 것이다.

예를 들어서, Ogg를 사용하면 입의 움직임을 압축된 음성과 동기화시킴으로써 캐릭터가 말을 하거나 노래를 부르는 장면에 좀 더 사실적인 느낌을 부여할 수 있다. 다른 압축 형식들에서는 이런 기능이 직접 지원되지 않으며, 구현하는 것이 더 힘들다.

게임 개발자가 사운드와 오디오를 자유자재로 다룰 수 있으려면 단지 압축률이 좋은 것만으로는 부족하며, 유연한 API가 제공되어야 한다. 이 부분에서도 다른 코덱들에 비해 Ogg Vorbis가 훨씬 더 뛰어나다.

압축 시나리오

이제 Ogg Vorbis를 사용하기로 했다고 치고, 압축된 음악을 게임에서 사용하는 몇 가지 유형들(속도 상의 요구와 오디오의 종류에 따른)을 살펴보도록 하겠다. 여기서는 Ogg Vorbis를 염두에 두고 이야기하지만, 여기에 나온 개념들을 다른 코덱들에 적용하는 것도 가능할 것이다.

게임에서 어떠한 코덱을 사용할 때 고려할 것들이 몇 가지 있다. 대부분의 게임들에서는 속도가 매우 중요하며, 따라서 오디오를 디코딩하는 데 필요한 프로세서 시간이 중요한 요인이 된다. Pentium III 급 프로세서를 장착한 데스크탑 컴퓨터들에서, 음향인지학적으로 압축된 오디오를 디코딩하는 데 걸리는 시간은 총 프로세서 시간의 약 2%에서 5% 정도이다.

그리고 디코딩되는 사운드의 종류도 중요하다. 예를 들어서, 배경 음악은 일반적으로 단 하나의 스트레오 트랙으로 구성되며, 한 시점에서는 여러 트랙들 중 하나만 재생된다. 좀 더 복잡한 음악이라면 여러 트랙들을 겹칠 수도 있는데, 그런 경우 여러 파일들을 동시에 디코딩해야 한다. 음향 효과들은 일반적으로 짧은 사운드이며 게임의 이벤트와 연결된다. 따라서 이벤트가 발생한 시간과 사운드가 재생되는 시간 사이의 지연이 매우 중요하다. 또한 많은 음향 효과들이 동시에 재생된다는 점도 염두에 둬야 한다.

이러한 것들을 염두에 두고, 압축된 오디오를 게임에 효과적으로 통합하는 몇 가지 방법들을 간략히 살펴보자.

시나리오 1: 실시간 디코딩

게임에서 Ogg를 사용하는 가장 일반적인 방식은 오디오를 즉석에서 디코딩하는 것이다. 이렇게 하면 CPU 시간을 조금 더 소비하게 되나, 대신 파일 전체를 메모리에 모두 담아둘 필요가 없다(그냥 하드 디스크에서 조금씩 읽어 오면 된다).

이 시나리오에서는 하나의 오디오 스트림이 Ogg 형식으로 완전히 인코딩되고 게임이 실행되는 도중 실시간적으로 디코딩 및 재생된다. 재생 전에 파일 전체를 디코딩할 필요가 없으며, 또 디코딩된 커다란 파일을 플레이어의 하드 드라이브에 저장해 놓을 필요도 없다. 또한 플레이어가 자신이 좋아하는 음악을 게임에 추가하는 것도 가능해진다.

시나리오 2: 캐시로 디코딩

상황에 따라서는 캐시에 디코딩해 두는 것이 더 나을 수 있다. 여러 오디오 조각들을 동시에 재생해야 한다면, 그것들 모두를 실시간적으로 디코딩하는 것이 불가능하거나 너무 많은 프로세서 시간을 소비할 수 있다. 이런 경우 레벨 로딩이나 게임 시작 시에 파일들을 캐시에 디코딩해 두는 것이 낫다. 이렇게 하면 게임 플레이에 즉시 필요하지 않은 모든 오디오 데이터를 그냥 압축된 채로 놔두고 필요한 것들만 미리 디코딩해 둘 수 있다.

이는 또한 음향 효과(자주 재생되는 짧은 사운드들)를 효과적으로 재생하는 방법이기도 하다. 하나의 사운드가 처음 쓰일 때에는 즉석에서 디코딩하고, 일단 디코딩된 것을 캐시에 저장해 뒀다가 다시 사용하면 되는 것이다. 실시간 디코딩에 걸리는 시간이 문제된다면 앞에서 말한 것처럼 레벨 로딩이나 게임 시작 시에 미리 다 디코딩해둘 수도 있다.

시나리오 3: 압축된 전송

위의 두 시나리오 모두 부적합한 경우라도 압축이 무용지물이 되는 것은 아니다. 압축을 단지 저장 및 전송의 용도로만 사용한다고 해도 이득을 얻을 수 있다. 게임 배포 CD나 웹에서 다운받는 패키지 등에서 Ogg를 사용하면 CD 용량이나 다운로드 시간을 크게 줄일 수 있으며, 절약된 공간에 게임 코드나 그래픽을 더 많이 집어넣을 수 있게 된다. 게임이나 데모를 설치할 때, 모든 음악을 디코딩해서 하드 드라이브에 저장해둔다. 이 시나리오는 게임 패치나 추가적인 패키지 등에도 적용된다.

Ogg를 사용하는 코드 예제들

그럼 ogg Vorbis를 사용하는 것이 얼마나 쉬운지 알 수 있는 몇 가지 코드 예제들을 제시해 보겠다. vorbisfile 라이브러리를 사용하려면 다음과 같이 코드에 API 헤더들을 포함시켜야 한다.

 #include <vorbis/vorbisfile.h>

그리고 vorbisfile, vorbis, ogg 라이브러리들을 링크해야 한다. Windows의 경우, 개발자의 취향에 따라 이 라이브러리들을 동적으로 링크할 수도 있고 정적으로 링크할 수도 있다(자세한 내용은 CD-ROM에 있는 SDK 파일들과 API 레퍼런스를 참고할 것).

파일을 메모리에 풀기

vorbisfile API에서 파일들을 메모리에 풀어 넣는 것은 매우 쉽다. 우선 다음과 같이 OggVorbis_File 변수를 선언한다.

 OggVorbis_File vf;

그런 다음 이미 열린 FILE*로 Ogg Vorbis 파일을 열어야 한다. 이 때 반드시 이진(binary) 모드로 파일을 열어야 한다. 일부 플랫폼들에서는 fopen() 등의 함수를 호출할 때 기본적으로 이진 모드가 설정되지만, Windows에서는 그렇지 않으므로 이진 모드를 명시적으로 지정해 주어야 한다.

 int err;
 err = ov_open(fp, &vf, NULL, 0);

fp는 이미 열린 파일에 대한 FILE*이다. vf는 OggVorbis_File 구조체이며, 나머지 두 매개변수들은 위의 예처럼 그냥 NULL과 0을 지정하는 것이 일반적이다(두 매개변수들의 구체적인 의미에 대해서는 API 레퍼런스를 참고할).

ov_open은 파일 열기가 성공하면 0, 그렇지 않으면 0보다 작은 값을 돌려준다. 실패의 경우 반환값은 API의 표준 오류 코드들 중 하나를 의미한다(각 오류 코드에 대해서는 역시 API 레퍼런스를 참고할 것).

vorbisfile 라이브러리로 Ogg 파일을 열었다면, 그 파일은 라이브러리가 소유하게 된다. 작업이 끝나면 라이브러리가 파일을 닫는다. 그 파일 포인터로 외부적인 파일 작업들을 수행해서는 안 된다. 일단 파일을 연 후에는 다음과 같이 파일 정보를 읽어야 한다.

 vorbis_info *vi;
 vi = ov_info(&vf, -1);

vorbis_info 구조체 변수에는 샘플링 비율, 명목 상의 비트율, 채널 개수 등 주어진 Ogg 파일에 관한 정보가 들어간다. 두 번째 매개변수는 정보를 얻고자 하는 논리적인 비트스트림을 지정한다(-1은 현재의 논리적 비트스트림을 가리킨다). 필요하다면 ov_comment() 함수로 파일에 대한 주석을 읽을 수도 있다.

Ogg 파일을 디코딩해서 메모리에 넣으려면 디코딩된 데이터를 담을 메모리부터 할당해 두어야 한다. ov_pcm_total()로 PCM 표본들의 전체 개수를 얻고 그것에 채널 개수와 표본 당 바이트 수(16 비트 오디오의 경우 보통 2 바이트)를 곱하면 필요한 메모리 크기가 나온다.

 int size;
 char *buffer;
 size = vi->channels * 2 * ov_pcm_total(&vf, -1);
 buffer = (char *)malloc(size);

디코딩된 출력을 담을 버퍼를 할당했으면, 루프를 돌려서 ov_read()로 PCM 표본들을 메모리 버퍼에 집어넣으면 된다.

 int eof = 0;
 char *buf = buffer;
 int current_section;
 while (!eof) {
    long ret = ov_read(&vf, buf, 1024, 0, 2, 1,
               &current_section);
    if (ret == 0) {
        /* 0이면 파일의 끝에 도달한 것임 */
        eof = 1;
    } else if (ret < 0) {
        /* 0보다 작으면 오류가 발생한 것임 */
    } else {
        /* 읽기 성공. 버퍼 포인터를 전진시킨다. */
        buf += ret;
    }
 }

모든 복잡한 작업들은 vorbisfile 라이브러리에서 일어나므로 코드가 매우 간단하다. 오류 점검이나(Ogg를 다룰 때 대부분의 오류들은 치명적이지 않으며 무시할 수 있다) 오버플로우 점검은 생략했다.

일단 파일을 다 읽었으면, ov_clear()로 마무리해야 한다.

 ov_clear(&vf);

실시간 디코딩

실시간 디코딩은 파일 전체를 메모리 버퍼에 모두 읽어들이는 대신 한 번에 한 조각씩 읽어들이면서 디코딩한다는 점만 빼고는 위의 코드와 매우 비슷하다. ov_read()로 한 블럭을 읽고 그것을 오디오 처리 파이프라인의 다음 단계로 보내면 된다. 대부분의 경우 다음 단계는 오디오 출력 장치가 될 것이다.

간결함을 위해서 시스템에 국한된 오디오 출력 부분은 다루지 않겠다. 대신 표본들의 버퍼와 버퍼 크기를 받아서 적절히 출력하는 audio_output()이라는 함수가 존재한다고 가정한다.

전반적인 구조는 이전 예제 코드와 거의 동일하다. 루프의 각 반복에서 가장 먼저 하는 일은 앞에서와 마찬가지로 ov_read()를 호출하는 것이다.

 char pcmout[4096];
 while (!eof) {
    long ret = ov_read(&vf, pcmout, sizeof(pcmout),
                        0, 2, 1, &current_section);
    /* ... */
 }

이번에는 버퍼의 크기가 고정되어 있는데, 왜냐하면 코드는 오직 작은 블럭 단위로 출력을 처리하기 때문이다.

앞의 예제에서는 읽기 오류나 파일의 끝이 아닌 경우 버퍼 포인터를 전진시켰다. 이번에는 실시간적인 디코딩을 위해 한 번에 한 블럭씩 처리하는 것이므로, 현재 블럭을 audio_out()에 전달하기만 하면 된다.

 audio_out(pcmout, ret);

호출 성공 시 ret에는 버퍼에 들어간 바이트들의 개수가 설정된다.

인코딩

마지막으로, 게임 개발 과정에서 오디오 데이터를 인지음향학적 압축 기법으로 인코딩하는 측면을 살펴보자. 압축된 Ogg 파일들을 만들 때 선택 및 조정할 수 있는 요인들이 몇 가지 존재하며, 더 나은 음악과 음향 효과를 위해서는 그런 요인들을 잘 알아둘 필요가 있다.

대부분의 코덱들에서는 ‘크기’를 변경하면 암묵적으로 음질이 변하게 된다. Ogg의 경우에는 ‘음질’을 조정함으로써 크기가 변한다. 일반적으로 음질과 크기는 비례한다. 즉 파일 크기가 클수록 음질이 좋고, 반대로 음질을 낮출수록 크기가 줄어든다. 그러나 음질을 높이기 위해 크기를 너무 크게 하거나 크기를 줄이기 위해 음질을 너무 낮추는 것은 현실적으로 바람직하지 못하다.

압축된 오디오의 크기는 비트율(bit rate)로 측정된다. 비트율은 1초의 음을 저장하는 데 필요한 비트들의 개수이다. 일반적인 게이머는 미세한 음질 저하를 인식하지 못할 것이므로 비트율을 필요 이상으로 높게 잡는 것은 별로 좋지 못한 일이다. Ogg는 비트율을 64kbps(초 당 킬로비트) 정도로 잡아도 음질이 나쁘지 않으며, 원본(보통 128 kbps) 수준과 비교해도 거의 떨어지지 않는다.

대부분의 게임에서, 게이머가 주의를 가장 집중하는 부분은 음악이 아니다. 따라서 귀에 거슬릴 정도만 아니라면 비트율을 충분히 낮게 잡아도 큰 문제는 없을 것이다.

채널 수와 오디오의 샘플링 비율 역시 인코딩에서 중요한 요인들이다. 이들은 음악의 품질에 영향을 미치나(예를 들어 모노보다는 사운드가 듣기 좋다), 크기의 변화율에 대한 음질의 변화율이 선형적인 것은 아니다. 스테레오를 모노로 만들어서 크기를 반으로 줄였을 때, 대부분의 경우 음악의 품질은 아주 약간만 떨어지게 된다. 마찬가지로, 샘플링 비율을 반으로 줄여도 음질(귀로 인식하는) 자체가 반으로 떨어지지는 않을 수도 있다. 저장 공간을 줄이는 것이 매우 중요하다면 Ogg로 압축하기 전에 다운믹스와 리샘플링을 가하는 것도 나쁘지 않다. 음질의 손실보다는 저장 공간의 절약에 의한 이득이 더 클 것이다.

결론

게임 오디오의 중요성이 커짐에 따라, 오디오 데이터의 크기를 줄이기 위해 더 나은 형태의 압축을 사용하는 것에 대한 관심도 높아질 것이다. 많은 게임들은 빡빡한 예산 하에서 개발되므로, 오픈 소스와 로얄티 없는 솔루션의 매력을 무시할 수는 없을 것이다. 독자의 다음 프로젝트에서는 Ogg Vorbis를 이용한 음향인지학적 압축을 고려해 보기 바란다.

참고자료