2025年6月9日 星期一 乙巳(蛇)年 三月十三 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 文件格式与编码

AAC之ADTS

时间:02-05来源:作者:点击数:29

1、ADTS头的理解

在Android中,使用MediaCodec编码PCM音频为AAC,得到编码后的AAC数据如果直接保存文件是无法播放的,需要在每一帧AAC数据的前面加上ADTS头,用于描述这一帧AAC的大小、采样率、声道数量等,这样播放时才知道如何解码。

百度时答案很多都是一模一样的,估计都是转载国外的方法,我想了解更多一点,但是看来看去都一样的,经过许多的参考与理解之后,终于感觉差不多了,这里记录一下自己的理解,决不千篇一律,ADTS头由7个字节组成(也可以是9个字节,但是一般用7个字节),7个字节的组成示例图如下:

在这里插入图片描述

可以看到,7个字节被分成了A、B、C、D、E、F、G、H、I、J、K、L、M、O、P等15个部分,每个部分的含义如下:

在这里插入图片描述

截图来自这里:https://wiki.multimedia.cx/index.php/ADTS

据网上的一些文章说,7个字节共56位,前面28位为固定头信息(adts_fixed_header),后面28位为可变头信息(adts_variable_header),人人都转载,人人都这么说,没有自己的理解,根据我的理解,其实只有上图中的M部分(也就是帧长)这一部分会变,其他地方都是不变的,了解这个对于写代码实现时提高效率是很有帮助的,因为MediaCodec在编码PCM数据为AAC数据时,就是编码后的AAC数据长度可能每次不一样,其它的什么采样率啊、通道数肯定是每次都一样不会再变的了。

2、添加ADTS头(使用byte数组实现)

下面开始讲解如何实现添加ADTS头信息,先转一个网上的例子,如下:

  • public static void addADTStoPacket(byte[] packet, int packetLen) {
  • int profile = 2;
  • int freqIdx = 4;
  • int chanCfg = 1;
  • packet[0] = (byte)0xFF;
  • packet[1] = (byte)0xF9;
  • packet[2] = (byte)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
  • packet[3] = (byte)(((chanCfg&3)<<6) + (packetLen>>11));
  • packet[4] = (byte)((packetLen&0x7FF) >> 3);
  • packet[5] = (byte)(((packetLen&7)<<5) + 0x1F);
  • packet[6] = (byte)0xFC;
  • }

对于从来不使用位运算的我来说,自然无法去理解这个代码,只是觉得很牛,但是也需要验证一下它的结果,于是把7个字节打印出来,对照https://wiki.multimedia.cx/index.php/ADTS上面说的每一个字节的功能去比对字节是不是正确的,验证结果是OK的!

