1. 스트리밍 사운드

게임에서는 빼 놓을 수 없는 부분이 사운드 부분입니다. 일반적으로 효과음들은 크기가 작기 때문에 메모리에 올려놓은 후에 필요할 때마다 사용합니다만 대사나 배경음악은 용량이 크기 때문에 메모리에 올려놓으면 시스템에 부담이 될 수 있습니다. 그래서 이런 크기가 크고 동시 출력이 필요 없는 사운드등은 주로 스트리밍 방식으로 처리합니다.
스트리밍 방식은 의미 그대로 출력할 내용을 그때 그때 읽는 방식입니다. 여기서는 직접 스트리밍 사운드 처리하는 방식을 다루어 보겠습니다. (몇가지 저수준 API만 지원해준다면 큰 어려움없이 구현할 수 있습니다.)

2. 스트리밍 구현 원리

기본적으로 스트리밍을 이해하기 위해서

0-1-2-3-4-5-6-7

위와 같은 사운드 버퍼를 생성해서 무한 루프로 출력해보면 0, 1, 2, .. , 6, 7, 0, 1, ... 의 순서대로 출력됩니다. 마지막까지 출력된 후에 다시 처음부터 플레이 되는 식입니다. 이 점을 이용하면 아래와 같이 버퍼보다 더 큰 크기의 사운드를 연주할 수 있습니다.

0-1-2-3-4-5-6-7-8-9-A-B-C-D-E-F-G-H-I-J-K-L-M-N

만약 3이 연주되고 있다고 했을 때 0, 1, 2 에 각 8, 9, A 의 내용을 복사하면 해당 버퍼는 다음과 같이 됩니다.

8-9-A-3-4-5-6-7

만약 그 상태로 계속 연주한다고 가정하면 7 다음에 8, 9, A 가 연주될 것입니다. 바로 연속해서 다음 사운드가 연주 되는 것입니다.
이런 식으로 이미 출력된 부분에 대해서 다음에 연주되어야 할 내용을 계속해서 채워주게 되면 짧은 길이의 사운드 버퍼에서도 길이의 제한 없이 아주 큰 사운드를 출력할 수 있게 됩니다.

3. 스트리밍 장점/응용

사운드의 크기가 100메가 정도 되더라도 단 30k의 사운드 버퍼로도 동일하게 출력할 수 있게 됩니다.

- 작은 크기의 사운드 버퍼만 있으면 된다
- 별도의 메모리가 필요없다
- 파일은 출력되어야 할 일부분만 타이밍에 맞추어서 읽으면 되기 때문에 파일 접근에 대한 부담이 적다

직접 wav 에서 pcm 파일을 읽는 다면 위와 같은 장점이 있습니다. 앞서 배경음악 등을 출력할 때 생기는 파일 크기로 인한 메모리의 부담이 거의 사라짐을 볼 수 있습니다.

하지만 최고 퀄리티인 44khz 16비트 스테레오 방식의 pcm 은 초당 176 kb 정도를 읽어야 하기 때문에 초당 파일에 접근해야 하는 게 부담이 될 수도 있습니다. 이 경우에는 사운드의 퀄리티를 우선적으로 조절해야 하겠습니다만 ADPCM 이나 MP3 같은 압축 방식을 도입하면 큰 도움이 됩니다. 파일을 읽는 것보다는 CPU의 힘을 사용하는 것이 훨씬 효율적이기 때문입니다.
사운드 파일은 음원의 패턴이 일정하지 않기 때문에 무손실 압축으로는 거의 압축을 할 수 없으나, ADPCM이나 MP3는 음원을 느낄 수 없을 정도로만 손실하고 압축할 수 있기 때문에 많이 사용됩니다.
ADPCM은 음원의 변화량에 대한 특성을 이용해서 압축하는 방식으로 정확하게 1/4로 압축합니다. (셈플이 TIP란에 있습니다.) 알고리즘의 간단해서 많이 사용됩니다.
MP3는 DCT를 이용해서 주파수별로 분리해서 압축하는 방식으로 줄어든 크기에 비해 느끼는 손실을 미세할 정도로 높은 퀄리티를 보여줍니다. (이미지계의 JPG와 비교해 볼 수 있겠습니다.)

MOD 나 IT 같은 시퀀싱 모듈 음악파일도 위와 동일한 방식으로 접근할 수 있습니다. 음원을 디코딩해서 얻듯이 갱신될 부분에 대해서만 순서대로 음원을 시퀀싱해서 만들어내면 됩니다.
시퀀싱하고 믹싱하는 프로세스를 분할하기 때문에 전체 프로세스에 큰 영향을 주지 않고도 구현할 수 있고, 게임의 분위기에 따라 동적으로 여러 가지를 조절할 수 있기 때문에 언리얼 같은 게임에서도 사용되고 있습니다.

