понедельник, 13 июня 2011 г.

Введение в OpenAL

Графика, графика, графика. А попробуйте отключить звук у своей любимой игры. Ну, как впечатления? То-то же. :)
Аннотация
Итак, звук и музыка. Неотъемлемые части любой игры. Существует несколько библиотек для работы с ними. Это всем известные DirectSound / DirectMusic, несколько менее распространенных: FMOD, Audiere, BASS (правда они решают несколько иные задачи). Есть еще одна. Она тоже еще не очень распространена, но поддержка со стороны такого гиганта мультимедиа индустрии как Creative Labs, играет большую роль. Как вы уже, наверное, догадались, речь идет об Open Audio Library (OpenAL). Это бесплатная, мультиплатформенная, расширяемая, с доступными исходными текстами библиотека для работы со звуком в трехмерном пространстве. Название очень созвучно с OpenGL, неправда ли? И не с проста. Человеку, имеющему опыт работы в OpenGL, будет очень легко понять принципы работы с аудио библиотекой. Дальше, при более близком рассмотрении вы увидите практически абсолютное сходство. Плюс, OpenAL небольшая, полная документация занимает 50 страниц текста.
Основные концепции и принципы.
Все функции процессинга и изменения состояния в ОpenAL, применяются к так называемым аудио-контекстам (audio context), которых может быть несколько. Все контексты создаются для аудио устройства (audio device), которое, естественно, одно.
Основой библиотеки служат 3 кита: единственный слушатель (Listener) – место, откуда мы слышим окружающие звуки, множество буферов (Buffers) звука, которые содержат несжатые аудио данные и множество источников звука (Sources), располагающиеся в 3д пространстве и воспроизводящие звуки из буферов.
В OpenAL, так же как и в OpenGL, все функции подчиняются определённому соглашению об именовании. Все они начинаются с префикса “al” затем идет имя функции, а в конце, суффикс, определяющий тип ключевого параметра: i – int, f – float, v – вектор, и т.д.
Подобным образом объявлены и типы: ALuint, ALboolean, ALclampf и т.д.
OpenAL, является моделью конечного автомата, и всегда она (и все её объекты) находятся в каком-либо состоянии. Множества этих состояний контролируется с помощью функций Get, Enable, Disable и т.д.
Имеется даже поддержка расширений (а вы как думали! :)), коих уже существует несколько.
Звуковой класс.
Ну вот мы и добрались до программирования.
Для работы с библиотекой вам понадобится OpenAL SDK, который можно скачать по адресу: http://connect.creativelabs.com/developer/. Надеюсь прописать пути в настройках VC++ вы сможете самостоятельно.:)
Итак, подключите нужные заголовочные файлы и библиотеки:
#include <al.h>
#include <alc.h>
#include <alu.h>
#include <alut.h>

#pragma comment(lib, "alut.lib")
#pragma comment(lib, "OpenAL32.lib")
Разрабатывать мы с вами будем самодостаточный класс для проигрывания звуков различных форматов.
Но перед использованием возможностей OpenAL сначала необходимо проинициализировать библиотеку, для дальнейшего корректного её функционирования.
Итак, 2 функции: Инициализация и Деинициализация.
bool InitializeOpenAL()
{
    // Позиция слушателя.
  ALfloat ListenerPos[] = { 0.0, 0.0, 0.0 };

  // Скорость слушателя.
  ALfloat ListenerVel[] = { 0.0, 0.0, 0.0 };

// Ориентация слушателя. (Первые 3 элемента – направление «на», последние 3 – «вверх»)
  ALfloat ListenerOri[] = { 0.0, 0.0, -1.0,  0.0, 1.0, 0.0 };

    // Открываем заданное по умолчанию устройство
    pDevice = alcOpenDevice(NULL);
    // Проверка на ошибки
    if (!pDevice)
    {
        ERRMSG("Default sound device not present");
        return false;
    }
    // Создаем контекст рендеринга
    pContext = alcCreateContext(pDevice, NULL);
    if (!CheckALCError()) return false;
  
    // Делаем контекст текущим
    alcMakeContextCurrent(pContext);

    // Устанавливаем параметры слушателя
    // Позиция
    alListenerfv(AL_POSITION,    ListenerPos);
    // Скорость
    alListenerfv(AL_VELOCITY,    ListenerVel);
    // Ориентация
    alListenerfv(AL_ORIENTATION, ListenerOri);
    return true;
}

