6、音频AAC编码实战:AAC编码参数详解
好,咱们进入实战环节。上一章我们把音频采集搞定了,拿到了原始的PCM数据。但PCM这玩意儿太“胖”了,直接传网络?想都别想。所以这一章,咱们得给它“瘦身”——用AAC编码器把它压成AAC格式。
说白了,AAC编码就是把一堆数字(PCM采样点),通过一套复杂的数学变换,变成另一堆更小的数字。你不需要懂傅里叶变换的细节,但得知道怎么调参数、怎么启动它、怎么把数据拿回来。
核心要点:编码器配置对了,后面全是顺水推舟。配置错了,你调三天三夜也出不来好声音。
6.1 AAC编码参数详解
配置AAC编码器,主要就三个参数:采样率(SampleRate)、声道数(Channel)、码率(Bitrate)。这三个参数决定了编码后的音质和大小。
6.1.1 采样率(SampleRate)
采样率,就是每秒钟采集多少个声音样本。单位是Hz。常见的值有:
| 采样率 | 常见场景 | 我的建议 |
|---|---|---|
| 8000 Hz | 电话语音 | 除非做VoIP,否则别用 |
| 16000 Hz | 宽带语音 | 语音聊天可以用 |
| 44100 Hz | CD音质 | 音乐直播首选 |
| 48000 Hz | DVD/专业音频 | 视频录制常用,我习惯用这个 |
嗯,这里要注意:编码器的采样率必须和采集端的采样率保持一致。你采集用的是44100,编码器设成48000,那出来的声音就会变调,像开了快进一样。我在项目中就踩过这个坑,采集卡默认是44100,我编码器设成了48000,结果直播间的观众都说我声音像唐老鸭……
6.1.2 声道数(Channel)
声道数,就是有几个声音通道。
- 1(单声道):所有声音混在一起。省带宽,语音场景够用。
- 2(双声道/立体声):左右声道分开。音乐、电影场景必备。
我个人建议,如果是做直播推流,除非你确定观众都是戴着耳机听立体声,否则用单声道就够了。为什么呢?因为立体声的数据量是单声道的两倍,但很多手机外放根本听不出区别。省下来的带宽,留给视频不好吗?
6.1.3 码率(Bitrate)
码率,就是编码后每秒产生的数据量。单位是bps(比特每秒)。码率越高,音质越好,文件也越大。
AAC编码器常用的码率范围:
- 32 kbps:勉强能听,语音还行,音乐就别想了。
- 64 kbps:语音清晰,音乐能听出一些细节。
- 96 kbps:音乐场景的“及格线”。
- 128 kbps:大多数人的“甜点区”,音质和体积平衡得很好。
- 192 kbps+:高音质,适合发烧友。
我的经验:做直播推流,音频码率我一般设在64-96 kbps之间。再高的话,对音质提升有限,但带宽占用却直线上升。你想想看,视频动不动就2Mbps,音频占个64kbps,比例很合理。
6.2 编码器配置与启动
好,参数讲完了,咱们来看看代码怎么写。Android平台上,我们用的是MediaCodec来做AAC编码。
6.2.1 创建并配置编码器
先上代码,我再一句句解释:
// 1. 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType("audio/mp4a-latm");
// 2. 配置编码器
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, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64000);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
// 3. 启动编码器
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
这里有几个点,我得提醒你:
- MIME类型:必须是
"audio/mp4a-latm",这是AAC的标准MIME。写错了编码器直接罢工。 - AAC Profile:我用的是
AACObjectLC(Low Complexity),这是最通用的AAC档次。兼容性最好,几乎所有设备都支持。 - CONFIGURE_FLAG_ENCODE:这个标志告诉MediaCodec,我们要用它来编码,而不是解码。别搞反了。
注意:配置编码器时,KEY_AAC_PROFILE不是必填项。但如果不填,有些设备会用默认值,可能不是你想要的。我曾经在小米某款手机上没设这个参数,结果编码出来的AAC流在某些播放器上无法解码。所以,还是显式设置一下比较稳妥。
6.2.2 输入PCM数据
编码器启动后,就可以往里喂PCM数据了:
// 获取输入缓冲区
int inputIndex = encoder.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(pcmData, 0, pcmData.length);
// 喂给编码器
encoder.queueInputBuffer(inputIndex, 0, pcmData.length, System.nanoTime() / 1000, 0);
}
这里有个小细节:queueInputBuffer的第四个参数是时间戳(微秒)。这个时间戳必须单调递增,否则编码器会报错。我一般用System.nanoTime() / 1000来生成。
6.3 编码数据回调与封装
数据喂进去之后,编码器会异步输出编码后的AAC数据。我们需要从输出缓冲区里取出来。
6.3.1 获取编码输出
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 编码器输出格式变了,通常是刚开始编码时触发
MediaFormat outputFormat = encoder.getOutputFormat();
// 这里可以拿到AAC的配置信息(如ADTS头)
} else if (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
// 处理编码后的AAC数据
byte[] aacData = new byte[bufferInfo.size];
outputBuffer.get(aacData);
// 释放缓冲区
encoder.releaseOutputBuffer(outputIndex, false);
}
嗯,这里要注意:INFO_OUTPUT_FORMAT_CHANGED这个事件,在编码器启动后只会触发一次。它告诉你编码器的输出格式已经确定了。这时候,你可以从outputFormat里拿到AAC的配置信息,比如采样率、声道数等。这些信息在封装ADTS头时要用到。
6.3.2 封装ADTS头
MediaCodec输出的AAC数据是“裸流”(raw AAC),没有头部信息。直接丢给播放器,它不知道这是什么格式。所以我们需要给每一帧AAC数据加上ADTS头。
ADTS头长这样:
/**
* 添加ADTS头
* @param packet 原始AAC数据
* @param packetLen 原始数据长度
* @param sampleRate 采样率
* @param channelConfig 声道配置
* @return 带ADTS头的AAC数据
*/
public static byte[] addADTStoPacket(byte[] packet, int packetLen,
int sampleRate, int channelConfig) {
int profile = 2; // AAC LC
int freqIdx = getSampleRateIndex(sampleRate);
int chanCfg = channelConfig;
// ADTS头固定7个字节
byte[] adtsHeader = new byte[7];
// Syncword: 0xFFF
adtsHeader[0] = (byte) 0xFF;
adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer=0, no CRC
// 长度 = 7字节头 + 数据长度
int fullLength = 7 + packetLen;
adtsHeader[2] = (byte) ((profile << 6) | (freqIdx << 2) | (chanCfg >> 2));
adtsHeader[3] = (byte) (((chanCfg & 0x03) << 6) | (fullLength >> 11));
adtsHeader[4] = (byte) ((fullLength & 0x7FF) >> 3);
adtsHeader[5] = (byte) (((fullLength & 0x07) << 5) | 0x1F);
adtsHeader[6] = (byte) 0xFC;
// 合并
byte[] result = new byte[7 + packetLen];
System.arraycopy(adtsHeader, 0, result, 0, 7);
System.arraycopy(packet, 0, result, 7, packetLen);
return result;
}
这段代码看起来有点复杂,其实就是拼了7个字节的头部信息。你不需要背下来,但要知道每个字段的含义:
- Syncword:固定0xFFF,告诉播放器“我是一帧AAC数据”。
- Profile:AAC档次,我们用的是LC(值为2)。
- Sample Rate Index:采样率对应的索引值,需要查表。
- Channel Configuration:声道数,1或2。
- Frame Length:整帧长度,包括ADTS头。
避坑指南:我曾经在封装ADTS头时,把fullLength算错了,少加了7个字节。结果播放器一直报错,说数据不完整。排查了半天才发现是长度没算对。所以,fullLength = 7 + packetLen,千万别忘了那7个字节的头。
6.4 知识体系总览
说了这么多,咱们来张图总结一下AAC编码的整个流程:
这张图把整个流程串起来了。从参数配置开始,到最终输出带ADTS头的AAC流,一共六步。每一步都有对应的代码实现,咱们这一章都讲到了。
好了,AAC编码这块就这些内容。参数别设错,ADTS头别忘加,编码器就能乖乖干活。下一章咱们要讲怎么把这些AAC数据打包成FLV格式,然后推给RTMP服务器。嗯,到时候见。
公众号:蓝海资料掘金营,微信deep3321