fmod를 이용한 게임 사운드 프로그래밍
Project/Win32API로 던그리드 모작

fmod를 이용한 게임 사운드 프로그래밍

Woon2World :: Programming & Art Life

 

사운드

 

 

 

 

 


FMOD

https://www.fmod.com/

 

FMOD

The sonic universe of Creaks We talked to the creative team responsible for the audio of Creaks, the latest game from renowned Czech game developer, Amanita Design. Visit blog

www.fmod.com

https://documentation.help/FMOD-Studio-API/FMOD_System_CreateSound.html

 

System::createSound - FMOD Studio API Documentation

System::createSound C++ Syntax FMOD_RESULT System::createSound( const char *name_or_data, FMOD_MODE mode, FMOD_CREATESOUNDEXINFO *exinfo, FMOD::Sound **sound ); C Syntax FMOD_RESULT FMOD_System_CreateSound( FMOD_SYSTEM *system, const char *name_or_data, FM

documentation.help

 

FMOD는 사운드를 다룰 수 있는 저수준 API다.

위 링크는 fmod 홈페이지고, 아래 링크는 내가 이용하면서 참고했던 문서화 사이트....

 

누가 나한테 fmod 강의라도 해줬으면 싶었다.

딱 와닿게 설명해주는 게시글이나 유튜브 영상도 없고 ㅠㅠ

 

fmod를 이용해 사운드 클래스를 만들었지만

그다지 좋은 설계는 아니다.

 

사운드매니저, 사운드컴포넌트, 사운드 이런 식으로 사운드 대가족을 만들지 않았다는 점에는 의의가 있지만,

 

기능이 좀 빈약하고, 직접 RAII를 구현하기보다 스마트 포인터에 담아서 사용하는 편이 사용자에게 있어 이 객체가 RAII 객체임을 이해시키는 데 도움이 될 거라 생각했는데, 꽤나 타이핑하기도, 함수의 매개변수 타입을 정하는 것도 버거워졌다.

 

그래도!!

 

  1. 리소스의 중복 로드를 힘들게 만들고 ( 스마트 포인터를 아는 사용자라면, 중복 로드할 리가 없겠지.. )
  2. 중복 재생되는 사운드는 나중에 재생되는 사운드가 소리가 더 작게!
  3. 동시 재생 사운드 한계를 넘어 재생하려고 할 경우 가장 작은 볼륨을 가진 사운드를 빼고 재생이 되도록!

 

하는 나름의 내세울만한 부분이 있다.

 

 

 

 


개요

  • sound
    : FMOD API를 통해 사운드를 처리하는 클래스입니다.

  • sound_ptr
    : std::shared_ptr< sound > 와 같습니다. 짧은 타이핑을 위해 using을 이용하였습니다.

    • enum class sound::mode
      : 현재 normal과 loop의 두 가지 모드가 존재합니다.
      normal은 한 번만 재생하고 loop는 반복해서 재생합니다.

    • enum class sound::sound_tag
      : 현재 BGM과 SE 두 가지 태그가 존재합니다.
      특정한 tag를 가진 사운드를 한꺼번에 처리하기 위한 열거형입니다.

    • static sound_ptr make( const char* file_path, mode mod, const float volume, const float gradient )
      : 사운드를 생성합니다. 사운드 객체는 이 함수를 통해서밖에 생성할 수 없습니다.
      file_path로 사운드 파일을 로드하고, mod로 재생 모드를 선택해줍니다.
      volume은 사운드의 볼륨입니다. 기본값으로 1.0f가 지정되어 있습니다.
      gradient는 동시 재생 시 볼륨이 감소될 비율입니다. 기본값으로 0.5f가 지정되어 있습니다.

    • void play()
      : 사운드를 재생합니다.
      이미 해당 사운드를 재생하고 있을 경우 점진적으로 감소된 볼륨으로 재생합니다.

    • void amplify( const float val )
      : 사운드의 볼륨에 val을 곱해 증폭시킵니다.
      이미 재생되고 있는 사운드와 앞으로 재생될 사운드에 모두 적용합니다.

    • void mute()
      : 사운드를 침묵시킵니다. 침묵 상태로 사운드는 재생됩니다.
      이미 재생되고 있는 사운드와 앞으로 재생될 사운드에 모두 적용합니다.

    • void listen()
      : 사운드의 침묵 상태를 해제합니다.
      이미 재생되고 있는 사운드와 앞으로 재생될 사운드에 모두 적용합니다.

    • void pause()
      : 사운드를 일시 정지시킵니다.
      이미 재생되고 있는 사운드와 앞으로 재생될 사운드에 모두 적용합니다.

    • void resume()
      : 사운드를 재개합니다. pause() 직전에 재생되던 부분부터 다시 재생됩니다.
      이미 재생되고 있는 사운드와 앞으로 재생될 사운드에 모두 적용합니다.

    • void stop()
      : 현재 재생되고 있는 모든 사운드를 제거합니다.

    • static void update()
      : FMOD 시스템을 업데이트하고, 모든 종료된 사운드들에 대해 종료 처리를 합니다.
      일정 시간 간격마다 단 한 번 호출해주면 좋습니다.

    • static void tag_reserve( sound_tag tag, const size_t cnt )
      : std::vector::reserve()를 wrapping 한 것에 불과합니다.
      특정 태그의 사운드를 cnt개 만큼 할당할 메모리를 예약합니다.

    • static void tag_push( sound_tag tag, const sound_ptr& s )
      : sound_ptr를 특정 태그에 저장합니다.

    • static void clear()
      : 모든 태그의 sound_ptr들을 제거합니다.

    • static void tag_clear( sound_tag tag )
      : 특정 태그의 sound_ptr들을 제거합니다.

    • static void tag_play( sound_tag tag )
      : 특정 태그의 sound_ptr들을 play 합니다.

    • static void tag_amplify( sound_tag tag, const float val )
      : 특정 태그의 sound_ptr들을 amplify 합니다.

    • static void tag_mute( sound_tag tag )
      : 특정 태그의 sound_ptr들을 mute 합니다.

    • static void tag_listen( sound_tag tag )
      : 특정 태그의 sound_ptr들을 listen합니다.

    • static void tag_pause( sound_tag tag )
      : 특정 태그의 sound_ptr들을 pause 합니다.

    • static void tag_resume( sound_tag tag )
      : 특정 태그의 sound_ptr들을 resume 합니다.

    • static void tag_stop( sound_tag tag )
      : 특정 태그의 sound_ptr들을 stop 합니다.

 

 

 

 


코드

 

#ifndef _sound
#define _sound
 
#include "fmod.hpp"
#include <array>
#include <vector>
#include <list>
#include "TMP.h"
 
class sound
{
public:
    enum class mode {
        normal = FMOD_LOOP_OFF, loop = FMOD_LOOP_NORMAL
    };
 
    enum class sound_tag {
        BGM = 0, SE,
        SIZE
    };
 
    void play()
    {
        if ( !( available_channel_cnt ) )
        {
            // 모든 채널을 사용중일 경우
            // 가장 작은 볼륨으로 재생되는 채널을 선점한다.
            sound* min_sound = this;
            float min_volume_found = min_volume;
            for ( auto other_sound : sound_insts )
            {
                auto other_min_volume = other_sound->min_volume;
                if ( other_min_volume < min_volume_found )
                {
                    min_sound = other_sound;
                    min_volume_found = other_min_volume;
                }
            }
 
            min_sound->fmod_channels.pop_back();
            min_sound->min_volume /= min_sound->gradient;
            ++available_channel_cnt;
        }
 
        fmod_channels.emplace_back( nullptr );
 
        // 한 사운드가 둘 이상의 채널에서 재생될 경우 ( 동시 재생 )
        // 나중에 재생된 사운드의 볼륨을 줄인다.
        min_volume *= gradient;
        system->playSound( fmod_sound, nullptr, false&fmod_channels.back() );
        fmod_channels.back()->setVolume( min_volume );
        fmod_channels.back()->setPaused( is_paused );
        fmod_channels.back()->setMute( is_muted );
        --available_channel_cnt;
    }
 
    void amplify( const float val )
    {
        volume *= val;
        min_volume *= val;
        for ( auto ch : fmod_channels )
        {
            float vol;
            ch->getVolume( &vol );
            ch->setVolume( vol * val );
        }
    }
 
    void mute()
    {
        is_muted = true;
        for ( auto ch : fmod_channels )
        {
            ch->setMute( true );
        }
    }
 
    void listen()
    {
        is_muted = false;
        for ( auto ch : fmod_channels )
        {
            ch->setMute( false );
        }
    }
 
    void pause()
    {
        is_paused = true;
        for ( auto ch : fmod_channels )
        {
            ch->setPaused( true );
        }
    }
 
    void resume()
    {
        is_paused = false;
        for ( auto ch : fmod_channels )
        {
            ch->setPaused( false );
        }
    }
 
    void stop()
    {
        available_channel_cnt += fmod_channels.size();
        fmod_channels.clear();
        min_volume = volume;
    }
 
    // 모든 재생 완료된 사운드에 대해 종료 처리를 한다.
    // 일정 시간 간격으로 단 한 번 호출한다. ( 사운드마다 호출하지 않는다. 정적 함수이다. )
    static void update()
    {
        system->update();
 
        for ( auto s : sound_insts )
        {
            // 먼저 재생된 채널이 먼저 종료!
            while ( s->fmod_channels.size() )
            {
                bool is_playing = false;
                bool is_paused = s->is_paused;
                
                s->fmod_channels.front()->isPlaying( &is_playing );
 
                if ( !is_playing && !is_paused )
                {
                    ++available_channel_cnt;
                    s->fmod_channels.pop_front();
                    s->min_volume /= s->gradient;
                }
                else
                {
                    break;
                }
            }
        }
    }
 
    // 특정 태그의 사운드를 지정한 개수만큼 예약한다.
    static void tag_reserve( sound_tag tag, const size_t cnt )
    {
        tagged_sounds[ etoi( tag ) ].reserve( cnt );
    }
 
    static void tag_push( sound_tag tag, const std::shared_ptr< sound >& s )
    {
        tagged_sounds[ etoi( tag ) ].push_back( s );
    }
 
    static void tag_push( sound_tag tag, std::shared_ptr< sound >&& s )
    {
        tagged_sounds[ etoi( tag ) ].push_backstd::move( s ) );
    }
 
    static void clear()
    {
        for ( auto& tagged_sound_set : tagged_sounds )
        {
            tagged_sound_set.clear();
        }
    }
 
    static void tag_clear( sound_tag tag )
    {
        tagged_sounds[ etoi( tag ) ].clear();
    }
 
    static void tag_play( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->play();
        }
    }
 
    static void tag_amplify( sound_tag tag, const float val )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->amplify( val );
        }
    }
 
    static void tag_mute( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->mute();
        }
    }
 
    static void tag_listen( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->listen();
        }
    }
 
    static void tag_pause( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->pause();
        }
    }
 
    static void tag_resume( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->resume();
        }
    }
 
    static void tag_stop( sound_tag tag )
    {
        for ( auto s : tagged_sounds[ etoi( tag ) ] )
        {
            s->stop();
        }
    }
 
    sound( const sound& other ) = delete;
    sound& operator=const sound& other ) = delete;
 
    ~sound()
    {
        if ( fmod_sound )
        {
            fmod_sound->release();
            sound_insts.erase( at );
            ++available_sound_cnt;
        }
    }
 
    static auto make( const char* file_path, mode mod, const float volume = 1.0f, const float gradient = sound::default_gradient )
    {
        return std::shared_ptr< sound >new sound{ file_path, mod, volume, gradient } };
    }
 