void DestroyOpenAL()
{
  // Очищаем все буффера
  for (TBuf::iterator i = Buffers.begin(); i != Buffers.end(); i++)
    alDeleteBuffers(1, &i->second.ID);
  // Выключаем текущий контекст
  alcMakeContextCurrent(NULL);
  // Уничтожаем контекст
  alcDestroyContext(pContext);
  // Закрываем звуковое устройство
  alcCloseDevice(pDevice);
}
Как вы видите, все довольно тривиально. В инициализации мы открываем звуковое устройство, заданное по умолчанию. В win32 системах используется DirectSound. Если вы хотите явно задать желаемое устройство, вместо NULL передайте строку с названием вашего устройства. Далее создаем контекст и делаем его текущим, т.е. активным. Как я уже упоминал, все операции в дальнейшем будут применимы только с текущему контексту. Аудио контекстов (не удивляйтесь, в терминах OpenAL они, также как и в OpenGL, называются контекстами рендеринга) может быть множество.
Затем происходит настройка параметров слушателя, которых, я напомню, у нас 1 на контекст. За разъяснением значения параметров, читайте комментарии, и раздел про генерацию источников звука.
В деинициализации всё наоборот. Сначала удаляем все используемые буфера (о них мы позже поговорим) потом уничтожаем контекст, и устройство.
Хочется отметить, что в состав OpenAL входит подбиблиотека, облегчающая работу со звуком. Называется она alut (Вот вам еще одно сходство с OGL. Там glut). В неё, пока что, входит всего 5 функций: 2 для инициализации/деинитиализации, и 3 для загрузки wav файлов. С помощью alut инициализацию OpenAL можно провести вызовом:
alutInit(NULL, NULL);
Так как мы легких путей не ищем, а хотим разобраться во всём, то основным методом я сделал инициализацию именно через alc* функции. Каким пользоваться в вашем приложении, решать вам.
Так же было реализовано 2 функции проверки результатов выполнения al* и alc* функций. Это CheckALError() и CheckALCError() соответственно. Вот их реализация:
ALboolean CheckALCError()
{
  ALenum ErrCode;
  string Err = "ALC error: ";
  if ((ErrCode = alcGetError(pDevice)) != ALC_NO_ERROR)
  {
    Err += (char *)alcGetString(pDevice, ErrCode);
    ERRMSG(Err.data());
    return AL_FALSE;
  }
  return AL_TRUE;
}

ALboolean CheckALError()
{
  ALenum ErrCode;
  string Err = "OpenAL error: ";
  if ((ErrCode = alGetError()) != AL_NO_ERROR)
  {
    Err += (char *)alGetString(ErrCode);
    ERRMSG(Err.data());
    return AL_FALSE;
  }
  return AL_TRUE;
}
Все до ужаса тривиально. Получаем код ошибки. Если он не равен AL_NO_ERROR (ошибки нет), то вызываем функцию alGetString/alcGetString, которая возвращает предопределенную строку, содержащую информацию о заданной ошибке.
Теперь взглянем на класс звука:
class remSnd  
{
public:
  ALfloat mVel[3];
  ALfloat mPos[3];
  bool  mLooped;
  
  // Functions
  bool Open(const std::string &Filename, bool Looped, bool Streamed);
  bool IsStreamed();
  void Play();
  void Close();
  void Update();
  void Move(float X, float Y, float Z);
  void Stop();

