Создаём музыкальную игру с библиотекой Oboe от Google — часть 1
Недавно Google представила библиотеку Oboe для созданий аудиоприложений с низкой задержкой. Мы перевели руководство по созданию одного из таких приложений.
4К открытий4К показов
Совсем недавно 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()
следующим образом:
Соберите и запустите игру. Вы должны услышать фоновую дорожку и звук хлопка при нажатии на экран.
В следующей части мы займёмся механикой игры и визуальной частью.
4К открытий4К показов