C++/Game

[게임 프레임워크 개발 일지] #14 Sound Panning에 관한 삽질

Kareus 2023. 6. 11. 05:52

일단 Sound Manager를 만들던 과정부터 설명해야겠네요.

 

SFML에서는 sf::Sound와 sf::Music을 지원합니다.

sf::Sound는 메모리에 데이터를 통째로 올려놓고 재생시키는 방식이고,

sf::Music은 재생할 때 파일에서 짧은 분량 (1초 정도)을 스트리밍하듯이 매번 불러와서 재생하는 방식입니다.

효과음처럼 짧은 소리는 sf::Sound를, 길이가 긴 배경음악은 sf::Music을 사용하라는 뜻이죠.

대부분의 게임을 대상으로 한 사운드 라이브러리가 이런 방식을 지원합니다.

 

공식 튜토리얼에서도 사람들이 자주하는 실수라고 언급하는 내용이지만,

sf::Sound에는 사운드 데이터가 저장되어 있지 않고, sf::SoundBuffer에 저장해놓습니다.

sf::Sprite와 sf::Texture의 관계와 같다고 생각하면 된다는 군요.

 

그래서 sf::SoundBuffer가 지역 변수이거나 해서 지워진다면 sf::Sound가 제대로 재생되지 않을 수 있습니다.

저는 Storage에는 sf::SoundBuffer를 저장해놓고, 필요할 때마다 sf::Sound 객체를 생성해서 쓰게끔 했습니다.

class Storage
{
    ...
    std::unordered_map<std::string, std::shared_ptr<sf::SoundBuffer>> sfx;
    std::unordered_map<std::string, std::shared_ptr<sf::Music>> music;
    
    sf::Sound getSFX(const std::string& name);
}

sf::Sound Storage::getSFX(const std::string& name)
{
    if (!sfx.contains(name)) return sf::Sound();
    return sf::Sound(*sfx[name]);
}

 

그러고나서 documentation을 읽어보니 Spatial Audio 관련 함수들이 있더군요.

3D 공간에서 소리 관련 효과들을 적용시킬 수 있습니다.

소리가 나는 위치, 온전히 들리게 할 거리, 감쇠 factor (소리가 멀어짐에 따라 줄어드는 값) 같은 걸 설정할 수 있습니다.

sf::Listener에서는 청자의 위치나 바라보는 방향도 설정할 수 있습니다.

 

문제는 이러한 3D 설정은 소리가 mono일 때만 작동한다는 점입니다.

제가 만들 건 2D 게임이니 대부분은 무시한다고 쳐도 panning (좌우 스피커 밸런스 조절)을 할 수가 없다는 점이 컸습니다.

panning 역시 별도의 함수가 없어서 setPosition을 사용해서 해야 되는데, 사운드가 stereo면 작동을 하지 않습니다.

 

그렇게 사운드 라이브러리에 대한 머나먼 구글링 여정을 시작했습니다.

정보가 너무 찾기 힘들어서 제대로 알아본 건지는 모르겠습니다만, 하여튼 찾아본 라이브러리는 이렇습니다.

openAL (SFML이 사용 중), SDL, rtaudio, SoLoud 등등

 

openAL에 panning 관련 함수는 (position 빼고) 찾아볼 수가 없었습니다.

 

SDL에는 SDL_mixer라고 라이브러리를 지원해주는데, music을 한 번에 하나밖에 재생할 수 없고

그마저도 재생 핸들이 따로 없어서 효과를 줄 수가 없습니다.

 

이에 다른 깃헙 유저분이 SDL_audiolib이라는 걸 만들긴 했는데, 빌드가 안 돼서 테스트를 해볼 수가 없었습니다.

그리고 주석을 보니 panning할 때 mixing을 따로 하는 게 아니라서

pan을 1 (완전 오른쪽)로 설정한다고 해서 왼쪽에서 나던 소리가 오른쪽으로 옮겨가서 나는 게 아니라, 그냥 음소거가 된다고 합니다.

SDL에서 직접 코드를 짜서 해볼까 했더니, 튜토리얼도 부족하고 이렇게 큰 라이브러리에서 오디오 쪽만 똑 떼와서 진행할 수가 없더군요. 그냥 포기했습니다.

 

rtaudio같은 경우에는 좀 더 로우레벨에서 사운드 처리가 가능해보였습니다.

제가 기능을 만들어서 쓰면 다 된다는 얘기겠지만, 할 일이 너무 많아지기에 (그리고 그럴 능력이 있을 것 같지도 않기에) 보류했습니다.

 