  // Construction/destruction
  remSnd();
  virtual ~remSnd();

private:
  // Идентификатор источника
  ALuint      mSourceID;
  // Потоковый ли наш звук?
  bool      mStreamed;

  bool LoadWavFile (const std::string &Filename);
};
Структура не сложная. На что стоит обратить внимание:
1)  Функция Open(). Открывает файл с именем Filename, определяет его тип (пока что наш класс будет уметь проигрывать лишь wav файлы), и подготавливает источник к проигрыванию данных из буфера, или буферов. Напомню, что сам источник не содержит аудио данных, он содержит только идентификатор буфера, в котором хранятся эти самые данные. Параметр Looped означает, что наш звук будет проигрываться бесконечно в цикле, а параметр Streamed - что будет реализовано потоковое проигрывание. Это нужно для фоновой музыки, или больших по размеру звуков. При стандартном способе проигрывания весь звук загружается в буфер, и оттуда играется. Это приемлемо для коротких звуков. А теперь представьте, что нам надо проиграть музыку длинною в 5-6 минут. Загрузка всей музыки в один буфер, в котором семплы хранятся в несжатом PCM формате, будет как очень долгой, так и совершенно не рациональной по отношению к расходуемой памяти (для примера ogg файл размером в 4Мб в PCM формате занимает 30-40Mб). Так как OpenAL не содержит встроенной поддержки потокового проигрывания, но содержит немного средств, для её реализации, то мы реализуем алгоритм потокового проигрывания, основанный на разбитии всего аудио-файла на маленькие кусочки, последовательной загрузки этих кусочков в буфера, и поочерёдного их проигрывания. Для wav файлов данную функциональность я не реализовывал по ненадобности, поэтому для wav звуков параметр Streamed попросту игнорируется. Все подробности мы обсудим во второй части статьи, где научимся проигрывать Ogg файлы.
2)  Член класса mSourceID, содержит уникальный идентификатор источника звука, который используется, где необходимо взаимодействие с этим источником.
3)  Функции Play(), Stop(), Close(), я полагаю, говорят сами за себя. Функция Update() – предназначена для смены буферов для потокового звука. Если звук цельный, т.е. полностью содержится в единственном буфере, то эта функция ничего не делает.
4)  mVel[3], mPos[3] – положение и скорость источника в 3Д пространстве. Эти 2 переменные содержат x,y,z координаты. Надо отметить, также, что координатная система в OpenAL абсолютно такая же, как и в OpenGL
Теперь, давайте, взглянем на реализацию наших функций.
bool remSnd::Open(const string &Filename, bool Looped, bool Streamed)
{
  // Проверяем файл на наличие
  ifstream a(Filename.c_str());
  if (!a.is_open()) return false;
  a.close();

  mLooped    = Looped;

  // Создаем источник соответствующий нашему звуку
  alGenSources(1, &mSourceID);
  if (!CheckALError()) return false;

  alSourcef (mSourceID, AL_PITCH,    1.0f);
  alSourcef (mSourceID, AL_GAIN,    1.0f);
  alSourcefv(mSourceID, AL_POSITION,  mPos);
  alSourcefv(mSourceID, AL_VELOCITY,  mVel);
  alSourcei (mSourceID, AL_LOOPING,  mLooped);

  // Extract ext
  string Ext = ExtractFileExt(Filename).data();
  if (Ext == "WAV") return LoadWavFile(Filename);
/*if (Ext == "OGG") 
  {
    mStreamed = Streamed;
    return LoadOggFile(Filename, Streamed);
  } */
  return false;
}
Источник звука создается функцией alGenSources(), которой в качестве параметров передается количество создаваемых источников (у нас 1) и адрес переменной (или первого элемента массива), которая будет содержать идентификатор сгенерированного источника. Далее идет установка базовых параметров источника.
AL_PITCH – тон звука
AL_GAIN – усиление звука. Этот параметр влияет на то, как будет изменяться сила звука, по мере изменения расстояния от источника до слушателя.
AL_POSITION – позиция источника в трёхмерных координатах.
AL_VELOCITY – скорость движения звука. Работает это параметр не так как можно предположить изначально. Если вы установите этот параметр в какое-то значение, то при выполнении программы, ваш звук не будет двигаться согласно скорости заданной этим параметром. Этот параметр используется, всего лишь, как контейнер значения скорости, использовать который вы можете, как захотите.
AL_LOOPING – значение этого параметра определяет, будет ли ваш звук зациклен.
Существует еще несколько параметров управляющих поведением источника звука. Это, например, DIRECTION, CONE_INNER_ANGLE, CONNER_OUTER_ANGLE, и др. В данной статье я на них останавливаться не буду, за подробностями обращайтесь к документации в SDK.
Так же есть очень важный параметр, без которого никакого звука не будет – AL_BUFFER. Этот параметр являет идентификатор созданного буфера, содержащего аудиоданные. Так как, пока что, буфер мы еще не создали, то на данном этапе данное свойство для источника мы не задаем.
Далее мы определяем тип звукового файла по его расширению. Пока что, работаем только с файлами типа WAV, раздел для OGG формата раскомментируем во второй части статьи.
void remSnd::Play()
{
  alSourcePlay(mSourceID);
}

