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_DURATIONREACHED -> {
// 如果录制超时,则停止录制会回调上层
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:
-
- launch a thread to record audio in file.
-
- control the state of recording and invoke according callbacks in main thread.
-
- 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("$audioFilePath${File.separator}${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_DURATIONREACHED -> {
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)
面试复习笔记
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《960页Android开发笔记》
《1307页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。