material

Простейший звуковой движок на библиотеке Bass

Теги: Статьи, Программирование, Игры

Сегодня у меня отличное настроение, и я решил как-то разнообразить приветственную фразу - нехорошо, если каждая статья будет начинаться с одних и тех же слов, а без приветствия - тоже нельзя. Итак,

Всем добрейшего денечка

А мы приступаем к чтению моей новой интересной статьи. На этот раз будем писать простейший звуковой движок с использованием библиотеки bass.dll на Delphi.

Техническое задание

Что должен уметь наш звуковой движок? Ну, если он будет простейшим, то и содержать в себе должен только самые необходимые функции:

  • Инициализация и очистка памяти
  • Воспроизведение файла
  • Зацикливание
  • Остановка
  • Пауза

Зацикленное воспроизведение нужно для фоновой музыки и некоторых звуковых эффектов, имеющих определенную продолжительность. Например, для электрического щита как в Galaxy Boom Mini - звуковой эффект будет повторяться до тех пор, пока мы сами его не прервем.

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

Что такое bass.dll?

Bass.dll - это распространенная мультимедийная библиотека, предназначенная для работы со звуком. С ее использованием, например, написан такой известный музыкальный плеер как AIMP. Здесь можно подробнее почитать о библиотеке, а по этой ссылке - скачать ее с официального сайта.

Вместе с библиотекой, кстати, распространяются и ее заголовочные файлы для различных языков, в том числе, и для Delphi. Заголовочный файл bass.pas мы будем подключать к проекту в разделе uses.

Планирование структуры

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

Где будем хранить потоки?

Для воспроизведения звука библиотека Bass использует потоки - собственный поток создается для каждого нового звука. Обратиться к потоку для получения и установки его параметров (например, громкость воспроизведения) можно по его идентификатору, возвращаемому библиотекой после его создания:

vStream := bass_StreamCreateFile(...);

Вот переменная vStream и будет хранить в себе идентификатор потока. Очень важно помнить о том, что по окончании воспроизведения поток никуда не денется, и память, затраченную на его создание, необходимо очищать вручную соответствующей функцией Bass.dll.

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

Вот, как будет объявляться наш массив:

TSound = record
...id: Cardinal;//Идентификатор потока
...event: Cardinal;//Id события окончания воспроизведения
...isLoop: Boolean;//Зациклено ли воспроизведение?
...isAlive: Boolean;//Жив ли поток?
end;

vSounds: array of TSound;//Массив потоков

Как видите, каждый элемент массива является записью. Здесь есть пара не указанных мною раньше переменных:

  • Event - будет хранить в себе идентификатор события, которое мы назначим для отслеживания окончания воспроизведения потока. Память, занимаемую этим событием, тоже следует очищать.
  • IsLoop - зациклено ли воспроизведение мелодии? Зацикленные потоки не будут удаляться автоматически, и пользователь сам должен инициировать их остановку.

Как будем удалять потоки?

Изначально мы планировали удалять потоки в таймере. То есть, заводим отдельный таймер для звукового движка, скажем, срабатывающий раз в 1-2 секунды, и в нем проверяем каждый поток:

if (bass_ChannelGetLength(stream, 0) = bass_ChannelGetPosition(stream, 0))...

А удовлетворяющие условию - удаляем. Но по некоторым весомым причинам мы отказались от этого способа в пользу второго - назначения события завершения с последующим удалением в нем потока-инициатора.

Использовать ли ООП?

Хороший вопрос. Сначала именно так мы и поступили, но затем столкнулись с некоторыми проблемами, заставившими нас отказаться от объектной реализации в пользу процедурной. Дело в том, что на событие Bass не может быть назначен метод класса, это ограничение накладывается скрытым параметром - указателем на объект, передаваемом Delphi в каждом методе, а используемое нами событие объявлено в Bass.dll как обычная процедура.

Внешние функции движка

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

function soundInitialize(vHandle: LongWord): Boolean;
function soundDestroy(): Boolean;
function soundPlay(vFileName: String; vVolume: Real; vIsLoop: Boolean = false): Integer;
function soundPause(vIsPause: Boolean): Boolean;
function soundStop(vId: Integer): Boolean;

Инициализация и удаление Bass

Здесь все очень просто: создаем две простые, однострочные функции:

//Инициализация библиотеки Bass
function soundInitialize(vHandle: LongWord): Boolean;
begin
...result := bass_Init(-1, 44100, 0, vHandle, nil);
end;

//Удаление библиотеки Bass
function soundDestroy(): Boolean;
begin
...result := bass_Free();
end;

В качестве параметра в первую из них передаем handle нашего приложения.

Воспроизведение звукового файла

