steps3D - Tutorials - Библиотека FMOD и ее использование для создания 3D-звука и аудио-эффектов

Библиотека FMOD и ее использование для создания 3D-звука и аудио-эффектов

Довольно давно я уже писал про использование аудио-библиотек (Audierre и OpenAL), но с тех пор OpenAL практически не обновлялся и не развивался. Поэтому в этой статье я хочу рассказать о еще одной аудио-библиотек - FMOD от компании Firelight Technologies.

FMOD это одна из старейших аудиобиблиотек на рынке. Свою жизнь она начала в 1995 году но не как аудиобиблиотека, а как Firelight MOD Player - инструмент для воспроизведения MOD (или им подобных) файлов. В марте 1999 года FMOD впервые включила в с свой состав API и в декабре 1999 года вышла FMOD 3.0.

В 2013 году FMOD была полностью переписана и в ней почвилось два API - низкоуровневый FMOD API и высокоуровневый FMOD Studio API. При этом сам FMOD Studio API полностью реализован на основе низкоуровневого FMOD API. Далее мы будем рассматривать только низкоуровневый FMOD API.

Это не open-source библиотека, но она доступна сразу под несколькими лицензиями, включая и FMOD Non-commercial License, делающей ее бесплатной для некоммерческого применения. Эта библиотека активно развивается и поддерживается, она является кросс-платформенной. В число поддерживаемых платформ входят Microsoft Windows, mac OS, Linux и ряд игровых консолей.

FMOD поддерживает большое число проигрываемых аудио-форматов, включая AIFF, FLAC, MP3, Ogg Vorbis, WAV и ряд других. Целый ряд игровых движков, таких как Unity, Unreal Engine, Cry Engine используют FMOD в качестве своего звукового движка.

Хотя FMOD и написан на С++, он предоставляет сразу два API - C++ API и C API. Они не просто идентичны, но еще и интероперабельны - вы можете смешивать вызовы из них между собой. Мы будем далее рассматривать основные элементы их С++ API, он довольно прост и легко интегрируется в игровой движок.

В C++ API все классы живут в пространстве имен FMOD и все методы возвращают значение типа FMOD_RESULT, успешному завершению вызова соответствует значение FMOD_OK. Необходимые файлы для подключения FMOD для своей платформы можно легко скачать с сайта www.fmod.com. Обратите внимание, что в FMOD объекты не строятся традиционным путем с использованием конструктора, вместо это для их создания служат методы create* другоих объектов или же функция FMOD::System_Create. Для уничтожения объектов служит специальный метод release, также возвращающий значение типа FMOD_RESULT. Для сборки под Windows нам понадобится подключить заголовочный файл fmod.hpp и библиотеку fmodex_vc.lib.

В низкоуровневом API есть пять базовых объектов:

Первое, что нужно для начала работы с библиотекой FMOD это создать экземпляр класса FMOD::System как показано ниже.

FMOD::System * system = nullptr;
 
if ( FMOD::System_Create ( &system ) != FMOD_OK )
    fatal () << "Error initializing FMOD::System !" << std::endl;

Далее мы проверяем на наличие подходящий драйверов и инициализируем систему, задавая число используемых звуковых каналов (максимальное число одновременно проигрываемых звуков).

int driverCount = 0;
 
system->getNumDrivers ( &driverCount );
 
if ( driverCount < 1 )
    fatal () << "Cannot find required audio drivers !" << std::endl;
    
system->init ( 36, FMOD_INIT_NORMAL, nullptr );

Каждому проигрываемому звуку соответствует объект FMOD::Sound, являющийся фактически контейнером аудио-данных. Для создания такого объекта служит метод System::createSound.

FMOD_RESULT System::createSound ( const char* name,
                                  FMOD_MODE mode,
                                  FMOD_CREATTESOUNDEXINFO * exinfo,
                                  FMOD::Sound ** sound );

Параметр name обычно задает имя файла, из которого нужно прочесть аудио-данные. Обычно в качестве параметра exinfo передается nullptr, а в качестве параметра mode - FMOD_DEFAULT (для использования трехмерного звука нужно задать флаг FMOD_3D).

Если mode содержит битовый флаг FMOD_OPENMEMORY, то тогда параметр name это просто указатель на область памяти со звуковыми данными. Длина этой области передается через поле length структуры, на которую указывает exinfo, при этом поле size этой структуры должно быть равно sizeof(FMOD_CREATTESOUNDEXINFO).

