写点什么

Android AAC 音频硬编解码你知道多少?

用户头像
Engineer-Jsp
关注
发布于: 2021 年 04 月 02 日

一、使用 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.BUFFERFLAGEND_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)


AAC音频头构成


在实时 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-1 sps 视频头和 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 数据进行播放,播放完毕后释放索引对应的输出缓冲区

感谢信,致热爱编程的你

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


发布于: 2021 年 04 月 02 日阅读数: 58
用户头像

Engineer-Jsp

关注

还未添加个人签名 2021.03.30 加入

还未添加个人简介

评论

发布
暂无评论
Android AAC音频硬编解码你知道多少?