这里突然有种冲动,我想按照自己的想法,也来写一个实现,顺便可以练一练二进制的操作运算,隐约记得java已经有二进制的表现形式了,于是百度了一下,是在Java7的时候出来的,可以使用0B打头来写二进制,就像0X打头代表16进制一个原理,而且数字之间还可以使用下划线。我的实现如下(练习时使用的是Java语言):

  • public class ADTSTest {
  • public static void main(String[] args) {
  • short syncword = 0B0000_0000_0000; // 12位
  • short MPEGVersion = 0B0; // 1位
  • short layer = 0B00; // 2位
  • short protectionAbsent = 0B0;// 1位
  • short profile = 0B01; // 2位
  • short MPEG4SamplingFrequencyIndex = 0B0000; // 4位
  • short privateBit = 0B0; // 1位
  • short channelConfigurations = 0B000; // 3位
  • short originality = 0B0; // 1位
  • short home = 0B0; // 1位
  • short copyrightedIdBit = 0B0; // 1位
  • short copyrightedIdStart = 0B0; // 1位
  • short frameLength = 0B0_0000_0000_0000; // 13位
  • short bufferFullness = 0B000_0000_0000; // 11位
  • short numberOfAACFrames = 0B01; // 2位
  • byte[] adtsBytes = new byte[7];
  • // 添加同步字节(12位),所有位必须都是1
  • adtsBytes[0] = (byte) (syncword >>> 4); // 右移4位为丢掉最低4位
  • adtsBytes[1] = (byte) (syncword & 0B1111 << 4); // &0B1111为保留低4位, <<4为往到字节的高4位的位置
  • // 添加MPEG版(1位),0为MPEG-4,1为MPEG-2
  • adtsBytes[1] = (byte) (adtsBytes[1] | (MPEGVersion << 3));
  • // 设置Layer(2位),总是0
  • adtsBytes[1] = (byte) (adtsBytes[1] | (layer << 1));
  • // 设置protection absent(1位),如果没有CRC则设置为1,如果有CRC则设置为0
  • adtsBytes[1] = (byte) (adtsBytes[1] | protectionAbsent);
  • // 设置profile(2位)减1,即MPEG-4 Audio Object Type,参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Audio_Object_Types
  • adtsBytes[2] = (byte) ((profile - 1) << 6);
  • // 设置MPEG4采样频率(即音频的采样率)的索引(4位),参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
  • adtsBytes[2] = (byte) (adtsBytes[2] | (MPEG4SamplingFrequencyIndex << 2));
  • // 设置私有位(1位),保证永远不会被MPEG使用,在编码时设置为0,在解码时忽略
  • adtsBytes[2] = (byte) (adtsBytes[2] | (privateBit << 1));
  • // 设置通道配置(即设置通道数量)(3位),参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Channel_Configurations
  • adtsBytes[2] = (byte) (adtsBytes[2] | (channelConfigurations >>> 2));
  • adtsBytes[3] = (byte) ((channelConfigurations & 0B11) << 6);
  • // 设置originality(1位),编码时设置为0,解码时忽略
  • adtsBytes[3] = (byte) (adtsBytes[3] | (originality << 5));
  • // 设置home(1位),编码时设置为0,解码时忽略
  • adtsBytes[3] = (byte) (adtsBytes[3] | (home << 4));
  • // 设置版权ID位(1位),即集中注册的版权标识符的下一位, 编码时设置为0,解码时忽略
  • adtsBytes[3] = (byte) (adtsBytes[3] | (copyrightedIdBit << 3));
  • // 设置版权ID开始(1位),表示此帧的版权ID位是版权ID的第一位, 编码时设置为0,解码时忽略
  • adtsBytes[3] = (byte) (adtsBytes[3] | (copyrightedIdStart << 2));
  • // 设置帧长度(13位),此值必须包含7或9个字节的报头长度:FrameLength =(ProtectionAbsent == 1 ? 7 : 9)+ size(AACFrame)
  • adtsBytes[3] = (byte) (adtsBytes[3] | (frameLength >>> 11));
  • adtsBytes[4] = (byte) ((frameLength & 0B111_1111_1111) >>> 3);
  • adtsBytes[5] = (byte) ((frameLength & 0B111) << 5);
  • // 设置Buffer fullness(11位),不知道如何理解这个意思,据说全为1表示动态码率
  • adtsBytes[5] = (byte) (adtsBytes[5] | (bufferFullness >>> 6));
  • adtsBytes[6] = (byte) ((bufferFullness & 0B11_1111) << 2);
  • // 设置ADTS帧中的AAC帧(RDB)的数量(2位)减1,为了获得最大的兼容性,请始终为每个ADTS帧使用1个AAC帧
  • adtsBytes[6] = (byte) (adtsBytes[6] | (numberOfAACFrames - 1));
  • for (int i = 0; i < adtsBytes.length; i++) {
  • System.out.println(getBinary(adtsBytes[i]));
  • }
  • }
  • /** 把一个byte转换为二进制输出 */
  • private static String getBinary(byte b) {
  • String binaryString = Integer.toBinaryString(Byte.toUnsignedInt(b));
  • int length = binaryString.length();
  • for (int index = 0; index < 8 - length; index++) {
  • binaryString = "0" + binaryString;
  • }
  • return binaryString;
  • }
  • }

注:Audio Object Types,它是用来两个位来设置的,虽然类型表中40几种类型,但两个位最大值为11,换为10进制为3,也就是说我们只能选0 ~ 3的4个类型,最常用的类型是2,了解这个有什么用呢?相当有用,这就是说如果你要按ADTS来保存AAC的话,这个设置基本上都会设置为2(AAC LC),因为0、1、3的类型不常用,这样我们就可以减少计算,写代码时直接写死为2的类型即可。

