Android работа с аудио

Особенности обработки аудио в ОС Android

И как с ними бороться

Подумайте, какие ассоциации вызывает у вас операционная система Google Android? Наверняка, одной из первых в голове всплыла «распространенность», «популярность». Или, при подобающем настроении, такое словосочетание как «зоопарк устройств». Что и говорить, выбор в пользу Android уже давным-давно сделали почти все известные разработчики мобильных гаджетов.

В крупных компаниях этот шаг знаменует собой начало большого пути для подразделения R&D (Research and Development). Ведь базовые возможности Android (по крайней мере, до релиза Lollipop) были весьма скромны и могли устроить только завсегдатаев XDA Developers, которые все необходимое и сами могут дописать. В поисках примеров можно даже не уходить в дебри Android. Скажем, аппараты с поддержкой нескольких SIM-карт уже давно стали самым обычным явлением на рынке. А API для работы с ними был официально добавлен только в Google Android 5.1.

Сегодня мы подробно рассмотрим еще одну сторону ОС, которой разработчики Google Android не уделяют достойного внимания — работу со звуком. Зачем, в принципе, нужен звук на телефоне? В первую очередь, чтобы воспроизводить звонок. С этой задачей мобильные устройства справляться уже научились. Было бы здорово также вставить какой-нибудь аудиоплеер. и здесь компания Google без особых раздумий перекладывает все на производителей устройств. Беспроводное проигрывание через Bluetooth или динамики мобильных устройств зависит от ряда дополнительных факторов, требующих отдельного изучения, поэтому в данной статье мы рассмотрим, как обстоят дела с воспроизведением аудио исключительно через разъем для наушников.

До выхода Android L операционная система поддерживала «из коробки» только PCM-аудио с частотой дискретизации 44,1 или 48 кГц. К этому общему знаменателю по умолчанию приводится весь пропускаемый через систему аудиопоток. Исправление ситуации проходит на уровне конкретных производителей, которые устанавливают собственные ЦАП и пишут для них софт. Это могут позволить себе лишь крупные компании. Приобретая такое недешевое устройство как смартфон, хочется услышать адекватный по стоимости аудиочип, но на сегодняшний день это является скорее исключением из правил — большинство моделей ограничиваются лишь тем, что включено в однокристальную систему. А это значит, что воспроизведение происходит с принудительной конвертацией звука в формат, описанный в начале абзаца.

Любой, кто хотя бы немного знаком с обработкой звука, знает, что всякое препятствие на его пути чревато самыми тяжелыми последствиями. При желании проследить всю обработку звука в ОС Android можно через исходный код. Уже при поверхностном изучении настороженность вызывают следующие моменты:

  1. Для принудительной конвертации в нативный формат применяются как минимум целых три конвертера — в audioflinger, speex и webrtc. Здесь никакого прогресса не наблюдается с самых ранних версий, Google лишь исправляет баги.
  2. Слишком высокий тайминг в аудиосервере Android (audioflinger/libstagefright) при большом числе потоков.
  3. Программная регулировка громкости — критичный для аудиофилов аспект, с которым, увы, ничего не поделаешь в принципе.
  4. Колоссальные проблемы с поддержкой ALSA-драйверов (Advanced Linux Sound Architecture). Этот вопрос решается на уровне производителей устройств. Некоторые из них уже предлагают удачные решения, например, Sony и HTC.

Помимо R&D-отделов больших компаний, над улучшением звука Android активно работают энтузиасты, разрешающие порой чуть ли не безвыходные проблемы. Плоды этих титанических трудов можно оценить на пресловутом XDA Developers.

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

Тем не менее, прогресс в звуковой составляющей современных смартфонов очевиден. Как это ни удивительно, даже чипсетные кодеки порой играют неплохо, например, ЦАП Hexagon, устанавливаемые в SoC Qualcomm Snapdragon. Что касается однокристальных систем, менее выдающихся в плане звука (модели Samsung Exynos, Mediatek MTK), то их производители сейчас нередко устанавливают сторонние ЦАП. К сожалению, при таком подходе обычно игнорируется сопроводительная документация, что приводит к затруднениям на более высоких уровнях.