FMOD::Sound * sound = nullptr;
 
system->createSound ( "test.wav", FMOD_3D, 0, &sound );

На самом деле FMOD::Sound это просто контейнер для звуковых данных, за воспроизведение данного звука отвечает сама система, у которой есть для этого метод System::playSound.

Базовым интерфейсом для управления воспроизведением звука является FMOD::ChannelControl. И FMOD::Channel и FMOD::ChannelGroup его поддерживают.

Также у FMOD::ChannelControl есть целая группа методов по управлению воспроизведением.

FMOD_RESULT ChannelControl::isPlaying ( bool * playing );
FMOD_RESULT ChannelControl::stop      ();
FMOD_RESULT ChannelControl::setPaused ( bool paused   );
FMOD_RESULT ChannelControl::setPitch  ( float pitch   );
FMOD_RESULT ChannelControl::setVolume ( float volume  );        // [0,1]
FMOD_RESULT ChannelControl::setMute   ( bool mute     );
FMOD_RESULT ChannelControl::removeDSP ( DSP * dsp );

Следующий фрагмент кода запускает на однократное воспроизведение заданного звука.

sound->setMode ( FMOD_LOOP_OFF );
system->playSound ( sound, nullptr, false, &channel );

Ниже приводится описание метода playSound.

FMOD_RESULT System::playSound ( Sound * sound, ChannelGroup * group, bool paused, Channel ** channel );

В 3D у каждого звука есть минимальное и максимальное расстояние, используемые для управления зависимостью интенсивности звука от расстояния для него. Задаются они при помощи метода Sound::set3DMinMaxDistance.

FMOD_RESULT Sound::set3DMinMaxDistance ( float minDistance, float maxDistance );

Поддерживается несколько режимов вычисления того, как именно происходит ослабевание звука в зависимости от расстояния. Какой режим следует использовать задается флагом параметра mode. По умолчанию используется флаг FMOD_3D_INVERSEROLLOF, другими вариантами являются FMOD_3D_LINEARROLLOF и FMOD_3D_LINEARSQUAREROLLOF.

Также для полной поддержки 3D-звука также необходимо:

Для задания положения, ориентации и скорости наблюдателя (камеры) служит метод System::set3DListenerAttributes.

FMOD_RESULT System::set3DListenerAttributes(
  int listener,
  const FMOD_VECTOR * pos,
  const FMOD_VECTOR * vel,
  const FMOD_VECTOR * forward,
  const FMOD_VECTOR * up
);

Параметры источника звука задаются не для FMOD::Sound (который является просто контейнером звуковых данных), а для классов FMOD::Channel и FMOD::ChannelGroup.

Для задания положения и скорости источника звука (Channel или ChannelGroup) служит метод ChannelControl::set3DAttributes.

FMOD_RESULT ChannelControl::set3DAttributes (
    const FMOD_VECTOR * pos,
    const FMOD_VECTOR * vel
);

Создать новую группу каналов можно при помощи вызова System::createChannelGroup:

FMOD_RESULT System::createChannelGroup(
  const char *name,
  ChannelGroup **channelgroup
);

Также можно для уже существующих звуков задать содержащую их группу каналов при помощи следующего вызова:

channel->setChannelGroup ( group );

FMOD Core API поддерживает задание геометрии (mesh) для создания в реальном времени эффекта закрывания звука различными объектами. Для этого используется специальный вызов System::createGeometry, создающий объект класса FMOD::Geometry.

FMOD_RESULT System::createGeometry ( int maxPolygons, int maxVertices, Geometry ** geometry );

У класса FMOD::Geometry есть рядом методов, позволяющих добавлять многоугольники к нему и управлять положением и ориентаций всего меша.

FMOD_RESULT Geometry::addPolygon(
  float directocclusion,
  float reverbocclusion,
  bool doublesided,
  int numvertices,
  const FMOD_VECTOR *vertices,
  int *polygonindex
);
 
FMOD_RESULT Geometry::setPolygonAttributes(
  int index,
  float directocclusion,
  float reverbocclusion,
  bool doublesided
);
 
FMOD_RESULT Geometry::setPosition(
  const FMOD_VECTOR *position
);
 
FMOD_RESULT Geometry::setRotation(
  const FMOD_VECTOR *forward,
  const FMOD_VECTOR *up
);

Библиотека FMOD также поддерживает большое количество различных DSP эффектов. Эти эффекты создаются при помощи вызова System::createDSPByType.