这完全是按照https://wiki.multimedia.cx/index.php/ADTS这里描述的ADTS头结果来写的,花了我一上午的时间,写得很慢,因为二进制操作的太少了,理解是没问题的,但是脑子转得比较慢,所以花了一上午,还是很值得的,起码学到了东西。如果验证代码是否正确呢?ADTS头是7个字节,那我们就把上面的所有二进制位全部设置为1,看是否能输出全部为1,结果如下:

  • 11111111
  • 11111111
  • 10111111
  • 11111111
  • 11111111
  • 11111111
  • 11111110

刚开始我以为我程序出问题了,结果怎么有两个0啊?后来发现是没问题的,因为第17、18位(从左到右数)是profile的设置,它在设置到字节中时减了1,这是ADTS头结构中是这么规定的要减1,所以11减1后就是10了,最后的两位也是同理减了1,所以有0。那接下来就是验证全部为0的情况,这时要小心,因为有两个地方需要减1的,为了不产生负数,在需要减1的参数上,我们就设置为1,也就是有两个地方要设置为1,不能全部设置为0,这样输出结果就是全部为0了,说明代码是没问题的。

3、添加ADTS头(使用long实现)

后来,我又想,这byte[]分为7个字节来操作真是太麻烦了,因为经常要跨字节操作,long不是占8个字节吗,它是连续的8个字节,那我何不用long来保存这个ADTS头呢,最后再把long转换回byte数组,于是我的另一版实现如下,使用long的低7个字节保存ADTS,实现如下:

  • public class ADTSTest {
  • public static void main(String[] args) {
  • long syncword = 0B1111_1111_1111; // 12位
  • long MPEGVersion = 0B1; // 1位
  • long layer = 0B11; // 2位
  • long protectionAbsent = 0B1;// 1位
  • long profile = 0B11; // 2位
  • long MPEG4SamplingFrequencyIndex = 0B1111; // 4位
  • long privateBit = 0B1; // 1位
  • long channelConfigurations = 0B111; // 3位
  • long originality = 0B1; // 1位
  • long home = 0B1; // 1位
  • long copyrightedIdBit = 0B1; // 1位
  • long copyrightedIdStart = 0B1; // 1位
  • long frameLength = 0B1_1111_1111_1111; // 13位
  • long bufferFullness = 0B111_1111_1111; // 11位
  • long numberOfAACFrames = 0B11; // 2位
  • long adts = 0x0L;
  • byte[] adtsBytes = new byte[7];
  • // 添加同步字节(12位),所有位必须都是1
  • adts = adts | (syncword << 44); // 共12位,要移到56位的位置(从右向左数第56位),则需要左右位数为:56 - 12 = 44
  • // 添加MPEG版(1位),0为MPEG-4,1为MPEG-2
  • adts = adts | (MPEGVersion << 43); // 共1位,需要移到44的位置,则44 - 1 = 43
  • // 设置Layer(2位),总是0
  • adts = adts | (layer << 41); // 位置43 - 2位
  • // 设置protection absent(1位),如果没有CRC则设置为1,如果有CRC则设置为0
  • adts = adts | (protectionAbsent << 40); // 位置41 - 1位
  • // 设置profile(2位)减1,即MPEG-4 Audio Object Type,参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Audio_Object_Types
  • adts = adts | ((profile - 1) << 38); // 位置40 - 2位
  • // 设置MPEG4采样频率(即音频的采样率)的索引(4位),参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
  • adts = adts | (MPEG4SamplingFrequencyIndex << 34); // 位置38 - 4位
  • // 设置私有位(1位),保证永远不会被MPEG使用,在编码时设置为0,在解码时忽略
  • adts = adts | (privateBit << 33); // 位置34 - 1位
  • // 设置通道配置(即设置通道数量)(3位),参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Channel_Configurations
  • adts = adts | (channelConfigurations << 30); // 位置33 - 3位
  • // 设置originality(1位),编码时设置为0,解码时忽略
  • adts = adts | (originality << 29); // 位置30 - 1位
  • // 设置home(1位),编码时设置为0,解码时忽略
  • adts = adts | (home << 28); // 位置29 - 1位
  • // 设置版权ID位(1位),即集中注册的版权标识符的下一位, 编码时设置为0,解码时忽略
  • adts = adts | (copyrightedIdBit << 27); // 位置28 - 1位
  • // 设置版权ID开始(1位),表示此帧的版权ID位是版权ID的第一位, 编码时设置为0,解码时忽略
  • adts = adts | (copyrightedIdStart << 26); // 位置27 - 1位
  • // 设置帧长度(13位),此值必须包含7或9个字节的报头长度:FrameLength =(ProtectionAbsent == 1 ? 7 : 9)+ size(AACFrame)
  • adts = adts | (frameLength << 13); // 位置26 - 13位
  • // 设置Buffer fullness(11位),不知道如何理解这个意思,据说全为1表示动态码率
  • adts = adts | (bufferFullness << 2); // 位置13 - 11位
  • // 设置ADTS帧中的AAC帧(RDB)的数量(2位)减1,为了获得最大的兼容性,请始终为每个ADTS帧使用1个AAC帧
  • adts = adts | (numberOfAACFrames - 1); // 最后两位,无需左移
  • adtsBytes[0] = (byte) (adts >>> 48); // 右移48 = (7字节 - 1) * 8
  • adtsBytes[1] = (byte) (adts >>> 40); // 右移40 = (6字节 - 1) * 8
  • adtsBytes[2] = (byte) (adts >>> 32); // 右移32 = (5字节 - 1) * 8
  • adtsBytes[3] = (byte) (adts >>> 24); // 右移24 = (4字节 - 1) * 8
  • adtsBytes[4] = (byte) (adts >>> 16); // 右移16 = (3字节 - 1) * 8
  • adtsBytes[5] = (byte) (adts >>> 8); // 右移 8 = (2字节 - 1) * 8
  • adtsBytes[6] = (byte) adts; // 右移 0 = (1字节 - 1) * 8
  • // 取出adts中的低7个字节
  • for (int i = 0; i < adtsBytes.length; i++) {
  • System.out.println(getBinary(adtsBytes[i]));
  • }
  • }
  • /** 把一个byte转换为二进制输出 */
  • private static String getBinary(byte b) {
  • String binaryString = Integer.toBinaryString(Byte.toUnsignedInt(b));
  • int length = binaryString.length();
  • for (int index = 0; index < 8 - length; index++) {
  • binaryString = "0" + binaryString;
  • }
  • return binaryString;
  • }
  • }

