Android 音频(一) _ 采样量化编码 & AudioRecord 录制音频

  • IO操作时耗时的,读写音频数据的代码应该在非UI线程中执行。而是否继续录制应该由用户动作触发,即UI线程触发。这里有多线程安全问题,需要一个线程安全的布尔值来控制音频录制:

var isRecording = AtomicBoolean(false) // 线程安全的布尔变量
val audioRecord: AudioRecord

// 是否继续录制
fun continueRecord(): Boolean {
return isRecording.get() &&
audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING
}

// 停止录制音频(供业务层调用以停止录音的 while 循环)
fun stop() {
isRecording.set(false)
}

解耦抽象

将对 AudioRecord 的所有操作都抽象在一个接口中:

interface Recorder {
var outputFormat: String // 输出音频格式
fun isRecording(): Boolean // 是否正在录制
fun getDuration(): Long // 获取音频时长
fun start(outputFile: File, maxDuration: Int) // 开始录制
fun stop() // 停止录制
fun release() // 释放录制资源
}

这个接口提供了录制音频的抽象能力。当上层类和这组接口打交道时,不需要关心录制音频的实现细节,即不和 AudioRecord 耦合。

为啥要多一层这样的抽象?因为具体实现总是易变的,万一哪天业务层需要直接生成 AAC 文件,就可以通过添加一个Recorder的实例方便地地替换原有实现。

给出 AudioRecord 对于Recorder接口的实现:

class AudioRecorder(override var outputFormat: String) : Recorder {
private var bufferSize: Int = 0 // 音频字节缓冲区大小
private var isRecording = AtomicBoolean(false) // 用于控制音频录制的线程安全布尔值
private var startTime = 0L // 记录音频开始录制时间
private var duration = 0L // 音频时长
// AudioRecord 实例
private val audioRecord by lazy {
bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize)
}
// 是否正在录制
override fun isRecording(): Boolean = isRecording.get()
// 获取音频时长
override fun getDuration(): Long = duration
// 开始音频录制
override fun start(outputFile: File, maxDuration: Int) {
if (audioRecord.state == AudioRecord.STATE_UNINITIALIZED) return
isRecording.set(true) // 在异步线程中标记开始录制
startTime.set(SystemClock.elapsedRealtime()) // 在异步线程中记录开始时间
// 创建文件输出流
outputFile.outputStream().use { outputStream ->
// 开始录制
audioRecord.startRecording()
val audioData = ByteArray(bufferSize)
// 持续读取音频数据到字节数组, 再将字节数组写入文件
while (continueRecord(maxDuration)) {
audioRecord.read(audioData, 0, audioData.size)
outputStream.write(audioData)
}
// 循环结束后通知底层结束录制
if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop()
}
// 如果录音长度超过最大时长,则回调给上层
if (duration >= maxDuration) handleRecordEnd(isSuccess = true, isReachMaxTime = true)
}
}

// 判断录音是否可以继续
private fun continueRecord(maxDuration: Int): Boolean {
// 实时计算录音时长
duration = SystemClock.elapsedRealtime() - startTime.get()
return isRecording.get() &&
audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING &&
duration < maxDuration
}
// 停止录音(在UI线程调用)
override fun stop() {
isRecording.set(false)
}
// 释放录音资源
override fun release() {
audioRecord.release()
}
}

下面是 MediaRecorder 对于Recorder接口的实现:

inner class MediaRecord(override var outputFormat: String) : Recorder {
private var starTime = AtomicLong() // 音频录制开始时间
// 监听录制是否超时的回调
private val listener = MediaRecorder.OnInfoListener { _, what, _ ->
when (what) {
MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
// 如果录制超时,则停止录制会回调上层
stop()
handleRecordEnd(isSuccess = true, isReachMaxTime = true)
}
else -> {
handleRecordEnd(isSuccess = false, isReachMaxTime = false)
}
}
}
// 录制错误监听器
private val errorListener = MediaRecorder.OnErrorListener { _, _, _ ->
handleRecordEnd(isSuccess = false, isReachMaxTime = false)
}
private val recorder = MediaRecorder()
private var isRecording = AtomicBoolean(false) // 用于控制音频录制的线程安全布尔值
private var duration = 0L // 音频时长
// 判断是否正在录制音频
override fun isRecording(): Boolean = isRecording.get()
// 录制音频时长
override fun getDuration(): Long = duration
// 开始录制音频
override fun start(outputFile: File, maxDuration: Int) {
// 枚举音频输出格式
val format = when (outputFormat) {
AMR -> MediaRecorder.OutputFormat.AMR_NB
else -> MediaRecorder.OutputFormat.AAC_ADTS
}
// 枚举音频编码格式
val encoder = when (outputFormat) {
AMR -> MediaRecorder.AudioEncoder.AMR_NB
else -> MediaRecorder.AudioEncoder.AAC
}
// 开始录制
starTime.set(SystemClock.elapsedRealtime())
isRecording.set(true)
recorder.apply {
reset()
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(format)
setOutputFile(outputFile.absolutePath)
setAudioEncoder(encoder)
setOnInfoListener(listener)
setOnErrorListener(errorListener)
setMaxDuration(maxDuration)
prepare()
start()
}
}
// 停止录制
override fun stop() {
recorder.stop()
isRecording.set(false)
duration = SystemClock.elapsedRealtime() - starTime.get()
}
// 释放录制资源
override fun release() {
recorder.release()
}
}

把和Recorder接口打交道的上层类定义为AudioManager,它是业务层访问音频能力的入口,提供了一组访问接口:

// 构造 AudioManager 时需传入上下文和音频输出格式
class AudioManager(val context: Context, val type: String = AAC) {
companion object {
const val AAC = “aac”
const val AMR = “amr”
const val PCM = “pcm”
}

private var maxDuration = 120 * 1000 // 默认最大音频时长为 120 s
// 根据输出格式实例化对应 Recorder 实例
private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
// 开始录制
fun start(maxDuration: Int = 120) {
this.maxDuration = maxDuration * 1000
startRecord()
}
// 停止录制
fun stop(cancel: Boolean = false) {
stopRecord(cancel)
}
// 释放资源
fun release() {
recorder.release()
}
// 是否正在录制
fun isRecording() = recorder.isRecording()
}

其中的startRecord()stopRecord()包含了AudioManager层控制播放的逻辑:

class AudioManager(val context: Context, val type: String = AAC) :
// 为了方便启动协程录制,直接继承 CoroutineScope,并调度协程到一个单线程线程池对应的 Dispatcher
CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {

private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
// 开始录制
private fun startRecord() {
// 请求音频焦点
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)

// 若音频正在录制,则返回
if (recorder.isRecording()) {
setState(STATE_FAILED) // 设置状态为失败
return
}
// 如果储存卡控件不足,则返回
if (getFreeSpace() <= 0) {
setState(STATE_FAILED) // 设置状态为失败
return
}
// 创建音频文件
audioFile = getAudioFile()
// 若创建失败,则返回
if (audioFile == null) setState(STATE_FAILED) // 设置状态为失败

cancelRecord.set(false)
try {
if (! cancelRecord.get()) {
setState(STATE_READY) // 设置状态为就绪
if (hasPermission()) { // 拥有录制和存储权限
// 启动协程开始录制
launch { recorder.start(audioFile !!, maxDuration) }
setState(STATE_START) // 设置状态为开始
} else {
stopRecord(false) // 没有权限则停止录制
}
}
} catch (e: Exception) {
e.printStackTrace()
stopRecord(false) // 发生异常时,停止录制
}
}
// 停止录制,需传入是否是用户主动取消录制
private fun stopRecord(cancel: Boolean) {
// 若不在录制中,则返回
if (! recorder.isRecording()) {
return
}
cancelRecord.set(cancel)
// 放弃音频焦点
audioManager.abandonAudioFocus(null)
try {
// 停止录音
recorder.stop()
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 录音结束后,回调状态
handleRecordEnd(isSuccess = true, isReachMaxTime = false)
}
}
}

因为AudioManager是和业务层打交道的类,所以这一层就多了些零碎的控制逻辑,包括音频焦点的获取、存储和录音权限的判断、创建时声音文件、录音状态的回调。