SoLoud는 사실 맨 처음에 살펴본 라이브러리입니다. openAL이나 SDL 등의 backend를 설정하고 이에 관한 편의 기능을 제공해주는 라이브러리입니다. 그런 탓에 backend부터 살펴보고자 미뤘었는데, 위 차례까지 가보니

openAL에서 다른 backend로 바꿔 쓰느니 그냥 SoLoud에 openAL 붙여서 쓰자 라는 생각이 들더군요.

실제로 써보니 panning은 잘 돌아가서 필요할 때는 이걸 쓰기로 결정했습니다.

정작 개발자 측에서는 latency가 높다고 이거 쓸 거면 openAL 직접 쓰는 게 나을거라더군요.

아니 openAL로 직접 썼는데 안 됐다니까??

뭐 정 안 되는 경우에는 PortAudio 같은 대체제로 다시 컴파일해서 쓰면 되긴 합니다.

WinMM 처럼 네이티브한 것도 쓸 수 있고...

 

유명한 라이브러리인 FMOD에서 살펴본 바로는, Spatialization을 적용하려면 mono 여야하는 건 변함이 없는 것 같습니다. 단지 FMOD에서는 stereo를 자동으로 채널을 분리해서 각각의 mono 오디오에 대해 효과를 적용해주는 것 같더군요.

확실히 이 정도까지의 효과를 적용한다면 유니티로 가서 개발하는 게 낫지 여기서 이러고 있을 게 아닌 것 같습니다.

 

이렇게 삽질해서 정리한 결과는 다음과 같습니다.

1. 단순 Stereo Panning이 필요한 경우에는 SoLoud를 써야겠다.

    latency 문제 같은 게 심하다면 다른 backend를 써야겠다. SFML에서 뭔가 못한다는 건 좀 찜찜하지만...

2. Spatialzation이 필요할 정도의 게임은 좋은 라이브러리를 쓰자.

    보통 3D 게임일텐데 그런 건 애초부터 유니티나 언리얼 써야지...

3. 아니면 오디오를 그냥 mono로 변환해서 쓰자. stereo가 그렇게 중요한 것도 아니고...

    L, R 채널 분리해서 각각 재생해서 효과를 적용해도 괜찮지 않을까 싶다.

 

1번에서 쓸 SoLoud는 GENie라는 컴파일 프로젝트 생성기를 이용해서 프로젝트를 만든 뒤에 컴파일해줘야 합니다.

build 폴더에 genie.lua가 있는데 여기다가 genie.exe를 넣고 cmd로 명령 돌리니까 프로젝트가 만들어지더군요.

local WITH_SDL = 0
local WITH_SDL2 = 0
local WITH_SDL_STATIC = 0
local WITH_SDL2_STATIC = 0
local WITH_PORTAUDIO = 0
local WITH_OPENAL = 0
local WITH_XAUDIO2 = 0
local WITH_WINMM = 0
local WITH_WASAPI = 0
local WITH_ALSA = 0
local WITH_JACK = 0
local WITH_OSS = 0
local WITH_COREAUDIO = 0
local WITH_VITA_HOMEBREW = 0
local WITH_NOSOUND = 0
local WITH_MINIAUDIO = 0
local WITH_NULL = 1
local WITH_TOOLS = 0

if (os.is("Windows")) then
	WITH_WINMM = 1
elseif (os.is("macosx")) then
	WITH_COREAUDIO = 1
else
	WITH_ALSA = 1
	WITH_OSS = 1
end

이 상태에서 생성하면 WINMM을 사용하는 라이브러리를 컴파일할 수 있습니다.

 

genie.exe vs2022

Visual Studio 2022를 대상으로 하는 컴파일 프로젝트를 생성합니다.

 

 

3번의 경우에는, stereo인 sf::SoundBuffer의 채널을 분리하는 함수를 구현했습니다.

스트리밍되는 사운드는 처음부터 따로 시스템을 만드는 게 아닌 한 채널을 분리할 방법이 없기 때문에

그냥 사운드 파일을 분리된 걸로 입력하는 게 낫습니다.

 

void separateChannels(sf::SoundBuffer& input, sf::SoundBuffer& left, sf::SoundBuffer& right)
{
    if (input.getChannelCount() != 2) return; //not stereo

    auto samples = input.getSamples();
    auto rate = input.getSampleRate();
    size_t size = input.getSampleCount() / 2;

    sf::Int16* L = new sf::Int16[size], *R = new sf::Int16[size];

    for (size_t i = 0; i < size; i++)
    {
        L[i] = samples[2 * i];
        R[i] = samples[2 * i + 1];
    }

    left.loadFromSamples(L, size, 1, rate);
    right.loadFromSamples(R, size, 1, rate);

    delete[] L;
    delete[] R;
}