private:
    static constexpr float default_gradient = 0.5f;
    static size_t available_sound_cnt;
    static size_t available_channel_cnt;
    static constexpr size_t fmod_max_channels = 32;
    static constexpr size_t fmod_max_sounds = 100;
    static std::list< sound* > sound_insts;
    static FMOD::System* system;
    static std::array< std::vector< std::shared_ptr< sound > >, etoi( sound_tag::SIZE ) > tagged_sounds;
 
    bool is_paused;
    bool is_muted;
    float min_volume;
    float volume;
    float gradient;
    FMOD::Sound* fmod_sound;
    std::deque< FMOD::Channel* > fmod_channels;
    std::list< sound* >::const_iterator at;
 
    sound( const char* file_path, mode mod, const float volume = 1.0f, const float gradient = default_gradient )
        : volume{ volume }, gradient{ gradient }, min_volume{ volume / gradient }, fmod_sound{ nullptr },
        is_paused { false }, is_muted { false }
    {
        if ( !available_sound_cnt )
        {
            std::cerr << "available_sound_cnt was 0.\n";
            return;
        }
        std::cout << file_path << '\n';
        system->createSound( file_path, etoi( mod ) | FMOD_LOWMEM, nullptr, &fmod_sound );
        sound_insts.push_backthis );
        at = --sound_insts.end();
        --available_sound_cnt;
    }
 
    struct _auto_system
    {
        _auto_system()
        {
            FMOD::System_Create( &system );
            FMOD::Memory_Initialize( malloc4 * 1024 * 1024 ), 4 * 1024 * 1024000 );
            system->init( fmod_max_channels, FMOD_INIT_NORMAL, nullptr );
        }
 
        ~_auto_system()
        {
            system->release();
        }
    };
 
    static _auto_system _s;
};
 