function soundPlay(vFileName: String; vVolume: Real; vIsLoop: Boolean = false): Integer;
var
...vPCharName: PChar;//Имя файла в формате PChar
...vStream: Integer;//Индекс потока для воспроизведения
...vFlag: Cardinal;//Флаг для зацикливания
begin
...if (vIsPaused) then exit;//Если воспроизведение на паузе
...vPCharName := PChar(vFileName);//Преобразование строки в PChar
...vFlag := BASS_SAMPLE_LOOP * Byte(vIsLoop);//Флаг для зацикливания воспроизведения
...vStream := innerGetStream();//Получение свободного потока
//Воспроизведение файла
...with vSounds[vStream] do begin
......id := bass_StreamCreateFile(false, vPCharName, 0, 0, vFlag);
......event := bass_ChannelSetSync(Id, BASS_SYNC_END, 0, @onCompleted, nil);
......bass_ChannelSetAttribute(Id, BASS_ATTRIB_VOL, vVolume);//Установка громкости
......bass_ChannelPlay(Id, true);//Начало воспроизведения потока
......isLoop := vIsLoop;
......isAlive := true;
...end;
...if (not(vIsLoop)) then result := -1
...else result := vStream;
end;

Сначала мы подготавливаем данные, получаем свободное место в нашем массиве (функция innerGetStream(), которая будет описана чуть позже). Переменная vFlag определяет специальный флаг, указывающий Bass на необходимость зациклить запущенный поток (Byte(vIsLoop) позволяет избежать лишних условий).

Затем создаем новый поток (bass_StreamCreateFile()), назначаем ему событие окончания воспроизведения (bass_ChannelSetSync(..., BASS_SYNC_END, ...)), устанавливаем громкость и запускаем поток. Если поток должен быть зациклен, наша функция возвратит пользователю идентификатор элемента массива, в который он был записан, чтобы впоследствии пользователь смог инициировать его остановку:

function soundStop(vId: Integer): Boolean;
begin
...if (vSounds[vId].isLoop) then result := innerRemoveStream(vId)
...else result := false;
end;

Это и есть функция принудительной остановки зацикленного потока. Внутренняя функция innerRemoveStream(), как раз, и занимающаяся этим, будет описана ниже.

Временная остановка и возобновление

function soundPause(vIsPause: Boolean): Boolean;
var
...i: Integer;
begin
...for i := 0 to High(vSounds) do begin
......if (vIsPause) then bass_ChannelPause(vSounds[i].id)
......else bass_ChannelPlay(vSounds[i].id, False);
...end;
...vIsPaused := vIsPause;
...result := true;
end;

Проходим по всем существующим потокам и останавливаем их или возобновляем, в зависимости от переданного сюда логического параметра «vIsPause». Эта функция может понадобиться при сворачивании полноэкранного приложения или потере окном фокуса (если создаете оконное приложение).

Событие окончания воспроизведения

Как мы помним, это событие назначается в функции soundPlay() и должно вызывать некоторую внутреннюю функцию для удаления потока, помечая при этом соответствующий элемент массива как «мертвый»:

procedure onCompleted(vHandle, vStream, vData: Cardinal; vUser: Pointer); stdcall;
var
...vId: Integer;
begin
...vId := innerFindStream(vStream);
...if (not(vSounds[vId].isLoop)) then innerRemoveStream(vId);
end;

Внутренняя функция innerFindStream() предназначена для поиска элемента массива по переданному в нее идентификатору потока.

Внутренние функции движка

Внутренних функций всего три:

function innerGetStream(): Integer;
function innerFindStream(vId: Cardinal): Integer;
function innerRemoveStream(vId: Integer): Boolean;

Поиск свободного места

function innerGetStream(): Integer;
var
...i: Integer;
begin
...result := -1;
...//Поиск свободного места в массиве
...for i := 0 to High(vSounds) do begin
......if (vSounds[i].isAlive) then continue;
......result := i;
......Break;
...end;
...//Создание нового потока, если не найден готовый
...if (result = -1) then begin
......SetLength(vSounds, Length(vSounds) + 1);
......result := High(vSounds);
...end;
end;

Функция подготавливает свободный элемент массива для записи в него информации о потоке. Сначала производится поиск «убитого» элемента и, если он не найден, массив расширяется. В результате функция возвращает индекс подготовленного элемента.

Поиск потока по его идентификатору

function innerFindStream(vId: Cardinal): Integer;
var
...i: Integer;
begin
...result := -1;
...for i := 0 to High(vSounds) do begin
......if (vSounds[i].id <> vId) then continue;
......result := i;
......break;
...end;
end;

Тут тоже все очень просто. Перебираем все элементы массива в поисках того, чей идентификатор потока (id) будет равен искомому.

Удаление потока по его идентификатору

function innerRemoveStream(vId: Integer): Boolean;
begin
...with vSounds[vId] do begin
......result := bass_ChannelRemoveSync(id, event) and bass_StreamFree(Id);
......isAlive := false;
...end;
end;

Удаляем событие и очищаем память, занимаемую потоком. Помечаем элемент как «мертвый» для дальнейшей записи на его место других потоков.

Подведем итоги

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

Недавно мы выяснили, что уже выпущенная нами логическая игра Crown и подготовленная альфа-версия Galaxy Boom: Mini расходуют неприлично много оперативной памяти компьютера. В результате проведения некоторых исследований, удалось быстро выявить причину: неправильная работа со звуковыми потоками в Bass - мы просто забывали их удалять.

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

Всем спасибо за внимание. Исходный код движка с демонстрационной программой вы можете скачать по прямой ссылке ниже.

perfectsound.zip
Скачать размер: 108kb

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

автор: SaiLight работа над проектом: 2016 год
выложено уже 20 проектов из 124 созданных команда Perfect Light, 2017 год