实时AAC音频/本地AAC音视频硬解码详细介绍附带Demo

本文详细介绍了Android环境下AAC音频硬解码的背景、优缺点、API使用方法,包括MediaCodec的详细操作,并提供了本地音视频文件与实时AAC音频的解码实例,同时解析了音视频编解码的CSD参数。通过代码示例,帮助开发者理解和应用AAC音频硬解码技术。
摘要由CSDN通过智能技术生成

一、使用AAC音频硬解码的背景

因为各种原因,在日常的开发中开发者或多或少都要接触一些音视频编解码相关的功能,所以有时候选择编解码工具就变得尤为重要,取决于你的项目属性又或者知识广度等等,下面作者结合自己的实际项目经验给大家分析一下

开发成本

开发成本在企业管理者的角度来说尤为重要,关系到企业的盈利与生存。所以为了降低成本很多开发者会考虑去使用Android 原生提供的一些API,而不是去使用第三方的一些开源库或者收费库,因为那样急需要花费额外的金钱并且还需要花费时间与精力去熟悉,所以也不推荐,除非时间和成本都在允许的范围内

维护成本

当项目迭代至成熟期时,维护成本就成了后续开发者要关注的事情,首先假设我们使用了第三方的库,如果你的产品已经卖出去了,而这时候第三方库不维护并且出现了一个致命的问题,那这样就会导致卖出去的产品都会被投诉并且短时间内还要花时间去移除之前使用的第三方库,如果耦合性过多,将导致无法挽回的经济损失。而如果使用的是Android 原生的API的话,因为本身是做产品的,所以只考虑当前设备,无须关心移植到其他平台或其他系统版本,前期做稳定,后期就不会有任何问题

二、使用AAC音频硬解码的优缺点

优点

开发方便快捷,有成熟的API调用,使用简单,网上也有大部分的参考资料

缺点

可移植性差,如果公司其他项目需要移植到新的硬件平台时,会有兼容性问题,大部分需要向原厂提工单才可解决

三、AAC音频硬解码的API介绍

MediaCodec 方法介绍

MediaCodec是Android原生提供的API,支持音视频的硬编码和硬解码,Android常用的源文件格式与编码后格式是音频的PCM编码成AAC,视频的NV21/YV12编码成H264,值得一提的是在选择和设置视频编码质量的时候,MediaFormat.KEY_PROFILE 在官方API介绍中,其可以控制视频的质量,实际则是Android7.0以下默认baseline,不管怎么设置都是默认baseline,所以这个变量属性,作者采用了删除线,在视频编码时,不推荐大家使用,避免出现问题

getInputBuffers()

从当前编解码器中获取输入缓冲区数组,用于向输入缓冲区中添加要编解码的数据

getOutputBuffers()

从当前编解码器中获取输出缓冲区数组,用于提取编解码之后的数据缓冲区

dequeueInputBuffer(long timeoutUs)

获取输入缓冲区数组中待使用(空闲)的缓冲区数组下标索引,timeoutUs为0时立即返回,小于0时表示一直等待直至输入缓冲区数组中有可用的缓冲区为止,大于0则表示等待时间为timeoutUs

getInputBuffer(int index)

获取输入缓冲区数组中待使用(空闲)的缓冲区,index参数为dequeueInputBuffer(long timeoutUs)的返回值,返回值大于等于0即表示有可用的输入缓冲区

queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)

向输入缓冲区数组中添加要编解码的数据,index参数为dequeueInputBuffer(long timeoutUs)的返回值,offset为要编解码数据的起始偏移,size为要编解码数据的长度,presentationTimeUs为PTS,flags为标记,正常使用时可默认填0,编解码至结尾时可填MediaCodec.BUFFER_FLAG_END_OF_STREAM值

dequeueOutputBuffer(BufferInfo info, long timeoutUs)

从输出缓冲区数组中获取编解码成功的缓冲区下标索引,info参数表示传入一个BufferInfo Java bean class , 编解码器会把处理完后的数据信息等以bean类型返回给开发者,timeoutUs意义跟之前介绍的dequeueInputBuffer(long timeoutUs)方法大致相同,返回值大于等于0即表示有可用的输出缓冲区

getOutputBuffer(int index)

获取输出缓冲区数组中编解码完成的缓冲区,index参数为dequeueOutputBuffer(BufferInfo info, long timeoutUs)方法的返回值,返回值大于等于0即表示有可用的输出缓冲区

