ААудио

AAudio — это новый API Android C, представленный в версии Android O. Он предназначен для высокопроизводительных аудиоприложений, требующих низкой задержки. Приложения взаимодействуют с AAudio, считывая и записывая данные в потоки.

API AAudio по своей конструкции минимален и не выполняет следующие функции:

  • Перечисление аудиоустройств
  • Автоматизированная маршрутизация между конечными точками аудио
  • Файловый ввод-вывод
  • Декодирование сжатого звука
  • Автоматическое представление всех входных данных/потоков в одном обратном вызове.

Начиная

Вы можете вызвать AAudio из кода C++. Чтобы добавить набор функций AAudio в ваше приложение, включите заголовочный файл AAudio.h:

#include <aaudio/AAudio.h>

Аудиопотоки

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

Поток определяется следующим:

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

Аудиоустройство

Каждый поток привязан к одному аудиоустройству.

Аудиоустройство — это аппаратный интерфейс или виртуальная конечная точка, которая действует как источник или приемник непрерывного потока цифровых аудиоданных. Не путайте аудиоустройство (встроенный микрофон или Bluetooth-гарнитуру) с устройством Android (телефоном или часами), на котором работает ваше приложение.

Вы можете использовать метод getDevices() AudioManager для обнаружения аудиоустройств, доступных на вашем устройстве Android. Метод возвращает информацию о type каждого устройства.

Каждое аудиоустройство имеет уникальный идентификатор на устройстве Android. Вы можете использовать идентификатор для привязки аудиопотока к определенному аудиоустройству. Однако в большинстве случаев вы можете позволить AAudio выбрать основное устройство по умолчанию, а не указывать его самостоятельно.

Аудиоустройство, подключенное к потоку, определяет, предназначен ли поток для ввода или вывода. Поток может перемещать данные только в одном направлении. Определяя поток, вы также задаете его направление. Когда вы открываете поток, Android проверяет, совпадают ли аудиоустройство и направление потока.

Режим общего доступа

Поток имеет режим совместного использования:

  • AAUDIO_SHARING_MODE_EXCLUSIVE означает, что поток имеет эксклюзивный доступ к своему аудиоустройству; устройство не может использоваться каким-либо другим аудиопотоком. Если аудиоустройство уже используется, поток может не иметь монопольного доступа. Эксклюзивные потоки, скорее всего, будут иметь меньшую задержку, но они также с большей вероятностью будут отключены. Вам следует закрыть эксклюзивные потоки, как только они вам больше не нужны, чтобы другие приложения могли получить доступ к устройству. Эксклюзивные потоки обеспечивают минимально возможную задержку.
  • AAUDIO_SHARING_MODE_SHARED позволяет AAudio микшировать звук. AAudio смешивает все общие потоки, назначенные на одно и то же устройство.

Вы можете явно установить режим общего доступа при создании потока. По умолчанию режим общего доступа — SHARED .

Аудио формат

Данные, передаваемые через поток, имеют обычные атрибуты цифрового звука. Они заключаются в следующем:

  • Пример формата данных
  • Количество каналов (выборок на кадр)
  • Частота дискретизации

AAudio допускает следующие форматы сэмплов:

aaudio_format_t Тип данных C Примечания
AAUDIO_FORMAT_PCM_I16 int16_t обычные 16-битные выборки, формат Q0.15
AAUDIO_FORMAT_PCM_FLOAT плавать от -1,0 до +1,0
AAUDIO_FORMAT_PCM_I24_PACKED uint8_t в группах по 3 упакованные 24-битные семплы, формат Q0.23
AAUDIO_FORMAT_PCM_I32 int32_t обычные 32-битные образцы, формат Q0.31
AAUDIO_FORMAT_IEC61937 uint8_t сжатый звук, завернутый в стандарт IEC61937 для сквозной передачи HDMI или S/PDIF

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

aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
     convertFloatToPcm16(...)
}

Создание аудиопотока

