音视频探索(2):AAC编码解析 您所在的位置:网站首页 androidapi 音视频探索(2):AAC编码解析

音视频探索(2):AAC编码解析

#音视频探索(2):AAC编码解析| 来源: 网络整理| 查看: 265

1.AAC编码格式分析1.1 AAC简介

高级音频编码(AdvancedAudio Coding,AAC)一种基于MPEG-4的音频编码技术,它由杜比实验室、AT&T等公司共同研发,目的是替换MP3编码方式。作为一种高压缩比的音频压缩算法,AAC的数据压缩比约为18:1,压缩后的音质可以同未压缩的CD音质相媲美。因此,相对于MP3、WMA等音频编码标准来说,在相同质量下码率更低,有效地节约了传输带宽,被广泛得应用于互联网流媒体、IPTV等领域(低码率,高音质)。主要有以下特点:

比特率:AAC- 最高512kbps(双声道时)/MP3- 32~320kbps

采样率:AAC- 最高96kHz / MP3 - 最高48kHz

声道数:AAC– 最高48个全音域声道/MP3 - 两声道

采样精度:AAC- 最高32bit / MP3 - 最高16bit

AAC的不足之处是,它属于有损压缩的格式,相对于APE和FLAC等主流无损压缩,音色“饱满度”差距比较大。另外,除了流媒体网络传输,其所能支持的设备较少。

1.2 AAC编码封装格式

音频数据在压缩编码之前,要先进行采样与量化,以样值的形式存在。音频压缩编码的输出码流,以音频帧的形式存在。每个音频帧包含若干个音频采样的压缩数据,AAC的一个音频帧包含960或1024个样值,这些压缩编码后的音频帧称为原始数据块(RawData Block),由于原始数据块以帧的形式存在,即简称为原始帧。原始帧是可变的,如果对原始帧进行ADTS的封装,得到的原始帧为ADTS帧;如果对原始帧进行ADIF封装,得到的原始帧为ADIF帧。它们的区别如下:

ADIF:AudioData Interchange Format,音频数据交换格式。这种格式明确解码必须在明确定义的音频数据流的开始处进行,常用于磁盘文件中; ADTS:AudioData Transport Stream,音频数据传输流。这种格式的特点是它一个有同步字的比特流,且允许在音频数据流的任意帧解码,也就是说,它每一帧都有信息头。

一个AAC原始数据库长度是可变的,对原始帧加上ADTS头进行ADTS封装就形成了ADTS帧。AAC音频的每一帧(ADTS帧)体由ADTS Header和AAC Audio Data(包含1~4个音频原始帧)组成,其中,ADTS Header占7个字节或9个字节,由两部分组成:固定头信息(adts_fixed_header)、可变头信息(adts_variable_header)。固定头信息中的数据每一帧都是相同的,主要定义了音频的采样率、声道数、帧长度等关键信息,这是解码AAC所需关键信息;可变头信息则在帧与帧之间可变。

下面是多个ADTS帧组成的AAC数据流结构,示意图如下:

a) 固定信息头说明:syncword:占12bits。同步头,表示一个ADTS帧的开始,总是0xFFF。正是因为它的存在,才支持解码任意帧;ID: 占1bit。MPEG的版本,0为MPGE-4,1为MPGE-2;Layer: 占2bits。总是”00”;protection_absent:占1bit。=0时,ADTS Header长度占9字节;=1时,ADTS Header占7字节profile: 占2bit。使用哪个级别的AAC,值00、01、10分别对应Mainprofile、LC、SSR;sampling_frequency_index:占4bits。表示使用的采样率下标,通过这个下标在Sampling Frequencies[ ]数组中查找得知采样率的值,如0xb,对应的采样率为8000Hz;channel_configuration:表示声道数,如1-单声道,2-立体声免费学习地址:https://ke.qq.com/course/3202131?flowToken=1042495【文章福利】免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~(b)可变信息头

说明:

frame_length:占13bits。表示一个ADTS帧的长度,即ADTS头(7或9字节)+sizeof(AAC Frame);adts_buffer_fullness:占11bits。值0x7FF,说明是码率可变的码流number_of_raw_data_blocks_In_frame:占2bits。表示ADTS帧中有(number_of_raw_data_blocks_In_frame+1)个AAC原始帧(3) 将AAC打包成ADTS格式

众所周知,在使用MediaCodec将PCM压缩编码为AAC时,编码器输出的AAC是没有ADTS头的原始帧,如果我们直接保存为AAC文件或推流,VLC等工具是无法将AAC数据流解码播放的。因此,我们需要对MediaCodec编码PCM输出的AAC原始帧添加ADTS数据头,然后再进行文件保存或者推流。MediaCodec部分代码如下:

private void encodeBytes(byte[] audioBuf, int readBytes) { ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers(); ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers(); int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(TIMES_OUT); if(inputBufferIndex >= 0){ ByteBuffer inputBuffer = null; if(!isLollipop()){ inputBuffer = inputBuffers[inputBufferIndex]; }else{ inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex); } if(audioBuf==null || readBytes= 0); } //----------------------------添加ADTS头,7个字节------------------------------- private void addADTStoPacket(byte[] packet, int packetLen) { int profile = 2; int chanCfg = 1; int sampleRate = mSamplingRateIndex ; packet[0] = (byte) 0xFF; packet[1] = (byte) 0xF1; packet[2] = (byte) (((profile - 1) 3); packet[5] = (byte) (((packetLen & 7) objecttype); /* profile_objecttype */ put_bits(&pb, 4, ctx->sample_rate_index); // 采样率 put_bits(&pb, 1, 0); /* private_bit */ put_bits(&pb, 3, ctx->channel_conf); /* 通道,channel_configuration */ put_bits(&pb, 1, 0); /* original_copy */ put_bits(&pb, 1, 0); /* home */ /* adts_variable_header */ put_bits(&pb, 1, 0); /* copyright_identification_bit */ put_bits(&pb, 1, 0); /* copyright_identification_start */ put_bits(&pb, 13, full_frame_size); /* aac_frame_length,ADTS帧长度 */ put_bits(&pb, 11, 0x7ff); /* adts_buffer_fullness */ put_bits(&pb, 2, 0); /* number_of_raw_data_blocks_in_frame */ flush_put_bits(&pb); return 0; }

从adts_write_frame_header()来看,除了profile、sampling_frequency_index、channel_configuration以及acc_frame_length值可能会因为编码器的配置不一样而不用,其他字段基本相同,甚至profile也可以直接设置默认值。既然如此,画个大概

下面是使用UtraEdit软件打开aac文件,一个ADTS帧表现如下:

2. MP4封装格式分析

由于MP4格式较为复杂,本文只对其做个简单的介绍。MP4封装格式是基于QuickTime容器格式定义,媒体描述与媒体数据分开,目前被广泛应用于封装h.263视频和AAC音频,是高清视频/HDV的代表。MP4文件中所有数据都封装在box中(d对应QuickTime中的atom),即MP4文件是由若干个box组成,每个box有长度和类型,每个box中还可以包含另外的子box。box的基本结构如下:

其中,size指明了整个box所占用的大小,包括header部分。如果box很大(例如存放具体视频数据的mdatbox),超过了uint32的最大数值,size就被设置为1,并用接下来的8位uint64来存放大小。通常,一个MP4文件由若干box组成,常见的mp4文件结构:

一般来说,解析媒体文件,最关心的部分是视频文件的宽高、时长、码率、编码格式、帧列表、关键帧列表,以及所对应的时戳和在文件中的位置,这些信息,在mp4中,是以特定的算法分开存放在stblbox下属的几个box中的,需要解析stbl下面所有的box,来还原媒体信息。下表是对于以上几个重要的box存放信息的说明:

3.将H.264和AAC封装成MP4文件

为了深入的理解H.264、AAC编码格式,接下来我们将通过AndroidAPI中提供的MediaCodec和MediaMuxer实现对硬件采集的YUV格式视频数据和PCM格式音频数据进行压缩编码,并将编码好的数据封装成MP4格式文件。MediaCodec被引入于Android4.1,它能够访问系统底层的硬件编码器,我们可以通过指定MIME类型指定相应编码器,来实现对采集音、视频进行编解码;MediaMuxer是一个混合器,它能够将H.264视频流和ACC音频流混合封装成一个MP4文件,也可以只输入H.264视频流。