releaseOutputBuffer(int index, boolean render)

释放编解码器输出缓冲区数组中的缓冲区,index为要释放的缓冲区数组下标索引,它为dequeueOutputBuffer(BufferInfo info, long timeoutUs)方法的返回值,render参数为渲染控制,如果在编解码时设置了可用的surface,render为true时则表示将此数据缓冲区输出到surface渲染

stop()

关闭编解码

release()

释放编解码资源

MediaCodec 参数介绍

本篇文章关于MediaCodec 参数的介绍只描述日常开发中出现频率最频繁的,其他一些参数很少使用或者使用之后没效果,这里就不再做过多阐述

MediaFormat.KEY_AAC_PROFILE

要使用的AAC配置文件的键(仅AAC音频格式时使用),常量在android.media.MediaCodecInfo.CodecProfileLevel 中声明,音频编码中最常用的变量是MediaCodecInfo.CodecProfileLevel.AACObjectLC

MediaFormat.KEY_CHANNEL_MASK

音频内容的通道组成的键,在音频编码中需要根据硬件支持去有选择性的选择支持范围内的通道号

MediaFormat.KEY_BIT_RATE

音视频平均比特率,以位/秒为单位(bit/s)的键

MediaFormat.KEY_CHANNEL_COUNT

音频通道数的键

MediaFormat.KEY_COLOR_FORMAT

输入视频源的颜色格式,日常开发中可根据查询设备颜色格式支持进行选择

MediaFormat.KEY_FRAME_RATE

视频帧速率的键,以帧/秒(frame/s)为单位

MediaFormat.KEY_I_FRAME_INTERVAL

关键帧间隔的键

MediaFormat.KEY_MAX_INPUT_SIZE

编解码器中数据缓冲区最大大小的键,以字节(byte)为单位

四、AAC音频硬解码

本地音视频文件里的AAC音频硬解码介绍,MediaExtractor方法详解

解析本地音视频文件里的AAC音频,需要我们借助一些MediaCodec之外的API即MediaExtractor,如果不熟悉或之前没使用过,没关系!作者会在本篇文章中做一个详细的概述,帮助你加深印象

setDataSource(String path)

设置音视频文件的绝对路径或音视频文件的http地址,path参数可以是本地音视频文件的绝对路径或网络上的音视频文件http地址

getTrackCount()

获取音视频数据中的轨道数,正常情况下的音视频有audio/xxx及video/xxx

getTrackFormat(int index)

获取音视频数据中音频或视频的 android.media.MediaFormat,这个很重要后面还会有代码示例来介绍,index参数为音频或视频数据轨道的索引,返回值是 android.media.MediaFormat

selectTrack(int index)

选择要extract的数据轨道,index参数为指定的音频或视频轨道的索引,后面也是会通过代码示例详细介绍

readSampleData(ByteBuffer byteBuf, int offset)

读取音频或视频轨道中的数据到给定的 ByteBuffer 缓冲区中,byteBuf参数为要保存数据的目标缓冲区,offset参数为音频或视频的数据起始偏移量,返回值为int类型,大于0表示还有数据未处理完,否则表示数据已经全部处理完成

getSampleTime()

获取该帧音频或视频的的时间戳即PTS,返回值为long类型,以微秒(us)为单位,如无可用返回-1

advance()

此方法表示开始处理下一帧音频或视频,如果还有数据返回true,已无数据则返回false

release()

释放资源,在 advance() 返回 false或中断read操作后使用,表示数据处理完毕或不再读取数据

实时AAC音频硬解码介绍

实时AAC音频硬解码其实跟本地音视频AAC音频硬解码大同小异,唯一差异就是实时的不需要去使用MediaExtractor进行音频轨与视频轨进行分离,可以直接使用MediaCodec进行音频硬解码,但需要解析实时流里的ADTS音频头,否则MediaCodec解码器是无法识别出该数据源是否是AAC音频。正常情况下需要开发者解析ADTS头中的一些关键信息,如采样率索引(可根据采样率进行换算)、通道数。

下面作者就给大家介绍关于ADTS头的解析及ADTS其他位的意义:

ADTS头的解析及ADTS其他位的意义

ADTS头结构:
AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)

