Вот и снова я! Меня зовут Дмитрий Булгаков, я Android-разработчик в HiFi-стриминге Звук, и это третья часть нашего большого гайда, в котором я рассказываю, как можно создать аудиоплеер в приложении. Мы поговорим о дополнительных настройках приложения с аудиоплеером и аудио эффектах, которые можно применять к звуку.

Дополнительная настройка приложения с аудиоплеером

MediaSession

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

Приведу пример того, как работает старый Media API, использующий MediaSessionCompat. Мы создаём медиасессию и передаём туда ресивер, который должен реагировать на нажатия медиа-кнопок устройства.

val component = ComponentName(application, MediaButtonReceiver::class.java)

val session = MediaSessionCompat(
    application,
    “MyMediaSession”,
    component,
    PendingIntent.getBroadcast(
        application,
        0,
        Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(component),
        PendingIntent.FLAG_UPDATE_CURRENT
    )
)

Ресивер в свою очередь получает некоторый Intent для обработки, где содержится подробная информация о действиях, которые должны осуществиться дальше. Например, по кнопке play должно начаться воспроизведение.

class MediaButtonReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent?) {
        if (intent?.action == Intent.ACTION_MEDIA_BUTTON) {
            val keyEvent = intent.getParcellableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
            if (keyEvent == null || keyEvent.action != KeyEvent.ACTION_DOWN) {
                return
            }

            when (keyEvent.keyCode) {
                KeyEvent.KEYCODE_MEDIA_PLAY -> {
                    // start playback
                }
                // and so on
            }
        }
    }
}

Медиасессия должна обновляться с помощью добавления в неё информации о воспроизводимом контенте (названиях треков, альбомов и так далее).

val mediaMetadataBuilder = MediaMetadataCompat.Builder()
    .putString(MediaMetadataCompat.METADATA_KEY_TITLE, “Beat It”)
    .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, “Thriller”)
    .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, ”Michael Jackson”)
    .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 2980000)

session.setMetadata(mediaMetadataBuilder.build())

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

val capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE
val positionInMillis = 0L
val playbackSpeed = 1f
val forwardBackwardBundle = Bundle()

forwardBackwardBundle.putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV)
forwardBackwardBundle.putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT)

val playbackStateBuilder = PlaybackStateCompat.Builder()
    .setState(PlaybackStateCompat.STATE_PLAYING, positionInMillis, playbackSpeed)
    .setExtras(capabilities)

session.setPlaybackState(playbackStateBuilder.build())

А вот как работа с медиасессией может выглядеть с миграцией на Media 3: всю информацию она получает прямиком из плеера, всего в одну строку кода. 

val mediaSession = MediaSession.Builder(context, exoPlayer).build()

Таким образом с помощью медиасессии мы отображаем состояние воспроизведения в уведомлениях и даже на таких подключенных устройствах, как умные часы и Android Auto.

Audio Track

У нас остался неразрешённым вопрос: что ExoPlayer делает с полученными аудиоданными для воспроизведения на устройстве?

Воспроизведение в ExoPlayer основано на использовании AudioTrack. AudioTrack — это стандартный класс из Android SDK, который нужен для отправки аудиоданных в систему (его можно использовать и без ExoPlayer). Например, вы делаете приложение-камертон для настройки музыкальных инструментов и вам нужно воспроизвести какую-нибудь простую синусоиду определённой частоты. Для таких целей использование ExoPlayer — оверинжиниринг, так как для этой задачи вообще не потребуются его основные функции.

Схема работы Audio Track с системой Android
Схема работы Audio Track с системой Android

Давайте теперь разберем устройство Audio Track «под капотом». Интерфейс, с которым взаимодействует разработчик из Java кода, на самом деле является прокси-клиентом, а сама реализация находится в системе Android в Hardware Abstraction Layer. 

Это слой системы Android, позволяющий приложениям взаимодействовать с физическими компонентами устройства, например, с микрофоном или аудиовыходом. Android SDK позволяет приложениям связываться с HAL посредством биндера и отправлять в него аудиоданные для воспроизведения. HAL, в свою очередь, обрабатывает все аудиоданные, смешивает различные звуки системы Android и отправляет их в драйвер аудиоустройства, реализованный в ядре Linux, на котором строится система Android. Для старых версий Android может использоваться драйвер Open Sound System, в более новых за это отвечает Advanced Linux Sound Architecture.

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

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

val audioAttributes = AudioAttributes.Builder()
     .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
     .setUsage(AudioAttributes.USAGE_MEDIA)
     .build()

val format = AudioFormat.Builder()
    .setSamplingRate(48000)
    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
    .build()

val audioSessionId = generateAudioSessionId()

val audioTrack = AudioTrack(
    audioAttributes,
    format,
    AudioFormat.CHANNEL_OUT_MONO,
    AudioTrack.MODE_STREAM,
    audioSessionId
)

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

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

Для отправки данных в AudioTrack и их озвучивания, вам нужно лишь передать команду play и записать байты в AudioTrack (например,с помощью буфера байтов). 

audioTrack.play()
audioTrack.write(byteBuffer, 0, byteBuffer.position())

Audio Session

Если вы хотите больше контроля над звучанием плеера, можете создать и использовать аудиосессию перед воспроизведением. Аудиосессия — это аудиопоток, который система Android может контролировать. Это может пригодиться для добавления аудиоэффектов системой. Таким образом можно создать и задать ID аудиосессии в ExoPlayer. Этот же ID используется при создании AudioTrack.

val audioManager = context.getSystemService(AudioManager::class.java)
val audioSessionId = audioManager?.generateAudioSessionId() ?: AudioManager.ERROR

exoPlayer.audioSessionId = audioSessionId

Аудиофокус

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

