您当前的位置:首页 > 计算机 > 文件格式与编码

AAC之ADTS

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

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毫秒,所以没有任何影响,而且自己写的实现自己非常清楚怎么修改,以后要是有哪里需要改变的都比较容易)。

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