4. MediaRecorder 录制音频并保存为 AAC

MediaRecorder 录制音频并保存为 AAC 学习卡片

MediaRecorder 录音链路与状态机

MediaRecorder 把麦克风采集、AAC 编码、M4A 封装串成一条内部流水线,开发者只需按状态机顺序调用 API。核心约束是:必须先配置再 prepare,prepare 成功后才能 start,stop 之后必须 reset 或 release 才能复用。任何一步顺序错误都会抛 IllegalStateException。

MediaRecorder 录音数据流与状态机 麦克风 AudioSource.MIC PCM AAC 编码器 AAC_LC / 128kbps AAC帧 M4A 封装 MPEG_4 容器 输出文件 record.m4a 状态机切换路径 Idle Initialized Prepared Recording Stopped setAudioSource prepare() start() stop() reset() / release() 回到 Idle 关键配置参数 setAudioSource MIC 采集源 setOutputFormat MPEG_4 容器格式 setAudioEncoder AAC 编码器 setAudioEncodingBitRate 128000 bps setAudioSamplingRate 44100 Hz

权限声明与运行时请求

录音必须声明 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() 后必须重新创建实例。