Android直播从入门到精通(3):PCM转AAC

1.什么是AAC?

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

  • MPEG-2 AAC LC 低复杂度规格(Low Complexity)–比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率以及音质方面,都能找到平衡点
  • MPEG-2 AAC Main 主规格
  • MPEG-2 AAC SSR 可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LC低复杂度规格(Low Complexity)------现在的手机比较常见的MP4文件中的音频部份就包括了该规格音频文件
  • MPEG-4 AAC Main 主规格 ------包含了除增益控制之外的全部功能,其音质最好
  • MPEG-4 AAC SSR 可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition)
  • MPEG-4 AAC LD 低延迟规格(Low Delay)
  • MPEG-4 AAC HE 高效率规格(High Efficiency)-----这种规格适合用于低码率编码,有Nero ACC 编码器支持

目前使用最多的是LCHE。其中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。

Android直播从入门到精通(3):PCM转AAC_第1张图片

图中AAC即为AAC-LC,aacPlus v1,v2分别代表Hev1和HEv2

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技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方。

2.AAC文件格式

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
Android直播从入门到精通(3):PCM转AAC_第2张图片
可以看出adts的aac流是一个个adts_frame组成序列



2.2 adts_frame
Android直播从入门到精通(3):PCM转AAC_第3张图片
每个adts帧包含以下数据结构adts_fixed_header,adts_variable_header,raw_data_block序列



2.3 adts_fixed_header

Android直播从入门到精通(3):PCM转AAC_第4张图片
字段定义在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协议.

  • “11” Layer I
  • “10” Layer II
  • “01” Layer III
  • “00” reserved

protection_bit:1bit,表示是否有crc校验。1 - 无 0 - 有
profile:2bit,决定用哪种profile.
Android直播从入门到精通(3):PCM转AAC_第5张图片
sampling_frequency_index:4bit,采样频率index。
Android直播从入门到精通(3):PCM转AAC_第6张图片
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,则参照下图
Android直播从入门到精通(3):PCM转AAC_第7张图片
original_copy:1bit, 0 - 无版权保护 1 - 有版权保护
home:1bit,表明当前数据是拷贝流还是原始流。0 - 拷贝流 1 - 原始流



2.4 adts_variable_header

Android直播从入门到精通(3):PCM转AAC_第8张图片
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

Android直播从入门到精通(3):PCM转AAC_第9张图片
id_syn_ele:3bit,元素类型id,定义如下图:

Android直播从入门到精通(3):PCM转AAC_第10张图片

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文件,如下图:

Android直播从入门到精通(3):PCM转AAC_第11张图片
按照上一节的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),刚好对应图中划红线的地方。

3.AAC编码

AAC编码流程在ISO/IEC 13818-7中制定:
Android直播从入门到精通(3):PCM转AAC_第12张图片
其编码流程概述如下: 当音频信号送至编码端时,会分别送至听觉心里模型(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大类,这里就不展开了。

4.利用ffmpeg和fdk-aac将pcm编码成aac格式

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

你可能感兴趣的:(音视频)