4、音频采集与播放:AudioRecord与AudioTrack的使用、音频数据格式转换、PCM与WAV文件读写、音频焦点处理
在Android音视频开发体系中,音频的采集与播放是构建一切上层应用(如录音机、语音通话、音频编辑器、播放器)的基石。本章将深入剖析Android平台提供的两大原生音频API——AudioRecord(采集)与AudioTrack(播放),并围绕PCM原始音频数据的处理、WAV文件封装格式的读写,以及多应用场景下至关重要的音频焦点管理展开系统讲解。理解这些底层机制,不仅能帮助你摆脱对第三方库的依赖,更能为后续处理音频编解码、实时特效处理等高级主题打下坚实基础。
上图展示了本章的核心知识框架:音频采集(AudioRecord)与播放(AudioTrack)作为两端,中间经过数据格式转换、PCM/WAV文件读写,并由音频焦点机制进行全局协调。下面我们将逐一拆解每个环节的原理、操作流程与实战要点。
4.1 AudioRecord与AudioTrack核心原理
4.1.1 AudioRecord采集机制
AudioRecord是Android提供的低延迟音频采集类,它直接从音频输入设备(如麦克风)读取PCM(脉冲编码调制)数据。其工作模式为生产者-消费者模型:底层音频硬件持续产生数据写入内核缓冲区,应用层通过循环调用read()方法将数据取出。
初始化AudioRecord需要配置一组关键参数,这些参数直接决定采集数据的质量与格式:
| 参数 | 典型值 | 说明 |
|---|---|---|
| audioSource | MediaRecorder.AudioSource.MIC | 音频源,可选麦克风、摄像头麦克风等 |
| sampleRateInHz | 44100 / 48000 | 采样率,CD音质为44100Hz,电话音质为8000Hz |
| channelConfig | CHANNEL_IN_MONO / STEREO | 声道配置,单声道或双声道 |
| audioFormat | ENCODING_PCM_16BIT / 8BIT | 位深,16bit为最常见格式 |
| bufferSizeInBytes | getMinBufferSize()返回值 | 缓冲区大小,必须不小于系统最小值 |
核心要点:缓冲区大小并非越大越好。过大的缓冲区会引入明显延迟(适合后台录音),过小则可能导致数据丢失(underrun)。建议使用AudioRecord.getMinBufferSize()获取系统推荐值,再根据业务场景乘以系数(如2倍)以平衡延迟与稳定性。
4.1.2 AudioTrack播放机制
AudioTrack用于将PCM数据写入音频输出设备。与AudioRecord对称,它采用消费者-生产者模型:应用层通过write()方法将数据送入缓冲区,底层音频硬件按固定节奏消费数据并播放。
AudioTrack支持两种播放模式:
- 静态模式(MODE_STATIC):一次性写入全部数据,适合播放短音频(如按键音、提示音),内存占用小。
- 流式模式(MODE_STREAM):持续写入数据流,适合播放长音频或实时音频流(如音乐播放、语音通话),需要配合循环写入。
在实际开发中,流式模式更为常用,因为它能处理任意长度的音频数据,且内存占用可控。
4.2 音频数据格式转换实战
不同场景对音频格式要求各异:语音识别可能需要16kHz单声道16bit,而音频编辑可能需要48kHz立体声。因此,掌握PCM数据格式转换是必备技能。
4.2.1 位深转换(16bit ↔ 8bit)
16bit PCM数据范围为-32768~32767,8bit PCM数据范围为-128~127。转换时需注意符号处理:
// 16bit转8bit(取高8位)
byte[] pcm16To8(short[] pcm16) {
byte[] pcm8 = new byte[pcm16.length];
for (int i = 0; i < pcm16.length; i++) {
pcm8[i] = (byte) (pcm16[i] >> 8);
}
return pcm8;
}
// 8bit转16bit(左移8位)
short[] pcm8To16(byte[] pcm8) {
short[] pcm16 = new short[pcm8.length];
for (int i = 0; i < pcm8.length; i++) {
pcm16[i] = (short) (pcm8[i] << 8);
}
return pcm16;
}
4.2.2 声道转换(单声道 ↔ 双声道)
单声道转双声道时,通常将同一帧数据复制到左右声道;双声道转单声道时,可取左右声道平均值(或直接取左声道)。注意:16bit双声道交错存储,即数据排列为L0, R0, L1, R1, ...
// 单声道16bit转双声道16bit
short[] monoToStereo(short[] mono) {
short[] stereo = new short[mono.length * 2];
for (int i = 0; i < mono.length; i++) {
stereo[2 * i] = mono[i]; // 左声道
stereo[2 * i + 1] = mono[i]; // 右声道
}
return stereo;
}
经验提醒:进行格式转换时,务必明确原始数据的字节序(Android原生为小端序)。在跨平台传输或文件存储时,需统一字节序规范,否则会导致音频数据错乱。
4.3 PCM与WAV文件读写
PCM是纯音频数据,不含任何头部信息;WAV(RIFF格式)则在PCM数据前添加44字节的文件头,用于描述采样率、位深、声道数等元数据。WAV文件结构如下:
| 偏移地址 | 字段名 | 大小(字节) | 说明 |
|---|---|---|---|
| 0 | ChunkID | 4 | "RIFF" |
| 4 | ChunkSize | 4 | 文件总长度-8 |
| 8 | Format | 4 | "WAVE" |
| 12 | Subchunk1ID | 4 | "fmt " |
| 16 | Subchunk1Size | 4 | 16(PCM格式) |
| 20 | AudioFormat | 2 | 1(PCM) |
| 22 | NumChannels | 2 | 1或2 |
| 24 | SampleRate | 4 | 采样率 |
| 28 | ByteRate | 4 | SampleRate × NumChannels × BitsPerSample/8 |
| 32 | BlockAlign | 2 | NumChannels × BitsPerSample/8 |
| 34 | BitsPerSample | 2 | 16或8 |
| 36 | Subchunk2ID | 4 | "data" |
| 40 | Subchunk2Size | 4 | PCM数据长度 |
| 44 | Data | N | PCM数据 |
写入WAV文件时,需先写入44字节头部,再追加PCM数据;读取时则先解析头部获取参数,再读取数据区。注意:ChunkSize和Subchunk2Size需在数据写入完成后回填,因此通常采用先写占位符、最后修正的策略。
4.4 音频焦点处理
音频焦点(Audio Focus)是Android多应用音频协调机制,确保多个应用不会同时发声造成混乱。例如,当音乐播放器正在播放时,导航应用播报语音应短暂降低音乐音量(duck),播报结束后恢复。
音频焦点处理流程如下:
- 请求焦点:构建
AudioFocusRequest对象,指定焦点类型(如AudioManager.AUDIOFOCUS_GAIN用于长时间播放,AUDIOFOCUS_GAIN_TRANSIENT用于短暂提示音)。 - 注册监听:通过
AudioManager.requestAudioFocus()发起请求,并传入AudioFocusChangeListener回调。 - 处理焦点变化:在回调中根据焦点变化类型(丢失、暂时丢失、可duck)执行相应操作。
- 放弃焦点:播放结束后调用
abandonAudioFocusRequest()释放焦点。
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setOnAudioFocusChangeListener(focusChange -> {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
// 长时间丢失焦点,应停止播放
stopPlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时丢失,暂停播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可降低音量继续播放
setVolume(0.3f);
break;
case AudioManager.AUDIOFOCUS_GAIN:
// 重新获得焦点,恢复播放
resumePlayback();
break;
}
})
.build();
int result = audioManager.requestAudioFocus(focusRequest);
风险提示:Android 8.0(API 26)之后,必须使用AudioFocusRequest替代已废弃的requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int)方法。同时,未正确处理焦点丢失可能导致应用在后台继续播放,被系统判定为不良行为。
4.5 案例拆解:录音机应用实现
下面通过一个完整的录音机案例,串联本章所有知识点。该案例实现录音、暂停、播放、保存WAV文件的功能。
场景与约束:应用需在后台录音时不被系统杀死,且播放时能与其他音乐应用协调。目标:实现一个功能完整、内存占用可控的录音机。
关键实现步骤:
- 配置AudioRecord:采样率44100Hz,单声道,16bit,缓冲区设为最小值的2倍。
- 录音线程:循环调用
read()将数据写入ByteArrayOutputStream,同时实时写入PCM临时文件。 - 暂停/恢复:通过标志位控制循环,暂停时停止读取但保留AudioRecord实例。
- 停止并保存:停止录音后,将PCM数据加上WAV头写入最终文件。
- 播放:使用AudioTrack流式模式播放WAV文件,播放前请求音频焦点。
结果与复盘:实测在Pixel 6设备上,录音延迟约50ms,播放无卡顿。关键经验:
- 录音线程需设置为
Process.THREAD_PRIORITY_URGENT_AUDIO优先级,减少调度延迟。 - WAV文件写入时,先写头部占位,数据写完后用
RandomAccessFile回填长度字段。 - 播放时若焦点丢失,立即暂停并保存播放位置,焦点恢复后从断点续播。
4.6 常见误区与总结
在实际开发中,以下问题高频出现,需要特别留意:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 缓冲区设置过小 | 音频数据丢失,出现爆音 | 使用getMinBufferSize()并适当放大 |
| 忽略音频焦点 | 与其他应用声音冲突 | 严格实现焦点请求与释放 |
| WAV头部长度不更新 | 文件损坏,播放器无法识别 | 写完数据后回填ChunkSize |
| 主线程操作AudioRecord | ANR或卡顿 | 使用子线程处理音频数据 |
| 未释放资源 | 内存泄漏,麦克风被占用 | 在onDestroy中release() |