FMOD_RESULT System::createDSPByType(
  FMOD_DSP_TYPE type,
  DSP **dsp
);

Параметр type задает тип создаваемого DSP-эффекта, ниже приводятся допустимые значение для него.

typedef enum FMOD_DSP_TYPE {
  FMOD_DSP_TYPE_UNKNOWN,
  FMOD_DSP_TYPE_MIXER,
  FMOD_DSP_TYPE_OSCILLATOR,
  FMOD_DSP_TYPE_LOWPASS,
  FMOD_DSP_TYPE_ITLOWPASS,
  FMOD_DSP_TYPE_HIGHPASS,
  FMOD_DSP_TYPE_ECHO,
  FMOD_DSP_TYPE_FADER,
  FMOD_DSP_TYPE_FLANGE,
  FMOD_DSP_TYPE_DISTORTION,
  FMOD_DSP_TYPE_NORMALIZE,
  FMOD_DSP_TYPE_LIMITER,
  FMOD_DSP_TYPE_PARAMEQ,
  FMOD_DSP_TYPE_PITCHSHIFT,
  FMOD_DSP_TYPE_CHORUS,
  FMOD_DSP_TYPE_VSTPLUGIN,
  FMOD_DSP_TYPE_WINAMPPLUGIN,
  FMOD_DSP_TYPE_ITECHO,
  FMOD_DSP_TYPE_COMPRESSOR,
  FMOD_DSP_TYPE_SFXREVERB,
  FMOD_DSP_TYPE_LOWPASS_SIMPLE,
  FMOD_DSP_TYPE_DELAY,
  FMOD_DSP_TYPE_TREMOLO,
  FMOD_DSP_TYPE_LADSPAPLUGIN,
  FMOD_DSP_TYPE_SEND,
  FMOD_DSP_TYPE_RETURN,
  FMOD_DSP_TYPE_HIGHPASS_SIMPLE,
  FMOD_DSP_TYPE_PAN,
  FMOD_DSP_TYPE_THREE_EQ,
  FMOD_DSP_TYPE_FFT,
  FMOD_DSP_TYPE_LOUDNESS_METER,
  FMOD_DSP_TYPE_ENVELOPEFOLLOWER,
  FMOD_DSP_TYPE_CONVOLUTIONREVERB,
  FMOD_DSP_TYPE_CHANNELMIX,
  FMOD_DSP_TYPE_TRANSCEIVER,
  FMOD_DSP_TYPE_OBJECTPAN,
  FMOD_DSP_TYPE_MULTIBAND_EQ,
  FMOD_DSP_TYPE_MAX
} FMOD_DSP_TYPE;

Для задания параметров DSP служит метод setParameterFloat как показано ниже.

myDsp->setParameterFloat ( FMOD_DSP_LOWPASS_CUTOFF, 1200 );

Созданный DSP-эффект можно подключить к FMOD::Channel или FMOD::ChannelGroup при помощи метода FMOD::ChannelControl::addDSP.

FMOD_RESULT ChannelControl::addDSP (
  int index,
  DSP * dsp
);

Здесь параметр index задает положение данного DSP в цепочке DSP-эффектов данного канала.

Кроме того, FMOD поддерживает 2 типа реверба и виртуальную систему 3D-реверба. Наиболее простой и удобной в использовании является система 3D-реверба. Она позволяет создавать в сцене (и притом довольное большие количество) реверб-сфер, при попадании внутрь которой включается соответствующий эффект. Для создания таких сфер служит следующий метод, создающий объект класса FMOD::Reverb3D:

FMOD_RESULT System::ceateReverb3D  ( Reverb3D ** reverb );

Для задания атрибутов такой реверб-сферы служит метод Reverb3D::set3DAttributes:

FMOD_RESULT Reverb3D::set3DAttributes(
  const FMOD_VECTOR *position,
  float mindistance,
  float maxdistance
);

Для задания дополнительных свойств реверб-сфер служит метод Reverb3D::setPropertiess:

FMOD_RESULT Reverb3D::setProperties (
  const FMOD_REVERB_PROPERTIES *properties
);

Сама структура FMOD_REVERB_PROPERTIES описывается следующим образом:

typedef struct FMOD_REVERB_PROPERTIES {
  float   DecayTime;
  float   EarlyDelay;
  float   LateDelay;
  float   HFReference;
  float   HFDecayRatio;
  float   Diffusion;
  float   Density;
  float   LowShelfFrequency;
  float   LowShelfGain;
  float   HighCut;
  float   EarlyLateMix;
  float   WetLevel;
} FMOD_REVERB_PROPERTIES;