序号字段位数含义
Asyncword12AAC音频头固定起始标记
BMPEG Version10是MPEG-4,1是MPEG-2
CLayer2always 0
Dprotection absent1没有CRC设置为1,如果有CRC则设置为0
Eprofile2the MPEG-4 Audio Object Type minus 1
FSampling Frequency Index4MPEG-4 Sampling Frequency Index(15 is forbidden)
Gprivate bit1编码时设置为0,解码时忽略
HChannel Configuration3MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
Ioriginality1编码时设置为0,解码时忽略
Jhome1编码时设置为0,解码时忽略
Kcopyrighted id bit1编码时设置为0,解码时忽略
Lcopyright id start1编码时设置为0,解码时忽略
Mframe length13此值必须包含7或9个字节的标头长度:FrameLength = header(protection absent == 1?7:9)+ size(AAC Frame Length)
OBuffer fullness11No Description
Prdbs2ADTS帧中的AAC帧数(RDBs)减去1,为获得最大兼容性,请始终为每个ADTS帧使用1个AAC帧
Qcrc16如果 protection absent 字段为0,表示携带CRC 2字节的数据

在实时AAC音频硬解码时,我们只需要解析采样率索引(可根据采样率进行换算)、通道数即可,音频采样率索引见MPEG-4 Sampling Frequency Index,接下来还会向各位介绍更重要的音视频参数

五、音视频编解码的CSD参数

音频编解码的CSD参数介绍

在Android中如果调用麦克风进行录音,结合视频使用MediaMuxer进行音视频合成时,是需要开发者传入CSD参数的,否则Android在播放或展示时会出现不识别等其他问题,所以需要开发者在编解码时需要调用MediaFormat设置CSD参数

在音频编解码中,CSD参数只需要设置一个,那就是csd-0即ADTS音频头,在解析本地音视频中的AAC音频时,开发者可以调用MediaFormat取到这个csd-0参数对应的ADTS音频头,然后进行后续的其他操作,后续代码示例还会再次介绍。如果解析的是实时AAC音频,那就需要参照第四步骤对ADTS头进行解析,然后计算CSD参数并设置到MediaFormat中,然后配置到MediaCodec中进行解码,具体算法将在后面的代码示例中提到

视频编解码的CSD参数介绍

在Android中如果调用摄像头进行录像,结合音频使用MediaMuxer进行音视频合成时,是需要开发者传入CSD参数的,否则Android在播放或展示时会出现不识别等其他问题,所以需要开发者在编解码时需要调用MediaFormat设置CSD参数

在视频编解码中,CSD参数需要设置2个,那就是csd-0csd-1sps视频头和pps视频头,在解码本地h264编码视频时可以调用MediaFormat获取sps视频头和pps视频头,减少sps/pps视频头运算和查找的操作,简单快捷且高效!具体使用会在代码示例中再次提及

六、代码示例

本地音视频文件中的AAC音频硬解码

    /**
     * set decode file path
     *
     * @param decodeFilePath decode file path
     */
    public void setDecodeFilePath(String decodeFilePath) {
        if (TextUtils.isEmpty(decodeFilePath)) {
            throw new RuntimeException("decode file path must not be null!");
        }
        mediaExtractor = getMediaExtractor(decodeFilePath);
    }

上述代码片段为设置一个需要解码的文件的绝对路径,路径为null时抛出一个运行时异常,提示路径不能为null,然后就是获取MediaExtractor对象,为提取音频做准备

    /**
     * get media extractor
     *
     * @param videoPath need extract of tht video file absolute path
     * @return {@link MediaExtractor} media extractor instance object
     * @throws IOException
     */
    protected MediaExtractor getMediaExtractor(String videoPath) {
        MediaExtractor mMediaExtractor = new MediaExtractor();
        try {
            // set file path
            mMediaExtractor.setDataSource(videoPath);
            // get source file track count
            int trackCount = mMediaExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                // get current media track media format
                MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(i);
                // if media format object not be null
                if (mediaFormat != null) {
                    // get media mime type
                    String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
                    // media mime type match audio
                    if (mimeType.startsWith(AUDIO_MIME_TYE)) {
                        // set media track is audio
                        mMediaExtractor.selectTrack(i);
                        // you can using media format object call getByteBuffer method and input key "csd-0" get it value , if you want.
                        // it is aac adts audio header.
                        adtsAudioHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_0).array();
                        // get audio sample
                        sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                        // get audio channel count
                        channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                        return mMediaExtractor;
                    }
                    // >>>>>>>>>>> expand start >>>>>>>>>>>
                    // media mime type match video
                    // else if (mimeType.startsWith(VIDEO_MIME_TYE)) {
                    // get video sps header
                    // byte[] spsVideoHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_0).array();
                    // get video pps header
                    // byte[] ppsVideoHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_1).array();
                    // }
                    // <<<<<<<<<<< expand end <<<<<<<<<<<
                }
            }
        } catch (IOException e) {
            Log.d(TAG, "happened io exception : " + e.toString());
            if (mMediaExtractor != null) {
                mMediaExtractor.release();
            }
        }
        return null;
    }