Библиотека AAudio следует шаблону проектирования построителя и предоставляет AAudioStreamBuilder.

  1. Создайте AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. Установите конфигурацию аудиопотока в построителе, используя функции построителя, соответствующие параметрам потока. Доступны следующие дополнительные функции набора:

    AAudioStreamBuilder_setDeviceId(builder, deviceId);
    AAudioStreamBuilder_setDirection(builder, direction);
    AAudioStreamBuilder_setSharingMode(builder, mode);
    AAudioStreamBuilder_setSampleRate(builder, sampleRate);
    AAudioStreamBuilder_setChannelCount(builder, channelCount);
    AAudioStreamBuilder_setFormat(builder, format);
    AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);
    

    Обратите внимание, что эти методы не сообщают об ошибках, таких как неопределенная константа или значение, выходящее за пределы диапазона.

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

    На всякий случай проверьте состояние аудиопотока после его создания, как описано в шаге 4 ниже.

  3. Когда AAudioStreamBuilder настроен, используйте его для создания потока:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

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

    AAudioStreamBuilder_setDeviceId() AAudioStream_getDeviceId()
    AAudioStreamBuilder_setDirection() AAudioStream_getDirection()
    AAudioStreamBuilder_setSharingMode() AAudioStream_getSharingMode()
    AAudioStreamBuilder_setSampleRate() AAudioStream_getSampleRate()
    AAudioStreamBuilder_setChannelCount() AAudioStream_getChannelCount()
    AAudioStreamBuilder_setFormat() AAudioStream_getFormat()
    AAudioStreamBuilder_setBufferCapacityInFrames() AAudioStream_getBufferCapacityInFrames()
  5. Вы можете сохранить конструктор и повторно использовать его в будущем, чтобы создавать больше потоков. Но если вы больше не планируете его использовать, вам следует удалить его.

    AAudioStreamBuilder_delete(builder);
    

Использование аудиопотока

Переходы между состояниями

Поток AAudio обычно находится в одном из пяти стабильных состояний (состояние ошибки «Отключено» описано в конце этого раздела):

  • Открыть
  • Началось
  • Приостановлено
  • Покрасневший
  • Остановлено

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

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

Обратите внимание, что вы можете запросить паузу или сброс только в выходном потоке:

Эти функции асинхронны, и изменение состояния не происходит немедленно. Когда вы запрашиваете изменение состояния, поток перемещает одно из соответствующих переходных состояний:

  • Начало
  • Пауза
  • Промывка
  • Остановка
  • Закрытие

На диаграмме состояний ниже стабильные состояния показаны прямоугольниками с закругленными углами, а переходные состояния — пунктирными прямоугольниками. Хотя это не показано, вы можете вызвать close() из любого состояния.

Жизненный цикл AАудио

AAudio не предоставляет обратные вызовы для оповещения об изменениях состояния. Одну специальную функцию AAudioStream_waitForStateChange(stream, inputState, nextState, timeout) можно использовать для ожидания изменения состояния.

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

Например, после запроса на паузу поток должен немедленно перейти в переходное состояние Pausing и через некоторое время перейти в состояние Paused — хотя нет никакой гарантии, что это произойдет. Поскольку вы не можете дождаться состояния Paused, используйте waitForStateChange() , чтобы дождаться любого состояния, кроме Pausing . Вот как это делается:

aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;
aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;
int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;
result = AAudioStream_requestPause(stream);
result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);

Если состояние потока не Pausing ( inputState , которое, как мы предполагали, было текущим состоянием во время вызова), функция немедленно возвращается. В противном случае он блокируется до тех пор, пока состояние не перестанет быть «Пауза» или пока не истечет время ожидания. Когда функция возвращает значение, параметр nextState показывает текущее состояние потока.

Вы можете использовать тот же метод после вызова запуска, остановки или сброса запроса, используя соответствующее переходное состояние в качестве входного состояния. Не вызывайте waitForStateChange() после вызова AAudioStream_close() поскольку поток будет удален, как только он закроется. И не вызывайте AAudioStream_close() пока waitForStateChange() выполняется в другом потоке.

Чтение и запись в аудиопоток

Существует два способа обработки данных в потоке после его запуска:

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

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

aaudio_result_t result =
    AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
  // Error!
}
if (result != numFrames) {
  // pad the buffer with zeros
  memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
      sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}

Вы можете заполнить буфер потока перед запуском потока, записав в него данные или тишину. Это необходимо сделать в неблокирующем вызове с параметром timeoutNanos, равным нулю.

Данные в буфере должны соответствовать формату данных, возвращаемому AAudioStream_getDataFormat() .

Закрытие аудиопотока

Когда вы закончите использовать поток, закройте его:

AAudioStream_close(stream);

После закрытия потока вы не сможете использовать его ни с какой функцией потока AAudio.

Отключенный аудиопоток

Аудиопоток может быть отключен в любой момент, если произойдет одно из следующих событий:

  • Соответствующее аудиоустройство больше не подключено (например, если наушники отключены).
  • Возникает внутренняя ошибка.
  • Аудиоустройство больше не является основным аудиоустройством.

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

Если вы используете обратный вызов данных (в отличие от одного из методов прямого чтения/записи), вы не получите никакого кода возврата при отключении потока. Чтобы получить информацию, когда это произойдет, напишите функцию AAudioStream_errorCallback и зарегистрируйте ее с помощью AAudioStreamBuilder_setErrorCallback() .

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

Обратите внимание: если вы откроете новый поток, его настройки могут отличаться от настроек исходного потока (например, FramePerBurst):

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error) {
    // Launch a new thread to handle the disconnect.
    std::thread myThread(my_error_thread_proc, stream, userData);
    myThread.detach(); // Don't wait for the thread to finish.
}