А выше «железа» у нас прописано ядро Linux — база, на которой функционирует ОС Android. Здесь находится все, что обеспечивает работу аппаратной начинки. Конкретно за звук отвечает ALSA — Advanced Linux Sound Architecture. Пионером в реализации ALSA стала компания Samsung, а вообще в ранних устройствах на базе Android эта архитектура еще не поддерживалась, поскольку сама Google еще не пришла к необходимости единообразия на данном уровне разработки.

Сама по себе архитектура ALSA является весьма оригинальной, что отчасти объясняет проблема в создании низкоуровневого ПО. Даже на написание даже простого драйвера требуется много времени. К тому же, в отличие от десктопных систем, у смартфонов есть своя специфика. Поскольку мы имеем дело с телефоном, обязательна реализация голосовой связи. Кроме того, требуется грамотное управление питанием — об автономной работе Android-устройств лишний раз и говорить нечего. Наконец, учитывая ограниченные ресурсы прикладного ЦП, встает вопрос о декодировании популярных форматов другими аппаратными средствами.

Читайте также:  Не отображает камеру андроид

Типичный сценарий работы над ALSA-драйверами сегодня выглядит следующим образом. Поставщик SoC или кодека предоставляет производителю устройства некую «рыбу» в комплекте с многотомной документацией, при виде которой у Linux-сообщества потекли бы слюнки. Но работникам R&D-отделов производителя такой энтузиазм, мягко говоря, не свойственен. В результате чего пользователи получают ПО, где взамен реализованных возможностей железа предлагаются лишь бесчисленные баги и вообще полнейшие нелепости.

В качестве примера можно привести компанию Qualcomm, которая никакой документацией с аудиторией не делится. Но хотя бы выкладывает исходный код драйверов на своем сайте codeaurora.org. С другими поставщиками чипов ситуация тоже непростая. Даже такие либеральные в этом плане компании как Texas Instruments или Intel, публикующие все спецификации своих устройств еще до начала поставок, иной раз хранят молчание, когда речь заходит о звуке.

Что касаются производителей «второго эшелона» (как правило, многочисленных и малоизвестных компаний из Китая), то в соответствии с лицензией GPL они не обнародуют исходный код ядра вообще. С этической точки зрения выглядит это весьма скверно: на основе открытого кода Linux создается по сути закрытый, засекреченный продукт.

Как же свести весь этот «зоопарк» к общему знаменателю, чтобы любой обладатель Android-устройства мог получить качественный звук? Интерфейс ALSA-драйверов един, и, если доступны их исходные файлы, можно попытаться самостоятельно улучшить качество звука, чтобы использовать возможности устройства на 100%.

Поскольку взаимодействие осуществляется на уровне ядра, для всех нововведений потребуется наличие рут-доступа. Это позволит обойти верхние уровни аудиосистемы Android и взаимодействовать с ALSA-драйверами напрямую. Что и делает программа, которую мы задействуем для сравнительного тестирования аудиотрактов.

Источник

Работа с потоковым аудио

Введение

Конструктор

Для создания объекта нужно указать:

audioSource Откуда ведётся запись. В нашем случае это MediaRecorder.AudioSource.MIC
sampleRateInHz Частота дискретизации в герцах. Документация утверждает, что 44100Гц поддерживается всеми устройствами
channelConfig Конфигурация каналов. Может быть CHANNEL_IN_MONO или CHANNEL_IN_STEREO . Моно работает везде.

Важно: эти константы не совпадают с количеством каналов, которое они обозначают. В этот параметр нельзя передавать 1 или 2.

audioFormat Формат входных данных, более известный как кодек. Может быть ENCODING_PCM_16BIT или ENCODING_PCM_8BIT
bufferSizeInBytes Размер того самого внутреннего буфера. Из него можно считывать аудиопоток. Размер порции считывания не должен превышать эту величину. У этого параметра есть минимально допустимое значение, которое можно получить через getMinBufferSize() .

При своём создании объект пытается получить нужные ему ресурсы системы. Насколько удачно у него это получилось, можно узнать, вызвав функцию getState() . Если она вернёт STATE_INITIALIZED , то всё нормально, если STATE_UNINITIALIZED — значит, произошла ошибка.