4. 구현

샘플 소스에 구현된 내용이 있으니 참고하시고 약간의 설명이 필요한 부분에 대해서만 다루어보겠습니다.

- 플레이 절대 위치 얻기

만약 버퍼의 크기가 10 이라고 하면 GetCurrentPos 같은 방법으로는 0 에서 9까지의 값밖에 얻을 수 없을 것입니다. 약간의 난점이 되는 것은 만약 1이라는 위치일 때 그 위치를 1로 봐야 하는 지 11로 봐야 하는 지에 대한 것입니다. 만약 체크할 때 마다 위치를 읽었을 때 나온 값이 아래와 같다고 가정하면

0, 4, 7, 8, 2, 3, 5, ...

직관적으로 8 과 2 사이에 사운드가 끝에서 처음으로 돌아간 것을 알 수 있습니다. (사운드는 절대로 뒤로 플레이 되지 않는 다고 가정할 수 있기 때문에) 이 경우는 바로 전에 저장해두었던 위치보다 현재의 위치 값이 더 작은 경우가 그런 경우라고 할 수 있습니다. 그므로 2의 위치에서는 위치가 10+2 즉, 12 라고 판단할 수 있습니다.
물론 8 과 2 사이에 끝에서 처음으로 가는 것이 두 번 생길 수도 있습니다만, 갱신을 자주해 준다고 가정하면 무시해도 상관없습니다. 실제로 이런 현상이 일어날 때는 새로 갱신되지 않은 사운드가 반복해서 들리게 되는 데, 동영상 스트리밍 서비스에서 대역폭이 못따라줄 때 이런 식의 사운드가 튀는 현상이 발생합니다. 이런 현상은 사운드 버퍼가 작을 수록 많이 일어납니다. 만약 사운드 버퍼가 1초정도 된다면 적어도 갱신 루틴이 호출되는 간격은 1초보다 작아야 합니다.

- 루프

반복은 처리가 비교적 간단한 데, 파일의 끝부분까지 복사를 했으면 파일 포인터를 다시 처음 위치로 옮겨서 다시 처음부터 처리를 하면 됩니다. 이 경우 복사한 위치 값이 다시 0보다 작아지면 출력된 절대 위치랑 비교가 힘들어져서 셈플에서는 계속 누적시키고 대신 m_startpos 란 변수를 이용해서 실제 파일의 위치를 계산하도록 처리했습니다.

- 1회 재생

오히려 1 회 재생하는 것이 반복보다는 난이도가 있습니다. 만약 파일에서 읽어와 버퍼에 적은 마지막 위치가 3이고 할 때 정확하게 3까지만 연주하게 만들기가 힘들기 때문입니다. (물론 플렛폼에서 "사운드의 오프셋이 n  이면 멈춰라" 라는 식이 가능하면 됩니다만, 일반적인 환경에서는 기대할 수 없는 기능입니다.)
이 때는 3 이후에 갱신되는 것에 한해서는 무음으로 처리하고 갱신할 때 위치를 읽어서 3을 지나쳤을 때 - 무음이 연주되고 있을 때 - 사운드를 멈추면 됩니다.

그 밖에는 몇 가지 설정할 것이 있는 데 아래와 같은 기준으로 설정해주면 됩니다

- 사운드 버퍼의 크기

위에 말 한대로 사운드 버퍼가 클 수록 사운드가 튈 확률이 적습니다. 만약 게임 중에 과도학 외부 프로세스로 인해 일정시간 프로세스를 받지 못한다고 하더라도 사운드 버퍼가 적당하다면 끊기지 않게 플레이 할 수 있습니다. 무한정 크다고 더 좋은 것은 아니므로 상황에 맞는 적절한 크기로 설정해 주면 됩니다.

- 갱신 프로세스 호출 빈도

