AI 应用井喷式爆发,语音相关功能也不再局限于 IM 等社交应用,逐渐被广泛使用。语音正在从“聊天里的一个按钮”,变成 AI 产品的主要交互入口之一。
今天来分享一下 Android 中录音与回放的基础实现。
录制语音
录音需要 RECORD_AUDIO,它是危险权限。
<manifest ...>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
除了在 Manifest 里声明,还得在运行时向用户申请。
class RecorderActivity : AppCompatActivity() {
...
private val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
startRecording()
} else {
showToast(R.string.mic_permission_denied)
}
}
private fun requestMicPermissionThenStart() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
startRecording()
} else {
launcher.launch(Manifest.permission.RECORD_AUDIO)
}
}
}
在获取权限后,开始录音。
class RecorderActivity : AppCompatActivity() {
...
private var isRecording = false
private var recorder: MediaRecorder? = null
private val outputFile: File
get() = File(cacheDir, "record_demo.m4a")
private fun startRecording() {
if (isRecording) return
recorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
@Suppress("DEPRECATION")
MediaRecorder()
}
try {
recorder?.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setAudioEncodingBitRate(128_000)
setAudioSamplingRate(44_100)
setAudioChannels(1)
setOnInfoListener { mr, what, extra ->
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED ||
what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
) {
stopRecording()
}
}
setOnErrorListener { mr, what, extra ->
if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
stopRecording()
showToast(R.string.record_error)
}
}
setMaxDuration(60_000)
setMaxFileSize(10L * 1024 * 1024)
setOutputFile(outputFile.absolutePath)
prepare()
start()
}
isRecording = true
showToast(R.string.recording_started)
} catch (e: Exception) {
recorder?.release()
recorder = null
showToast(R.string.record_failed)
}
}
private fun stopRecording() {
val current = recorder ?: return
try {
current.stop()
} catch (e: RuntimeException) {
// 过早 stop() 或没有采集到有效数据时,文件不可用
outputFile.delete()
} finally {
current.release()
recorder = null
isRecording = false
}
showToast(R.string.recording_stopped)
}
override fun onPause() {
super.onPause()
// 退出前台时结束录音,避免后台占用麦克风
if (isRecording) stopRecording()
}
override fun onDestroy() {
super.onDestroy()
recorder?.release()
recorder = null
}
}
首先定义录音状态变量 isRecording,用于记录当前是否正在录音。
我们使用 MediaRecorder 来实现录音功能。MediaRecorder() 无参构造在 API 31 被标记为废弃,官方建议改用带 Context 的版本,所以这里做了版本分支。
MediaRecorder 第一步要确定从哪收声,也就是音频源:
AudioSource.MIC:主麦克风,最通用的选择AudioSource.VOICE_COMMUNICATION:针对通话场景优化,会开启回声消除和降噪AudioSource.VOICE_RECOGNITION:针对语音识别优化,同样带降噪AudioSource.CAMCORDER:与摄像头方向绑定的麦克风AudioSource.UNPROCESSED:未经处理的原始信号(API 24+),并非所有设备支持
「录一段话」这种场景,MIC 就够了。如果你录的同时还在外放,或者做实时通话,就该用 VOICE_COMMUNICATION 来避免回声。
输出格式和编码器的组合,我用的是 MPEG_4 + AAC,后缀 .m4a,通用性最好。采样率和码率可以显式指定,我用的是 44.1kHz 128kbps,你也可以不指定,系统会自动选择默认值。
同时可选配置还有声道数、最大录音时长、最大文件大小等。当触发最大时长或最大文件大小时,需要在 OnInfoListener 回调中处理停止录音。
MediaRecorder 内部维护了一个严格的状态机,各方法只能在特定状态下调用:

| 状态 | 进入方式 | 此刻能调用 |
|---|---|---|
| Initial | 构造 / reset() |
setAudioSource() |
| Initialized | setAudioSource() |
setOutputFormat() |
| DataSourceConfigured | setOutputFormat() |
setAudioEncoder()、setOutputFile()、setMaxDuration()、prepare() |
| Prepared | prepare() |
start()、release() |
| Recording | start() |
stop()、getMaxAmplitude() |
| Error | 任意非法操作 | reset() |
| Released | release() |
无 |
换句话说,下面这个顺序是官方文档给出的标准流程:
setAudioSource() // 必须第一个
setOutputFormat() // 必须在 setAudioSource() 之后
setAudioEncoder() // 必须在 setOutputFormat() 之后
setOutputFile() // 文档要求在 setOutputFormat() 之后、prepare() 之前
prepare() // 配置收尾
start() // 开始
其中真正由框架强制校验的硬链条是 setAudioSource() → setOutputFormat() → setAudioEncoder() → prepare() → start(),这几步都是 native 方法,颠倒任何一步都会立刻抛 IllegalStateException。
setOutputFile() 是个例外:它的 Java 层实现只是把路径/文件存进成员字段,并不做状态检查,真正的校验被推迟到 prepare() 内部的 native 调用。所以实践中把它放到前面往往也不报错——但这只是当前实现的宽松,文档契约写的是「在 setOutputFormat() 之后」,换设备或系统版本可能翻车,建议仍按标准顺序写。
另外 prepare() 是同步阻塞的,低性能设备上可能耗掉几百毫秒,可以考虑放到子线程里执行。
停止录音这块有个必须写的 try-catch,不是防御式编程,是真的会抛异常。官方文档写得很明确:stop() 时如果还没收到有效的音视频数据,会故意抛一个 RuntimeException 出来,让应用自己去清理这个没构造完的输出文件。所以用户手抖点一下就松开,是可能踩到这个分支的。捕获之后务必把文件删掉,否则用户会播到一个损坏文件。
播放录音
播放录音可以用 MediaPlayer,它能满足简单的播放需求。不过现在官方其实更推荐 Jetpack Media3,它更现代、更灵活,但这是另外的话题。
class RecorderActivity : AppCompatActivity() {
...
private var player: MediaPlayer? = null
private var isPlaying = false
private val outputFile: File
get() = File(cacheDir, "record_demo.m4a")
private fun startPlaying() {
if (isPlaying) return
if (outputFile.exists().not()) {
showToast(R.string.no_record_file)
return
}
try {
player = MediaPlayer().apply {
setDataSource(outputFile.absolutePath)
setOnCompletionListener {
releasePlayer()
}
setOnErrorListener { mp, what, extra ->
releasePlayer()
Log.e(TAG, "playback error: what=$what extra=$extra")
true
}
prepare()
start()
}
isPlaying = true
showToast(R.string.playing_started)
} catch (e: Exception) {
player?.release()
player = null
showToast(R.string.play_failed)
}
}
private fun stopPlaying() {
player?.let {
try {
it.stop()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
it.release()
}
player = null
isPlaying = false
}
private fun releasePlayer() {
player?.release()
player = null
isPlaying = false
}
override fun onPause() {
super.onPause()
if (isPlaying) stopPlaying()
}
override fun onDestroy() {
super.onDestroy()
releasePlayer()
}
}
MediaPlayer 同样是状态机,但比 MediaRecorder 宽容一点。
pause()之后可以直接start()恢复,但stop()之后必须重新prepare()才能再播。setOnErrorListener返回true表示错误已被消费,这样回调链就到此为止,不会误触发OnCompletionListener。

写在最后
到这里,一个能录、能停、能回放的最小闭环就跑通了。MediaRecorder 负责采集并直接编码落盘,MediaPlayer 负责把文件播出来,两者都是状态机,把调用顺序和 release() 的时机守住,基本就不会出大问题。
还有几件事这篇文章没展开,也许你会碰到:
- 后台录音:Android 9(API 28)起后台应用拿不到麦克风,系统不报错,只是默默喂静音。要长时间录音得配前台服务,Android 10 起还要声明
android:foregroundServiceType="microphone"。 - 音频焦点:播放前用
AudioManager申请焦点,避免和音乐、通话互相打断,播完记得释放。 - 播放器选型:
MediaPlayer做单次回放够用,但播放列表、后台播放、通知栏控制这些需求一来,直接上 Jetpack Media3 更省心。
麻烦的地方从来不在「怎么录」,而在录完之后那些边界情况——权限被拒、录到一半切后台、文件损坏、异常打断等。把这些兜住,功能才算真的做完。
