本篇先从概念上剖析了整个音频处理流程:采样-量化-编码-压缩编码。然后通过实例代码,分析如何使用 AudioRecord & MediaRecorder 在安卓手机上录制音频的详细过程。
基础知识
模拟信号
音频承载着声音信息,而声音是连续变化的信息。物理中把承载信息的载体称为信号,把连续变化的信息称为模拟信号,它在坐标轴中表现为如下形态:
计算机只能处理0和1,即离散值。音频这种模拟信号得转换成离散值才能被计算机处理。这个转化过程称为模拟信号数字化,分为三个步骤:
1. 采样
采样是对连续信号在时间上进行离散,即按照特定的时间间隔在原始的模拟信号上逐点采集瞬时值。采样的可视化效果如下图所示:
原本连续的曲线被一根根离散的竖直线条代替。这些线条越密集,将它们相连后形成的曲线就越接近原始模拟信号。
物理中用采样频率来表示采样的密集程度,即每秒采样次数(采样数/秒),它用赫兹(Hz)表示
2. 量化
虽然连续值已被采样成若干离散值,但每个离散值的取值可能有无限多个。为了给每一个离散值都对应一个数字码,必须将无限种取值转化为有限种取值(对于只能处理二进制的计算机来说,取值的可能数应该是2的倍数)。物理中把这种通过四舍五入分级取整的方法称为量化。量化后的数字信号如下图所示:
量化后的音频变得死板有棱角,就好像人类和机器人的差别。
3. 编码
模拟信号经过采样变成离散值,每一个离散值经过量化都对应一个二进制,将这些二进制按时间序列组合在一起就称为编码。
经过采样量化编码形成的是音频的原始数据,这种原始数据格式称为PCM(Pulse Code Modulation),即是采样量化编码的英文表示。
.pcm 后缀的文件是非常非常大的,这增加了存储和网络传输的成本。遂 PCM 这样原始的无损音频还得经过一次压缩编码。
音频存在冗余信息,才能被压缩。比如人耳能辨识的声音频率范围为20Hz~20KHz,该频率以外的声音都是冗余信息。再比如强弱信号同时出现,强弱差距过大,以至于弱信号完全被掩盖,弱信号就是冗余信息。
音频有很多压缩编码的格式,以下是 Android 官方支持的格式:
在移动端最为常用的格式是 AAC,即 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。它采用了更加高效的编码方式,使得它拥有和 MP3 相当的音质及更小的体积。
压缩编码由两种执行方式,交由 GPU 或是 CPU 执行,前者称为硬编码后者称为软编码,硬编码速度快,但兼容差,会存在编码失败的情况。软编码速度慢,但兼容性好。
录制 PCM 音频
Android 提供了两种录制音频的方式:1. MediaRecorder
2. AudioRecord
如果没有优化音频的需求,完全可以使用 MediaRecorder 直接输出 AAC 格式的音频。
而音频优化,比如降噪,增益算法都是基于 PCM 格式的。这就不得不使用 AudioRecord 来录制音频。
构建 AudioRecord 对象
AudioRecord 的构造函数包含 6 个参数:
- 音频源:表示从哪里采集音频,通常是麦克风。
- 采样频率:即每秒钟采用次数,44100 Hz是目前所有安卓设备都支持的采样频率。
- 声道数:表示声音由几个声道组成,单声道是目前所有安卓设备都支持的声道数。
- 量化精度:表示采用多少位二进制来表达一次量化的离散值,通常用 16 位。
- 缓冲区大小:表示在内存开辟一块多大的缓冲区用于存放硬件采集的音频数据。
构建 AudioRecord 的模板代码如下:
const val SOURCE = MediaRecorder.AudioSource.MIC //通过麦克风采集音频 const val SAMPLE_RATE = 44100 // 采样频率为 44100 Hz const val CHANNEL_IN_MONO = AudioFormat.CHANNEL_IN_MONO // 单声道 const val ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT //量化精度为 16 位 var bufferSize: Int = 0 // 音频缓冲区大小 val audioRecord by lazy { // 计算缓冲区大小 bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT) // 构建 AudioRecord 实例 AudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, bufferSize) }
将构建 AudioRecord 的参数都常量化,以便在其他地方引用。其中缓冲区大小是通过AudioRecord.getMinBufferSize()
动态计算的,计算的依据是采样平率、声道数、量化精度。
读取音频数据写入文件
有了 AudioRecord 实例,就可以调用它的方法从硬件设备中读取音频数据了。它提供了 3 个方法来控制音频数据的读取,分别是开始录制startRecording()
、读一批音频数据read()
、停止录制stop()
,这 3 个方法通常用下面的模板来组合:
audioRecord.startRecording() while(是否继续录制){ audioRecord.read() } audioRecord.stop()
音频数据的大小以字节为单位,音频数据的读取是一批一批进行的,所以需要一个 while 循环持续不断地读取,每次读取多少字节由申请的缓冲区大小决定。
从硬件设备读取的音频字节先存放在字节数组中,然后再把字节数组写入文件就形成了 PCM 文件:
var bufferSize: Int = 0 // 音频缓冲区大小 val outputFile:File // pcm 文件 val audioRecord: AudioRecord // 构建 pcm 文件输出流 outputFile.outputStream().use { outputStream -> // 开始录制 audioRecord.startRecording() // 构建存放音频数据的字节数组 val audioData = ByteArray(bufferSize)// 对应 java 中的 byte[] // 持续读取音频数据 while (continueRecord()) { // 读一批音频数据到字节数组 audioRecord.read(audioData, 0, audioData.size) // 将字节数组通过输出流写入 pcm 文件 outputStream.write(audioData) } // 停止录制 audioRecord.stop() }
- 其中
outputStream()
是 File 的一个扩展方法,它使得代码语音更清晰,更连续:
public inline fun File.outputStream(): FileOutputStream { return FileOutputStream(this) }
use()
是一个 Closeable 的扩展方法,不管发生了什么,最终use()
都会调用close()
来关闭资源。这就避免了流操作的模板代码,降低了代码的复杂度:
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } var exception: Throwable? = null try { // 在 try 代码块中执行传入的 lambda return block(this) } catch (e: Throwable) { exception = e throw e } finally { // 在 finally 中执行 close() when { apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception) this == null -> {} exception == null -> close() else -> try { close() } catch (closeException: Throwable) {} } } }
- 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. * 2. control the state of recording and invoke according callbacks in main thread. * 3. 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_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) if (outputFormat == AAC) { setAudioSamplingRate(22050) setAudioEncodingBitRate(32000) } 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() } } /** * record audio by [android.media.AudioRecord] */ inner class AudioRecorder(override var outputFormat: String) : Recorder { private var bufferSize: Int = 0 private var isRecording = AtomicBoolean(false) private var startTime = AtomicLong() private var duration = 0L 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 } override fun stop() { isRecording.set(false) } override fun release() { audioRecord.release() } } }
下一篇会接着这个主题,继续分析如何利用 MediaCodec 将 PCM 文件进行硬编码转换成 AAC 文件。
talk is cheap, show me the code
完整代码在这个repo中的AudioManager
类中