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编码的整个流程:

AAC编码实战流程 ① 参数配置 SampleRate: 44100 Channel: 1 Bitrate: 64000 ② 创建编码器 MediaCodec createEncoderByType configure + start ③ 输入PCM数据 dequeueInputBuffer queueInputBuffer 时间戳递增 ④ 获取编码输出 dequeueOutputBuffer releaseOutputBuffer ⑤ 封装ADTS头 7字节头部 Syncword + 长度 ⑥ 输出AAC流 可推流/保存 每帧带ADTS头 核心:参数一致 → 编码器启动 → 数据输入 → 编码输出 → 封装 → 使用

这张图把整个流程串起来了。从参数配置开始,到最终输出带ADTS头的AAC流,一共六步。每一步都有对应的代码实现,咱们这一章都讲到了。

好了,AAC编码这块就这些内容。参数别设错,ADTS头别忘加,编码器就能乖乖干活。下一章咱们要讲怎么把这些AAC数据打包成FLV格式,然后推给RTMP服务器。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321