Написать пост

Базовый менеджер звука на OpenAL для вашего проекта. Разработка звукового движка

Аватар Типичный программист

Практически в любом приложении используется звук для создания атмосферы, указания на нажатие определенной кнопки и т.п. В связи с этим перед разработчиком встает задача создания особого механизма, позволяющего проигрывать звук в различных форматах.
Дабы помочь вам разобраться с основными аспектами, был подготовлен цикл статей, посвященных созданию базового звукового менеджера.

В первой статье вашему вниманию предложен алгоритм разработки звукового менеджера.

При работе со звуком вам необходимо удостовериться, что он проигрывается вместе с главным циклом и верно синхронизирован. Вашей первой мыслью, скорее всего, будет использование потоков. Однако вы можете обойтись и без них, но этот подход будет иметь некоторые недостатки (которые будут упомянуты позже).

Первое важное решение: какую использовать библиотеку? Подобных разработок немало, но лишь некоторые из них бесплатны. Если вы ищете универсальную библиотеку, которая будет работать на компьютерах и мобильных устройствах, то, возможно, ваше внимание привлечет OpenAL. Она поддерживается как iOS, так и операционными системами Windows, Linux, Mac.

OpenAL — это библиотека, написанная на языке C и использующая API, подобное тому, что применяется в OpenGL. В OpenGL все функции начинаются с префикса g1, в OpenAL используется префикс a1.

Для Windows вам необходимо будет использовать Исходный программный код, который отличается от основного кода, и является новым продуктомответвленную версию библиотеки. OpenAL была создана фирмой Creative, на сегодняшний день эта библиотека устарела и больше не обновляется (последняя версия API 1.1 от 2005 года). К счастью, существует Ответвление оригинального OpenALOpenAL Soft implementation, которая использует то же самое API, что и оригинальная OpenAL.

Для устройств Apple, работающих под управлением iOS, ситуация намного лучше. OpenAL напрямую поддерживается Apple, потому вам не надо ничего устанавливать, просто добавить в ваш проект ссылки на соответствующие библиотеки. Подробная информация представлена здесь.

Одним из самых больших недостатков OpenAL является отсутствие поддержки ОС Android, которая использует OpenSL. После недолгих поисков вы можете обнаружить «порты» OpenAL для Android. Они сопоставляют вызовы функций OpenAL вызовам соответствующих функций OpenSL, так что это фактически  Программное средство создания системной оболочки для стандартизации внешних обращений и изменения функциональной ориентации действующей системы]упаковщик. Один из них можно найти здесь (GitHub).

Он использует упомянутый ранее OpenAL Soft, но скомпонован с другими флагами.

После выбора библиотеки, вы должны выбрать поддерживаемые звуковые форматы, которые хотите проигрывать. Любимый MP3 — не лучший выбор, декодер сложный, а также есть зарегистрированные на данный формат патенты. Лучшим выбором является OGG. Декодер прост в использовании, открыт и OGG файлы часто имеют меньший размер, чем MP3 с теми же настройками. К тому же, он поддерживает несжатое WAV.

Разработка звукового движка

Начнем с разработки звукового движка и определимся, что вам нужно, чтобы он работал правильно.

В первую очередь, вам понадобится библиотека OpenAL. OpenAL является библиотекой C. Для упрощения манипуляций желательно добавить какую-либо объектно-ориентированную оболочку. Мы воспользуемся C++, но подобная конструкция может быть использована также и в других языках (конечно, вам понадобится «порт» библиотеки OpenAL с С на ваш язык).

Помимо OpenAL, вам также потребуется поддержка потоков. В нашем примере будет использована библиотека PTHREAD (версия для Windows). Если вы ориентируетесь на C++ 11, вы также можете воспользоваться встроенной поддержкой потоков.

Для OGG декомпрессии вам понадобится библиотека OGG Vorbis (скачать части libogg и libvorbis).

WAV-файлы используют не очень часто, в большинстве случаев для отладки, но желательно обеспечить поддержку и этого формата. Простую WAV-декомпрессию легко написать с нуля, потому лучше использовать свое решение, а не готовую библиотеку.

Проект состоит из двух основных классов, одного интерфейса (чисто виртуального класса), а также одного класса для каждого из поддерживаемых аудио-форматов (OGG, WAV…).

  1. SoundManager — главный класс, использующий одноэлементную модель. В данном случае это самый хороший вариант, так как вы, вероятно, проинициализируете только один экземпляр OpenAL. Этот класс используется для управления и обновления всех звуков, а также он содержит ссылки на все SoundObjects.
  2. SoundObject — наш главный класс для работы со звуком, в котором описаны такие методы, как: воспроизведение, пауза, перемотка, обновление…
  3. ISoundFileWrapper — интерфейс (чисто виртуальный класс) для различных форматов файлов, описания методов декомпрессии, заполнения буферов и т.д.
  4. Wrapper_OGG — класс , который реализует ISoundFIleWrapper. Для декомпрессии OGG-файлов.
  5. Wrapper_WAV — класс, который реализует ISoundFIleWrapper. Для декомпрессии WAV-файлов.

Инициализация OpenAL

Код, описанный в данном разделе, можно найти в классе Soundmanager. Начнем с кода для инициализации OpenAL.

			alGetError();

ALCdevice * deviceAL = alcOpenDevice(NULL);

if (deviceAL == NULL)
   {
     LogError("Failed to init OpenAL device.");
     return;
   }

ALCcontext * contextAL = alcCreateContext(deviceAL, NULL);

AL_CHECK(alcMakeContextCurrent(contextAL));
		

В дальнейшем устройства и контекстные переменные нам больше не понадобятся, но только до этапа освобождения ресурсов. OpenAL сохраняет все эти данные во внутренних структурах.

