В основе «пирамиды потребностей» тех, кому нужны Android-приложения для работы со звуком, лежит скорость реакции системы на действия пользователя. Предположим, некая программа быстро запустится и покажет прекрасную картинку с клавиатурой рояля. Для начала — неплохо, но если моменты касания клавиш и начала звучания (пусть – просто восхитительного) разделяет заметное время, программу закроют и больше к ней не вернутся.
Поговорим об особенностях воспроизведения звука с высокой скоростью отклика на Android-устройствах, основанных на процессорах Intel Atom (Bay Trail). Применённый подход можно использовать и на других платформах от Intel. Здесь мы рассматриваем Android 4.4.4., подобное исследование для платформы Android M ещё находится в работе.
Предварительные сведения
Воспроизведение аудиоматериалов с высокой задержкой – одна из проблем Android, которая особенно сильно сказывается на приложениях для работы со звуком. Большие временные интервалы между действием пользователя и началом звучания плохо влияют на программы для создания звука, на игры, на софт для диджеев и караоке-приложения. Если подобные приложения, в ответ на некие действия, воспроизводят звуки с задержками, которые пользователь находит слишком большими, это серьёзно портит его впечатления.
В ходе исследования мы будем пользоваться понятием круговой задержки (Round-Trip Latency, RTL). В нашем случае это – это время, разделяющее момент выполнения пользователем или системой действия, требующего создания и проигрывания аудио-сигнала, и моментом начала звучания.
Пользователи сталкиваются с задержкой воспроизведения звука в Android, кода они, например, касаются объекта, взаимодействие с которым должно вызвать звук, а звук воспроизводится не сразу. На большинстве ARM и x86-устройств круговая задержка составляет от 300 до 600 мс., в основном – в приложениях, которые используют стандартные средства Android для вывода звука, описание которых можно найти в документе Design For Reduced Latency.
Пользователи подобного не приемлют. Допустимая круговая задержка должна быть гораздо ниже 100 мс., а в большинстве случаев – и ниже 20 мс. В идеале, для профессионального использования, задержка должна быть ниже 10 мс. Ещё во внимание нужно принять то, что в Android-приложениях, работающих со звуком, общая задержка складывается из трёх составляющих. Первая – это задержка касания (Touch Latency).
Вторая – задержка обработки звука (Audio Processing Latency). Третья – задержка постановки в очередь буфера с аудиоданными (Buffer Queuing).
Здесь мы сосредоточимся на сокращение задержки обработки звука, а не всех трёх вышеупомянутых составляющих. Однако, улучшив один из факторов, мы снизим и общую задержку.
Устройство звуковой подсистемы в Android
Как и другие механизмы Android, звуковую подсистему можно представить, состоящей из нескольких слоёв.
Звуковая подсистема Android
Здесь можно узнать подробности о вышеприведённой схеме.
Отметим, что уровень аппаратных абстракций (Hardware Abstraction Layer, HAL) звуковой подсистемы Android служит связующим звеном между высокоуровневыми API, рассчитанными на обработку звука в android.media, и нижележащими аудиодрайверами и аппаратным обеспечением.
OpenSL ES
Использование API OpenSL ES – это наиболее надёжный способ эффективной обработки аудиосигнала, который должен воспроизводиться в ответ на действия пользователя или приложения. Задержек, и при использовании OpenSL ES, не избежать, но в документации к Android рекомендуется пользоваться именно этот API.
Причина подобной рекомендации заключается в том, что OpenSL использует механизм постановки буферов с аудиоданными в очередь (Buffer Queueing), что повышает эффективность при работе в среде Android Media Framework. Всё это реализовано на машинном коде Android, то есть – может давать более высокую производительность, так как такой код не подвержен проблемам, характерным для Java или виртуальной машины Dalvik.
Мы считаем, что использование механизмов OpenSL ES – это шаг вперёд в разработке аудио-приложений для Android. Кроме того, в документации к Android Native Development Kit есть сведения о том, что при выпуске новых релизов Android планируется улучшать и реализацию OpenSL.
Здесь мы рассмотрим использование API OpenSL ES посредством NDK. Для начала, вот три уровня кода, составляющие основу разработки звуковых приложений для Android с использованием OpenSL
- Верхний уровень – это среда разработки приложений Android SDK, которая основана на Java.
- Более низкий уровень программного окружения, называемый Android NDK, позволяет разработчикам писать код на C или C++, который можно использовать в приложениях с помощью механизма Java Native Interface (JNI).
- Нижний уровень – это API OpenSL ES, который поддерживается в Android, начиная с версии 2.3. API встроен в NDK.
OpenSL работает, как и несколько других API, используя механизм обратного вызова. В OpenSL обратный вызов может быть использован только для уведомления приложения о том, что новый буфер может быть поставлен в очередь (для воспроизведения или записи звука). В других API функции обратного вызова так же поддерживают указатели на буферы с аудиоданными, которые приложение может заполнять данными или получать данные из них. Но в OpenSL, по выбору, API может быть реализовано так, чтобы функции обратного вызова действовали как сигнальный механизм для того, чтобы все вычисления выполнялись в потоке, ответственном за обработку звука. Этот процесс включает в себя постановку в очередь буферов с данными после получения назначенных сигналов.
Google рекомендует использовать при работе с OpenSL политику планирования Sched_FIFO. Эта политика основана на технике применения кольцевого буфера.
Политика планирования Sched_FIFO
Так как Android основан на Linux, здесь задействуется планировщик Linux CFS. CFS может выделять ресурсы центрального процессора непредсказуемо. Например, он способен передать управление потоку с более высоким, на его взгляд, приоритетом, лишив полномочий потока, который кажется ему менее привлекательным. Таковы особенности CFS, если подобное коснётся потока, который занят обработкой звука, это способно вызвать проблемы с таймингами буферов. В результате – большие задержки, появление которых трудно предсказать.
Основное решение данной проблемы заключается в том, чтобы не использовать CFS для потоков, занятых интенсивной работой со звуком и, вместо политики планирования SCHED_NORMAL (другое её название – SCHED_OTHER), которую реализует CFS, применять политику SCHED_FIFO.
Задержка планирования
Задержка планирования – это время, которое проходит между тем моментом, когда поток готов к запуску, и моментом, когда завершится переключение контекста, то есть – началом исполнения потока на процессоре. Чем меньше эта задержка – тем лучше, а если она больше двух миллисекунд – проблемы со звуком гарантированы. Длительные задержки планирования обычно возникают при смене режимов работы процессора. Среди них – запуск или остановка, переход между защищённым ядром и обычным ядром, переключение режимов энергопотребления или настройка частоты и энергопотребления процессора.
Руководствуясь вышеприведёнными соображениями, рассмотрим схему реализацию обработки звука на Android.
Интерфейс кольцевого буфера
Первое, что нужно сделать для правильной организации работы, это – подготовить интерфейс кольцевого буфера, которым можно будет пользоваться из кода. Для этого понадобится четыре функции:
- Функция для создания кольцевого буфера.
- Функция записи в буфер.
- Функция чтения из буфера.
- Функция для уничтожения буфера.
Вот пример кода:
circular_buffer* create_circular_buffer(int bytes);
int read_circular_buffer_bytes(circular_buffer *p, char *out, int bytes);
int write_circular_buffer_bytes(circular_buffer *p, const char *in, int bytes);
void free_circular_buffer (circular_buffer *p);
Желаемый эффект заключается в том, чтобы при выполнении операции чтения производилось считывание запрошенного числа байтов – вплоть до того объёма информации, который уже был в буфер записан. Функция записи будет записывать данные в буфер с учётом оставшегося в нём свободного места. Они возвращают количество прочитанных или записанных байтов – эти числа лежат в диапазоне от нуля до запрошенного при вызове функции числа.
Поток-потребитель (функция обратного вызова ввода/вывода, в случае проигрывания, или поток, занятый обработкой звука в случае записи) осуществляет чтение данных из кольцевого буфера и затем выполняет с прочитанными аудиоданными какие-то операции. В то же самое время, асинхронно, поток-поставщик, занимается заполнением кольцевого буфера данными, останавливаясь лишь тогда, когда буфер полон. Если подобрать подходящий размер кольцевого буфера, два этих потока будут работать слаженно, не мешая друг другу.
Ввод/вывод звука
Используя интерфейс, который мы рассматривали выше, функции ввода/вывода звука могут быть написаны с использованием функций обратного вызова OpenSL. Вот пример функции, обрабатывающей входной поток:
/ обработчик функции обратного вызова, вызывается каждый раз, когда завершена запись в буфер
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
OPENSL_STREAM *p = (OPENSL_STREAM *) context;
int bytes = p->inBufSamples*sizeof(short);
write_circular_buffer_bytes(p->inrb, (char *) p->recBuffer,bytes);
(*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue,p->recBuffer,bytes);
}
// получает с устройства буфер с данными заданного размера
int android_AudioIn(OPENSL_STREAM *p,float *buffer,int size){
short *inBuffer;
int i, bytes = size*sizeof(short);
if(p == NULL || p->inBufSamples == 0) return 0;
bytes = read_circular_buffer_bytes(p->inrb, (char *)p->inputBuffer,bytes);
size = bytes/sizeof(short);
for(i=0; i < size; i++){
buffer[i] = (float) p->inputBuffer[i]*CONVMYFLT;
}
if(p->outchannels == 0) p->time += (double) size/(p->sr*p->inchannels);
return size;
}
В функции обратного вызова (строки 2-8), которая вызывается каждый раз, когда готов новый полный буфер (recBuffer), все данные записываются в кольцевой буфер. После этого функция снова ставится в очередь на выполнение (строка 7). Функция обработки звука (строки 10-21), пытается прочесть запрошенное число сэмплов (строка 14) в inputBuffer, а затем скопировать эти данные на выход (преобразуя их к формату с плавающей точкой). Функция возвращает количество скопированных сэмплов.
Вот пример функции, осуществляющей вывод звука.
// передаёт буфер заданного размера на устройство
int android_AudioOut(OPENSL_STREAM *p, float *buffer,int size){
short *outBuffer, *inBuffer;
int i, bytes = size*sizeof(short);
if(p == NULL || p->outBufSamples == 0) return 0;
for(i=0; i < size; i++){
p->outputBuffer[i] = (short) (buffer[i]*CONV16BIT);
}
bytes = write_circular_buffer_bytes(p->outrb, (char *) p->outputBuffer,bytes);
p->time += (double) size/(p->sr*p->outchannels);
return bytes/sizeof(short);
}
// обработчик функции обратного вызова, вызывается каждый раз, когда завершено воспроизведение данных из буфера
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
OPENSL_STREAM *p = (OPENSL_STREAM *) context;
int bytes = p->outBufSamples*sizeof(short);
read_circular_buffer_bytes(p->outrb, (char *) p->playBuffer,bytes);
(*p->bqPlayerBufferQueue)->Enqueue(p->bqPlayerBufferQueue,p->playBuffer,bytes);
}
Функция обработки звука (строки 2-13) берет определённое количество данных, сохранённых в формате с плавающей точкой, конвертирует их в целые числа, записывает полный буфер outputBufer в кольцевой буфер, сообщает о количестве записанных сэмплов. Функция обратного вызова OpenSL (строки 16-22) считывает все сэмплы и ставит их в очередь.
Для того чтобы всё это правильно работало, нужно передавать на выход данные о количестве сэмплов, считанных с ввода, вместе с буфером. Вот цикл, который занимается преобразованием входных данных в выходные.
while(on)
samps = android_AudioIn(p,inbuffer,VECSAMPS_MONO);
for(i = 0, j=0; i < samps; i++, j+=2)
outbuffer[j] = outbuffer[j+1] = inbuffer[i];
android_AudioOut(p,outbuffer,samps*2);
}
В этом фрагменте кода в строках 5-6 производится обход считанных сэмплов и копирование их в выходные каналы. Здесь представлено преобразование входного монофонического сигнала в выходной стереофонический, именно поэтому одни и те же входные данные копируются в две идущих друг за другом позиции выходного буфера. Теперь, когда происходит постановка буфера в очередь, в потоках OpenSL, для того, чтобы запустить механизм обратного вызова, нам нужно поставить в очередь буфер для записи и ещё один – для воспроизведения после того, как мы начнём воспроизведение звука. Это позволит обеспечить срабатывание функции обратного вызова тогда, когда буферы нужно будет заменить.
Только что мы рассмотрели простой пример реализации потока ввода/вывода звука с использованием OpenSL. Каждая реализация будет уникальной и потребует модификаций HAL и драйвера ALSA для того, чтобы выжать из реализации OpenSL всё, что можно.
Доработка звуковой подсистемы Android на платформе x86
Различные реализации OpenSL не гарантируют, что на всех устройствах удастся достичь желаемого (до 40 мс.) уровня задержек при прохождении аудиосигнала к «быстрому микшеру» Android. Однако если выполнить модификации в Media Server, HAL, в драйвере ALSA, различные устройства могут, с переменным успехом, показывать хорошие результаты в деле обработки звука с низкими задержками. В ходе проведения исследования, посвящённого изучению того, что нужно для повышения скорости отклика при работе со звуком на Android, мы реализовали соответствующее решение на планшете Dell Venue 8 7460.
В результате экспериментов была создана гибридная система для обработки медиа-данных. В ней поток, обрабатывающий входные данные, управляется выделенным быстрым сервером, который обрабатывает исходный звуковой сигнал. Затем сигнал передаётся медиа-серверу, реализованному в Android, который использует поток «быстрого микшера». Серверы, обрабатывающие входные и выходные данные, используют механизм планирования OpenSL Sched_FIFO.
Реализация быстрой обработки звука, рисунок предоставлен Эриком Серре (Eric Serre)
В результате внесенных модификаций удаётся добиться вполне приемлемой RTL в 45 миллисекунд. Эта реализация опирается на Intel Atom SoC и на особенности устройства, использованного в эксперименте. Тест проведён на Intel Software Development Platform и доступен через Intel Partner Software Development Program.
Реализация OpenSL и политики планирования SCHED_FIFO демонстрирует эффективную обработку звука, генерируемого в режиме реального времени. Надо отметить, что эта реализация не доступна на всех устройствах, так как создана для вышеупомянутого планшетного компьютера, с учётом его программных и аппаратных особенностей.
Для того чтобы выяснить, как методика обработки звука, представленная в этом материале, покажет себя на других устройствах, нужно провести соответствующие тесты. Проведя такие испытания, мы можем предоставить результаты разработчикам-партнёрам.
Выводы
Мы обсудили особенности использования OpenSL для создания функции обратного вызова и очереди буферов в приложении, которое занимается обработкой звука на Android. Кроме того, здесь отражены усилия, приложенные Intel для достижения возможности работы со звуком с низкими задержками с использованием модифицированного Media Framework.
Для того чтобы реализовать подобную систему своими силами, следуйте рекомендациям Google и учитывайте особенности построения приложений для быстрой обработки звука, о которых мы рассказали в этом материале. Полученные результаты позволяют говорить о том, что снизить задержки при обработке звуковых данных на Android – задача вполне реальная, но битва за скорость звука на платформе Android x86 продолжается.
Автор: Intel