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

e06918f7cddf0ac681117c3a293dd8bd

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

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


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



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

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


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

Запись звука


Для записи звука создадим класс 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? Потому что мы сейчас будем её анимировать и нам не нужны стандартные визуальные эффекты нажатия, тени и прочего.




    



По клику на кнопку будем выполнять методы 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'ом, он позволяет выходить за границы доступного диапазона: кнопка будет словно пульсировать, кратковременно выходя за верхнюю границу:


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

© Habrahabr.ru