void remSnd::Close()
{
  alSourceStop(mSourceID);
  if (alIsSource(mSourceID)) alDeleteSources(1, &mSourceID);
}

void remSnd::Stop()
{
  alSourceStop(mSourceID);
}

void remSnd::Move(float X, float Y, float Z)
{
  ALfloat Pos[3] = { X, Y, Z };
  alSourcefv(mSourceID, AL_POSITION, Pos);
}
Управляющие функции. Все в пару строк.
Play() – начинаем проигрывание, вызовом одной единственной функции – alSourcePlay(), передавая ей в качестве параметра идентификатор источника. Думаю, здесь уместно сказать о состояниях, в которых может прибывать источник звука, и которые мы можем получить с помощью функции alGetSourcei(). Их всего 4: AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED. Названия, как мне кажется, ясно говорят сами за себя. Исключением, разве что является AL_INITIAL. Это состояние соответствует проинициализированному источнику, но который еще ни разу не играл мелодию из буфера.
Процедура Stop() останавливает проигрывание звука, если таковое имело место
Close() – останавливает проигрывание, и удаляет источник по заданному идентификатору. Здесь я нарочно применил функцию alIsSource(), чтобы показать, что есть средства проверки соответствия идентификаторов реальным объектам. Такая же функция существует и для буферов.
Move() двигает источник в соответствии с новыми координатами.
Теперь подошла очередь загрузки звука в буфера. Поэтому, рассмотрим функцию, которая это реализует для WAV файлов: LoadWavFile().
typedef struct 
{
  unsigned int  ID;
   string    Filename;
  unsigned int  Rate;
  unsigned int  Format;
} SndInfo;

map<ALuint, SndInfo> Buffers;

