Запись звука и отображение громкости на Android

от автора

Всем привет! Меня зовут Юрий Дорофеев, я Android-разработчик и преподаватель в Mail.ru Group. Если вы когда-нибудь записывали аудиосообщения, то видели, как анимируется интерфейс в зависимости от громкости вашего голоса. Давайте повторим этот эффект:

Доступ к микрофону

Чтобы начать запись внутри Android-приложения, нужно сначала дать ему доступ к этой функциональности. Создадим в Android-манифесте тег uses-permission и укажем разрешение RECORD_AUDIO:

<uses-permission android:name="android.permission.RECORD_AUDIO" /> 

Также нам придётся запросить его в runtime’е при помощи ActivityCompat.requestPermission:

ActivityCompat.requestPermissions(     this,     arrayOf(android.Manifest.permission.RECORD_AUDIO),     777, ) 

Запись звука

Для записи звука создадим класс RecordController. У него должны быть два основных метода: start и stop. Для записи голоса хорошо подходит кодек ААС:

fun start() {     Log.d(TAG, "Start")     audioRecorder = MediaRecorder().apply{         setAudioSource(MediaRecorder.AudioSource.MIC)         setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS)         setAudioEncoder(MediaRecorder.AudioEncoder.AAC)         setOutputFile(getAudioPath())         prepare()         start()         } } 

Записывать аудио будем во временную директорию:

private fun getAudioPath(): String {     return "${context.cacheDir.absolutePath}${File.pathSeparator}${System.currentTimeMillis()}.wav" } 

После окончания записи нужно применить к медиарекордеру stop и release:

fun stop() {     audioRecorder?.let {         Log.d(TAG, "Stop")         it.stop()         it.release()     }     audioRecorder = null   } 

Чтобы работать с состоянием, нужно его каким-то образом получать. Для этого сделаем функцию, которая будет говорить, ведётся ли сейчас запись:

fun isAudioRecording() = audioRecorder != null 

Кнопка записи

Теперь сделаем кнопку записи. Создадим новое View и расположим его в центре экрана. Почему это не кнопка, а View? Потому что мы сейчас будем её анимировать и нам не нужны стандартные визуальные эффекты нажатия, тени и прочего.

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="<http://schemas.android.com/apk/res/android>"     xmlns:app="<http://schemas.android.com/apk/res-auto>"     xmlns:tools="<http://schemas.android.com/tools>"     android:layout_width="match_parent"     android:layout_height="match_parent"     tools:context=".MainActivity">      <View         android:id="@+id/start_button"         android:layout_width="100dp"         android:layout_height="100dp"         android:background="@drawable/oval"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent" />  </androidx.constraintlayout.widget.ConstraintLayout> 

По клику на кнопку будем выполнять методы start или stop. При первом клике мы начинаем запись, а при втором останавливаем, поэтому берём из рекордера состояние, в зависимости от которого применяем нужную логику:

private fun onButtonClicked() {     if (recordController.isAudioRecording()) {         recordController.stop()     } else {         recordController.start()     } } 

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

Громкость

Теперь давайте сделаем отслеживание громкости записи. Добавим соответствующую функцию:

fun getVolume() = audioRecorder?.maxAmplitude ?: 0 

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

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

private fun onButtonClicked() {     if (recordController.isAudioRecording()) {         recordController.stop()         countDownTimer?.cancel()         countDownTimer = null     } else {         recordController.start()         countDownTimer = object : CountDownTimer(60_000, 100) {             override fun onTick(p0: Long) {                 val volume = recordController.getVolume()                 Log.d(TAG, "Volume = $volume")                 handleVolume(volume)             }              override fun onFinish() {             }         }.apply{             start()         }     } } 

Анимация

Теперь решим следующую задачу: при нажатии кнопки визуально непонятно, идёт ли запись и насколько громко. Создадим метод handleVolume, реагирующий на громкость и меняющий размер кнопки. У View есть множество способов анимирования, самый простой — это animate, который позволяет очень удобно задавать простые анимации.

Насколько нужно увеличивать кнопку? MediaRecorder возвращает значение громкости в виде 16-ти битного int с максимальным значением 32767. Давайте рассчитаем, насколько далеко мы находимся от этого предела, чтобы пропорционально увеличить кнопку:

private fun handleVolume(volume: Int) {     val scale = min(8.0, volume / MAX_RECORD_AMPLITUDE + 1.0).toFloat()     Log.d(TAG, "Scale = $scale")      audioButton.animate()         .scaleX(scale)         .scaleY(scale)         .setInterpolator(interpolator)         .duration= VOLUME_UPDATE_DURATION } 

Интересно, что эта анимация работает автоматически: если мы несколько раз в цикле запустим animate, то наложения не произойдёт, каждая новая анимация будет завершать предыдущую. Только надо не забыть завершать запись и анимацию в методах onDestroy или onPause на случай поворота экрана или других событий, связанных с Activity.

Для более живой анимации воспользуемся OvershootInterpolator‘ом, он позволяет выходить за границы доступного диапазона: кнопка будет словно пульсировать, кратковременно выходя за верхнюю границу:

Всё оказалось достаточно просто. Можно вместо изменения размера рисовать гистограмму громкости или ещё что-нибудь, что придёт в голову вам или вашему дизайнеру ?


ссылка на оригинал статьи https://habr.com/ru/articles/581626/