Можно включать и выключать отдельные реверб-сферы при помощи метода setActive, метод getActive позволяет узнать включена ли в данный момент заданная реверб-сфера.

FMOD_RESULT Reverb3D::setActive ( bool active );
FMOD_RESULT Reverb3D::getActive ( bool * active );

Ниже приводится исходный код простого OpenGL приложения, демонстрирующего рендеринг геометрии вместе с 3D звукамим.

//
// FMDO 3D sound example 
//
// Author: Alexey V. Boreskov <steps3d@gmail.com>, <steps3d@narod.ru>
//

#include    <algorithm>
#include    <fmod.hpp>
#include    "GlutCameraWindow.h"
#include    "Program.h"
#include    "BasicMesh.h"
#include    "Texture.h"
#include    "Framebuffer.h"
#include    "ScreenQuad.h"
#include    "randUtils.h"

#define SIZE    1000.0f

inline FMOD_VECTOR  toFmod ( const glm::vec3& v )
{
    return { v.x, v.y, v.z };
}

class   SoundWindow : public GlutCameraWindow
{
    Program         program, program2;
    VertexBuffer    posBuf;             // vector of vec4
    VertexBuffer    drawBuf;            // vector of DrawElementsIndirectCommand
    BasicMesh     * room      = nullptr;
    BasicMesh     * knot      = nullptr;  
    Texture     decalMap;
    Texture     stoneMap;

    std::vector<BasicMesh *>    boxes;

    FMOD::System * system    = nullptr;    
    FMOD::Sound  * sound1    = nullptr;
    FMOD::Sound  * sound2    = nullptr;
    FMOD::Sound  * sound3    = nullptr;
    FMOD::Channel * channel1 = nullptr;
    FMOD::Channel * channel3 = nullptr;
    FMOD::Geometry * geometry = nullptr;
    
public:
    SoundWindow () : GlutCameraWindow ( 100, 100, 900, 900, "FMOD 3D sound example" ) 
    {
        if ( !program.loadProgram ( "decal.glsl" ) )
            exit ( "Error loading shader: %s\n", program.getLog ().c_str () );
        
        program.bind       ();
        program.setTexture ( "imageMap", 0 );
        program.unbind     ();

        createBoxes ();

        decalMap.load2D ( "../../Textures/oak.jpg" );
        stoneMap.load2D ( "../../Textures/block.jpg" );

        camera.moveTo ( glm::vec3 ( -0.5, 0.5, 5 ) );
        
        if ( FMOD::System_Create ( &system ) != FMOD_OK )
            exit ( "Error initializing FMOD::System !" );
        
        int driverCount = 0;
 
        system->getNumDrivers ( &driverCount );
 
        printf ( "Driver count %d\n", driverCount );
        
        if ( driverCount < 1 )
            exit ( "Cannot find required audio drivers !" );
    
        system->init ( 36, FMOD_INIT_NORMAL, nullptr );
        
            // load all sounds
        system->createSound ( "wavdata/Gun1.wav",  FMOD_3D, 0, &sound1 );
        system->createSound ( "wavdata/laser.wav", FMOD_3D, 0, &sound2 );
        system->createSound ( "wavdata/meow1.wav", FMOD_3D, 0, &sound3 );

            // create geometry
        system->createGeometry ( 100, 1000, &geometry );
        
        
            // setup meow sound
        auto pos = toFmod ( glm::vec3 ( 1.0f, 1.0f, 2.0f) );
        auto vel = toFmod ( glm::vec3 ( 0.0f) );

        sound3->setMode ( FMOD_LOOP_NORMAL );
        system->playSound ( sound3, nullptr, false, &channel3 );
        channel3->set3DAttributes ( &pos, &vel );
    }

    ~SoundWindow ()
    {
        system->release ();
    }
    
    void redisplay () override
    {
        //glEnable ( GL_DEPTH_TEST  );
        glClear  ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
        
        program.bind             ();
        program.setUniformMatrix ( "proj",  camera.getProjection () );
        program.setUniformMatrix ( "mv",    camera.getModelView  () );
        
        decalMap.bind ();
        room -> render ();
        decalMap.unbind ();

        stoneMap.bind ();

        for ( auto * p : boxes )
            p -> render ();

        stoneMap.unbind ();

        program.unbind ();

    }