上述代码片段为获取MediaExtractor对象,在设置文件路径后调用其getTrackCount()方法获取文件的所有轨道数,再使用for循环去逐一匹配我们需要的媒体源轨道,调用其getTrackFormat(int index)方法获取该轨道的MediaFormat,最后再去匹配该轨道MediaFormat的mime type,如果匹配到其mime type以关注的mime type字符开始时,获取其csd-0参数的值(音频中对应ADTS头)、采样率、通道数并调用selectTrack(int index)方法将该轨道设置为选定的轨道。

视频相关的参数获取也在代码片段中的expand范围内给出,大家可以了解一下,作者也将其添加上来了,只不过是在代码中注释了,为的就是给大家拓展一下这方面的知识

    @Override
    public void start() {
        if (mediaExtractor == null) {
            Log.e(TAG, "media extractor is null , so return!");
            return;
        }
        if (adtsAudioHeader == null || adtsAudioHeader.length == 0) {
            Log.e(TAG, "aac audio adts header is null , so return!");
            return;
        }
        aacDecoder = createDefaultDecoder();
        if (aacDecoder == null) {
            Log.e(TAG, "aac audio decoder is null , so return!");
            return;
        }
        if (worker == null) {
            isDecoding = true;
            worker = new Thread(this, TAG);
            worker.start();
        }
    }

上述代码片段为准备开始提取AAC音频并进行MediaCodec硬解码,首先判断前面代码片段中MediaExtractor对象是否为空,完事在判断获取轨道时的ADTS头是否正常取到,最后生成一个AAC音频解码器,如果生成无异常,开启一个工作线程进行音频的提取和解码

    /**
     * create default aac decoder
     *
     * @return {@link MediaCodec} aac audio decoder
     */
    private MediaCodec createDefaultDecoder() {
        try {
            MediaFormat mediaFormat = new MediaFormat();
            mediaFormat.setString(MediaFormat.KEY_MIME, AUDIO_DECODE_MIME_TYPE);
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount);
            ByteBuffer byteBuffer = ByteBuffer.allocate(adtsAudioHeader.length);
            byteBuffer.put(adtsAudioHeader);
            byteBuffer.flip();
            mediaFormat.setByteBuffer(CSD_MIME_TYPE_0, byteBuffer);
            MediaCodec aacDecoder = MediaCodec.createDecoderByType(AUDIO_DECODE_MIME_TYPE);
            aacDecoder.configure(mediaFormat, null, null, 0);
            aacDecoder.start();
            return aacDecoder;
        } catch (IOException e) {
            Log.e(TAG, "create aac audio decoder happened io exception : " + e.toString());
        }
        return null;
    }