Оптимизация производительности

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

Настройка буферов для минимизации задержки

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

Емкость буфера — это общий объем данных, который может храниться в буфере. Вы можете вызвать AAudioStreamBuilder_setBufferCapacityInFrames() чтобы установить емкость. Этот метод ограничивает емкость, которую вы можете выделить, максимальным значением, которое допускает устройство. Используйте AAudioStream_getBufferCapacityInFrames() чтобы проверить фактическую емкость буфера.

Приложению не обязательно использовать всю емкость буфера. AAudio заполняет буфер до размера , который вы можете установить. Размер буфера не может быть больше его емкости, а зачастую и меньше. Управляя размером буфера, вы определяете количество пакетов, необходимых для его заполнения, и, таким образом, контролируете задержку. Используйте методы AAudioStreamBuilder_setBufferSizeInFrames() и AAudioStreamBuilder_getBufferSizeInFrames() для работы с размером буфера.

Когда приложение воспроизводит звук, оно записывает в буфер и блокирует его до завершения записи. AAudio читает из буфера дискретными пакетами. Каждый пакет содержит несколько аудиокадров и обычно меньше размера считываемого буфера. Система контролирует размер и скорость пакета, эти свойства обычно определяются схемой аудиоустройства. Хотя вы не можете изменить размер пакета или скорость пакета, вы можете установить размер внутреннего буфера в соответствии с количеством содержащихся в нем пакетов. Как правило, вы получаете минимальную задержку, если размер буфера вашего AAudioStream кратен сообщаемому размеру пакета.

Буферизация звука

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

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

Вот пример цикла оптимизации буфера:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (go) {
    result = writeSomeData();
    if (result < 0) break;

    // Are we getting underruns?
    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

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

Использование обратного вызова с высоким приоритетом

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

Функция обратного вызова имеет следующий прототип:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

Используйте построение потока для регистрации обратного вызова:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

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

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

Например, вы можете использовать обратный вызов для непрерывной генерации синусоидального сигнала следующим образом:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    int64_t timeout = 0;

    // Write samples directly into the audioData array.
    generateSineWave(static_cast<float *>(audioData), numFrames);
    return AAUDIO_CALLABCK_RESULT_CONTINUE;
}

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

Обратный вызов выполняет неблокирующее чтение из входного потока, помещая данные в буфер выходного потока:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    AAudioStream *inputStream = (AAudioStream *) userData;
    int64_t timeout = 0;
    aaudio_result_t result =
        AAudioStream_read(inputStream, audioData, numFrames, timeout);

  if (result == numFrames)
      return AAUDIO_CALLABCK_RESULT_CONTINUE;
  if (result >= 0) {
      memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
          sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
      return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }
  return AAUDIO_CALLBACK_RESULT_STOP;
}

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

Настройка режима производительности

Каждый AAudioStream имеет режим производительности , который сильно влияет на поведение вашего приложения. Есть три режима:

  • AAUDIO_PERFORMANCE_MODE_NONE — режим по умолчанию. Он использует базовый поток, который уравновешивает задержку и экономию энергии.
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY использует меньшие буферы и оптимизированный путь данных для уменьшения задержки.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING использует более крупные внутренние буферы и путь данных, который обеспечивает компромисс между задержкой и меньшим энергопотреблением.

Вы можете выбрать режим производительности, вызвав setPerformanceMode() , и узнать текущий режим, вызвав getPerformanceMode() .

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

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

В текущей версии AAudio для достижения минимально возможной задержки вы должны использовать режим производительности AAUDIO_PERFORMANCE_MODE_LOW_LATENCY вместе с обратным вызовом с высоким приоритетом. Следуйте этому примеру:

// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);

Безопасность резьбы

API AAudio не является полностью потокобезопасным . Вы не можете вызывать некоторые функции AAudio одновременно из более чем одного потока. Это связано с тем, что AAudio избегает использования мьютексов, которые могут вызвать прерывание потока и сбои.

В целях безопасности не вызывайте AAudioStream_waitForStateChange() , не читайте и не записывайте в один и тот же поток из двух разных потоков. Аналогично, не закрывайте поток в одном потоке во время чтения или записи в него в другом потоке.

Вызовы, возвращающие настройки потока, такие как AAudioStream_getSampleRate() и AAudioStream_getChannelCount() , являются потокобезопасными.

Эти вызовы также потокобезопасны:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() за исключением AAudioStream_getTimestamp()

Известные проблемы

  • Задержка звука высока для блокировки write(), поскольку в версии Android O DP2 не используется дорожка FAST. Используйте обратный вызов, чтобы снизить задержку.

Дополнительные ресурсы

Чтобы узнать больше, воспользуйтесь следующими ресурсами:

Справочник по API

Кодлабы

Видео