    void    keyTyped ( unsigned char key, int modifiers, int x, int y ) override
    {
        GlutWindow::keyTyped ( key, modifiers, x, y );

        if ( key == 27 || key == 'q' || key == 'Q' )    // quit requested
            ::exit ( 0 );
            
        glm::vec3   mv ( 0 );

        if ( key == 'w' || key == 'W' )
            mv = step * camera.getViewDir ();
        else
        if ( key == 'x' || key == 'X' )
            mv = -sideStep * camera.getViewDir ();
        else
        if ( key == 'a' || key == 'A' )
            mv = -step * camera.getSideDir ();
        else
        if ( key == 'd' || key == 'D' )
            mv = sideStep * camera.getSideDir ();
        else
        if ( key == ' ' )
        {
            auto    p = toFmod ( glm::vec3 ( -1.0f, 1.0, 3.0f ) );
            auto    v = toFmod ( glm::vec3 ( 0.0f ) );
            
            sound1->setMode ( FMOD_LOOP_OFF );  // FMOD_LOOP_NORMAL
            system->playSound ( sound1, nullptr, false, &channel1 );
            channel1->set3DAttributes ( &p, &v );
        }

        camera.moveBy ( glm::vec3 ( mv.x, 0.0f, mv.z ) );
        setListener ( camera.getPos (), glm::vec3 ( 0.0f ), camera.getViewDir (), camera.getUpDir () );
    }

    void mouseWheel ( int wheel, int dir, int x, int y ) override {}

    void    mousePassiveMotion ( int x, int y ) override
    {
        GlutCameraWindow::mousePassiveMotion ( x, y );
        setListener ( camera.getPos (), glm::vec3 ( 0.0f ), camera.getViewDir (), camera.getUpDir () );

    }
    void    setListener ( const glm::vec3& p, const glm::vec3& v, const glm::vec3& frw, const glm::vec3& u )
    {
        FMOD_VECTOR pos     = toFmod ( p   );
        FMOD_VECTOR vel     = toFmod ( v   );
        FMOD_VECTOR forward = toFmod ( frw );
        FMOD_VECTOR up      = toFmod ( u   );

        system->set3DListenerAttributes ( 0,  &pos,  &vel,  &forward, &up );
    }

    void    idle ()
    {
        GlutWindow::idle ();
        system->update ();
    }

protected:
    void    createBoxes ()
    {
        room = createHorQuad ( glm::vec3 ( -SIZE, -0.1, -SIZE ), SIZE*2, SIZE*2 );

        boxes.push_back ( createBox ( glm::vec3 ( 0, -0.1, 2 ), glm::vec3 ( 2, 1, 3 ) ) );
        boxes.push_back ( createBox ( glm::vec3 (-2, -0.1, -1 ), glm::vec3 (2, 1, 2) ) );
        boxes.push_back ( createBox ( glm::vec3 ( 3, -0.1, 1), glm::vec3 (3, 1, 2) ) );
        
        addBox ( glm::vec3 ( 0, -0.1, 2 ), glm::vec3 ( 2, 1, 3 ) );
        addBox ( glm::vec3 (-2, -0.1, -1 ), glm::vec3 (2, 1, 2) );
        addBox ( glm::vec3 ( 3, -0.1, 1), glm::vec3 (3, 1, 2) );
    }
    
    void    addBox ( const glm::vec3& pos, const glm::vec3& size )
    {
        auto    sx = glm::vec3 ( size.x, 0.0f, 0.0f );
        auto    sz = glm::vec3 ( 0.0f, 0.0f, size.z );
        auto    sy = glm::vec3 ( 0.0f, 1.0f, 0.0f );
        
        addSide ( pos,           pos + sz,      sy );
        addSide ( pos + sx,      pos + sx + sz, sy );
        addSide ( pos + sx + sz, pos + sz,      sy );
        addSide ( pos,           pos + sz,      sy );
    }
    
    void    addSide ( const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& sy )
    {
        FMOD_VECTOR v [] = { toFmod ( p1 ), toFmod ( p2 ), toFmod ( p2 + sy ), toFmod ( p1 + sy ) };
               
        geometry->addPolygon( 1, 1, true, 4, v, nullptr );
    }
};

int main ( int argc, char * argv [] )
{
    GlutWindow::init ( argc, argv, 4, 6 );
    
    SoundWindow win;
    
    return GlutWindow::run ();
}

По этой ссылке можно скачать весь исходный код к этой статье.