7장 Audio - ① FMOD를 활용한 게임 오디오 구축
오디오는 게임에서 중요한 역할을 차지한다. 게임 플레이 상황에 대한 오디오 신호를 제공한다든지 게임의 전반적인 분위기를 강화하는 역할 등 퀄리티 높은 사운드는 게임에 많은 생명력을 부여한다. 이번 장에서는 오디오 관련 강력한 기능을 제공하는 FMOD API를 활용하여 게임에서 다양한 오디오를 추가하는 방법을 다룬다.
💡 7장의 목차
- 오디오 구축하기
- 3D 위치 기반 오디오
- 믹싱 및 이펙트
오디오 구축하기
기본적으로 오디오 시스템은 WAV나 OGG 파일 같은 사운드 파일을 독립적으로 로드하고 재생한다. 하지만 게임은 상황에 따라 다양한 사운드 파일이 로드되고 재생될 필요가 있으며 때로는 동시에 여러 사운드가 실행되어야 한다.
또한 게임은 사운드 채널의 수 만큼 동시에 플레이 할 수 있는 사운드의 개수가 제한되기 때문에 사운드 별로 우선순위를 가져야 한다. 만약 사운드 별로 우선순위가 없다면, 한 화면에 수 많은 적이 등장하는 화면이라면 적의 발자국 소리만으로 사운드 채널을 꽉 채울 수도 있다. 그래서 특정 사운드는 적의 발자국 소리보다 더 중요하기 때문에 더 높은 우선순위를 가져야 한다.
그리고 사운드에서도 3D 효과가 필요하다. 각종 FPS게임에서는 적의 발자국 소리나 총 소리는 적을 탐지하는 중요한 요소이다. 현실 세계에서는 소리가 들리면 어느 방향에서 어느 정도 거리에서 발생한 소리인지 추측이 가능하다. 현실성을 추구하는 게임이라면 소리에 거리와 방향 심지어는 차폐현상과 도플러 효과등을 더할 수 있어야 한다.
FMOD
파이어라이트 테크놀로지스(Firelight Technologies)가 제작한 FMOD는 게임에서 인기 있는 사운드 엔진 중 하나이다. FMOD API는 로우레벨 API와 스튜디오 API 두 부분으로 구성된다.
- FMOD 로우레벨 API: 저수준에서 사운드를 로드하고 연주하며 채널을 관리한다. FMOD 스튜디오에서 생성한 이벤트는 사용할 수 없다.
- FMOD 스튜디오 API: FMOD 저수준 API에 기반을 두며 FMOD 스튜디오에서 생성한 이벤트들을 활용할 수 있다. (저수준 API를 포함하기 때문에 저수준 API도 접근 가능하여 여기에서는 스튜디오 API를 활용하여 오디오를 구현)
오디오 시스템 생성
FMOD 스튜디오 시스템과 저수준 시스템에 접근하기 위한 포인터 변수를 다음과 같이 선언할 수 있다.
AudioSystem.h의 선언 중
// ...(생략)
// FMOD 스튜디오 시스템
FMOD::Studio::System* mSystem;
// FMOD 저수준 시스템
FMOD::System* mLowLevelSystem;
// ...(생략)
선언한 포인터 변수에는 다음과 같이 FMOD 시스템 객체를 생성 및 초기화를 통해서 FMOD 객체들을 사용할 수 있다.
// FMOD 스튜디오 시스템의 인스턴스 생성
result = FMOD::Studio::System::create(&mSystem);
// result != FMOD_OK 일 경우 예외처리 생략
// FMOD 시스템의 initialize 함수 호출
result = mSystem->initialize(
512, // 동시에 출력할 수 있는 사운드의 최대 갯수
FMOD_STUDIO_INIT_NORMAL, // 기본 설정 사용
FMOD_INIT_NORMAL, // 기본 설정
nullptr // 대부분 nullptr
);
// result != FMOD_OK 일 경우 예외처리 생략
// 저수준의 시스템 포인터를 얻어와서 mLowLevelSyste에 저장한다.
result = mSystem->getLowLevelSystem(&mLowLevelSystem);
뱅크와 이벤트
- 뱅크(bank): 이벤트나 샘플 데이터, 그리고 스트리밍 데이터를 담고 있는 컨테이너. 사운드 디자이너는 FMOD 스튜디오에서 한 개 이상의 뱅크를 생성해야한다. 그리고 게임은 이 뱅크들을 런타임 시 로드한다. 로드 후에는 뱅크 내부에 포함된 이벤트의 접근이 가능해진다.
- 이벤트(event): 게임에서 재생하는 사운드에 해당한다. 이벤트는 여러개의 관련 사운드 파일과 파라미터, 이벤트 타이밍에 대한 정보 등을 가진다. 게임에서는 사운드 파일을 직접 재생하기 보다는 이 이벤트를 통해서 사운드 파일을 재생한다.
- 버스(bus): 사운드 이벤트의 그룹. 유형별로 사운드 이벤트를 그룹화 할 수 있고, 그룹별로 사운드 이벤트를 조작할 수도 있다.
- 샘플 데이터(sample data): 이벤트가 참조하는 원본 오디오 데이터. 이 데이터는 (WAV, OGC 파일과 같은) 사운드 파일에서 가져온다. 이벤트는 샘플 데이터가 메모리에 존재하기 전까지는 관련 사운드를 재생할 수 없다.
- 스트리밍 데이터(streaming data): 한 번에 작은 크기로 메모리로 스트림되는 샘플 데이터. 스트리밍 데이터를 사용하는 이벤트는 데이터를 미리 로드할 필요 없이 사운드 재생이 가능하다. 일반적으로 음악 및 대화 파일이 스트리밍 데이터로 사용된다.
FMOD의 이벤트와 관련된 클래스
EventDescription
class: 샘플 데이터, 볼륨 설정, 파라미터 등 이벤트와 관련된 정보를 포함한다.EventInstance
class: 이벤트의 활성화된 인스턴스. 이벤트를 재생한다.- 예를 들어 폭발 이벤트가 있다면 전역적으로 하나의 폭발 이벤트 정보 인스턴스 (
EventDescription
)를 가진다.EventInstance
는 활성화된 폭발 이벤트의 수 만큼 인스턴스를 가진다.
뱅크 로딩과 언로딩
뱅크 로딩을 구현하기 위해서는 두가지를 고려해야한다.
- 중복 로딩이 되지 않도록 한번 로딩된 뱅크는 해시맵으로 관리할 것.
- 뱅크를 로딩할 때 뱅크에 포함되어있는 이벤트와 버스를 모두 로딩할 것.
- 이벤트와 버스도 모두 해시맵으로 관리할 것.
뱅크를 언로딩 할 때는 반대로 뱅크에 포함되어 있는 이벤트와 버스를 모두 언로딩 한 후 해당 뱅크를 언로딩 해야한다.
뱅크 로딩, 언로딩 구현 코드 (AudioSystem.cpp)
이벤트 인스턴스 생성하고 재생하기
이벤트를 재생하기 위한 PlayEvent
함수는 다음과 같이 두 가지 과정을 핵심으로 가진다.
EventDescription
으로 부터EventInstance
를 생성한다.- 생성된
EventInstance
를start()
하면 재생된다. - 재생된
EventInstance
를release()
하면 이벤트가 종료할 때 자동으로 이벤트 소멸자 실행을 예약한다.
void AudioSystem::PlayEvent(const std::string& name)
{
// 이벤트가 존재하는지 확인
// mEvents는 EventDescription 포인터를 value로 갖는 해시맵 이다.
auto iter = mEvents.find(name);
if (iter != mEvents.end())
{
// 이벤트의 인스턴스를 생성한다.
FMOD::Studio::EventInstance* event = nullptr;
iter->second->createInstance(&event);
if (event)
{
// 이벤트 인스턴스를 시작한다.
event->start();
// release는 이벤트 인스턴스가 정지할 때 이벤트 소멸자 실행을 예약한다.
event->release();
}
}
}
이러한 방식은 간단하지만 음원이 끝날 때까지 재생하고 종료하도록 했을 뿐이기에 아래와 같은 기능들을 사용할 수 없다.
- 이벤트가 반복되는 이벤트인 경우 정지시킬 방법이 없다.
- 이벤트 파라미터를 설정할 수 없다.
- 이벤트 볼륨을 변경하는 방법도 없다.
이벤트를 임의로 조작하기 위해서 PlayEvent
외부로 생성된 EventInstance
를 전달할 수 있어야 한다. 하지만, 생성된 EventInstance
를 반환하게 되면 오디오 시스템을 외부로 노출시키는 결과를 낳는다. 이런 상황은 사운드를 간단히 조작(볼륨 조절, 일시정지 등)하려는 사용자가 FMOD API까지 이해해야한다는 문제가 생긴다. 뿐만 아니라 FMOD에서는 release
함수를 통해서 자동으로 소멸자 호출을 진행한다. 그런데 사용자가 EventInstance
포인터를 가지고 있게되면 소멸된 EventInstance
에 접근하는 위험한 상황이 발생할 수 있다. 따라서 조금 더 견고한 해결책이 필요하다.
SoundEvent 클래스
앞서 다룬 PlayEvent
함수에서 직접 EventInstance
포인터를 반환받아 사용하는 대신 개발자가 정수 ID를 통해서 각 활성화된 이벤트 인스턴스에 접근할 방법을 제공할 필요가 있다. SoundEvent
클래스는 이를 위해서 아래와 같이 두개의 변수를 가진다.
SoundEvent.h 선언 중
private:
class AudioSystem* mSystem;
unsigned int mID;
또한 AudioSystem
클래스에 이벤트 ID를 key로 이벤트 객체를 value로 갖는 해시테이블을 추가하고 ID 값으로 이벤트 객체에 접근할 수 있는 함수 GetEventInstance(unsigned int id)
를 오직 SoundEvent
클래스에만 제공한다.
protected:
friend class SoundEvent;
FMOD::Studio::EventInstance* GetEventInstance(unsigned int id);
private:
// EventInstance를 관리하기 위한 이벤트 id 맵
// key: 부호없는 정수형 id
// value: EventInstance
std::unordered_map<unsigned int, FMOD::Studio::EventInstance*> mEventInstances;
// ... (생략)
이제 사용자가 SoundEvent
클래스를 통해서 이벤트를 조작할 수 있도록 인터페이스를 구현할 수 있다.
SoundEvent 클래스 구현 (SoundEvent.h / SoundEvent.cpp)
출처:
에이콘 출판 <Game Programming in C++>