其中录音状态的回调被定义成了若干个 lambda:

class AudioManager(val context: Context, val type: String = AAC) :
CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
// 状态常量
private val STATE_FAILED = 1
private val STATE_READY = 2
private val STATE_START = 3
private val STATE_SUCCESS = 4
private val STATE_CANCELED = 5
private val STATE_REACH_MAX_TIME = 6

// 主线程 Handler,用于将状态回调在主线程
private val callbackHandler = Handler(Looper.getMainLooper())
// 将录音状态回调给业务层的 lambda
var onRecordReady: (() -> Unit)? = null
var onRecordStart: ((File) -> Unit)? = null
var onRecordSuccess: ((File, Long) -> Unit)? = null
var onRecordFail: (() -> Unit)? = null
var onRecordCancel: (() -> Unit)? = null
var onRecordReachedMaxTime: ((Int) -> Unit)? = null

// 状态变更
private fun setState(state: Int) {
callbackHandler.post {
when (state) {
STATE_FAILED -> onRecordFail?.invoke()
STATE_READY -> onRecordReady?.invoke()
STATE_START -> audioFile?.let { onRecordStart?.invoke(it) }
STATE_CANCELED -> onRecordCancel?.invoke()
STATE_SUCCESS -> audioFile?.let { onRecordSuccess?.invoke(it, recorder.getDuration()) }
STATE_REACH_MAX_TIME -> onRecordReachedMaxTime?.invoke(maxDuration)
}
}
}
}

将状态分发回调的细节分装在setState()方法中,以降低录音流程控制代码的复杂度。

完整的AudioManager代码如下:

import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioFormat.CHANNEL_IN_MONO
import android.media.AudioFormat.ENCODING_PCM_16BIT
import android.media.AudioManager
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong

/**

  • provide the ability to record audio in file.
  • [AudioManager] exists for the sake of the following:
    1. launch a thread to record audio in file.
    1. control the state of recording and invoke according callbacks in main thread.
    1. provide interface for the business layer to control audio recording
      */
      class AudioManager(val context: Context, val type: String = AAC) :
      CoroutineScope by CoroutineScope(SupervisorJob() + Executors.newFixedThreadPool(1).asCoroutineDispatcher()) {
      companion object {
      const val AAC = “aac”
      const val AMR = “amr”
      const val PCM = “pcm”

const val SOURCE = MediaRecorder.AudioSource.MIC
const val SAMPLE_RATE = 44100
const val CHANNEL = 1
}

private val STATE_FAILED = 1
private val STATE_READY = 2
private val STATE_START = 3
private val STATE_SUCCESS = 4
private val STATE_CANCELED = 5
private val STATE_REACH_MAX_TIME = 6

/**

  • the callback business layer cares about
    */
    var onRecordReady: (() -> Unit)? = null
    var onRecordStart: ((File) -> Unit)? = null
    var onRecordSuccess: ((File, Long) -> Unit)? = null// deliver audio file and duration to business layer
    var onRecordFail: (() -> Unit)? = null
    var onRecordCancel: (() -> Unit)? = null
    var onRecordReachedMaxTime: ((Int) -> Unit)? = null

/**

  • deliver recording state to business layer
    */
    private val callbackHandler = Handler(Looper.getMainLooper())

private var maxDuration = 120 * 1000
private var recorder: Recorder = if (type == PCM) AudioRecorder(type) else MediaRecord(type)
private var audioFile: File? = null
private var cancelRecord: AtomicBoolean = AtomicBoolean(false)
private val audioManager: AudioManager = context.applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager

fun start(maxDuration: Int = 120) {
this.maxDuration = maxDuration * 1000
startRecord()
}

fun stop(cancel: Boolean = false) {
stopRecord(cancel)
}

fun release() {
recorder.release()
}

fun isRecording() = recorder.isRecording()

private fun startRecord() {
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)

if (recorder.isRecording()) {
setState(STATE_FAILED)
return
}

if (getFreeSpace() <= 0) {
setState(STATE_FAILED)
return
}

audioFile = getAudioFile()
if (audioFile == null) setState(STATE_FAILED)

cancelRecord.set(false)
try {
if (! cancelRecord.get()) {
setState(STATE_READY)
if (hasPermission()) {
launch { recorder.start(audioFile !!, maxDuration) }
setState(STATE_START)
} else {
stopRecord(false)
}
}
} catch (e: Exception) {
e.printStackTrace()
stopRecord(false)
}
}

