Android 音频(一) _ 采样量化编码 & AudioRecord 录制音频,算法题+JVM+自定义View

  1. 量化精度:表示采用多少位二进制来表达一次量化的离散值,通常用 16 位。
  2. 缓冲区大小:表示在内存开辟一块多大的缓冲区用于存放硬件采集的音频数据。

构建 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值