- 量化精度:表示采用多少位二进制来表达一次量化的离散值,通常用 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 Aud