上述代码片段为创建音频解码器,sampleRatechannelCountadtsAudioHeader都是前面代码片段中通过MediaExtractor从文件的媒体轨道中的MediaFormat获取的

    /**
     * aac audio format decode to pcm audi format
     */
    private void aacDecodeToPcm() {
        isLowVersion = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP;
        ByteBuffer[] aacDecodeInputBuffers = null;
        ByteBuffer[] aacDecodeOutputBuffers = null;
        if (isLowVersion) {
            aacDecodeInputBuffers = aacDecoder.getInputBuffers();
            aacDecodeOutputBuffers = aacDecoder.getOutputBuffers();
        }
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

        // initialization audio track , use for play pcm audio data
        // audio output channel param channelConfig according device support select
        int buffsize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
        AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
                AudioFormat.ENCODING_PCM_16BIT, buffsize, AudioTrack.MODE_STREAM);
        audioTrack.play();

        Log.d(TAG, "aac audio decode thread start");
        while (isDecoding) {
            // This method will return immediately if timeoutUs == 0
            // wait indefinitely for the availability of an input buffer if timeoutUs < 0
            // wait up to "timeoutUs" microseconds if timeoutUs > 0.
            int aacDecodeInputBuffersIndex = aacDecoder.dequeueInputBuffer(2000);
            // no such buffer is currently available , if aacDecodeInputBuffersIndex is -1
            if (aacDecodeInputBuffersIndex >= 0) {
                ByteBuffer sampleDataBuffer;
                if (isLowVersion) {
                    sampleDataBuffer = aacDecodeInputBuffers[aacDecodeInputBuffersIndex];
                } else {
                    sampleDataBuffer = aacDecoder.getInputBuffer(aacDecodeInputBuffersIndex);
                }
                int sampleDataSize = mediaExtractor.readSampleData(sampleDataBuffer, 0);
                if (sampleDataSize < 0) {
                    aacDecoder.queueInputBuffer(aacDecodeInputBuffersIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                } else {
                    try {
                        long presentationTimeUs = mediaExtractor.getSampleTime();
                        aacDecoder.queueInputBuffer(aacDecodeInputBuffersIndex, 0, sampleDataSize, presentationTimeUs, 0);
                        mediaExtractor.advance();
                    } catch (Exception e) {
                        Log.e(TAG, "aac decode to pcm happened Exception : " + e.toString());
                        continue;
                    }
                }

                int aacDecodeOutputBuffersIndex = aacDecoder.dequeueOutputBuffer(info, 2000);
                if (aacDecodeOutputBuffersIndex >= 0) {
                    if (((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)) {
                        Log.d(TAG, "aac decode thread read sample data done!");
                        break;
                    } else {
                        ByteBuffer pcmOutputBuffer;
                        if (isLowVersion) {
                            pcmOutputBuffer = aacDecodeOutputBuffers[aacDecodeOutputBuffersIndex];
                        } else {
                            pcmOutputBuffer = aacDecoder.getOutputBuffer(aacDecodeOutputBuffersIndex);
                        }
                        ByteBuffer copyBuffer = ByteBuffer.allocate(pcmOutputBuffer.remaining());
                        copyBuffer.put(pcmOutputBuffer);
                        copyBuffer.flip();

                        final byte[] pcm = new byte[info.size];
                        copyBuffer.get(pcm);
                        copyBuffer.clear();
                        audioTrack.write(pcm, 0, info.size);
                        aacDecoder.releaseOutputBuffer(aacDecodeOutputBuffersIndex, false);
                    }
                }
            }
        }
        Log.d(TAG, "aac audio decode thread stop");

        aacDecoder.stop();
        aacDecoder.release();
        aacDecoder = null;
        mediaExtractor.release();
        mediaExtractor = null;
        isDecoding = false;
        worker = null;
    }

上述代码片段稍长,作者就做一个简单的概括吧,先获取MediaCodec的输入输出缓冲区数组,然后读取文件的音频轨道数据填充到可用的输入缓冲区中,在进行音频的硬解码,最后从解码成功后存放的输出缓冲区数组中拿到解码后的PCM数据,通过AudioTrack播放出来,这个播放动作是为了验证解码出来的数据是否有异常

实时AAC音频文件的硬解码

实时的解码先不上代码,而是先帮助大家理解,我们需要怎么去解析AAC音频的ADTS头?要取哪些对我们有用的字节?别急,作者会细细的说

FF F1 6C 40 18 02 3C

上述数据是作者从实际项目开发中提取出来的AAC实时流的ADTS音频头,想通过这样的方式来解答之前提到的两个问题,这段字符表示7个16进制的字节,将其进行补全则为如下数据:

0xFF 0xF1 0x6C 0x40 0x18 0x02 0x3C

根据最前面提到的ADTS头结构可知,我们只需要关注7个字节中的前面个4字节,也就是0~3字节即可并取出其对应位的值用于生成解码器,所以我们只需要关心如下数据:

0xFF 0xF1 0x6C 0x40

然后接下来一一解析给大家看,首先是0xFF 0xF1

        // 解析 0xFF 0xF1
        // 将第0字节0xFF和第1字节0xF1通过位运算,将其转换成int类型,即65521
        // 再将65521 转换成二进制类型,即 1111111111110001

        // syncword : 111111111111 (即固定0xfff) 12位
        // MPEG Version: 0 (表 MPEG-4) 1位
        // Layer: 00 (固定 0) 2位
        // protection absent : 1 (表无CRC数据) 1位

接下来再解析0x6C 0x40,计算到前面低10位就行了,后续位用不上

        // 解析 0x6C 0x40
        // 将第2字节0x6C和第3字节0x40通过位运算,将其转换成int类型,即27712
        // 再将27712 转换成二进制类型,即 110110001000000,因不足16位,所以在高位补0,满足16位,补足后 0110110001000000

        // profile : 01 (aac profile) 2位
        // Sampling Frequency Index : 1011 (值为11,即采样率8000) 4位
        // private bit :0 (编码时设为0 ,解码可忽略) 1位
        // Channel Configuration : 001 (通道参数) 3位
        // ......

结合作者刚刚举例的案例,也可以自己写出ADTS音频头解析,下面作者开始贴实时AAC音频硬解码的代码片段

    @Override
    public void start() {
        if (worker == null) {
            isDecoding = true;
            waitTimeSum = 0;
            worker = new Thread(this, TAG);
            worker.start();
        }
    }

上述代码片段为启动工作线程,开始进行MediaCodec硬解码操作

    @Override
    public void run() {
        final long timeOut = 5 * 1000;
        final long waitTime = 500;
        while (isDecoding) {
            while (!aacFrameQueue.isEmpty()) {
                byte[] aac = aacFrameQueue.poll();
                if (aac != null) {
                    if (!hasAacDecoder(aac)) {
                        Log.d(TAG, "aac decoder create failure , so break!");
                        break;
                    }
                    // todo decode aac audio data.
                    // remove aac audio adts header
                    byte[] aacTemp = new byte[aac.length - 7];
                    // data copy
                    System.arraycopy(aac, 7, aacTemp, 0, aacTemp.length);
                    // decode aac audio
                    decode(aacTemp, aacTemp.length);
                }
            }
            // Waiting for next frame
            synchronized (decodeLock) {
                try {
                    // isEmpty() may take some time, so we set timeout to detect next frame
                    decodeLock.wait(waitTime);
                    waitTimeSum += waitTime;
                    if (waitTimeSum >= timeOut) {
                        Log.d(TAG, "realtime aac decode thread read timeout , so break!");
                        break;
                    }
                } catch (InterruptedException ie) {
                    worker.interrupt();
                }
            }
        }
        Log.d(TAG, "realtime aac decode thread stop!");
        if (aacDecoder != null) {
            aacDecoder.stop();
            aacDecoder.release();
            aacDecoder = null;
        }
        if (audioTrack != null) {
            audioTrack.stop();
            audioTrack.release();
            audioTrack = null;
        }
        aacFrameQueue.clear();
        adtsAudioHeader = null;
        isDecoding = false;
        worker = null;
    }

上述代码片段为线程执行解码,判断AAC队列中是否有数据,如果有就取出一个数据,先判空然后再检测AAC的ADTS音频头是否符合规范,如不符合或创建解码器发生异常都将直接退出循环结束线程工作,如果队列中没有数据则等待500ms,继续轮询队列里的数据,当线程工作结束,释放相关API的资源,任何时候都要对相关的一些创建操作进行回收且形成闭环,避免发生内存泄漏

    /**
     * put realtime aac audio data
     *
     * @param aac aac audio data
     */
    public void putAacData(byte[] aac) {
        if (isDecoding) {
            aacFrameQueue.add(aac);
            synchronized (decodeLock) {
                waitTimeSum = 0;
                decodeLock.notifyAll();
            }
        }
    }

上述代码为添加AAC实时数据到缓存队列中,这个数据可以是来自TCP等的实时流媒体数据,如果当前解码工作线程正在解码,则添加一个AAC到缓存队列中,重置等待时间并且唤醒等待中的对象锁,让线程拿到锁后继续执行

    /**
     * @param aac aac audio data
     * @return true means has aad decoder
     */
    private boolean hasAacDecoder(byte[] aac) {
        if (aacDecoder != null) {
            return true;
        }
        return checkAacAdtsHeader(aac);
    }

上述代码片段为校验AAC的ADTS音频头,如果accDecoder非空表示之前已经判断过,该AAC数据为正常AAC数据,这里不考虑极端情况,AAC音频混搭其他格式的音频,这样会导致播放出问题,正常情况交互下也不会这样干!如果accDecoder为空则先对ADTS进行一次校验

    /**
     * check aac adts audio header
     *
     * @param aac aac audio data
     */
    private boolean checkAacAdtsHeader(byte[] aac) {
        byte[] dtsFixedHeader = new byte[2];
        System.arraycopy(aac, 0, dtsFixedHeader, 0, dtsFixedHeader.length);
        int bitMoveValue = dtsFixedHeader.length * 8 - ADTS_HEADER_START_FLAG_BIT_SIZE;
        int adtsFixedHeaderValue = bytesToInt(dtsFixedHeader);
        int syncwordValue = ADTS_HEADER_START_FLAG << bitMoveValue;
        boolean isAdtsHeader = (adtsFixedHeaderValue & syncwordValue) >> bitMoveValue == ADTS_HEADER_START_FLAG;
        if (!isAdtsHeader) {
            Log.e(TAG, "adts header start flag not match , so return!");
            return false;
        }
        System.arraycopy(aac, 2, dtsFixedHeader, 0, dtsFixedHeader.length);
        return parseAdtsHeaderKeyData(dtsFixedHeader);
    }

上述代码片段为取出AAC音频中的第0、1两个字节,因为short双字节转换成Int不会造成精度丢失,先进行数据拷贝,完后计算数据bit的左右移动值,然后将第0、1两个字节转换成int类型,经过位运算得到ADTS的syncword即AAC的ADTS固定标识,如匹配不上表示不是AAC数据直接return,否则接着处理第2、3两个字节然后将其拷贝到数组中,接着再进行ADTS音频头的关键数据的解析

    /**
     * parse adts header key byte array data
     *
     * @param adtsHeaderValue adts fixed header byte array
     */
    private boolean parseAdtsHeaderKeyData(byte[] adtsHeaderValue) {
        int adtsFixedHeaderValue = bytesToInt(adtsHeaderValue);

        // bitMoveValue = 16(2 * 8) - 2(aac profile 3bit)
        int bitMoveValue = adtsHeaderValue.length * 8 - ADTS_HEADER_PROFILE_BIT_SIZE;
        // profile : 01 (aac profile) 2 bit
        int audioProfile = adtsFixedHeaderValue & (ADTS_HEADER_PROFILE_FLAG << bitMoveValue);
        // 1: AAC Main -- MediaCodecInfo.CodecProfileLevel.AACObjectMain
        // 2: AAC LC (Low Complexity)  -- MediaCodecInfo.CodecProfileLevel.AACObjectLC
        // 3: AAC SSR (Scalable Sample Rate) -- MediaCodecInfo.CodecProfileLevel.AACObjectSSR
        audioProfile = audioProfile >> bitMoveValue;

        // bitMoveValue = 16(2 * 8) - 2(aac profile 3bit) - 4(Sampling Frequency Index 4 bit)
        bitMoveValue -= ADTS_HEADER_SAMPLE_INDEX_BIT_SIZE;
        // Sampling Frequency Index : 1011 (value is 11,sample rate 8000) 4 bit
        int sampleIndex = adtsFixedHeaderValue & (ADTS_HEADER_SAMPLE_INDEX_FLAG << bitMoveValue);
        sampleIndex = sampleIndex >> bitMoveValue;
        sampleRate = samplingFrequencys[sampleIndex];

        // private bit :0 (encoding set 0 ,decoding ignore) 1 bit
        // Channel Configuration : 001 (Channel Configuration) 3 bit
        // bitMoveValue = bitMoveValue - 1(private bit 1bit) +  3(Channel Configuration 3bit)
        bitMoveValue -= (1 + ADTS_HEADER_CHANNEL_CONFIG_BIT_SIZE);
        channelConfig = adtsFixedHeaderValue & (ADTS_HEADER_SAMPLE_INDEX_FLAG << bitMoveValue);
        channelConfig = channelConfig >> bitMoveValue;
        // ......
        // create csd-0(audio adts header)
        adtsAudioHeader = new byte[2];
        adtsAudioHeader[0] = (byte) ((audioProfile << 3) | (sampleIndex >> 1));
        adtsAudioHeader[1] = (byte) ((byte) ((sampleIndex << 7) & 0x80) | (channelConfig << 3));

        Log.d(TAG, "audioProfile = " + audioProfile + " , sampleIndex = " + sampleIndex + "(" + sampleRate + ")" + " , channelConfig = " + channelConfig
                + " , audio csd-0 = " + Utils.bytesToHexStringNo0xChar(adtsAudioHeader));

        return createDefaultDecoder();
    }

上述代码片段为先将前面取到的AAC的第2、3字节转换成int类型,接着计算数据位的位移值,然后分别计算audioProfilesampleIndexsampleRatechannelConfig,最后再根据这些参数中的部分参数进行音频csd-0配置头的计算,如果一切正常,最后会调用createDefaultDecoder方法进行解码器的创建

    /**
     * create default decoder
     */
    private boolean createDefaultDecoder() {
        if (adtsAudioHeader == null || adtsAudioHeader.length == 0) {
            Log.e(TAG, "realtime aac decoder create failure , adts audio header is null , so return false!");
            return false;
        }
        try {
            aacDecoder = MediaCodec.createDecoderByType(AUDIO_DECODE_MIME_TYPE);
            MediaFormat mediaFormat = new MediaFormat();
            mediaFormat.setString(MediaFormat.KEY_MIME, AUDIO_DECODE_MIME_TYPE);
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelConfig);
            ByteBuffer byteBuffer = ByteBuffer.allocate(adtsAudioHeader.length);
            byteBuffer.put(adtsAudioHeader);
            byteBuffer.flip();
            mediaFormat.setByteBuffer(CSD_MIME_TYPE_0, byteBuffer);
            aacDecoder.configure(mediaFormat, null, null, 0);
        } catch (IOException e) {
            Log.e(TAG, "realtime aac decoder create failure , happened exception : " + e.toString());
            if (aacDecoder != null) {
                aacDecoder.stop();
                aacDecoder.release();
            }
            aacDecoder = null;
        }
        if (aacDecoder == null) {
            return false;
        }
        // initialization audio track , use for play pcm audio data
        // audio output channel param channelConfig according device support select
        int buffsize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
        // author using channelConfig is AudioFormat.CHANNEL_OUT_MONO
        audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
                AudioFormat.ENCODING_PCM_16BIT, buffsize, AudioTrack.MODE_STREAM);
        audioTrack.play();
        aacDecoder.start();
        return true;
    }

