7、音频编码实战:AAC编码器配置、AudioRecord与MediaCodec联动、编码数据回调
音频编码这块,说实话是音视频开发里最容易被忽视的环节。很多人觉得视频编码难,音频嘛,随便搞搞就行。但我在项目中吃过亏——有一次直播推流,视频清晰度没问题,但声音断断续续,用户直接开骂。查了半天,发现是AudioRecord和MediaCodec的配合出了问题。
嗯,今天我们就来把这块彻底讲透。从AAC编码器的配置,到AudioRecord怎么喂数据给MediaCodec,再到编码后的数据怎么回调出来。你跟着走一遍,以后遇到音频编码的问题,心里就有底了。
7.1 AAC编码器配置:参数不是随便填的
先说说AAC编码器的配置。MediaCodec创建编码器时,有几个关键参数需要你认真对待。
核心参数一览:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| MIME类型 | MediaFormat.MIMETYPE_AUDIO_AAC | 固定值,别写错 |
| 采样率 | 44100 Hz | 兼容性最好,大部分设备都支持 |
| 声道数 | 1(单声道)或 2(立体声) | 直播场景建议单声道,省带宽 |
| 比特率 | 64000 ~ 128000 bps | 看你的网络状况,我一般用96000 |
| AAC Profile | AACObjectLC | 低复杂度,兼容性最好 |
配置代码长这样:
MediaFormat format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
44100, // 采样率
1 // 单声道
);
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
format.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 4096);
MediaCodec encoder = MediaCodec.createEncoderByType(
MediaFormat.MIMETYPE_AUDIO_AAC);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
个人经验:KEY_MAX_INPUT_SIZE这个参数,我建议你显式设置一下。有些设备默认值很小,会导致输入缓冲区不够用,编码直接报错。我曾经在小米某款机型上踩过这个坑,折腾了两天才发现是这个问题。
7.2 AudioRecord与MediaCodec联动:数据流怎么走?
配置好编码器,接下来就是怎么把麦克风采集到的数据送进去。这里有个关键点:AudioRecord采集的是PCM裸数据,而MediaCodec需要的是特定格式的输入。
说白了,就是你要把PCM数据塞进MediaCodec的输入缓冲区。流程是这样的:
- AudioRecord从麦克风读取PCM数据
- 把PCM数据写入MediaCodec的输入队列
- MediaCodec编码后,从输出队列取出AAC数据
我画了一张图,帮你理清这个流程:
你看,整个流程其实不复杂。但实际写代码时,有几个细节要注意。
7.3 实战代码:AudioRecord采集线程
先写一个采集线程,专门从麦克风读数据:
class AudioCaptureThread extends Thread {
private AudioRecord audioRecord;
private MediaCodec encoder;
private volatile boolean isRunning = true;
@Override
public void run() {
int bufferSize = AudioRecord.getMinBufferSize(
44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
);
audioRecord.startRecording();
byte[] pcmBuffer = new byte[4096];
while (isRunning) {
int bytesRead = audioRecord.read(pcmBuffer, 0, pcmBuffer.length);
if (bytesRead > 0) {
feedEncoder(pcmBuffer, bytesRead);
}
}
audioRecord.stop();
audioRecord.release();
}
private void feedEncoder(byte[] data, int length) {
int inputIndex = encoder.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(data, 0, length);
encoder.queueInputBuffer(
inputIndex,
0,
length,
System.nanoTime() / 1000,
0
);
}
}
}
注意:AudioRecord的bufferSize不要随便写。用getMinBufferSize获取最小值,然后根据你的需求适当放大。太小了会导致采集丢数据,太大了延迟会变高。我一般取最小值的2倍,平衡得比较好。
7.4 编码数据回调:怎么拿到AAC?
数据送进编码器后,我们需要从输出端把编码好的AAC数据取出来。这里我习惯用一个单独的线程来轮询输出缓冲区:
class AudioEncodeThread extends Thread {
private MediaCodec encoder;
private volatile boolean isRunning = true;
@Override
public void run() {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (isRunning) {
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 暂时没有数据,等一下
continue;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 编码器格式变化,一般只触发一次
MediaFormat newFormat = encoder.getOutputFormat();
// 这里可以获取到AAC的配置信息(ADTS头等)
continue;
}
if (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
byte[] aacData = new byte[bufferInfo.size];
outputBuffer.get(aacData);
// 这里拿到的是纯AAC数据,没有ADTS头
// 如果需要ADTS头,需要自己添加
onAacDataAvailable(aacData, bufferInfo);
encoder.releaseOutputBuffer(outputIndex, false);
}
}
}
private void onAacDataAvailable(byte[] data, MediaCodec.BufferInfo info) {
// 回调给上层,比如写入文件或推流
if (listener != null) {
listener.onAacFrame(data, info);
}
}
}
避坑指南:MediaCodec输出的AAC数据默认不带ADTS头。如果你要直接写入文件或者推流,需要自己加上ADTS头。我曾经在这个问题上浪费了半天,播放器一直报错,后来才发现是缺少了ADTS头。
加ADTS头的代码网上很多,核心就是计算帧长度,然后填充7个字节的头信息。这里不展开了,你记得有这回事就行。
7.5 完整联动:两个线程怎么配合?
采集线程和编码线程是独立运行的。采集线程负责往输入队列塞数据,编码线程负责从输出队列取数据。它们通过MediaCodec内部的缓冲区进行通信,不需要额外的同步机制。
启动顺序要注意:先启动编码器,再启动采集线程。否则可能出现采集线程已经塞数据了,编码器还没准备好的情况。
// 正确的启动顺序
encoder.start();
captureThread.start();
encodeThread.start();
停止时顺序反过来:先停止采集,再停止编码器。确保所有数据都处理完:
// 停止顺序
captureThread.stopCapture();
encodeThread.stopEncode();
encoder.stop();
encoder.release();
核心要点总结:
- AAC编码器配置时,采样率用44100,比特率96000,Profile用LC
- AudioRecord的bufferSize取最小值的2倍,平衡延迟和稳定性
- 编码输出的AAC数据不带ADTS头,需要自己添加
- 启动顺序:编码器先启动,采集后启动
- 停止顺序:采集先停止,编码器后停止
嗯,音频编码这块其实不难,但细节很多。你按照这个流程走一遍,基本不会出大问题。如果遇到编码器报错,先检查参数配置,再检查数据格式,大部分问题都能解决。
公众号:蓝海资料掘金营,微信deep3321