AAC(Advanced Audio Coding)是一种音频编码标准,最早定义在MPEG-2标准(ISO/IEC 13818-7)中,后来在MPEG-4(ISO/IEC 14496-3)标准中又加入了SBR技术和PS技术(MPEG的介绍可以看这里:MPEG标准介绍)。AAC标准是作为MP3的继承者而设计出来的,相同的比特率之下,AAC比MP3有更好的音质。
为了适应不同的应用场景,AAC定义9种Profile
目前使用最多的是LC和HE。其中LC-AAC用于中高码率(>=80Kbps),HE-AAC(LC + SBR技术)主要用于中低码(<=80Kbps),而新近推出的HE-AACv2(LC+SBR+PS)主要用于低码率(<=48Kbps),事实上大部分编码器设成<=48Kbps自动启用PS技术,而>48Kbps就不加PS。流行的Nero AAC编码程序只支持LC,HE,HEv2这三种规格,编码后的AAC音频,规格显示都是LC。
HE:“High Efficiency”(高效性)。HE-AAC v1(又称AACPlusV1,SBR),用容器的方法实现了AAC(LC)+SBR技术。SBR其实代表的是Spectral Band Replication(频段复制)。简要叙述一下,音乐的主要频谱集中在低频段,高频段幅度很小,但很重要,决定了音质。如果对整个频段编码,若是为了保护高频就会造成低频段编码过细以致文件巨大;若是保存了低频的主要成分而失去高频成分就会丧失音质。SBR把频谱切割开来,低频单独编码保存主要成分,高频单独放大编码保存音质,“统筹兼顾”了,在减少文件大小的情况下还保存了音质,完美的化解这一矛盾。
HEv2:用容器的方法包含了HE-AAC v1和PS技术。PS指“parametric stereo”(参数立体声)。原来的立体声文件文件大小是一个声道的两倍。但是两个声道的声音存在某种相似性,根据香农信息熵编码定理,相关性应该被去掉才能减小文件大小。所以PS技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方。
AAC的音频文件格式有ADIF和ADTS:
ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。
简单来说,ADIF只有一个统一的头,所以必须得到所有的数据后解码,ADTS每一帧都有头信息,可以从任意帧开始解码,因此网络上的aac基本都是ADTS格式。
关于ADTS格式的定义见ISO/IEC 13818文档的Part 7,6.2节:
2.1 adts_sequence
可以看出adts的aac流是一个个adts_frame组成序列
2.2 adts_frame
每个adts帧包含以下数据结构adts_fixed_header,adts_variable_header,raw_data_block序列
2.3 adts_fixed_header
字段定义在ISO/IEC 13818的Part 7,8.1.1.1节中,其中部分字段同MP3定义一样,见ISO/IEC 11172的Part 3,2.4.2.3节:
syncword:12bit,所有位都位1,即’1111 1111 1111’
ID:1bit,始终为1。1 - MPEG audio,0 - 保留
Layer:2bit,始终为00。决定用那种layer协议.
protection_bit:1bit,表示是否有crc校验。1 - 无 0 - 有
profile:2bit,决定用哪种profile.
sampling_frequency_index:4bit,采样频率index。
private_bit:1bit,bit for private use. This bit will not be used in the future by ISO
channel_configuration:3bit,声道配置. 如果等于0,则声道配置在第一个raw_data_block中通过调用program_config_element设置;如果大于0,则参照下图
original_copy:1bit, 0 - 无版权保护 1 - 有版权保护
home:1bit,表明当前数据是拷贝流还是原始流。0 - 拷贝流 1 - 原始流
2.4 adts_variable_header
copyright_identification_bit:1bit,版权信息,暂不深究
copyright_identification_start:1bit,版权相关,暂不深究
frame_length:13bit,一个ADTS帧的字节数,包含headers和error_check的长度
adts_buffer_fullness:11bit, 如果值为7FF则表明当前码流的码率是可变的
number_of_raw_data_blocks_in_frame:2bit,表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧
2.5 raw_data_block
SCE: Single Channel Element,单通道元素。单通道元素基本上只由一个ICS组成。一个原始数据块最可能由16个SCE组成。
CPE: Channel Pair Element,双通道元素,由两个可能共享边信息的ICS和一些联合立体声编码信息组成。
CCE: Coupling Channel Element,藕合通道元素。代表一个块的多通道联合立体声信息或者多语种程序的对话信息。
LFE: Low Frequency Element,低频元素。包含了一个加强低采样频率的通道。
DSE: Data Stream Element,数据流元素,包含了一些并不属于音频的附加信息。
PCE: Program Config Element,程序配置元素。包含了声道的配置信息。它可能出现在ADIF 头部信息中。
FIL: Fill Element,填充元素。包含了一些扩展信息。如SBR,动态范围控制信息等。
后面就是元素内容的具体分析了,由于涉及到音频编码的知识,比较复杂,这里不做讨论。
2.6 aac格式解析示例
用二进制查看工具打开一个aac文件,如下图:
字段名 | 占用bit数 | 值 | 含义 |
---|---|---|---|
adts_fixed_header | - | 1111 1111 1111 1001 0101 0000 1000(0xFFF9508) | |
syncword | 12 | 0xFFF | 头标识 |
ID | 1 | 1 | MPEG audio |
Layer | 2 | 00 | 保留字段 |
protection_bit | 1 | 1 | 无crc校验 |
profile | 2 | 01 | 采用LC profile |
sampling_frequency_index | 4 | 0100 | 采样率44100Hz |
private_bit | 1 | 0 | ISO保留字段 |
channel_configuration | 3 | 010 | 双声道 |
original_copy | 1 | 0 | 无版权 |
home | 1 | 0 | 原始数据 |
adts_variable_header | - | 0000 0010 1110 0111 1111 1111 1100(0x02E7FFC) | |
copyright_identification_bit | 1 | 0 | 版权信息 |
copyright_identification_start | 1 | 0 | 版权信息 |
frame_length | 13 | 00 0010 1110 011 | 帧长度为371字节 |
adts_buffer_fullness | 11 | 1 1111 1111 11 | 码率可变 |
number_of_raw_data_blocks_in_frame | 2 | 00 | 有一个原始帧数据 |
raw_data_block | - | 001 | |
id_syn_ele | 3 | 001 | 双通道元素 |
channel_pair_element | … | … | … |
… | … | … | … |
第一帧长度为371字节,然后又开始下一帧(头三个字节0xFFF),刚好对应图中划红线的地方。
AAC编码流程在ISO/IEC 13818-7中制定:
其编码流程概述如下: 当音频信号送至编码端时,会分别送至听觉心里模型(Psychoacoustic Model)以求得编码所需之相关参数及增益控制(gain control)模块中,将信号做某个程度的衰减,以降低其峰值大小,如此可减少Pre-echo 的发生。之后,再以MDCT 将时域信号转换至频率域,而送入至TNS(Temporal Noise Shaping Module)模块中,来判断是否需要启动TNS,此模块系利用开回路预测(open-loop prediction) 来修饰其量化噪声,如此可将其量化噪声的分布,修饰到原始信号能量所能含盖的范围之下,进一步的减少Pre-echo 的发生,若TNS 被启动,则传出其预测差值;反之,则传出原始频谱值。AAC 为了提升其压缩效率,则使用了Joint Stereo Coding与预测(Prediction)模块来进一步消除信号间的冗余成份。在Joint Stereo Coding中又可分为Intensity Stereo Coding 与M/S Stereo Coding。在Intensity Stereo Coding模块中,是利用信号在高频时,人耳只对能量较敏感,对于其相位不敏感之特性,将其左右声道之频谱系数合并,以节省使用之位;在M/S Stereo Coding 模块中,利用左右声道之和与差,做进一步地压缩,若其差值能量很小,如此便可以用较少之位编码此一声道,将剩余之位应用于另一声道上的编码,如此来提升其压缩率。而预测模块的主要架构是使用Backward Adaptive Predictors,利用前两个音频帧来预测现在的音频帧,若决定启动此模块,则传出其预测差值,如此一来可以减少其数据量,达数据压缩之目的。经过上述处理频谱信号上的压缩tools程序后,则将其数据予以量化与编码,为了达到量化编码的最佳化,AAC 使用了双巢状式循环(two nested loop)的量化编码结构,以得最佳的压缩质量,最后则将其位串送至解码端,而完成整个编码程序。
AAC编码的原理比较复杂,涉及信息编码以及人耳的生理知识,按照功能大致可以划分为熵编码,量化编码,变换编码,预测编码,音频建模5大类,这里就不展开了。
ffmpeg作为音视频开发必不可少的工具,这里就不做介绍了。至于如何在编译环境搭建可以参考:
fdk-aac是一款开源的aac编解码实现库,源码地址:https://github.com/mstorsjo/fdk-aac
下面我们实现一个在Android上将pcm文件转成aac文件的功能。新建一个Android工程,导入ffmpeg和fdk-aac的so库,工程配置这里就不讲了,文章末尾有源码地址。
JNI java接口
package me.huaisu.audio.encode;
public class AacEncoder {
static {
System.loadLibrary("fdk-aac");
System.loadLibrary("avcodec");
System.loadLibrary("avdevice");
System.loadLibrary("avfilter");
System.loadLibrary("avformat");
System.loadLibrary("avutil");
System.loadLibrary("swresample");
System.loadLibrary("swscale");
System.loadLibrary("aac_encoder");
}
public native int encodePcmFile(String pcmFile, String aacFile);
}
JNI c++实现
#include
#include
#include "AACEncoder.h"
extern "C" JNIEXPORT jint JNICALL
Java_me_huaisu_audio_encode_AacEncoder_encodePcmFile(
JNIEnv* env,
jobject thiz,
jstring pcmFile,
jstring aacFile) {
AACEncoder* encoder = new AACEncoder();
const char* pcm_file = env->GetStringUTFChars(pcmFile, NULL);
if (pcm_file == NULL) {
return NULL;
}
const char* aac_file = env->GetStringUTFChars(aacFile, NULL);
if (aac_file == NULL) {
return NULL;
}
env->ReleaseStringUTFChars(pcmFile, pcm_file);
env->ReleaseStringUTFChars(aacFile, aac_file);
return encoder->encode(pcm_file, aac_file);
}
下面是具体的aac编码实现
//
// Created by Administrator on 2020/2/23.
//
#ifndef ANDROID_LIVE_AACENCODER_H
#define ANDROID_LIVE_AACENCODER_H
#ifdef __cplusplus
extern "C" {
#endif
#include
#include
#ifdef __cplusplus
}
#endif
#include "AndroidLog.h"
class AACEncoder {
private:
uint8_t** src_data = NULL;//一帧的数据,是个二位数组
int src_linesize;
int src_bufsize;//一帧数据的长度
AVFormatContext* pFormatContext;
AVStream* audioStream;
AVCodecParameters* param;
AVCodecContext* pCodecContext;
AVCodec* pCodec;
AVFrame* pFrame;
int frame_cnt = 0;
AVPacket *pkt;
int ret;
int initCodec();
int initAudioStream(const char* aac_file);
int initAudioFrame();
public:
AACEncoder();
~AACEncoder();
int encode(const char *pcm_file, const char *aac_file);
};
#endif //ANDROID_LIVE_AACENCODER_H
#include "AACEncoder.h"
AACEncoder::AACEncoder() {
}
AACEncoder::~AACEncoder() {
}
static void android_log_callback(void *ptr, int level, const char *fmt, va_list vl)
{
switch (level) {
case AV_LOG_VERBOSE:
LOGV(fmt, vl);
break;
case AV_LOG_DEBUG:
LOGD(fmt, vl);
break;
case AV_LOG_INFO:
LOGI(fmt, vl);
break;
case AV_LOG_WARNING:
LOGW(fmt, vl);
break;
case AV_LOG_ERROR:
LOGE(fmt, vl);
break;
}
}
int AACEncoder::initCodec() {
pCodec = avcodec_find_encoder_by_name("libfdk_aac");
if (!pCodec) {
LOGE("Codec not found\n");
return -1;
}
pCodecContext = avcodec_alloc_context3(pCodec);
if (!pCodecContext) {
LOGE("Codec context alloc fail\n");
return -1;
}
pCodecContext->codec_id = AV_CODEC_ID_AAC;
pCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
pCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
pCodecContext->sample_rate = 44100;
pCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
pCodecContext->channels = av_get_channel_layout_nb_channels(pCodecContext->channel_layout);
pCodecContext->bit_rate = 96000;
if (avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
LOGE("Can't open codec\n");
return -1;
}
return 0;
}
int AACEncoder::initAudioStream(const char* aac_file) {
avformat_alloc_output_context2(&pFormatContext, NULL, NULL, aac_file);
if (avio_open(&pFormatContext->pb, aac_file, AVIO_FLAG_READ_WRITE) < 0) {
LOGE("Could't open output file\n");
return -1;
}
audioStream = avformat_new_stream(pFormatContext, pCodec);
if (audioStream == NULL) {
LOGE("Could't create stream\n");
return -1;
}
param = avcodec_parameters_alloc();
ret = avcodec_parameters_from_context(param, pCodecContext);
if (ret < 0) {
LOGE("create parameters fail\n");
return -1;
}
audioStream->codecpar = param;
return 0;
}
/**
* ffmpeg一帧有1024个采样点,即pCodecContext->frame_size=1024
*
* 双声道,AV_SAMPLE_FMT_S16采样格式的数据方式存储如下:
* LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRR……
* 所有数据存在data[0],大小为1024 * 2(每个采样点占2字节) * 2(双声道) =4096
*
* 双声道,AV_SAMPLE_FMT_FLTP采样格式的数据方式存储如下:
* LLLLLLLLLLLLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRRRRRRRRRRRRRR……
* 左声道数据存在data[0],大小为1024 * 4(每个采样点占4字节)=4096
* 右声道数据存在data[1],大小为1024 * 4(每个采样点占4字节)=4096
*
* pFrame->linesize[0],表示data[0]数组的长度
* av_samples_get_buffer_size返回一帧的数据长度:
* 双声道、AV_SAMPLE_FMT_S16长度为4096
* 双声道,AV_SAMPLE_FMT_FLTP长度为8192
*/
int AACEncoder::initAudioFrame() {
ret = av_samples_alloc_array_and_samples(&src_data, &src_linesize, pCodecContext->channels,
pCodecContext->frame_size, pCodecContext->sample_fmt, 0);
if (ret < 0) {
LOGE("Could not allocate source samples\n");
return -1;
}
src_bufsize = av_samples_get_buffer_size(&src_linesize, pCodecContext->channels,
pCodecContext->frame_size, pCodecContext->sample_fmt, 1);
pFrame = av_frame_alloc();
pFrame->nb_samples = pCodecContext->frame_size;
pFrame->format = pCodecContext->sample_fmt;
pFrame->channels = pCodecContext->channels;
pFrame->channel_layout = pCodecContext->channel_layout;
pFrame->linesize[0] = src_linesize;
pFrame->sample_rate = pCodecContext->sample_rate;
return 0;
}
int AACEncoder::encode(const char* pcm_file, const char* aac_file)
{
//打印ffmpeg系统日志,方便排查问题
av_log_set_level(AV_LOG_VERBOSE);
av_log_set_callback(android_log_callback);
// 初始化编码器
if (initCodec() < 0) {
return -1;
}
// 创建AVStream
if (initAudioStream(aac_file) < 0) {
return -1;
}
// 写入aac文件头
avformat_write_header(pFormatContext, NULL);
// 初始化AVFrame,存放原始音频数据
if (initAudioFrame() < 0) {
return -1;
}
// 初始化AVPacket,存放编码后的aac数据
pkt = av_packet_alloc();
if (!pkt) {
LOGE("could not allocate the packet\n");
return -1;
}
FILE* fp_in = fopen(pcm_file, "rb");
if (!fp_in) {
LOGE("Can't open pcm input file\n");
return -1;
}
int pts = 0;
for (;;)
{
// 每次从pcm文件读取一帧数据
if ((ret = fread(src_data[0], 1, src_bufsize, fp_in)) <= 0) {
LOGE("Fail to read buf from input file\n");
return -1;
}
else if (feof(fp_in)) {
LOGE("End of input file\n");
break;
}
// 设置当前帧的显示位置
pFrame->pts = pts;
pts++;
// 将读到的帧数据赋值给AVFrame
pFrame->data[0] = src_data[0];
// 将AVFrame发送到编码器进行编码
ret = avcodec_send_frame(pCodecContext, pFrame);
if (ret < 0) {
LOGE("Error sending the frame to the encoder\n");
return -1;
}
while (ret >= 0) {
// 得到编码后的AVPacket
ret = avcodec_receive_packet(pCodecContext, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// LOGD("Error encoding audio frame %d\n", ret);
continue;
} else if (ret < 0) {
LOGE("Error encoding audio frame %d\n", ret);
return -1;
}
// LOGD("Success encode frame[%d] size:%d\n", frame_cnt, pkt->size);
frame_cnt++;
pkt->stream_index = audioStream->index;
// 将编码后的AVPacket数据写入aac文件
ret = av_interleaved_write_frame(pFormatContext, pkt);
av_packet_unref(pkt);
if (ret < 0) {
LOGE("Error write frame to output file,err code=%d", ret);
return -1;
}
}
}
//写入文件尾
av_write_trailer(pFormatContext);
fclose(fp_in);
avcodec_close(pCodecContext);
av_free(pCodecContext);
av_free(&pFrame->data[0]);
av_frame_free(&pFrame);
return 0;
}
源码地址:
Gitee:https://gitee.com/huaisu2020/Android-Live
Github:https://github.com/xh2009cn/Android-Live