4. MediaRecorder 录制音频并保存为 AAC
MediaRecorder 录音链路与状态机
MediaRecorder 把麦克风采集、AAC 编码、M4A 封装串成一条内部流水线,开发者只需按状态机顺序调用 API。核心约束是:必须先配置再 prepare,prepare 成功后才能 start,stop 之后必须 reset 或 release 才能复用。任何一步顺序错误都会抛 IllegalStateException。
权限声明与运行时请求
录音必须声明 RECORD_AUDIO 权限。Android 6.0 起属于危险权限,需在运行时请求。若使用 MediaRecorder 且输出到应用私有目录,无需存储权限;输出到公共目录才需要 WRITE_EXTERNAL_STORAGE(Android 10 以下)。
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 运行时请求
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) startRecording()
else Toast.makeText(this, "需要录音权限", Toast.LENGTH_SHORT).show()
}
// 触发
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
startRecording()
} else {
requestPermission.launch(Manifest.permission.RECORD_AUDIO)
}
完整录音实现
以下代码封装了从配置到停止的完整流程,输出文件位于应用私有目录 filesDir,无需额外存储权限。
class AudioRecorder(private val context: Context) {
private var recorder: MediaRecorder? = null
private var outputFile: File? = null
fun start(): File {
// 1. 创建输出文件
val file = File(context.filesDir, "record_${System.currentTimeMillis()}.m4a")
outputFile = file
// 2. 构建 MediaRecorder
recorder = MediaRecorder(context).apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setAudioEncodingBitRate(128_000)
setAudioSamplingRate(44_100)
setOutputFile(file.absolutePath)
// 3. 准备并启动
prepare()
start()
}
return file
}
fun stop(): File? {
return try {
recorder?.apply {
stop() // 停止录音,文件头写入完成
reset() // 回到 Idle,可复用
release() // 释放底层资源
}
recorder = null
outputFile
} catch (e: IllegalStateException) {
// 未 start 就 stop 会抛异常
recorder?.release()
recorder = null
null
}
}
fun release() {
recorder?.release()
recorder = null
}
}
状态机陷阱:调用
stop() 时若录音时长过短(< 1 秒),部分设备会抛 RuntimeException,且文件可能损坏。建议在 UI 层限制最短录音时间,或在 stop() 外层捕获异常并删除无效文件。
验证与结果判读
- 文件存在性:录音结束后检查
file.exists() && file.length() > 0。 - 格式正确性:用
MediaPlayer或MediaMetadataRetriever读取时长与编码信息,确认METADATA_KEY_MIMETYPE为audio/mp4a-latm。 - 状态机合规:若在
prepare()前调用start(),会抛IllegalStateException;若在stop()后未reset()就再次start(),同样抛异常。
// 验证录音文件
val retriever = MediaMetadataRetriever()
retriever.setDataSource(file.absolutePath)
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
val mime = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
Log.d("Recorder", "时长=${duration}ms, MIME=${mime}")
retriever.release()
复用建议:若需连续录制多段,每次
stop() 后调用 reset() 并重新 setOutputFile(),再 prepare() 即可,无需重新 new MediaRecorder()。但 release() 后必须重新创建实例。