验证结果也是OK的,代码比之前简洁了许多,感觉自己对二进制位的计算功力又上升了。

4、添加ADTS头(java最终优化版)

上面都是练习代码,写起来还是很麻烦的,要用到真实开发之前肯定是要先优化一下的,在优化之前再来回顾一下ADTS头的字节组成,如下:

在这里插入图片描述

我们前面分析说了,其实只有M部分(帧长)才会变,其它都是不变的,从上图可知,M部分横跨第2、第3、第4字节,所以在实战使用时,只需要每次更新设置这3个字节即可,其他字节就不要每次都计算了,这样就提高了效率。当然,采样频率和通道数可能会变,但这只是在我们初始化的时候会变,一但开始编码时就不会再变了。这3个地方需要使用3个变量来表示,除去这三个地方后的字节百分百无轮什么时候都不会再变了,所以不变的地方字节图如下:

在这里插入图片描述

如上图,采样频率索引、通道数、帧长这三个地方我使用x号来表示对应的bit,这些bit是需要动态设置的,方便我们根据需求来修改,这三个地方初始化时可以填充为0,不能填充为1,因为后面需要通过或运算来添加新的值进来,0或新的值结果为新的值(比如0或0为0,0或1为1),如果是1或新的值,则结果不一定为新的值(比如1或0结果是1,而我们想要的结果是或0结果就要为0),使用0来填充这些x,则所有7个字节的二进制位如下:

11111111111110010100000000000000000000000001111111111100

7个字节的二进制挺长的,使用计算器,或者自己写个Demo把它换成10进制或者16进制,为了更简洁,使用16进制是比较好的选择,这串二进制对应的16进制值为:0xFFF94000001FFCL,什么,十六进制有L这字符了,不要惊讶,java默认数字是int类型的,一个int表示不了7个字节,所以用long来表示,L表示这是一个长整型(学到没),这个long并没有保存完整的ADTS头信息,所以我们可以用一个adtsPart变量来表示,如果要表示完整的头信息,可以使用adtsFull变量来表示,如下:

  • /** 只保存了ADTS头中不变化的那部分信息 */
  • private static long adtsPart = 0xFFF94000001FFCL;
  • /** 保存了ADTS头中的完整信息 */
  • private static long adtsFull;

