Android 音频 API 看似简单,实则暗藏杀机:回调线程搞错、Buffer 不归还导致无声、录制播放切换时 crash。本文用 Claude Code 写一个线程安全、支持低延迟的音频采集与播放封装。
1、音频开发的痛
“就录个音、播个放,能有多难?”
然后你遇到了:
- AudioRecord 读到的数据全是 0,查了半天发现 Buffer 没还
read()阻塞了 2 秒,因为有其他线程在同时read- 录制中插拔耳机,
AudioTrack直接崩了- 低延迟模式在华为上有效,小米上报
ERROR_BAD_VALUE- 停止录制后忘记
release(),后台录音耗电被用户投诉
音频开发的难点不在 API 本身,而在于: 线程模型、Buffer 生命周期、设备兼容性、AudioSession 路由管理。这些隐性复杂度让一个「简单的录音功能」变成了一个需要仔细设计架构的模块。
2、Android 音频管道速览
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 麦克风 │ → │ AudioRecord │ → │ 你的代码 │ → │ 编码/传输 │
│ (硬件) │ │ (PCM 采集) │ │ (Buffer) │ │ (AAC/Opus)│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 网络/文件 │ → │ 解码器 │ → │ AudioTrack │ → │ 扬声器 │
│ (数据源) │ │ (PCM) │ │ (PCM 播放) │ │ (硬件) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
| API | 用途 | 最低延迟 | 引入版本 |
|---|---|---|---|
| AudioRecord | PCM 音频采集 | ~10ms (AAudio 路径) | API 3 |
| AudioTrack | PCM 音频播放 | ~10ms (AAudio 路径) | API 3 |
| AAudio | 高性能音频(C API) | ~5ms | API 26 (Android 8) |
| Oboe | Google 音频库(C++ 封装 AAudio/OpenSL ES) | ~3ms | 现代推荐 |
本文聚焦 AudioRecord + AudioTrack——覆盖面最广,不需要 NDK。低延迟通过正确的参数配置实现。
3、 用 Claude Code 写线程安全封装
3.1、 Prompt
帮我写一个 Android 音频采集与播放的线程安全封装。
需求:
【音频采集 AudioCapture】
1. 基于 AudioRecord,支持低延迟参数配置
2. 使用专用高优先级线程读取 PCM 数据
3. Buffer 循环队列,避免频繁 GC
4. 通过 Flow 将 PCM 数据暴露给上层
5. 处理权限、设备断开、录音中断
【音频播放 AudioPlayer】
1. 基于 AudioTrack,支持流式写入
2. 使用专用线程消费 PCM 数据
3. 支持播放队列缓存(防 underrun)
4. 处理 AudioSession 路由变化(插拔耳机切换输出)
【通用要求】
1. Kotlin 实现,协程 + Flow
2. 线程安全:读写锁保护共享状态
3. 完整的生命周期管理:start/stop/release
4. 错误处理:设备不支持、权限拒绝、AudioSession 异常
5. 中文注释,生产级代码质量
输出文件:
- audio/AudioCapture.kt
- audio/AudioPlayer.kt
- audio/AudioConfig.kt
3.2、 核心代码
3.2.1、AudioConfig.kt —— 音频参数配置
package com.example.audio
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
/**
* 音频配置
*
* 低延迟关键:
* 1. 采样率: 48kHz (Android 原生采样率,避免 SRC 重采样延迟)
* 2. Buffer 大小: 2-5ms 的帧数 (实时通信场景)
* 3. AAudio 路径: API 26+ 自动使用 AAudio 替代 OpenSL ES
*/
dataclass AudioConfig(
val sampleRate: Int = 48000, // 采样率 (Hz)
val channelConfig: Int = AudioFormat.CHANNEL_IN_MONO, // 单声道
val audioFormat: Int = AudioFormat.ENCODING_PCM_16BIT, // 16bit PCM
val framesPerBuffer: Int = 240 // 每帧采样数 (5ms @48kHz)
) {
/** 每帧字节数 = 帧数 × 声道数 × 字节/采样 */
val bytesPerFrame: Int
get() {
val channelCount = when (channelConfig) {
AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_OUT_MONO -> 1
AudioFormat.CHANNEL_IN_STEREO, AudioFormat.CHANNEL_OUT_STEREO -> 2
else -> 1
}
val bytesPerSample = when (audioFormat) {
AudioFormat.ENCODING_PCM_16BIT -> 2
AudioFormat.ENCODING_PCM_8BIT -> 1
AudioFormat.ENCODING_PCM_FLOAT -> 4
else -> 2
}
return framesPerBuffer * channelCount * bytesPerSample
}
/** 最小 Buffer 大小 (AudioRecord 要求) */
fun minRecordBufferSize(): Int {
return AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
}
/** 最小 Buffer 大小 (AudioTrack 要求) */
fun minTrackBufferSize(): Int {
return AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat)
}
/**
* 构建低延迟 AudioAttributes
*
* USAGE_VOICE_COMMUNICATION 触发 Android 的低延迟音频路径
*/
fun buildLowLatencyAttributes(): AudioAttributes {
return AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
}
}
3.2.2、AudioCapture.kt —— 音频采集
package com.example.audio
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Process
import android.util.Log
import androidx.core.content.ContextCompat
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* 低延迟音频采集器
*
* 架构:
* ┌─────────────┐ ┌──────────────┐ ┌──────────────┐
* │ AudioRecord │ → │ 读取线程 │ → │ SharedFlow │
* │ (硬件采集) │ │ (高优先级) │ │ (暴露给上层) │
* └─────────────┘ └──────────────┘ └──────────────┘
*
* 用法:
* val capture = AudioCapture(context, AudioConfig())
* capture.pcmFlow.collect { bytes -> /* 处理 PCM 数据 */ }
* capture.start()
* ...
* capture.stop()
* capture.release()
*/
class AudioCapture(
privateval context: Context,
privateval config: AudioConfig = AudioConfig()
) {
companionobject {
privateconstval TAG = "AudioCapture"
}
// MARK: - 状态
sealedclass State {
dataobject Idle : State()
dataobject Recording : State()
dataclass Error(val cause: Throwable) : State()
dataobject Released : State()
}
privateval _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state.asStateFlow()
// PCM 数据流(SharedFlow 保证多个收集者都能收到同一帧数据)
privateval _pcmFlow = MutableSharedFlow<ByteArray>(
replay = 0,
extraBufferCapacity = 64, // 缓存 64 帧防背压丢帧
onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
)
val pcmFlow: SharedFlow<ByteArray> = _pcmFlow.asSharedFlow()
// 控制
privateval isRecording = AtomicBoolean(false)
privatevar audioRecord: AudioRecord? = null
privatevar readThread: Thread? = null
privatevar scope: CoroutineScope? = null
// MARK: - 初始化
privatefun createAudioRecord(): AudioRecord {
val minBufSize = config.minRecordBufferSize()
val actualBufSize = maxOf(minBufSize, config.bytesPerFrame * 4)
return AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(config.audioFormat)
.setSampleRate(config.sampleRate)
.setChannelMask(config.channelConfig)
.build()
)
.setBufferSizeInBytes(actualBufSize)
.build()
}
// MARK: - 权限检查
fun hasPermission(): Boolean {
return ContextCompat.checkSelfPermission(
context, Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
// MARK: - 开始录制
fun start(): Boolean {
if (!hasPermission()) {
_state.value = State.Error(SecurityException("缺少 RECORD_AUDIO 权限"))
returnfalse
}
if (isRecording.get()) {
Log.w(TAG, "已在录制中")
returnfalse
}
returntry {
val record = createAudioRecord()
this.audioRecord = record
isRecording.set(true)
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 启动高优先级读取线程
readThread = Thread(priority = Process.THREAD_PRIORITY_URGENT_AUDIO) {
android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
readLoop(record)
}.apply {
name = "AudioCapture-Thread"
isDaemon = true
start()
}
_state.value = State.Recording
Log.i(TAG, "录制开始: ${config.sampleRate}Hz ${config.bytesPerFrame}B/frame")
true
} catch (e: Exception) {
Log.e(TAG, "启动录制失败", e)
_state.value = State.Error(e)
false
}
}
// MARK: - 读取循环(在高优先级线程中运行)
privatefun readLoop(record: AudioRecord) {
record.startRecording()
// 预分配 Buffer(复用,避免 GC)
val buffer = ByteArray(config.bytesPerFrame)
var totalFrames = 0L
while (isRecording.get() && !Thread.currentThread().isInterrupted) {
val bytesRead = try {
record.read(buffer, 0, buffer.size)
} catch (e: Exception) {
Log.e(TAG, "AudioRecord.read() 异常", e)
-1
}
when {
bytesRead == AudioRecord.ERROR_INVALID_OPERATION -> {
Log.e(TAG, "AudioRecord 未初始化")
_state.value = State.Error(IllegalStateException("AudioRecord 未初始化"))
break
}
bytesRead == AudioRecord.ERROR_BAD_VALUE -> {
Log.e(TAG, "AudioRecord 参数错误")
break
}
bytesRead == AudioRecord.ERROR_DEAD_OBJECT -> {
Log.e(TAG, "AudioRecord 对象已释放")
break
}
bytesRead > 0 -> {
totalFrames++
// 拷贝一份数据发送(因为 buffer 会被下一帧覆盖)
val pcmData = buffer.copyOf(bytesRead)
// 用 tryEmit 而非 emit(非挂起,不阻塞读取线程)
val emitted = _pcmFlow.tryEmit(pcmData)
if (!emitted) {
// SharedFlow buffer 满了,丢弃最旧帧
// 这说明消费者处理太慢,需要优化
Log.w(TAG, "帧丢弃 $totalFrames: 消费者处理太慢")
}
}
bytesRead == 0 -> {
// 读到了 0 字节,可能是缓冲区空了
// 短暂等待一下
Thread.sleep(1)
}
}
}
record.stop()
Log.i(TAG, "录制停止,总帧数: $totalFrames")
}
// MARK: - 停止录制
fun stop() {
isRecording.set(false)
// 等待读取线程结束
readThread?.join(1000)
readThread = null
scope?.cancel()
scope = null
_state.value = State.Idle
Log.i(TAG, "录制已停止")
}
// MARK: - 释放资源
fun release() {
if (isRecording.get()) {
stop()
}
audioRecord?.apply {
try {
release()
} catch (e: Exception) {
Log.e(TAG, "释放 AudioRecord 异常", e)
}
}
audioRecord = null
_state.value = State.Released
Log.i(TAG, "资源已释放")
}
}
3.2.3、AudioPlayer.kt —— 音频播放
package com.example.audio
import android.media.AudioManager
import android.media.AudioTrack
import android.os.Process
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
/**
* 低延迟音频播放器
*
* 架构:
* ┌──────────┐ ┌─────────────┐ ┌────────────┐
* │ 数据源 │ → │ Channel 队列 │ → │ 播放线程 │ → 扬声器
* │ (Flow) │ │ (防 underrun)│ │ (高优先级) │
* └──────────┘ └─────────────┘ └────────────┘
*/
class AudioPlayer(
privateval config: AudioConfig = AudioConfig()
) {
companionobject {
privateconstval TAG = "AudioPlayer"
}
// MARK: - 状态
sealedclass State {
dataobject Idle : State()
dataobject Playing : State()
dataobject Paused : State()
dataclass Error(val cause: Throwable) : State()
dataobject Released : State()
}
privateval _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state.asStateFlow()
// 播放队列
privateval playQueue = Channel<ByteArray>(Channel.BUFFERED)
privatevar audioTrack: AudioTrack? = null
privatevar playJob: Job? = null
privatevar scope: CoroutineScope? = null
// MARK: - 创建 AudioTrack
privatefun createAudioTrack(): AudioTrack {
val minBufSize = config.minTrackBufferSize()
val actualBufSize = maxOf(minBufSize, config.bytesPerFrame * 8)
val attributes = config.buildLowLatencyAttributes()
val format = AudioFormat.Builder()
.setEncoding(config.audioFormat)
.setSampleRate(config.sampleRate)
.setChannelMask(config.channelConfig)
.build()
return AudioTrack.Builder()
.setAudioAttributes(attributes)
.setAudioFormat(format)
.setBufferSizeInBytes(actualBufSize)
.setTransferMode(AudioTrack.MODE_STREAM) // 流式写入
.build()
}
// MARK: - 播放 PCM 数据
suspendfun play(pcmFlow: Flow<ByteArray>) {
if (_state.value == State.Playing) {
Log.w(TAG, "已在播放中")
return
}
val track = try {
createAudioTrack()
} catch (e: Exception) {
_state.value = State.Error(e)
return
}
this.audioTrack = track
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Job1: 从 Flow 收集数据写入队列
val collectJob = scope!!.launch {
try {
pcmFlow.collect { pcmData ->
playQueue.send(pcmData)
}
} catch (e: CancellationException) {
// 正常取消
} catch (e: Exception) {
Log.e(TAG, "数据收集异常", e)
_state.value = State.Error(e)
}
}
// Job2: 启动高优先级播放线程
playJob = scope!!.launch(Dispatchers.IO) {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
track.play()
_state.value = State.Playing
Log.i(TAG, "播放开始")
try {
while (isActive && track.playState == AudioTrack.PLAYSTATE_PLAYING) {
val pcmData = try {
playQueue.receive() // 阻塞等待数据
} catch (e: Exception) {
break
}
var offset = 0
var remaining = pcmData.size
// 写入可能不会一次写完,需要循环
while (remaining > 0) {
val written = track.write(pcmData, offset, remaining)
if (written < 0) {
Log.e(TAG, "AudioTrack.write() 错误: $written")
break
}
offset += written
remaining -= written
}
}
} catch (e: Exception) {
Log.e(TAG, "播放异常", e)
_state.value = State.Error(e)
} finally {
collectJob.cancel()
track.pause()
track.flush()
track.stop()
_state.value = State.Idle
Log.i(TAG, "播放停止")
}
}
}
// MARK: - 暂停/恢复
fun pause() {
audioTrack?.pause()
_state.value = State.Paused
}
fun resume() {
audioTrack?.play()
_state.value = State.Playing
}
// MARK: - 停止播放
fun stop() {
playJob?.cancel()
playJob = null
scope?.cancel()
scope = null
// 清空队列
playQueue.cancel()
// 重建队列供下次使用
// (Channel 一旦 cancel 无法复用,实际可用 ConflatedBroadcastChannel 替代)
_state.value = State.Idle
}
// MARK: - 释放
fun release() {
stop()
audioTrack?.release()
audioTrack = null
_state.value = State.Released
}
}
4、上层集成示例
// 录制并同时播放(耳返)
class AudioLoopbackViewModel : ViewModel() {
privateval capture = AudioCapture(context, AudioConfig())
privateval player = AudioPlayer(AudioConfig())
fun startLoopback() {
// 采集
capture.start()
// 播放采集到的音频(耳返)
viewModelScope.launch {
player.play(capture.pcmFlow)
}
}
fun stopLoopback() {
capture.stop()
player.stop()
}
overridefun onCleared() {
capture.release()
player.release()
}
}
5、踩坑记录
| # | 坑 | 现象 | 根因 | 修复 |
|---|---|---|---|---|
| 1 | read() 返回 0 | 拿到的全是空数据 | Buffer 不够大,硬件一次提供的数据超过 Buffer | actualBufSize = max(minBufSize, bytesPerFrame * 4) |
| 2 | SharedFlow tryEmit 返回 false | 偶尔丢帧 | 消费者(编码器)处理太慢 | 加大 extraBufferCapacity 到 64,同时优化消费者 |
| 3 | 华为手机低延迟无效 | getMinBufferSize 返回值很大 | 厂商定制了音频 HAL | 降级策略:取 minBufSize 和 bytesPerFrame * 8 的较大值 |
| 4 | 插拔耳机后 AudioTrack 无声 | 播放中断 | AudioSession 路由变了但 AudioTrack 没重建 | 监听 AudioManager.ACTION_AUDIO_BECOMING_NOISY,自动暂停播放 |
| 5 | release() 后 crash | IllegalStateException | stop() 和 release() 并发调用 | 用 AtomicBoolean 保护状态,release() 内部先 stop() |
| 6 | read() 阻塞太久 | 录着录着卡了 500ms | 系统音频服务暂停(如来电) | 检测 ERROR_DEAD_OBJECT 自动重启 AudioRecord |
6、性能数据
| 场景 | 采集延迟 | 播放延迟 | 总往返延迟 | CPU 占用 |
|---|---|---|---|---|
| 默认参数 (44.1kHz, 4096 buffer) | 92ms | 92ms | 184ms | 3% |
| 低延迟参数 (48kHz, 240 buffer) | 10ms | 10ms | 20ms | 5% |
| AAudio 路径 (API 26+, 96 frames) | 5ms | 5ms | 10ms | 4% |
测试设备:Pixel 7, Android 15。延迟测量方法:音频回路测试(扬声器 → 麦克风)。
学习和提升音视频开发技术,欢迎你加入我们的知识星球

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。