매초 100 만큼씩의 일을 한다고 하면 약 1/10 간격으로 호출하면 평균적으로 한번에 10만큼씩의 일을 할 것이고 1/5 간격으로 호출하면 20만큼의 일을 하게 될 것입니다. 1초에 5번을 호출하건 10번을 호출하건 일정한 간격으로 호출하건 아니건 간에 타이밍만 놓치지 않는 다면 정확하게 플레이 될 것입니다. 다만 호출 간격이 작게 되면 한번에 할 일이 많아지게 되기 때문에 게임 중 프레임을 놓친다거나 하는 일이 생길 수 있습니다. (만약 1초에 한번씩 호출한다면 게임이 1초에 한번씩 버벅 거릴 가능성이 있습니다.)
1초에 100번 호출한다고 해서 10번 호출하는 것보다 딱히 좋을 것이 없기 때문에 - 오히려 위치를 얻는 다거나 하는 일을 더 많이 해서 미세하게 더 많은 일을 하게 됩니다. - 자주 호출하되 부담이 되지 않는 적당한 범위에서 결정하면 됩니다.

- 쓰레드 사용

갱신 루틴을 프레임에 넣을 경우에 게임 중에는 전혀 문제가 없지만 만약 프레임안에 로딩 루틴이 있어서 몇 초간의 딜레이가 생긴다면 그 동안은 사운드가 튈 수 있습니다.
쓰레드를 사용하면 이 문제를 쉽게 해결할 수 있지만, 공유로 인한 여러 가지 문제에 대한 처리를 반드시 해 두어야 합니다. (가장 빈번한 상황은 오브젝트는 삭제하는 순간에, 쓰레드 루틴에서는 그 오브젝트의 펑션을 실행하고 있거나 실행될 예정인 상황인데 아주 운 좋은 시스템이 아니면 반드시 문제를 일으킬 것입니다.)
다만 쓰레드 사용으로 인해 생기는 문제에 대한 여러 가지를 고려해야 합니다. 단순히 스트리밍 오브젝트간에만 관련된 것은 아니라 파일 억세스나 하드웨어 디바이스 접근에 따른 여러 가지 문제를 고려해야 합니다. 파일은 한 파일로 관리한다고 했을 경우 로딩 중에 쓰레드에 의해 파일 포인터의 위치가 바뀐다면 로딩 루틴이 무효화될 수 있습니다. 그리고 특정 하드웨어에서 lock 해서 동시에 접근하는 것이 불가능하다고 하면 그 시스템에서는 게임이 제대로 실행되지 않을 것입니다. 특히나 PC 처럼 다양한 하드웨어가 조합되는 경우에는 처리에 신중을 기해야 합니다.
(그래서 개인적으로는 쓰레드를 사용하는 것 보다는 게임 프레임에 붙여서 일정프레임마다 호출해주는 방식을 더 선호합니다. - 로딩시에는 음악이 안 나오도록 게임 디자인 파트와 합의를 봐야 겠죠. ^^)

5. 샘플코드


간단하게 pcm 형태의 wav 파일을 출력하는 루틴입니다. 다이렉트 사운드와 윈도우 기본의 waveout 인터페이스를 통해 출력하는 루틴을 포함해봤습니다.
waveout은 기본 윈도우 API로 한 채널만 운영이 가능하기 때문에 게임에서는 사용되지 않습니다. 다만 일반적인 방식으로 접근 하는 것을 구현해보고자 작성한 것입니다. (이런 식의 인퍼테이스로 접근가능한 환경이라면, 콘솔이나, 플렛폼에 상관없이 같은 방식으로 접근 가능합니다.)

스트리밍 처리에서 중요한 것은 주기적으로 다음의 출력 내용을 갱신해 주는 것일 텐데, 샘플에는 쓰레드를 사용한 것과 사용하지 않은 것에 대한 두 가지 코드를 모두 작성해 놓았습니다. (쓰레드는 멀티 쓰레드모드인 경우에만 사용됩니다.) _MT 키워드를 사용했으므로 프로젝트 세팅에 따라 적당히 바뀝니다.
역시나 쓰레드를 사용하게 되면 공유를 신경 안 쓰면 프로그램이 의문사 해버리는 일이 발생하기 때문에, 조잡하지만 약간 처리를 해 놓았습니다. (오히려 사운드 처리보다 더 복잡합니다.)

샘플에선 루틴을 간단하게 가기 위해서 일반 PCM 방식의 wav 파일만 구현해 놓았습니다. (WAV의 ADPCM 방식 디코딩은 이전에 적은 글의 코드를 참고하세요.)


댓글을 달아 주세요

  1. hold ya head 2007/10/18 03:58  댓글주소  수정/삭제  댓글쓰기

    중대하고 유용한 위치!

  2. game hentai xxx 2007/10/18 05:54  댓글주소  수정/삭제  댓글쓰기

    좋은 위치! 너를 감사하십시요.

  3. alicelove 2007/11/08 09:28  댓글주소  수정/삭제  댓글쓰기

    좋은 위치는 그것 찾아본 즐겼다!