private fun hasPermission(): Boolean {
return context.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
&& context.checkCallingOrSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}

private fun stopRecord(cancel: Boolean) {
if (! recorder.isRecording()) {
return
}
cancelRecord.set(cancel)
audioManager.abandonAudioFocus(null)
try {
recorder.stop()
} catch (e: Exception) {
e.printStackTrace()
} finally {
handleRecordEnd(isSuccess = true, isReachMaxTime = false)
}
}

private fun handleRecordEnd(isSuccess: Boolean, isReachMaxTime: Boolean) {
if (cancelRecord.get()) {
audioFile?.deleteOnExit()
setState(STATE_CANCELED)
} else if (! isSuccess) {
audioFile?.deleteOnExit()
setState(STATE_FAILED)
} else {
if (isAudioFileInvalid()) {
setState(STATE_FAILED)
if (isReachMaxTime) {
setState(STATE_REACH_MAX_TIME)
}
} else {
setState(STATE_SUCCESS)
}
}
}

private fun isAudioFileInvalid() = audioFile == null || ! audioFile !!.exists() || audioFile !!.length() <= 0

/**

  • change recording state and invoke according callback to main thread
    */
    private fun setState(state: Int) {
    callbackHandler.post {
    when (state) {
    STATE_FAILED -> onRecordFail?.invoke()
    STATE_READY -> onRecordReady?.invoke()
    STATE_START -> audioFile?.let { onRecordStart?.invoke(it) }
    STATE_CANCELED -> onRecordCancel?.invoke()
    STATE_SUCCESS -> audioFile?.let { onRecordSuccess?.invoke(it, recorder.getDuration()) }
    STATE_REACH_MAX_TIME -> onRecordReachedMaxTime?.invoke(maxDuration)
    }
    }
    }

private fun getFreeSpace(): Long {
if (Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) {
return 0L
}

return try {
val stat = StatFs(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
stat.run { blockSizeLong * availableBlocksLong }
} catch (e: Exception) {
0L
}
}

private fun getAudioFile(): File? {
val audioFilePath = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath
if (audioFilePath.isNullOrEmpty()) return null
return File(“ a u d i o F i l e P a t h audioFilePath audioFilePath{File.separator} U U I D . r a n d o m U U I D ( ) . {UUID.randomUUID()}. UUID.randomUUID().type”)
}

/**

  • the implementation of [Recorder] define the detail of how to record audio.
  • [AudioManager] works with [Recorder] and dont care about the recording details
    */
    interface Recorder {

/**

  • audio output format
    */
    var outputFormat: String

/**

  • whether audio is recording
    */
    fun isRecording(): Boolean

/**

  • the length of audio
    */
    fun getDuration(): Long

/**

  • start audio recording, it is time-consuming
    */
    fun start(outputFile: File, maxDuration: Int)

/**

  • stop audio recording
    */
    fun stop()

/**

  • release the resource of audio recording
    */
    fun release()
    }

/**

  • record audio by [android.media.MediaRecorder]
    */
    inner class MediaRecord(override var outputFormat: String) : Recorder {
    private var starTime = AtomicLong()
    private val listener = MediaRecorder.OnInfoListener { _, what, _ ->
    when (what) {
    MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
    stop()
    handleRecordEnd(isSuccess = true, isReachMaxTime = true)
    }
    else -> {
    handleRecordEnd(isSuccess = false, isReachMaxTime = false)
    }
    }
    }
    private val errorListener = MediaRecorder.OnErrorListener { _, _, _ ->
    handleRecordEnd(isSuccess = false, isReachMaxTime = false)
    }
    private val recorder = MediaRecorder()
    private var isRecording = AtomicBoolean(false)
    private var duration = 0L
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

[外链图片转存中…(img-azWd3LB0-1713772143496)]

最后

跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-H7MTYJZp-1713772143498)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值