using sound_ptr = std::shared_ptr< sound >;
 
FMOD::System* sound::system;
std::list< sound* > sound::sound_insts;
size_t sound::available_sound_cnt = sound::fmod_max_sounds;
size_t sound::available_channel_cnt = sound::fmod_max_channels;
sound::_auto_system sound::_s;
std::array< std::vector< std::shared_ptr< sound > >, etoi( sound::sound_tag::SIZE ) > sound::tagged_sounds;
 
#endif
cs

 

 

생성자와 소멸자를 보면 sound_insts.push_back( this ); 와
sound_insts.erase( at ); 을 볼 수 있다.

 

동시 재생 가능 개수를 32개로 정해놨는데, 이를 초과하여 재생할 경우 가장 작은 볼륨으로 재생이 되고 있는 사운드를 제거하는 작업이 필요하다.

 

엄밀하게는, 하나의 채널을 제거하는 것이다.

 

따라서 사운드 객체마다 자신의 가장 작은 볼륨을 들고 있게 만들었다.

 

그리고 이 사운드 객체들은 std::list< sound* > sound_insts 에 들어있어 ( 생성자와 소멸자에서 해준 것이 이것. )

sound_insts 에서 가장 작은 볼륨을 가진 sound 의 맨 마지막 채널을 제거하는 것으로 해당 기능이 구현 가능하다.

 