Причин ошибки может быть две: слишком маленький буфер и недопустимый формат. Первого нужно избегать вызовом getMinBufferSize() . Второго, на самом деле, им же.

getMinBufferSize()

Этот статический метод выдаёт минимальный размер внутреннего буфера, при котором объект AudioRecord сможет работать. Параметры имеют тот же смысл, что и для конструктора. Следует заметить, что использование именно этой величины для записи — не лучшая идея. Если система будет ещё чем-то занята, то программа всё равно может не успевать считывать все данные подряд, и в записи будут дырки. Мне встречался совет брать размер в десять раз больше.

Получение списка форматов

Метод getMinBufferSize() имеет приятную особенность — ругаться на недопустимые для данного устройства параметры, возвращая ERROR_BAD_VALUE или ERROR . Это означает, что перебирая все возможные сочетания, можно узнать, какие форматы поддерживает устройство.

Считывание данных

Для получения данных из внутреннего буфера служит метод read() . Он существует в трёх вариантах:

  • read(byte[] audioData, int offsetInBytes, int sizeInBytes)
  • read(short[] audioData, int offsetInShorts, int sizeInShorts)
  • read(ByteBuffer audioBuffer, int sizeInBytes)

Их параметры:

audioData массив, в который будут записаны данные
audioBuffer буфер, в который будут записаны данные
offsetInBytes /
offsetInShorts
индекс, с которого начнётся запись
sizeInShorts размер запрашиваемого блока данных. В байтах для ByteBuffer и byte[] , в коротких целых для short[]

Если всё нормально, то метод вернёт количество прочитанных байт, если это вариант с ByteBuffer или byte[] , или прочитанных коротких целых для short[] . Если на момент вызова объект не был правильно инициализирован, то выдаст ERROR_INVALID_OPERATION, а если что-то не так с параметрами — ERROR_BAD_VALUE

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

Подход, отход, фиксация

Чтобы программа могла получать данные от микрофона, нужно указать в файле AndroidManifest,xml соответствующее разрешение:

Чтобы начать запись, нужно вызвать метод startRecording() , а чтобы закончить — stop() . Запускать и останавливать запись можно сколько угодно раз.

После того, как работа с объектом закончена, следует вызвать метод release() . Он освободит все системные ресурсы, захваченные объектом. После этого объект нельзя использовать, а ссылающуюся на него переменную следует установить в null .

Важно: эти три метода, в отличие от упоминавшихся ранее, выбросят IllegalStateException , если их вызвать для неинициализированного (ну и слово. ) объекта или не в том порядке. Поэтому обращаться с ними нужно «аккуратно», т.е. через блок try .

Пример использования

Приведённый ниже класс делает всё то, о чём сказано выше. Кроме того, он посылает зарегистрированным у него Handler -ам сообщения о принятых данных. Эти данные будут обрабатываться в другом потоке, поэтому, чтобы не затереть ещё не обработанные данные новыми, использован циклический буфер.

В коде использован класс AudioFormatInfo . Он представляет собой POJO с тремя полями, описывающими формат записи: sampleRateInHz , channelConfig и audioFormat .

Источник

Android и звук: как делать правильно

В статье рассматривается архитектура и API для создания приложений, воспроизводящих музыку. Мы напишем простое приложение, которое будет проигрывать небольшой заранее заданный плейлист, но «по-взрослому» — с использованием официально рекомендуемых практик. Мы применим MediaSession и MediaController для организации единой точки доступа к медиаплееру, и MediaBrowserService для поддержки Android Auto. А также оговорим ряд шагов, которые обязательны, если мы не хотим вызвать ненависти пользователя.