bool remSnd::LoadWavFile(const string &Filename)
{
  // Структура содержащая аудиопараметры
  SndInfo    buffer;
   // Формат данных в буфере
  ALenum    format;
   // Указатель на массив данных звука
  ALvoid    *data;
   // Размер этого массива
  ALsizei    size;
   // Частота звука в герцах
  ALsizei    freq;
   // Идентификатор циклического воспроизведения
  ALboolean  loop;
   // Идентификатор буфера
  ALuint    BufID = 0;

  // Заполняем SndInfo данными
  buffer.Filename = Filename;
  // Ищем, а нет ли уже существующего буфера с данным звуком?
  for (TBuf::iterator i = Buffers.begin(); i != Buffers.end(); i++)
  {
    if (i->second.Filename == Filename) BufID = i->first;
  }

  // Если звук загружаем впервые
  if (!BufID)
  {
    // Создаём буфер
    alGenBuffers(1, &buffer.ID);
    if (!CheckALError()) return false;
    // Загружаем данные из wav файла
    alutLoadWAVFile((ALbyte *)Filename.data(), &format, &data,
                     &size, &freq, &loop);
    if (!CheckALError()) return false;
  
    buffer.Format      = format;
    buffer.Rate      = freq;
    // Заполняем буфер данными
    alBufferData(buffer.ID, format, data, size, freq);
    if (!CheckALError()) return false;
    // Выгружаем файл за ненадобностью
    alutUnloadWAV(format, data, size, freq);
    if (!CheckALError()) return false;

    // Добавляем этот буфер в массив
    Buffers[buffer.ID] = buffer;
  }
  else 
    buffer = Buffers[BufID];

  // Ассоциируем буфер с источником
  alSourcei (mSourceID, AL_BUFFER, buffer.ID);

  return true;
}
Как мне кажется, большинство моментов понятно из комментариев. Необходимо сказать пару слов способе хранения буферов в программе. Для этого мы создали структуру SndInfo, которая содержит в себе все важные параметры буфера со звуковыми данными. Это идентификатор буфера, имя файла, частота звука и его формат (моно 8 бит, моно 16 бит, стерео 8 бит, стерео 16 бит). Тут же, хочу сказать, что 3д позиционирование работает только для моно звуков, для стерео – нет.
Затем мы создали map для централизованного хранения всех структур. Для чего? OpenAL обладает очень полезной особенностью – множество источников звука могут использовать один и тот же звуковой буфер. Вот для этого мы и создали ассоциативный массив описаний уже существующих буферов. При попытке загрузки звука, который был уже загружен ранее, сначала произойдет поиск звука уже в существующих буферах, и только при отрицательных результатах, произойдет создание буфера и загрузка в него звука из файла. Если же, такой звук уже существует в массиве Buffers, то к текущему источнику будет проассоциирован идентификатор соответствующего буфера. Оптимизация налицо: уменьшение количества занимаемой памяти (за счет избежания дублирований) и ускорение загрузки звуков.
Обратите внимание, что здесь мы опять встретились со старым знакомым – alut библиотекой в лице 2-х функций: alutLoadWAVFile() и alutUnloadWAV(), которые здорово упрощают загрузку wav файлов, но не позволяют реализовать потоковое проигрывание, так как заполняют единственный буфер всем содержимым файла. Так что мой вам совет: не используйте больших WAV файлов. :-) Очень важной строкой является:
alSourcei (mSourceID, AL_BUFFER, buffer.ID),
которая и осуществляет ассоциирование источника и буфера, используя и идентификаторы.
Применение.
Применение легко и понятно:
1)  Создаем объект звукового класса: remSnd Snd3D;
2)  Инициализируем OpenAL библиотеку, в функции инициализации приложения:
InitializeOpenAL();
3)  Загружаем звук:  Snd3D.Open("motor_a8.wav", true, false); Напомню, первый параметр – зацикленность, второй – потоковое проигрывание (для wav не поддерживается).
4)  Мучаем звук, как хотим: Snd3D.Play(), Snd3D.Move(…), Snd3D.Stop().
5)  Удаляем звук в процедуре деинициализации приложения: Snd3D.Close().
6)  Деинициализируем OpenAL: DestroyOpenAL();
Полезные ссылки.
1)  Официальный ресурс: http://www.openal.org/.
2)  Creative OpenAL SDK: http://connect.creativelabs.com/developer/
3)  Devmaster.net Tutorials (in English): http://www.devmaster.net/articles/openal-tutorials/lesson1.php
4)  Тут на сайте в форумах девелоперы, предпочитающие Delphi, кричат, что их обделили. :) Для них тоже есть туториалы: http://www.noeska.com/doal/tutorials.aspx

Комментариев нет:

Отправить комментарий