在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数据长度可能每次不一样,其它的什么采样率啊、通道数肯定是每次都一样不会再变的了。
下面开始讲解如何实现添加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了,说明代码是没问题的。
后来,我又想,这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的,代码比之前简洁了许多,感觉自己对二进制位的计算功力又上升了。
上面都是练习代码,写起来还是很麻烦的,要用到真实开发之前肯定是要先优化一下的,在优化之前再来回顾一下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。
现在做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的符号它不香吗?非要搞成字母,如下:
最后,截取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)
- }
-
写完代码的我,突然有种想法,想看看自己写的实现比网上的快多少,因为音频数据量小,所以处理非常快,使用System.currentTimeMillis()来测试的话,你会看到运行时间为0,所以这里使用了一个更精确的时间:System.nanoTime(),它返也是返回当前时间,单位为纳秒,单位换算如下:
测试如下:
- 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毫秒,所以没有任何影响,而且自己写的实现自己非常清楚怎么修改,以后要是有哪里需要改变的都比较容易)。