上述代码片段为如果前面代码的音频csd-0配置头不合法则直接return,接着就是把之前步骤解析AAC音频头得到的参数设置到解码器当中,如发生异常则return false,否则创建一个AudioTrack进行解码音频数据后的播放,验证数据是否正常被解析

    /**
     * aac audio data decode
     *
     * @param buf    aac audio data
     * @param length aac audio data length
     */
    private void decode(byte[] buf, int length) {
        try {
            ByteBuffer[] codecInputBuffers = aacDecoder.getInputBuffers();
            ByteBuffer[] codecOutputBuffers = aacDecoder.getOutputBuffers();
            long kTimeOutUs = 0;
            int inputBufIndex = aacDecoder.dequeueInputBuffer(kTimeOutUs);
            if (inputBufIndex >= 0) {
                ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
                dstBuf.clear();
                dstBuf.put(buf, 0, length);
                aacDecoder.queueInputBuffer(inputBufIndex, 0, length, 0, 0);
            }
            ByteBuffer outputBuffer;
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            int outputBufferIndex = aacDecoder.dequeueOutputBuffer(info, kTimeOutUs);
            while (outputBufferIndex >= 0) {
                outputBuffer = codecOutputBuffers[outputBufferIndex];
                byte[] outData = new byte[info.size];
                outputBuffer.get(outData);
                outputBuffer.clear();
                if (audioTrack != null) {
                    audioTrack.write(outData, 0, info.size);
                }
                aacDecoder.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = aacDecoder.dequeueOutputBuffer(info, kTimeOutUs);
            }
        } catch (Exception e) {
            Log.e(TAG, "realtime aac decode happened exception : " + e.toString());
        }
    }

最后代码片段就是介绍MediaCodec的硬解码,这里就大概描述一下,因为跟本地音视频解码是一样的流程了,首先是获取输入输出缓冲区数组,然后将要解码的AAC音频数据填充到可用的输入缓冲区中,具体那个输入缓冲区可用,可以调用dequeueInputBuffer方法,该方法返回值大于等于0时表示该返回值对应的输入缓冲区数组的索引,然后就是解码了,从输出缓冲区数组中取得已经解码成功的输出缓冲区,可以调dequeueOutputBuffer方法获取它的有效索引,最后就是取出解码后的PCM数据进行播放,播放完毕后释放索引对应的输出缓冲区

七、Demo地址

AacDecoder

感谢信,致热爱编程的你

感谢各位粉丝、看管老爷们一直以来的支持和厚爱!以后作者出博客只出精品只出对大家有用的干货!让你在看博客的同时也能一起思考问题,从而达到边读博客边提升自身的知识软实力!在即将到来的新年,作者在此给你们提前拜一个早年了,祝大家新年快乐,完事如果,工作如意,身体健康!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Engineer-Jsp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值