3.1 将YUV视频数据编码为H.264

首先,创建并配置一个MediaCodec对象,通过指定该对象MIME类型为"video/avc",将其映射到底层的H.264硬件编码器。然后再调用MediaCodec的configure方法来对编码器进行配置,比如指定视频编码器的码率、帧率、颜色格式等信息。

MediaFormatmFormat = MediaFormat.createVideoFormat(“"video/avc"”, 640 ,480); //码率,600kbps-5000kbps,根据分辨率、网络情况而定 mFormat.setInteger(MediaFormat.KEY_BIT_RATE,BIT_RATE); //帧率,15-30fps mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,FRAME_RATE); //颜色格式,COLOR_FormatYUV420Planar或COLOR_FormatYUV420SemiPlanar mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat); //关键帧时间间隔,即编码一次关键帧的时间间隔 mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,FRAME_INTERVAL); //配置、启动编码器 MediaCodec mVideoEncodec = MediaCodec.createByCodecName(mCodecInfo.getName()); mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); mVideoEncodec.start();

其次,每个编译器都拥有多个输入、输出缓存区,当API= 0){ // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端 ByteBuffer inputBuffer = null; if(!isLollipop()){ inputBuffer =inputBuffers[inputBufferIndex]; }else{ inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex); } // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理 inputBuffer.clear(); inputBuffer.put(mFrameData); mVideoEncodec.queueInputBuffer(inputBufferIndex,0,mFrameData.length,getPTSUs(),0); }

原始数据流被编码处理后,编码好的数据会保存到被APP绑定的输出缓存区,通过调用MediaCodec的dequeueOutputBuffer(MediaCodec.BufferInfo,long)实现。当输出缓存区的数据被处理完毕后(比如推流、混合成MP4),就可以调用MediaCodec的releaseOutputBuffer(int,boolean)方法将输出缓存区还给编码器。

// 返回一个输出缓存区句柄,当为-1时表示当前没有可用的输出缓存区 // mBufferInfo参数包含被编码好的数据,timesOut参数为超时等待的时间 MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = -1; do{ outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT); if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){ Log.e(TAG,"获得编码器输出缓存区超时"); }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){ // 如果API小于21,APP需要重新绑定编码器的输入缓存区; // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED if(!isLollipop()){ outputBuffers = mVideoEncodec.getOutputBuffers(); } }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){ // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次 // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步) MediaFormat newFormat = mVideoEncodec.getOutputFormat(); MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get(); if(mMuxerUtils != null){ mMuxerUtils.setMediaFormat(MediaMuxerUtils.TRACK_VIDEO,newFormat); } Log.i(TAG,"编码器输出缓存区格式改变,添加视频轨道到混合器"); }else{ // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据 ByteBuffer outputBuffer = null; if(!isLollipop()){ outputBuffer = outputBuffers[outputBufferIndex]; }else{ outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex); } // 如果API= 0);

这里有几点需要说明下,因为如果处理不当,可能会导致MediaMuxer合成MP4文件失败或者录制的MP4文件播放时开始会出现大量马赛克或者音视频不同步异常。

a) 如何保证音、视频同步?

要保证录制的MP4文件能够音视频同步,需要做到两点:其一当我们获得输出缓存区的句柄outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,需要将视频轨道(MediaFormat)设置给MediaMuxer,同时只有在确定音频轨道也被添加后,才能启动MediaMuxer混合器;其二就是传入MediaCodec的queueInputBuffer中PTUs时间参数应该是单调递增的,比如:

long prevPresentationTimes= mBufferInfo.presentationTimeUs; private long getPTSUs(){ longresult = System.nanoTime()/1000; if(result= 0){ // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端 ByteBuffer inputBuffer = null; if(!isLollipop()){ inputBuffer = inputBuffers[inputBufferIndex]; }else{ inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex); } // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理 if(audioBuf==null || readBytes


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有