val audioAttributes = AudioAttributes.Builder()
   .setUsage(AudioAttributes.USAGE_MEDIA)
   .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
   .build()

val audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
   .setAudioAttributes(audioAttributes)
   .setOnAudioFocusChangeListener(audioFocusListener)
   .build()

val audioManager = context.getSystemService(AudioManager::class.java)
val audioFocus = audioManager?.requestAudioFocus(audioFocusRequest)

if (audioFocus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
   // start playback
}

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

val audioFocusListener = AudioManager.OnAudioFocusChangeListener {       
    focusChange ->

    when(focusChange) {
        AudioManager.AUDIOFOCUS_GAIN,
        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> {
            // audio focus gain, resume playback
        }
        AudioManager.AUDIOFOCUS_LOSS,
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // audio focus loss, pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // audio focus loss, but maybe just set lower volume
        }
    }
}

Ставим на паузу при возможном шуме

Бывают ситуации, в которых громкий звук приложения при резком отключении гарнитуры может доставить пользователю неудобства. С такой проблемой можно справиться, создав и зарегистрировав специальный ресивер, который будет получать сообщение об отключении гарнитуры. Так воспроизведение можно ставить на паузу и не волноваться, что звук продолжит играть в неподходящий момент.

class BecomingNoisyReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent?) {
        if (intent?.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
            // pause playback
        }
    }
}

// somewhere in your player code
context.registerReceiver(
    BecomingNoisyReceiver(),
    IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
)

Обработка звука

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

Настройки эквалайзера в приложении Звук
Настройки эквалайзера в приложении Звук

Эквалайзер

Начнём с эквалайзера. Его можно назвать частотным фильтром, который увеличивает интенсивность одних частот и уменьшает интенсивность других. Тонкая настройка эквалайзера дарит пользователю возможность выделить в звуке нравящиеся ему оттенки, подобно тому, как фотографы накладывают фильтры на фото, чтобы те выглядели ярче. В Android вы можете создать эквалайзер с использованием Android Audio Effects API. 

При работе с этим API нужно помнить, что ряд эффектов может не поддерживаться на некоторых устройствах, вроде Samsung, Huawei, Xiaomi, OnePlus и особенно на старых версиях Android. В этом случае API бросает исключение при создании аудио эффекта или при его задействовании.

Давайте на примере разберемся, как работать с эффектами в эквалайзере. Вам нужно создать объект эффекта, задав в него ID аудиосессии и приоритет использования Audio Effects Engine. Приоритет позволяет системе определить, какому приложению нужно передать полномочия использовать эффект. Далее необходимо включить эффект и настроить его дополнительной конфигурацией. 

val priority = 100
val equalizer = Equalizer(priority, audioSessionId)

equalizer.enabled = true

val numberOfPresets = equalizer.numberOfPresets.toInt()
val presets = Array<String>(numberOfPresets) { presetIdx ->
    equalizer.getPresetName(presetIdx.toShort())
}

val popPreset = presets.indexOf(“Pop”)
if (popPreset in 0 until numberOfPresets) {
    equalizer.usePreset(popPreset.toShort())
}

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

val numberOfBands = equalizer.numberOfBands.toInt()
val minBandLevel = equalizer.bandLevelRange[0].toInt()
val maxBandLevelRange = equalizer.bandLevelRange[1].toInt()

for (band in 0 until numberOfBands) {
    val randomLevel = Random(Date().time).nextInt(minBandLevel, maxBandLevel)
    equalizer.setBandLevel(band.toShort(), randomLevel.toShort())
}

Усилитель

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

val gain = LoudnessEnhancer(audioSessionId)

gain.enabled = true

val strength = Random(Date().time).nextInt(2000)
gain.setTargetGain(strength)

Бас бустер

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

val priority = 100
val bassBoost = BassBoost(priority, audioSessionId)

bassBoost.enabled = true

val strength = Random(Date().time).nextInt(1000)
bassBoost.setStrength(strength.toShort())

Виртуализатор

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

  • Виртуализация окружения позволяет располагать источник звука вокруг слушателя.

  • Более сложная пространственная виртуализация позволяет располагать источники звука в виртуальном трехмерном пространстве.

Для создания эффекта виртуализации необходимо выбрать один из режимов работы эффекта: binaural, transural и auto. Binaural отличается от transural режима тем, что больше подходит для прослушивания в наушниках — когда правый канал слышен только правым ухом, а левый канал только левым ухом. Когда же пользователь слушает музыку на стационарной акустической системе (на колонках), его уши слышат оба канала трека, поэтому алгоритм виртуализации должен работать иначе.

Однако, как и в случае с другими эффектами, не все режимы поддерживаются всеми устройствами, поэтому желательно задавать режим auto в большинстве случаев использования эффекта виртуализации. Интенсивность эффекта также меняется в диапазоне от 0 до 1000.

val priority = 100
val virtualizer = Virtualizer(priority, audioSessionId)

virtualizer.enabled = true

virtualizer.forceVirtualizationMode(Virtualizer.VIRTUALIZATION_MODE_AUTO)
val strength = Random(Date().time).nextInt(1000)
virtualizer.setStrength(strength.toShort())

Заключение

Вот мы и закончили разбираться, как можно создать аудиоплеер в приложении. Теперь вы знаете об инструментах разработки, с помощью которых можно построить плеер.

В гайде (часть 1, часть 2) я разобрал «анатомию» плеера и его компоненты, показал, как создавать каждый из них в ExoPlayer, а еще рассказал о дополнительных настройках приложения с помощью Media 3 и Android SDK. Также я описал способы улучшения плеера с помощью эквалайзера из фреймворка Android AudioFX.

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

Если у вас есть вопросы по последней части нашего гайда — пишите в комментариях! 

С удовольствием отвечу.

Комментарии (0)