接下来开始写函数了,还记得之前那张图吧,帧长度横跨第2、3、4字节,这是从左往右数的,但是保存字节数组时是从左往右排的,而且索引从0开始,则从左往右数的话,帧长度横跨索引为3、4、5的字节,因为这3个字节会变,所以我们初始化的时候就不需要保存这三个字节,只保存0、1、2、6这4个固定不变的字节,代码如下:

  • public class ADTSUtil {
  • /** 只保存了ADTS头中不变化的那部分信息 */
  • private static long adtsPart = 0xFFF94000001FFCL;
  • /** 保存了ADTS头中的完整信息 */
  • private static long adtsFull;
  • private static byte byte_0;
  • private static byte byte_1;
  • private static byte byte_2;
  • private static byte byte_6;
  • /**
  • * 初始化ADTS头,计算出ADTS头中固定不变化的那些信息
  • * @param samplingFrequencyIndex 音频采样频率的索引
  • * @param channelCount 声道数量
  • */
  • public static void initADTS(long samplingFrequencyIndex, long channelCount) {
  • adtsPart = adtsPart | (samplingFrequencyIndex << 34) | (channelCount << 30);
  • byte_0 = (byte) (adtsPart >>> 48); // 右移48 = (7字节 - 1) * 8
  • byte_1 = (byte) (adtsPart >>> 40); // 右移40 = (6字节 - 1) * 8
  • byte_2 = (byte) (adtsPart >>> 32); // 右移32 = (5字节 - 1) * 8
  • byte_6 = (byte) adtsPart; // 右移 0 = (1字节 - 1) * 8
  • }
  • /** 往oneADTSFrameBytes数组中的最前面7个字节中填入ADTS头信息 */
  • public static void addADTS(byte[] oneADTSFrameBytes) {
  • adtsFull = adtsPart | (oneADTSFrameBytes.length << 13);// 一个int32位,所以不用担心左移13位数据丢失的问题
  • oneADTSFrameBytes[0] = byte_0;
  • oneADTSFrameBytes[1] = byte_1;
  • oneADTSFrameBytes[2] = byte_2;
  • oneADTSFrameBytes[3] = (byte) (adtsFull >>> 24); // 右移24 = (4字节 - 1) * 8
  • oneADTSFrameBytes[4] = (byte) (adtsFull >>> 16); // 右移16 = (3字节 - 1) * 8
  • oneADTSFrameBytes[5] = (byte) (adtsFull >>> 8); // 右移 8 = (2字节 - 1) * 8
  • oneADTSFrameBytes[6] = byte_6;
  • }
  • }

OK,代码相当简洁了,addADTS(byte[] oneADTSFrameBytes)函数,参数是用于保存一帧音频数据(ADTS头数据 + AAC数据),传到此函数时就会往它的最前面7个字节添加ADTS头信息,可以看到,我们有4个字节不再进行计算,只有5次简单的运算(一个或运算,一个左移、三个右移),相比网上的答案,如下:

  • public static void addADTStoPacket(byte[] packet, int packetLen) {
  • int profile = 2;
  • int freqIdx = 4;
  • int chanCfg = 1;
  • packet[0] = (byte)0xFF;
  • packet[1] = (byte)0xF9;
  • packet[2] = (byte)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
  • packet[3] = (byte)(((chanCfg&3)<<6) + (packetLen>>11));
  • packet[4] = (byte)((packetLen&0x7FF) >> 3);
  • packet[5] = (byte)(((packetLen&7)<<5) + 0x1F);
  • packet[6] = (byte)0xFC;
  • }

上面是网上的答案,虽然很简单简洁,但是但是运算次数远比我的实现要多,就算这里也把字节0、1、2、6进行全局保存,每次只计算3、4、5,它里面的计算量还是比我的实现要多的,它的包含三个与运算,两个左移运算,两个右移运算,两个加法运算,而我们的实现主要是位移运算,这要比与运算或加法运算要快的多,当然,音频数据其实并不大,这点运算没什么,看不出太大的差别,但是我们应该尽量追求更好的实现。像视频的图像编码,这些运算的差别就会非常大的,因为图像的内容很大,一帧1920x1080的图片yuv大小差不多3M。