이 모든 것은 별도의 추가 함수 구현 없이 play()와 update()에서 자동으로 처리된다.

 

 

sound( const char* file_path, mode mod, const float volume = 1.0f, const float gradient = default_gradient )
    : volume{ volume }, gradient{ gradient }, min_volume{ volume / gradient }, fmod_sound{ nullptr },
    is_paused { false }, is_muted { false }
{
    if ( !available_sound_cnt )
    {
        std::cerr << "available_sound_cnt was 0.\n";
        return;
    }
    std::cout << file_path << '\n';
    system->createSound( file_path, etoi( mod ) | FMOD_LOWMEM, nullptr, &fmod_sound );
    sound_insts.push_backthis );
    at = --sound_insts.end();
    --available_sound_cnt;
}
 
~sound()
{
    if ( fmod_sound )
    {
        fmod_sound->release();
        sound_insts.erase( at );
        ++available_sound_cnt;
    }
}
cs

 

 

 

특정 사운드가 어떤 시점에 제거될지 미리 예측하기란 불가능하므로 ( 프로그래밍 제약을 두지 않는 이상 )

sound_insts는 상수 시간에 erase 할 수 있는 std::list를 사용했다.

 

채널은 원래 먼저 재생된 것이 먼저 종료되는 FIFO 구조를 가지므로 std::queue에 적합하겠지만,

가장 작은 볼륨을 가진 채널을 제거할 때에는 제거할 채널이 맨 앞이 아니라 맨 뒤에 있다.

따라서 맨 앞과 맨 뒤에서의 삽입, 삭제가 용이한 std::deque를 사용했다.

 

 

더보기

 

#ifndef _game
#define _game
 
#include <windows.h>
#include "timer.h"
#include "scene.h"
#include "sound.h"
 
class game
{
public:
    void process_input( UINT msg, WPARAM w_param, LPARAM l_param )
    {
        switch ( msg )
        {
        case WM_KEYDOWN:
            keyboard( w_param );
            break;
        }
    }
 
    void on_wtimer( UINT id )
    {
        // if ( game_timer.timer_id == id )
        // {
        auto lag = game_timer.getlag();
        auto ms_per_frame = game_timer.get_ms_per_frame();
 
        if ( lag < ms_per_frame * 2 )
        {
            update();
 
            if ( lag < ms_per_frame )
            {
                render();
            }
        }
        // }
    }
 
    game( HWND hWnd, const UINT timer_id, const float fps, const float clock = 10.f ) : hWnd{ hWnd },
        game_timer{ hWnd, timer_id, fps, clock } {}
    game( const game& ) = default;
    game& operator=const game& ) = default;
 