В первом приближении задача выглядит просто: в activity создаем MediaPlayer, при нажатии кнопки Play начинаем воспроизведение, а Stop — останавливаем. Все прекрасно работает ровно до тех пор, пока пользователь не выйдет из activity. Очевидным решением будет перенос MediaPlayer в сервис. Однако теперь у нас встают вопросы организации доступа к плееру из UI. Нам придется реализовать binded-сервис, придумать для него API, который позволил бы управлять плеером и получать от него события. Но это только половина дела: никто, кроме нас, не знает API сервиса, соответственно, наша activity будет единственным средством управления. Пользователю придется зайти в приложение и нажать Pause, если он хочет позвонить. В идеале нам нужен унифицированный способ сообщить Android, что наше приложение является плеером, им можно управлять и что в настоящий момент мы играем такой-то трек из такого-то альбома. Чтобы система со своей стороны подсобила нам с UI. В Lollipop (API 21) был представлен такой механизм в виде классов MediaSession и MediaController. Немногим позже в support library появились их близнецы MediaSessionCompat и MediaControllerCompat.

Следует сразу отметить, что MediaSession не имеет отношения к воспроизведению звука, он только об управлении плеером и его метаданными.

MediaSession

Итак, мы создаем экземпляр MediaSession в сервисе, заполняем его сведениями о нашем плеере, его состоянии и отдаем MediaSession.Callback, в котором определены методы onPlay, onPause, onStop, onSkipToNext и прочие. В эти методы мы помещаем код управления MediaPlayer (в примере воспользуемся ExoPlayer). Наша цель, чтобы события и от аппаратных кнопок, и из окна блокировки, и с часов под Android Wear вызывали эти методы.

Полностью рабочий код доступен на GitHub (ветка master). В статьи приводятся только переработанные выдержки из него.

Для доступа извне к MediaSession требуется токен. Для этого научим сервис его отдавать

и пропишем в манифест

MediaController

Теперь реализуем activity с кнопками управления. Создаем экземпляр MediaController и передаем в конструктор полученный из сервиса токен.

MediaController предоставляет как методы управления плеером play, pause, stop, так и коллбэки onPlaybackStateChanged(PlaybackState state) и onMetadataChanged(MediaMetadata metadata). К одному MediaSession могут подключиться несколько MediaController, таким образом можно легко обеспечить консистентность состояний кнопок во всех окнах.

Наша activity работает, но ведь идея исходно была, чтобы из окна блокировки тоже можно было управлять. И тут мы приходим к важному моменту: в API 21 полностью переделали окно блокировки, теперь там отображаются уведомления и кнопки управления плеером надо делать через уведомления. К этому мы вернемся позже, давайте пока рассмотрим старое окно блокировки.

Как только мы вызываем mediaSession.setActive(true), система магическим образом присоединяется без всяких токенов к MediaSession и показывает кнопки управления на фоне картинки из метаданных.

Однако в силу исторических причин события о нажатии кнопок приходят не напрямую в MediaSession, а в виде бродкастов. Соответственно, нам надо еще подписаться на эти бродкасты и перебросить их в MediaSession.

MediaButtonReceiver

Для этого разработчики Android любезно предлагают нам воспользоваться готовым ресивером MediaButtonReceiver.

Добавим его в манифест

MediaButtonReceiver при получении события ищет в приложении сервис, который также принимает «android.intent.action.MEDIA_BUTTON» и перенаправляет его туда. Поэтому добавим аналогичный интент-фильтр в сервис

Если подходящий сервис не найден или их несколько, будет выброшен IllegalStateException.

Теперь в сервис добавим

Метод handleIntent анализирует коды кнопок из intent и вызывает соответствующие коллбэки в mediaSession. Получилось немного плясок с бубном, но зато почти без написания кода.

На системах с API >= 21 система не использует бродкасты для отправки событий нажатия на кнопки и вместо этого напрямую обращается в MediaSession. Однако, если наш MediaSession неактивен (setActive(false)), его пробудят бродкастом. И для того, чтобы этот механизм работал, надо сообщить MediaSession, в какой ресивер отправлять бродкасты.
Добавим в onCreate сервиса

На системах с API Так это выглядит

Android 4.4

MIUI 8 (базируется на Android 6, то есть теоретически окно блокировки не должно отображать наш трек, но здесь уже сказывается кастомизация MIUI).

Уведомления

Однако, как ранее упоминалось, начиная с API 21 окно блокировки научилось отображать уведомления. И по этому радостному поводу, вышеописанный механизм был выпилен. Так что теперь давайте еще формировать уведомления. Это не только требование современных систем, но и просто удобно, поскольку пользователю не придется выключать и включать экран, чтобы просто нажать паузу. Заодно применим это уведомление для перевода сервиса в foreground-режим.