5、添加ADTS头(Kotlin最终优化版)

现在做Android开发的,应该都是用Kotlin语言了吧,所以最终用到我项目中时,代码还是要转换为Kotlin语言的,代码如下:

  • /** ADTS头信息添加工具,ADTS头含义参考:https://wiki.multimedia.cx/index.php/ADTS */
  • object ADTSUtil {
  • /** 只保存了ADTS头中不变化的那部分信息 */
  • private var adtsPart = 0xFFF94000001FFCL
  • /** 保存了ADTS头中的完整信息 */
  • private var adtsFull = 0L
  • private var byte_0: Byte = 0
  • private var byte_1: Byte = 0
  • private var byte_2: Byte = 0
  • private var byte_6: Byte = 0
  • /** 采样频率对应的索引,具体参考:https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies */
  • private val sampleRateIndexMap = mapOf(
  • 96000 to 0, 88200 to 1, 64000 to 2, 48000 to 3,
  • 44100 to 4, 32000 to 5, 24000 to 6, 22050 to 7,
  • 16000 to 8, 12000 to 9, 11025 to 10, 8000 to 11,
  • 7350 to 12)
  • /**
  • * 初始化ADTS头
  • * @param sampleRateInHz 音频的采样频率,
  • * @param channelCount 音频的声道数量
  • */
  • fun initADTS(sampleRateInHz: Int, channelCount: Int) {
  • val sampleRateIndex = sampleRateIndexMap[sampleRateInHz] ?: error("ADTS不支持${sampleRateInHz}采样率,具体支持参考sampleRateIndexMap集合")
  • adtsPart = adtsPart or (sampleRateIndex.toLong() shl 34) or (channelCount.toLong() shl 30)
  • byte_0 = (adtsPart ushr 48).toByte() // 右移48 = (7字节 - 1) * 8
  • byte_1 = (adtsPart ushr 40).toByte() // 右移40 = (6字节 - 1) * 8
  • byte_2 = (adtsPart ushr 32).toByte() // 右移32 = (5字节 - 1) * 8
  • byte_6 = adtsPart.toByte() // 右移 0 = (1字节 - 1) * 8
  • }
  • /** 往oneADTSFrameBytes数组中的最前面7个字节中填入ADTS头信息 */
  • fun addADTS(oneADTSFrameBytes: ByteArray) {
  • adtsFull = adtsPart or (oneADTSFrameBytes.size.toLong() shl 13)
  • oneADTSFrameBytes[0] = byte_0
  • oneADTSFrameBytes[1] = byte_1
  • oneADTSFrameBytes[2] = byte_2
  • oneADTSFrameBytes[3] = (adtsFull ushr 24).toByte() // 右移24 = (4字节 - 1) * 8
  • oneADTSFrameBytes[4] = (adtsFull ushr 16).toByte() // 右移16 = (3字节 - 1) * 8
  • oneADTSFrameBytes[5] = (adtsFull ushr 8).toByte() // 右移 8 = (2字节 - 1) * 8
  • oneADTSFrameBytes[6] = byte_6
  • }
  • }

这里顺便记忆一下Kotlin的位运算符号,不知道它为什么要搞特殊,继承Java的符号它不香吗?非要搞成字母,如下:

  • shl:左移(shift left ),等同于:<<
  • shr:右移(shift right ),等同于:>>
  • ushr:无符号右移(unsigned shift right),等同于:>>>
  • or:或运算,等同于:|
  • and:与运算,等同于:&

最后,截取ADTSUtil在真实项目中的调用部分代码,如下:

1、在MediaCodec初始化之前初始化ADTS

  • // 初始化ADTS
  • val sampleRateInHz = 44100
  • val channelCount = 1
  • ADTSUtil.initADTS(sampleRateInHz, channelCount)
  • // 初始化MediaCodec
  • mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
  • mMediaCodec.start()