private:
    HWND hWnd;
    timer game_timer;
    std::unique_ptr< scene > game_scene;
 
    void update()
    {
        game_timer.update();
        sound::update();
    }
 
    void render()
    {
 
    }
 
    void keyboard( WPARAM key )
    {
        static std::vector< sound_ptr > test;
        static bool play = true;
        static bool mute = false;
        static sound_ptr bgm;
        static int menu_level = 0;
 
        auto init = []()
        {
            test.push_back( sound::make( "sounds/SE/iceball.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/coin.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/Hit_Player.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/heartbeat.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/fire.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/ice_spell_freeze_frost_02_iceslime.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/devana_spin.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/joejoon.wav", sound::mode::normal ) );
            test.push_back( sound::make( "sounds/SE/Fantasy_Game_Item_Collect_Magic_K.wav", sound::mode::normal ) );
            bgm = sound::make( "sounds/BGM/0.Town.wav", sound::mode::loop );
 
            for ( auto s : test )
            {
                sound::tag_push( sound::sound_tag::SE, s );
            }
            sound::tag_push( sound::sound_tag::BGM, bgm );
            sound::tag_play( sound::sound_tag::BGM );
        };
 
        std::cout << static_cast<char>(key) << " pushed ";
 
        switch ( menu_level )
        {
        case 0:
            init();
            ++menu_level;
 
        case 1:
            std::cout << "-----------------------------------------\n";
            std::cout << "1~9: 이펙트 재생 m: 이펙트 음소거/음소거해제\n";
            std::cout << "q: bgm 볼륨 증가 w: bgm 볼륨 감소\n";
            std::cout << "e: 이펙트 볼륨 증가 r: 이펙트 볼륨 감소\n";
            std::cout << "t: bgm 전환 p: bgm 정지/재개\n";
            std::cout << "c: 재초기화 (생성자 소멸자 정상 동작 확인)\n";
            std::cout << "-----------------------------------------\n";
            ++menu_level;
            break;
 
        case 2:
            switch ( key )
            {
            case '1'case '2'case '3'case '4'case '5'case '6'case '7'case '8'case '9':
                test[ key - '1' ]->play();
                break;
 
            case 'M':
                mute = 1 - mute;
                if ( mute )
                {
                    sound::tag_mute( sound::sound_tag::SE );
                }
                else
                {
                    sound::tag_listen( sound::sound_tag::SE );
                }
                break;
 
            case 'Q':
                sound::tag_amplify( sound::sound_tag::BGM, 1.1 );
                break;
 
            case 'W':
                sound::tag_amplify( sound::sound_tag::BGM, 0.91 );
                break;
 
            case 'E':
                sound::tag_amplify( sound::sound_tag::SE, 1.1 );
                break;
 
            case 'R':
                sound::tag_amplify( sound::sound_tag::SE, 0.91 );
                break;
 
            case 'T':
                sound::tag_clear( sound::sound_tag::BGM );
                bgm = sound::make( "sounds/BGM/title.wav", sound::mode::loop );
                sound::tag_push( sound::sound_tag::BGM, bgm );
                sound::tag_play( sound::sound_tag::BGM );
                break;
 
            case 'P':
                play = 1 - play;
                if ( play )
                {
                    sound::tag_resume( sound::sound_tag::BGM );
                }
                else
                {
                    sound::tag_pause( sound::sound_tag::BGM );
                }
                break;
 
            case 'C':
                sound::tag_clear( sound::sound_tag::BGM );
                sound::tag_clear( sound::sound_tag::SE );
                bgm.reset();
                test.clear();
                init();
            }
            break;
 
        case 3:
            break;
        }
    }
 
    void mouse( UINT msg, float x, float y )
    {
 
    }
 
    void motion( UINT msg, float x, float y )
    {
 
    }
};
#endif
 
 
cs

 

 

지금은 구현된 scene 클래스가 없어 game 객체에서 입력을 처리하고 있지만

이후에는 scene 객체에 입력 이벤트로써 전달될 것이다.

 

 

 

 


'Project > Win32API로 던그리드 모작' 카테고리의 다른 글

프로젝트 던그리드 완성  (2) 2021.12.17
게임 클래스와 씬 클래스 추가  (0) 2021.11.06
타이머 간략화  (0) 2021.11.06
게임 타이머 구현  (0) 2021.09.21
git repository 생성  (0) 2021.09.17