В функции alcMakeContextCurrent описан макрос AL_CHECK, который используется для проверки ошибок OpenAL в режиме отладки. Его код представлен ниже:

			const char * GetOpenALErrorString(int errID)
{
    if (errID == AL_NO_ERROR) return "";
    if (errID == AL_INVALID_NAME) return "Invalid name";
    if (errID == AL_INVALID_ENUM) return " Invalid enum ";
    if (errID == AL_INVALID_VALUE) return " Invalid value "; 
    if (errID == AL_INVALID_OPERATION) return " Invalid operation ";
    if (errID == AL_OUT_OF_MEMORY) return " Out of memory like! ";

    return " Don't know ";
}
		
			inline void CheckOpenALError(const char* stmt, const char* fname, int line)
{
    ALenum err = alGetError();
    if (err != AL_NO_ERROR)
       {
           LogError("OpenAL error %08x, (%s) at %s:%i - for %s", err, 
           GetOpenALErrorString(err), fname, line,stmt);
       }
};
		
			#ifndef AL_CHECK
#ifdef _DEBUG
     #define AL_CHECK(stmt) do
        { \
           stmt; \
           CheckOpenALError(#stmt, __FILE__, __LINE__); \
        }
        while (0);

#else
     #define AL_CHECK(stmt) stmt
#endif
#endif
		

Этот макрос можно использовать при каждом вызове OpenAL.

Далее вам следует проинициализировать источники данных и буферы, но можете сделать это и позже, когда они действительно понадобятся.

Буферы, как вы, вероятно, думаете, содержат несжатые данные, которые проигрывает OpenAL. Источник (фактически — звук, который будет воспроизводиться) загружает данные из ассоциированных с ним буферов. Есть определенные ограничения на количество буферов и источников. Точное значение зависит от вашей системы. Давайте создадим для начала 512 буферов и 16 источников, чтобы иметь возможность воспроизводить 16 звуков одновременно.

			for (int i = 0; i < 512; i++)
   {
      SoundBuffer buffer;
      AL_CHECK( alGenBuffers((ALuint)1, &buffer.refID) );
      this->buffers.push_back(buffer);
   }

for (int i = 0; i < 16; i++)
   {
      SoundSource source;
      AL_CHECK( alGenSources((ALuint)1, &source.refID)) ;
      this->sources.push_back(source);
   }
		

Вы можете заметить, что функция alGen* в качестве второго параметра получает указатель на unsigned int, который является идентификатором созданного буфера или звука. Для удобства можно создать простую структуру, элементами которой будут идентификатор и булевая переменная, показывающая состояние: свободен буфер или используется каким-либо звуком.

Помимо этого, создадим два списка:

  1. список всех источников и буферов;
  2. список, содержащий только те ресурсы, которые свободны (не связаны с каким-либо звуком).
			for (uint32 i = 0; i < this->buffers.size(); i++)
   {
      this->freeBuffers.push_back(&this->buffers[i]);
   }

for (uint32 i = 0; i < this->sources.size(); i++)
   {
      this->freeSources.push_back(&this->sources[i]);
   }
		

 

Если вы используете потоки, то также должны проинициализировать их.

Теперь, когда вы подготовили все необходимое, пришло время добавить звук в ваш движок.

Логика воспроизведения звука

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

Первое: вы можете загрузить все звуковые данные в один буфер и просто воспроизвести их. Это легкий и быстрый способ. Но, как обычно, у простых решений есть недостатки. Размер несжатых файлов намного больше, чем сжатых. Представьте себе, вы будете использовать больше одного звука. И в итоге, размер всех буферов может оказаться больше, чем доступной свободной памяти. Что теперь?

К счастью, есть второй подход: в один буфер загружается лишь небольшая часть файла, воспроизводится, а затем загружается новая порция. Звучит хорошо, не так ли? Но, на самом деле, это не так. Если воспользоваться этим методом, вы можете услышать паузы в конце воспроизведения данных перед новым заполнением буфера и последующим воспроизведением. Решается эта проблема просто: необходимо заполнять не один буфер, а несколько. Заполните несколько буферов (например, три), воспроизведите данные из первого и, как только его содержимое будет воспроизведено, немедленно запустите воспроизведение из второго и в это же время заполните уже использованный буфер новыми данными. Выполняйте эту операцию в цикле, пока не будет воспроизведен весь звук.

Количество используемых буферов может изменяться в зависимости от ваших потребностей. Если же ваш звуковой движок обновляется из отдельного потока, количество не будет проблемой. Вы можете выбрать практически любое количество буферов, и это будет просто отлично. Однако, если вы не используете update вместе с основным циклом вашего двигателя (не применяете потоки), то могут возникнуть проблемы при малом количестве буферов. Почему? Представьте, что у вас есть Windows – приложение, и вы перетащили окно программы по рабочему столу. В Windows это может привести к тому, что основной поток будет приостановлен и находиться в состоянии ожидания. Звук будет играть, так как сама OpenAL имеет свой собственный поток для воспроизведения звуков, но только до тех пор, пока в очереди есть буферы, которые можно воспроизводить. Если все они будут исчерпаны, звук прекратится. Причина тому — заблокированный основной поток, в результате чего буферы не обновляются.

Каждый буфер имеет свой размер в байтах, который устанавливается во время создания звука. Чтобы вычислить продолжительность звука в буфере, вы можете воспользоваться этим уравнением:

			duration = BUFFER_SIZE/(sound.freqency * sound.channels * sound.bitsPerChannel/8
		

Примечание: Если вы хотите вычислить продолжительность воспроизведения текущей записи, вы должны учитывать все буферы, но это не так просто. Мы рассмотрим это в одной из последующих статей.

Перевод статьи: «Basic OpenAL sound manager for your project»

6К открытий6К показов