2、在编码得到AAC数据的地方添加ADTS头信息

  • outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)
  • while (outputBufferIndex >= 0) {
  • if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
  • Timber.i("AAC有配置数据") // 这个配置数据无需写到文件中,不知道干嘛用的
  • } else {
  • // 申请一个数组,用于保存一帧ADTS数据(包括ADTS头和AAC数据)
  • val oneADTSFrameBytes = ByteArray(7 + mBufferInfo.size)
  • // 在数组的前面7个字节中填入ADTS头数据
  • ADTSUtil.addADTS(oneADTSFrameBytes)
  • // 取出outputBuffer中的AAC数据,并在oneADTSFrameBytes数组索引为7的位置开始填入AAC数据
  • outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex)!!
  • outputBuffer.get(oneADTSFrameBytes, 7, mBufferInfo.size)
  • // 把一帧ADTS数据(ADTS头 + AAC数据)写到文件
  • fileOutputStream.write(oneADTSFrameBytes)
  • }
  • mMediaCodec.releaseOutputBuffer(outputBufferIndex, false) // 把缓存对象还给MediaCodec
  • outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)
  • }

6、性能测试

写完代码的我,突然有种想法,想看看自己写的实现比网上的快多少,因为音频数据量小,所以处理非常快,使用System.currentTimeMillis()来测试的话,你会看到运行时间为0,所以这里使用了一个更精确的时间:System.nanoTime(),它返也是返回当前时间,单位为纳秒,单位换算如下:

  • 1毫秒 = 1000微秒
  • 1微秒 = 1000纳秒

测试如下:

  • val startTime = System.nanoTime()
  • ADTSUtil.addADTS(oneADTSFrameBytes)
  • Timber.i("${System.nanoTime() - startTime}")

分别测试了网上的实现与自己的实现测试数据如下:

  • 11979 8334
  • 11459 8334
  • 11980 6250
  • 10416 5730
  • 11458 6250
  • 11980 6250
  • 13021 5730
  • 14063 5729
  • 13542 5729
  • 13542 4166
  • 13542 4166
  • 11458 4167
  • 19792 7812
  • 13021 8854
  • 16666 7813
  • 22396 8855
  • 20312 6250
  • 14583 5729

左边一列是我的实现方法运行需要的时间,右边一列为网上的方法需要的时间,我脸打得好疼,看来,一切不能只靠猜想啊,要以实际数据为准。哎,真是奇了怪了,不为什么我的方法会用时多呢?方法对比如下:

  • /** 我的实现 */
  • fun addADTS(oneADTSFrameBytes: ByteArray) {
  • adtsFull = adtsPart or (oneADTSFrameBytes.size.toLong() shl 13)
  • oneADTSFrameBytes[0] = byte_0
  • oneADTSFrameBytes[1] = byte_1
  • oneADTSFrameBytes[2] = byte_2
  • oneADTSFrameBytes[3] = (adtsFull ushr 24).toByte() // 右移24 = (4字节 - 1) * 8
  • oneADTSFrameBytes[4] = (adtsFull ushr 16).toByte() // 右移16 = (3字节 - 1) * 8
  • oneADTSFrameBytes[5] = (adtsFull ushr 8).toByte() // 右移 8 = (2字节 - 1) * 8
  • oneADTSFrameBytes[6] = byte_6
  • }
  • /** 网上的实现 */
  • private fun addADTStoPacket(bytes: ByteArray, frameLength: Int) {
  • val profile = 2
  • val freqIdx = 4
  • val chanCfg = 1
  • bytes[0] = 0xFF.toByte()
  • bytes[1] = 0xF9.toByte()
  • bytes[2] = ((profile - 1 shl 6) + (freqIdx shl 2) + (chanCfg shr 2)).toByte()
  • bytes[3] = ((chanCfg and 3 shl 6) + (frameLength shr 11)).toByte()
  • bytes[4] = (frameLength and 0x7FF shr 3).toByte()
  • bytes[5] = ((frameLength and 7 shl 5) + 0x1F).toByte()
  • bytes[6] = 0xFC.toByte()
  • }

对比感觉网上的实现运算量应该比较多才对啊,而且它每次都分配三个局部变量(profile、freqIdx、chanCfg),即使这样运行时间也比我的实现要少,哎,懒得去找原因了,搞到这里已经花了我好多时间了,最终我还是用了自己的方法,毕竟自己辛苦写出来的,含泪也要用一用(其实是因为运行时间实在是太小了,虽然效率低一点,但运行时完全看不到影响,1万多纳秒的运行时间换成毫秒为0.01毫秒,所以没有任何影响,而且自己写的实现自己非常清楚怎么修改,以后要是有哪里需要改变的都比较容易)。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门