Нам не придется рисовать кастомное уведомление, поскольку Android предоставляет специальный стиль для плееров — Notification.MediaStyle.

Добавим в сервис два метода

И добавим вызов refreshNotificationAndForegroundStatus(int playbackState) во все коллбэки MediaSession.

Android 4.4

Android 7.1.1

Android Wear

Started service

В принципе у нас уже все работает, но есть засада: наша activity запускает сервис через binding. Соответственно, после того, как activity отцепится от сервиса, он будет уничтожен и музыка остановится. Поэтому нам надо в onPlay добавить

Никакой обработки в onStartCommand не надо, наша цель не дать системе убить сервис после onUnbind.

А в onStop добавить

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

ACTION_AUDIO_BECOMING_NOISY

Продолжаем полировать сервис. Допустим пользователь слушает музыку в наушниках и выдергивает их. Если эту ситуацию специально не обработать, звук переключится на динамик телефона и его услышат все окружающие. Было бы хорошо в этом случае встать на паузу.
Для этого в Android есть специальный бродкаст AudioManager.ACTION_AUDIO_BECOMING_NOISY.
Добавим в onPlay

В onPause и onStop

И по факту события встаем на паузу

Android Auto

Начиная с API 21 появилась возможность интегрировать телефон с экраном в автомобиле. Для этого необходимо поставить приложение Android Auto и подключить телефон к совместимому автомобилю. На экран автомобиля будет выведены крупные контролы для управления навигацией, сообщениями и музыкой. Давайте предложим Android Auto наше приложение в качестве поставщика музыки.

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

Исходный код выложен на GitHub (ветка MediaBrowserService).

Прежде всего надо указать в манифесте, что наше приложение совместимо с Android Auto.
Добавим в манифест

Здесь automotive_app_desc — это ссылка на файл automotive_app_desc.xml, который надо создать в папке xml

Преобразуем наш сервис в MediaBrowserService. Его задача, помимо всего ранее сделанного, отдавать токен в Android Auto и предоставлять плейлисты.

Поправим декларацию сервиса в манифесте

Во-первых, теперь наш сервис экспортируется, поскольку к нему будут подсоединяться снаружи.

И, во-вторых, добавлен интент-фильтр android.media.browse.MediaBrowserService.

Меняем родительский класс на MediaBrowserServiceCompat.

Поскольку теперь сервис должен отдавать разные IBinder в зависимости от интента, поправим onBind

Имплементируем два абстрактных метода, возвращающие плейлисты

И, наконец, имплементируем новый коллбэк MediaSession

Здесь mediaId — это тот, который мы отдали в setMediaId в onLoadChildren.

Плейлист

Трек

UPDATE от 27.10.2017: Пример на GitHub переведен на targetSdkVersion=26. Из релевантных теме статьи изменений необходимо отметить следующее:

  • android.support.v7.app.NotificationCompat.MediaStyle теперь deprecated. Вместо него следует использовать android.support.v4.media.app.NotificationCompat.MediaStyle. Соответственно, больше нет необходимости использовать android.support.v7.app.NotificationCompat, теперь можно использовать android.support.v4.app.NotificationCompat
  • Метод AudioManager.requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) теперь тоже deprecated. Вместо него надо использовать AudioManager.requestAudioFocus(AudioFocusRequest focusRequest). AudioFocusRequest — новый класс, добавленный с API 26, поэтому не забывайте проверять на API level.
    Создание AudioFocusRequest выглядит следующим образом

Теперь запрашиваем фокус

Разумеется, все вышеописанные изменения вносить необязательно, старые методы работать не перестали.

Вот мы и добрались до конца. В целом тема эта довольно запутанная. Плюс отличия реализаций на разных API level и у разных производителей. Очень надеюсь, что я ничего не упустил. Но если у вас есть, что исправить и добавить, с удовольствием внесу изменения в статью.

Еще очень рекомендую к просмотру доклад Ian Lake. Доклад от 2015 года, но вполне актуален.

Источник

Читайте также:  Android studio редактирование apk
Оцените статью