1. Android音频编解码基础概念
在Android开发中,音频处理是一个常见但容易被忽视的重要领域。作为一名有多年移动端音频开发经验的工程师,我经常遇到开发者对音频编解码基础概念理解不够深入的问题。让我们从最基础的部分开始梳理。
1.1 什么是音频编解码
音频编解码(Audio Codec)实际上是编码(Encode)和解码(Decode)两个过程的合称。这个术语来源于"COder/DECoder"的组合。在实际开发中,这两个过程往往密不可分:
-
编码过程:将原始的、未压缩的音频数据(通常是PCM格式)转换为压缩后的数据格式(如MP3、AAC等)。这个过程就像把一个大体积的RAW图片转换为JPEG格式,目的是为了减小存储空间和传输带宽需求。
-
解码过程:与编码相反,将压缩的音频数据转换回(或尽可能接近)原始格式,使其能够被音频硬件播放。这相当于把JPEG图片解压回可显示的位图。
重要提示:编解码过程通常是有损的,这意味着经过编码再解码的音频与原始音频不会完全相同。不同编解码器的算法差异会导致不同的音质损失特征。
1.2 关键音频数据格式
1.2.1 PCM格式详解
PCM(脉冲编码调制)是数字音频的"原始"表示形式,它直接来自模拟信号的数字化过程:
- 采样:以固定频率(如44.1kHz)测量模拟信号的振幅
- 量化:将连续振幅值离散化为数字值(如16位整数)
- 编码:将数字值按顺序存储
PCM数据的特点:
- 没有经过压缩,音质无损
- 文件体积大(CD音质的PCM约10MB/分钟)
- 是数字音频处理的基础格式
- Android设备内部处理和硬件驱动间传输的标准格式
在Android中,PCM数据通常以以下形式存在:
- 采样格式:ENCODING_PCM_16BIT(最常用)、ENCODING_PCM_8BIT
- 声道布局:CHANNEL_OUT_MONO(单声道)、CHANNEL_OUT_STEREO(立体声)
1.2.2 常见压缩音频格式对比
| 格式 | 压缩类型 | 音质 | 专利情况 | 典型用途 |
|---|---|---|---|---|
| MP3 | 有损 | 中等 | 已过期 | 音乐存储 |
| AAC | 有损 | 高 | 需要授权 | 流媒体、移动设备 |
| OGG | 有损 | 中高 | 开源 | 游戏、开源项目 |
| FLAC | 无损 | 无损 | 开源 | 高质量音频存档 |
| AMR | 有损 | 低 | 需要授权 | 语音通话 |
在实际项目中,选择音频格式需要考虑:
- 目标设备的兼容性
- 网络传输带宽限制
- 音质要求
- 专利授权成本
2. Android音频处理核心类解析
Android提供了多层次的音频API,从简单易用的高级API到底层控制的低级API,开发者可以根据需求选择合适的工具。
2.1 高级API:快速实现常见功能
2.1.1 MediaPlayer类
MediaPlayer是大多数简单播放需求的起点。它内部封装了完整的播放流水线:
java复制MediaPlayer player = new MediaPlayer();
player.setDataSource("path/to/audio.mp3");
player.prepare(); // 同步准备,或使用prepareAsync()
player.start();
关键特性:
- 支持多种压缩格式(取决于设备解码器)
- 自动处理解码、缓冲、同步
- 提供播放控制(暂停、跳转等)
- 支持网络流媒体播放
常见问题:
- 状态机复杂,需要正确处理prepare/start/stop等状态转换
- 资源释放不及时可能导致内存泄漏
- 自定义能力有限,难以实现特殊效果
2.1.2 MediaRecorder类
与MediaPlayer对应,MediaRecorder提供了简单的录音功能:
java复制MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile("path/to/record.3gp");
recorder.prepare();
recorder.start();
使用限制:
- 输出格式和编码器选项有限
- 无法实时处理录音数据
- 难以精确控制录音参数
2.2 低级API:精细控制音频流水线
2.2.1 AudioTrack:PCM播放专家
AudioTrack是播放PCM数据的核心类,它直接与音频硬件交互:
java复制int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioTrack track = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM
);
track.play();
// 然后不断写入PCM数据
byte[] pcmData = getPcmData();
track.write(pcmData, 0, pcmData.length);
关键参数解析:
- streamType:影响音频路由和音量控制(如STREAM_MUSIC、STREAM_VOICE_CALL)
- mode:
- MODE_STATIC:一次性写入所有数据,适合短音频
- MODE_STREAM:持续写入,适合长音频或实时生成
性能优化点:
- 缓冲区大小设置:太小会导致underrun,太大会增加延迟
- 使用非阻塞写入方式避免主线程卡顿
- 考虑使用AudioAttributes替代streamType(API 21+)
2.2.2 AudioRecord:原始PCM采集
AudioRecord是AudioTrack的对应物,用于从麦克风采集PCM数据:
java复制int sampleRate = 16000;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioRecord recorder = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
recorder.startRecording();
byte[] buffer = new byte[bufferSize];
int read = recorder.read(buffer, 0, buffer.length);
// 处理PCM数据
常见问题解决方案:
- 录音权限缺失:确保已请求RECORD_AUDIO权限
- 音质差:检查采样率(语音至少16kHz,音乐建议44.1kHz)
- 延迟高:减小缓冲区大小,但会增加CPU负载
2.2.3 MediaCodec:编解码核心引擎
MediaCodec是Android中最强大的编解码工具,支持硬件加速:
java复制// 创建解码器示例
MediaCodec decoder = MediaCodec.createDecoderByType("audio/mp4a-latm");
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2);
format.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
decoder.configure(format, null, null, 0);
decoder.start();
// 输入输出缓冲区处理(见后续章节详细流程)
关键概念:
- 异步模式(API 21+):使用Callback更高效
- 编解码器查询:MediaCodecList查找设备支持的编解码器
- 安全解码器:某些设备提供更安全的MediaCodec实现(带.secure后缀)
3. 完整音频处理流程实现
理解了核心类之后,让我们看看如何将它们组合起来实现完整的音频处理流水线。
3.1 音频播放流程深度解析
一个完整的自定义播放流程包括以下步骤:
- 媒体提取:使用MediaExtractor从容器格式(如MP4)中分离音频轨道
- 解码准备:根据音频轨道的MediaFormat创建和配置解码器
- PCM输出:设置AudioTrack准备接收解码后的数据
- 解码循环:
- 将压缩数据送入解码器输入缓冲区
- 从输出缓冲区获取PCM数据
- 将PCM写入AudioTrack
- 资源释放:按正确顺序释放所有资源
优化技巧:
- 使用双缓冲区减少等待时间
- 动态调整AudioTrack缓冲区大小以适应不同音频源
- 处理解码器刷新和流结束标志
3.2 音频录制编码流程实现
自定义录音编码流程:
- PCM采集:配置并启动AudioRecord
- 编码器准备:创建和配置MediaCodec编码器
- 封装器准备:创建MediaMuxer准备写入最终文件
- 处理循环:
- 从AudioRecord读取PCM
- 送入编码器输入缓冲区
- 从编码器获取压缩数据
- 写入封装器
- 收尾工作:
- 发送EOS标志
- 等待所有缓冲区处理完成
- 释放资源
延迟优化方案:
- 使用低延迟音频源(如AudioSource.VOICE_RECOGNITION)
- 减小AudioRecord缓冲区大小
- 选择低复杂度编码配置
- 考虑使用AudioRecord的回调模式
3.3 实战代码:MP3解码播放实现
以下是更完整的MP3解码播放示例,包含关键细节处理:
java复制public class Mp3Player {
private static final String TAG = "Mp3Player";
private static final long TIMEOUT_US = 10000;
private MediaExtractor extractor;
private MediaCodec decoder;
private AudioTrack audioTrack;
private boolean isPlaying;
public void play(String filePath) {
try {
// 1. 设置MediaExtractor
extractor = new MediaExtractor();
extractor.setDataSource(filePath);
// 找到音频轨道
int audioTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioTrackIndex = i;
break;
}
}
if (audioTrackIndex == -1) {
throw new RuntimeException("No audio track found");
}
extractor.selectTrack(audioTrackIndex);
MediaFormat format = extractor.getTrackFormat(audioTrackIndex);
// 2. 创建解码器
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, null, null, 0);
decoder.start();
// 3. 创建AudioTrack
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig = channelCount == 1 ?
AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(
sampleRate, channelConfig, audioFormat) * 2; // 双倍缓冲
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setChannelMask(channelConfig)
.setEncoding(audioFormat)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
);
audioTrack.play();
// 4. 解码循环
isPlaying = true;
new Thread(this::decodeLoop).start();
} catch (IOException e) {
Log.e(TAG, "Player initialization failed", e);
releaseResources();
}
}
private void decodeLoop() {
ByteBuffer[] inputBuffers = decoder.getInputBuffers();
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
while (!sawOutputEOS && isPlaying) {
// 5.1 向解码器输入数据
if (!sawInputEOS) {
int inputBufferIndex = decoder.dequeueInputBuffer(TIMEOUT_US);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
sawInputEOS = true;
decoder.queueInputBuffer(
inputBufferIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(
inputBufferIndex, 0, sampleSize,
presentationTimeUs, 0);
extractor.advance();
}
}
}
// 5.2 从解码器获取输出
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputBufferIndex >= 0) {
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
if (bufferInfo.size > 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] pcmData = new byte[bufferInfo.size];
outputBuffer.get(pcmData);
outputBuffer.clear();
audioTrack.write(pcmData, 0, pcmData.length);
}
decoder.releaseOutputBuffer(outputBufferIndex, false);
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = decoder.getOutputBuffers();
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 可以获取新的输出格式
MediaFormat newFormat = decoder.getOutputFormat();
Log.d(TAG, "Output format changed: " + newFormat);
}
}
if (sawOutputEOS) {
Log.d(TAG, "Playback completed");
}
releaseResources();
}
public void stop() {
isPlaying = false;
}
private void releaseResources() {
if (audioTrack != null) {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
}
if (decoder != null) {
decoder.stop();
decoder.release();
decoder = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
4. 高级话题与性能优化
4.1 低延迟音频处理
实时音频应用(如语音聊天、乐器APP)对延迟非常敏感。Android提供了低延迟音频支持:
-
使用高性能音频路径:
- 设置AudioTrack的performance mode为MODE_STREAM
- 使用AudioAttributes.FLAG_LOW_LATENCY
- 选择适当的采样率(通常48kHz)
-
AAudio API(API 26+):
- 专为低延迟设计的新API
- 更简单的数据模型
- 更可预测的行为
java复制AAudioStreamBuilder builder = new AAudioStreamBuilder();
builder.setDirection(AAudioStreamBuilder.DIRECTION_OUTPUT);
builder.setFormat(AudioFormat.ENCODING_PCM_FLOAT);
builder.setSampleRate(48000);
builder.setChannelCount(2);
builder.setPerformanceMode(AAudioStreamBuilder.PERFORMANCE_MODE_LOW_LATENCY);
AAudioStream stream = builder.build();
stream.requestStart();
// 写入音频数据
float[] buffer = new float[stream.getFramesPerBurst() * 2];
stream.write(buffer, 0, buffer.length, 1000000);
4.2 音频效果处理
Android提供了一系列音频效果处理器,可以应用于AudioTrack:
- Equalizer:均衡器
- BassBoost:低音增强
- Virtualizer:虚拟环绕声
- PresetReverb:预设混响
使用示例:
java复制Equalizer equalizer = new Equalizer(0, audioTrack.getAudioSessionId());
equalizer.setEnabled(true);
short bands = equalizer.getNumberOfBands();
short minLevel = equalizer.getBandLevelRange()[0];
short maxLevel = equalizer.getBandLevelRange()[1];
// 设置所有频段为最大增益
for (short i = 0; i < bands; i++) {
equalizer.setBandLevel(i, maxLevel);
}
4.3 常见问题排查指南
4.3.1 解码器初始化失败
可能原因:
- 不支持的MIME类型
- 解决方案:检查MediaCodecList支持的编解码器
- 格式配置不完整
- 解决方案:确保提供了所有必需的MediaFormat键值
- 资源不足
- 解决方案:减少并发解码实例
4.3.2 播放卡顿或杂音
可能原因:
- 缓冲区underrun
- 解决方案:增大AudioTrack缓冲区或优化数据供给速度
- 采样率不匹配
- 解决方案:确认AudioTrack与解码输出采样率一致
- 线程优先级问题
- 解决方案:提高供给线程的优先级
4.3.3 录音质量差
可能原因:
- 采样率太低
- 解决方案:使用16kHz或更高采样率
- 音频源选择不当
- 解决方案:尝试不同的AudioSource(如VOICE_RECOGNITION)
- 编码比特率不足
- 解决方案:提高编码比特率(至少64kbps for语音)
5. 现代Android音频开发趋势
随着Android版本的演进,音频API也在不断发展:
- Oboe库:Google推出的跨平台高性能音频库,封装了AAudio和OpenSL ES
- USB音频:支持专业USB音频设备(API 23+)
- MIDI支持:完整的MIDI API(API 23+)
- 动态处理效果:AudioEffect API的扩展
对于新项目,建议:
- 优先考虑AAudio/Oboe实现低延迟需求
- 使用AudioAttributes替代旧的stream type
- 关注Android扩展包中的新音频功能
在实现复杂音频应用时,可以考虑第三方库如:
- ExoPlayer:Google的高级媒体播放库
- Sonic:音频处理库(变速不变调)
- Librosa:音频分析功能(需要NDK)
音频开发是一个需要理论与实践相结合的领域。我建议开发者在理解基本原理的基础上,多进行实际测试和性能分析,因为不同Android设备的音频实现可能存在显著差异。