Создаём музыкальную игру с библиотекой Oboe от Google — часть 1
Недавно Google представила библиотеку Oboe для созданий аудиоприложений с низкой задержкой. Мы перевели руководство по созданию одного из таких приложений.
5К открытий5К показов
Совсем недавно Google представила библиотеку Oboe для создания аудиоприложений с минимальными задержками. Мы перевели их руководство по созданию простой игры.
Чему вы научитесь:
- Как проигрывать звуки с помощью библиотеки Oboe;
- Как создавать аудиопотоки с низкой задержкой;
- Как смешивать звуки;
- Как проигрывать звуки точно в нужное время;
- Как синхронизировать аудио с экранным интерфейсом.
Что вам понадобится:
- Android Studio 3.0.0 или выше;
- NDK и Build Tools;
- Устройство с Android Jelly Bean (16 уровень API) или выше для тестирования. Устройства Pixel подойдут лучше всего;
- Не помешает знание C++, но оно не является обязательным.
В чём суть игры?
Игра проигрывает клёвый четырёхбитный трек в цикле. Когда игра начинается, она также воспроизводит звук хлопка на первых трёх долях такта.
Пользователь должен повторить три хлопка с теми же промежутками во времени, нажимая на экран после начала второго такта.
При каждом нажатии пользователя игра воспроизводит звук хлопка. Если нажатие происходит в нужное время, экран загорается зелёным цветом. Если нажатие произошло слишком рано или слишком поздно, экран загорается оранжевым или фиолетовым соответственно.
Приготовления
Склонируйте проект
Склонируйте репозиторий Oboe на GitHub и переключитесь на ветку io18codelab:
Откройте проект в Android Studio
Запустите Android Studio и откройте проект:
- File → Open...
- Выберите папку «oboe/samples».
Обратите внимание, что этот проект содержит все примеры кода для библиотеки Oboe. Нам понадобится только RhythmGame.
Запустите проект
Выберите конфигурацию запуска RhythmGame:
Затем нажмите Ctrl+R для сборки и запуска шаблона приложения — оно должно скомпилироваться и запуститься, но всё, что оно делает, — показывает серый экран. Далее мы будем добавлять функциональность.
Откройте модуль RhythmGame
Файлы, с которыми мы будем работать, хранятся в модуле RhythmGame. Убедившись, что в окне Project выбран режим Android, разверните его.
Теперь разверните папку cpp/native-lib. На протяжении этой статьи мы будем редактировать Game.h и Game.cpp.
Сравниваем с финальной версией
Будет полезно в течение работы сравнивать текущую версию кода с финальной, которая хранится в ветке master. С Android Studio можно легко сравнивать ветки.
Android Studio нужно знать, где находится исполняемый файл git. Как правило, его можно узнать, введя which git в терминале. Затем вы сможете обновить путь до git в Android Studio через Preferences→Version Control→Git.Сначала нужно разрешить интеграцию с системой контроля версий:
- VCS→Enable Version Control Integration…
- Выберите git.
Теперь вы можете сравнивать свой код с версией в ветке master:
- Нажмите на текущую ветку в правом нижнем углу;
- Выберите master→Compare.
В результате откроется новое окно. Выберите вкладку «Diff». Должен появиться список файлов с разницей между ними.
Выберите любой файл, чтобы посмотреть различия.
Обзор архитектуры
Архитектура игры:
Пользовательский интерфейс
На левой части графика показаны объекты, связанные с пользовательским интерфейсом.
OpenGL Surface вызывает tick() каждый раз, когда экран нужно обновить, как правило, 60 раз в секунду. Затем Game даёт объектам для рендеринга интерфейса указание отобразить пиксели на поверхности OpenGL, после чего экран обновляется.
Интерфейс игры очень простой: единственный метод SetGLScreenColor() обновляет цвет экрана.
События нажатий
При каждом нажатии на экран вызывается метод tap(), в который передаётся время нажатия.
Аудио
На правой части графика показаны объекты, связанные с аудио. Oboe предоставляет класс AudioStream и связанные объекты, чтобы дать возможность Game отправлять аудиоданные на выходное устройство (динамики или наушники).
Каждый раз, когда AudioStream требуется больше данных, он вызывает метод AudioDataCallback::onAudioReady(). Он передаёт массив audioData в Game, где массив должен заполниться фреймами аудио в количестве numFrames.
Проигрываем звук при нажатии на экран
Чтобы воспроизвести звук хлопка нужен файл, в котором содержатся цифровые аудиоданные и аудиопоток.
Загружаем файл со звуком
В проекте в папке src/main/assets есть файл CLAP.raw, который содержит PCM-аудиоданные в следующем формате:
- Формат: 16-битное целое число;
- Каналы: 2 (стерео);
- Частота дискретизации: 48,000 кГц.
Если вы хотите просмотреть или воспроизвести этот файл, вы можете загрузить его в аудиоредактор Audacity, перейдя в меню по вкладкам File→Import→Raw Data и использовать указанные выше опции.
Чтобы загрузить этот файл в игру, нужно использовать метод SoundRecording::loadFromAssets. Откройте Game.h и объявите поле типа SoundRecording* с именем mClap и значением nullptr:
Теперь откройте Game.cpp и добавьте следующий код в метод start():
Это необходимо для загрузки PCM-аудиоданных в объект SoundRecording в начале игры.
Создаём аудиопоток
Чтобы создать аудиопоток, воспользуемся AudioStreamBuilder, который позволяет указывать желаемые свойства аудиопотока до его создания.
Устанавливаем свойства потока
Свойства потока должны совпадать со свойствами источника.
Откройте Game.h и объявите поле типа AudioStream* с именем mAudioStream:
Теперь добавьте следующий код в метод start() в Game.cpp:
Настраиваем обратный вызов
Воспользуемся обратным вызовом для передачи аудиоданных в поток. Мы делаем именно так, потому что такой подход обеспечивает лучшую производительность.
Чтобы использовать обратный вызов, нужно объявить объект, который реализует интерфейс AudioDataCallback. Вместо создания нового объекта можно реализовать этот интерфейс в Game. Откройте Game.h, найдите строку class Game { и замените её на class Game : public AudioStreamCallback {.
Теперь нужно переопределить метод AudioStreamCallback::onAudioReady():
Параметр audioData для onAudioReady является массивом, который заполняется аудиоданными с помощью mClap->renderAudio.
Примечание Каждый раз, когда вы получаете контейнерный массив типа void *, не забывайте привести его к формату текущего потока данных и предоставляйте данные только в этом формате. В противном случае это обернётся ужасными шумами!
Добавим реализацию onAudioReady() в Game.cpp:
Возвращаемое значение DataCallbackResult::Continue говорит потоку о вашем намерении продолжать посылать аудиоданные, а это значит, что обратные вызовы должны продолжаться. Если вернуть DataCallbackResult::Stop, обратные вызовы прекратятся и поток больше не будет воспроизводить аудио.
Чтобы завершить создание обратного вызова, нужно сообщить конструктору потоков, где найти объект обратного вызова с помощью setCallback() в start():
Создание и запуск аудиопотока
Когда всё готово, создать и запустить поток не составляет труда. Добавьте этот код в start():
Проверка ошибок: всегда проверяйте результат операций потока и тщательно обрабатывайте все ошибки. Почти все методы в Oboe возвращают объектResult, который можно проверить на наличиеResult::OKи привести к читабельной строке с помощьюconvertToText().
Обработка нажатий
Метод tap() вызывается при каждом нажатии на экран. Начнём воспроизведение звука хлопка, вызвав setPlaying(). Добавьте этот код в tap():
Соберите и запустите приложение. При нажатии на экран вы должны услышать звук хлопка.
Оптимизация задержки аудиопотока
Как вы могли заметить, между нажатием на экран и воспроизведением звука проходит определённое время, которое называют задержкой. Как правило, она исходит из двух источников: тачскрина и аудиопотока.
Мы можем оптимизировать задержку аудиопотока, изменив его свойства на следующие:
- Установив значение
PerfomanceModeравнымLowLatency; - Установив значение
SharingModeравнымExclusive; - Уменьшив размер внутреннего буфера.
Откройте Game.cpp и добавьте эти строки сразу после создания обратного вызова в start():
Установить размер буфера можно только после создания потока. Добавьте этот код после создания потока в start():
Так мы устанавливаем размер буфера равным двум «всплескам» — данным, которые записываются в течение каждого обратного вызова. Мы выбираем именно два всплеска, так как это хороший компромисс между задержкой и защитой от буферной недогрузки. Это часто называют многократной буферизацией и используют в графическом программировании.
Соберите и запустите приложение. Вы должны заметить, что время между нажатием и звуком хлопка уменьшилось и игра ощущается более отзывчивой. Хорошая работа!
Воспроизведение нескольких звуков
Один звук может быстро надоесть. Было бы неплохо проигрывать на фоне какой-нибудь бит, под который можно нажимать на экран.
На данный момент игра помещает в аудиопоток только звуки хлопков.
Используем микшер
Чтобы проигрывать несколько звуков одновременно, нужно смешать их с помощью Mixer.
Создаём фоновую дорожку и микшер
Откройте Game.h и объявите ещё один SoundRecording* для фоновой музыки и Mixer:
Теперь добавьте в Game.cpp этот код после загрузки звука хлопка в start():
Этот код загружает содержимое ресурса FUNKY_HOUSE.raw (который содержит PCM-данные в том же формате, что и ресурс звука хлопка) в объект SoundRecording. Воспроизведение начинается при запуске игры и продолжается бесконечно.
Звук хлопка и фоновая музыка добавляются в микшер.
Обновляем обратный вызов
Теперь нам нужно сделать так, чтобы обратный вызов использовал микшер вместо звука хлопка. Обновите onAudioReady() следующим образом:
Соберите и запустите игру. Вы должны услышать фоновую дорожку и звук хлопка при нажатии на экран.
В следующей части мы займёмся механикой игры и визуальной частью.
5К открытий5К показов










