美文网首页android源码androidAndroid 开发
Android MediaCodec硬解码AAC音频文件(实时A

Android MediaCodec硬解码AAC音频文件(实时A

作者: BlackMagician | 来源:发表于2017-05-17 13:39 被阅读900次

    今天在这里简单介绍一下,如何利用android MediaCodec解码AAC音频文件或者实时AAC音频帧并通过AudioTrack来播放。主要的思路就是从文件或者网络获取一帧帧的AAC的数据,送入解码器解码后播放。

    封装AudioTrack

    AudioTrack主要是用来进行主要是用来播放声音的,但是只能播放PCM格式的音频流。这里主要是简单的对AudioTrack进行了封装,加入了一些异常判断:

    /**
     * Created by ZhangHao on 2017/5/10.
     * 播放pcm数据
     */
    public class MyAudioTrack {
        private int mFrequency;// 采样率
        private int mChannel;// 声道
        private int mSampBit;// 采样精度
        private AudioTrack mAudioTrack;
    
        public MyAudioTrack(int frequency, int channel, int sampbit) {
            this.mFrequency = frequency;
            this.mChannel = channel;
            this.mSampBit = sampbit;
        }
    
        /**
         * 初始化
         */
        public void init() {
            if (mAudioTrack != null) {
                release();
            }
            // 获得构建对象的最小缓冲区大小
            int minBufSize = getMinBufferSize();
            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                    mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM);
            mAudioTrack.play();
        }
    
        /**
         * 释放资源
         */
        public void release() {
            if (mAudioTrack != null) {
                mAudioTrack.stop();
                mAudioTrack.release();
            }
        }
    
        /**
         * 将解码后的pcm数据写入audioTrack播放
         *
         * @param data   数据
         * @param offset 偏移
         * @param length 需要播放的长度
         */
        public void playAudioTrack(byte[] data, int offset, int length) {
            if (data == null || data.length == 0) {
                return;
            }
            try {
                mAudioTrack.write(data, offset, length);
            } catch (Exception e) {
                Log.e("MyAudioTrack", "AudioTrack Exception : " + e.toString());
            }
        }
    
        public int getMinBufferSize() {
            return AudioTrack.getMinBufferSize(mFrequency,
                    mChannel, mSampBit);
        }
    }
    

    这里简单介绍一下,在AudioTrack构造方法AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)里几个变量的含义:
    1.streamType:指定流的类型,主要包括以下几种:

    • STREAM_ALARM:警告声
    • STREAM_MUSCI:音乐声
    • STREAM_RING:铃声
    • STREAM_SYSTEM:系统声音
    • STREAM_VOCIE_CALL:电话声音
      因为android系统对不同的声音的管理是分开的,所以这个参数的作用就是设置AudioTrack播放的声音类型。

    2.sampleRateInHz : 采样率

    3.channelConfig : 声道

    4.audioFormat : 采样精度

    5.bufferSizeInBytes :缓冲区大小,可以通过AudioTrack.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)来获取

    6.mode : MODE_STATIC和MODE_STREAM:

    • MODE_STATIC : 直接把所有的数据加载到缓存区,不需要多次write,一般用于占用内存小,延时要求高的情况

    • MODE_STREAM : 需要多次write,一般用于像从网络获取数据或者实时解码的情况,本次的例子就是这种情况。

      我这里只是简单的介绍,大家可以去网上找更为详细的介绍。

    AAC解码器

    这里主要对MediaCodec进行封装,实现一帧帧去解码AAC。

    /**
     * Created by ZhangHao on 2017/5/17.
     * 用于aac音频解码
     */
    
    public class AACDecoderUtil {
        private static final String TAG = "AACDecoderUtil";
        //声道数
        private static final int KEY_CHANNEL_COUNT = 2;
        //采样率
        private static final int KEY_SAMPLE_RATE = 48000;
        //用于播放解码后的pcm
        private MyAudioTrack mPlayer;
        //解码器
        private MediaCodec mDecoder;
        //用来记录解码失败的帧数
        private int count = 0;
    
        /**
         * 初始化所有变量
         */
        public void start() {
            prepare();
        }
    
        /**
         * 初始化解码器
         *
         * @return 初始化失败返回false,成功返回true
         */
        public boolean prepare() {
            // 初始化AudioTrack
            mPlayer = new MyAudioTrack(KEY_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
            mPlayer.init();
            try {
                //需要解码数据的类型
                String mine = "audio/mp4a-latm";
                //初始化解码器
                mDecoder = MediaCodec.createDecoderByType(mine);
                //MediaFormat用于描述音视频数据的相关参数
                MediaFormat mediaFormat = new MediaFormat();
                //数据类型
                mediaFormat.setString(MediaFormat.KEY_MIME, mine);
                //声道个数
                mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, KEY_CHANNEL_COUNT);
                //采样率
                mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, KEY_SAMPLE_RATE);
                //比特率
                mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
                //用来标记AAC是否有adts头,1->有
                mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);
                //用来标记aac的类型
                mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
                //ByteBuffer key(暂时不了解该参数的含义,但必须设置)
                byte[] data = new byte[]{(byte) 0x11, (byte) 0x90};
                ByteBuffer csd_0 = ByteBuffer.wrap(data);
                mediaFormat.setByteBuffer("csd-0", csd_0);
                //解码器配置
                mDecoder.configure(mediaFormat, null, null, 0);
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            if (mDecoder == null) {
                return false;
            }
            mDecoder.start();
            return true;
        }
    
        /**
         * aac解码+播放
         */
        public void decode(byte[] buf, int offset, int length) {
            //输入ByteBuffer
            ByteBuffer[] codecInputBuffers = mDecoder.getInputBuffers();
            //输出ByteBuffer
            ByteBuffer[] codecOutputBuffers = mDecoder.getOutputBuffers();
            //等待时间,0->不等待,-1->一直等待
            long kTimeOutUs = 0;
            try {
                //返回一个包含有效数据的input buffer的index,-1->不存在
                int inputBufIndex = mDecoder.dequeueInputBuffer(kTimeOutUs);
                if (inputBufIndex >= 0) {
                    //获取当前的ByteBuffer
                    ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
                    //清空ByteBuffer
                    dstBuf.clear();
                    //填充数据
                    dstBuf.put(buf, offset, length);
                    //将指定index的input buffer提交给解码器
                    mDecoder.queueInputBuffer(inputBufIndex, 0, length, 0, 0);
                }
                //编解码器缓冲区
                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                //返回一个output buffer的index,-1->不存在
                int outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);
    
                if (outputBufferIndex < 0) {
                    //记录解码失败的次数
                    count++;
                }
                ByteBuffer outputBuffer;
                while (outputBufferIndex >= 0) {
                    //获取解码后的ByteBuffer
                    outputBuffer = codecOutputBuffers[outputBufferIndex];
                    //用来保存解码后的数据
                    byte[] outData = new byte[info.size];
                    outputBuffer.get(outData);
                    //清空缓存
                    outputBuffer.clear();
                    //播放解码后的数据
                    mPlayer.playAudioTrack(outData, 0, info.size);
                    //释放已经解码的buffer
                    mDecoder.releaseOutputBuffer(outputBufferIndex, false);
                    //解码未解完的数据
                    outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);
                }
            } catch (Exception e) {
                Log.e(TAG, e.toString());
                e.printStackTrace();
            }
        }
    
        //返回解码失败的次数
        public int getCount() {
            return count;
        }
    
        /**
         * 释放资源
         */
        public void stop() {
            try {
                if (mPlayer != null) {
                    mPlayer.release();
                    mPlayer = null;
                }
                if (mDecoder != null) {
                    mDecoder.stop();
                    mDecoder.release();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

    其实这里和我之前利用MediaCodec解码H264很类似,主要就是在因为解码数据类型不同,所以初始化时有区别。还有一点就是解码H624时,直接将解码后数据利用surface显示,而解码aac是将解码后的数据取出来,再利用AudioTrack播放。

    读取aac文件

    这里是利用线程读aac文件,获得一帧帧的aac帧数据,然后送入解码器播放。

    /**
     * Created by ZhangHao on 2017/4/18.
     * 播放aac音频文件
     */
    
    public class ReadAACFileThread extends Thread {
    
        //音频解码器
        private AACDecoderUtil audioUtil;
        //文件路径
        private String filePath;
        //文件读取完成标识
        private boolean isFinish = false;
        //这个值用于找到第一个帧头后,继续寻找第二个帧头,如果解码失败可以尝试缩小这个值
        private int FRAME_MIN_LEN = 50;
        //一般AAC帧大小不超过200k,如果解码失败可以尝试增大这个值
        private static int FRAME_MAX_LEN = 100 * 1024;
        //根据帧率获取的解码每帧需要休眠的时间,根据实际帧率进行操作
        private int PRE_FRAME_TIME = 1000 / 50;
        //记录获取的帧数
        private int count = 0;
    
        public ReadAACFileThread(String path) {
            this.audioUtil = new AACDecoderUtil();
            this.filePath = path;
            this.audioUtil.start();
        }
    
        @Override
        public void run() {
            super.run();
            File file = new File(filePath);
            //判断文件是否存在
            if (file.exists()) {
                try {
                    FileInputStream fis = new FileInputStream(file);
                    //保存完整数据帧
                    byte[] frame = new byte[FRAME_MAX_LEN];
                    //当前帧长度
                    int frameLen = 0;
                    //每次从文件读取的数据
                    byte[] readData = new byte[10 * 1024];
                    //开始时间
                    long startTime = System.currentTimeMillis();
                    //循环读取数据
                    while (!isFinish) {
                        if (fis.available() > 0) {
                            int readLen = fis.read(readData);
                            //当前长度小于最大值
                            if (frameLen + readLen < FRAME_MAX_LEN) {
                                //将readData拷贝到frame
                                System.arraycopy(readData, 0, frame, frameLen, readLen);
                                //修改frameLen
                                frameLen += readLen;
                                //寻找第一个帧头
                                int headFirstIndex = findHead(frame, 0, frameLen);
                                while (headFirstIndex >= 0 && isHead(frame, headFirstIndex)) {
                                    //寻找第二个帧头
                                    int headSecondIndex = findHead(frame, headFirstIndex + FRAME_MIN_LEN, frameLen);
                                    //如果第二个帧头存在,则两个帧头之间的就是一帧完整的数据
                                    if (headSecondIndex > 0 && isHead(frame, headSecondIndex)) {
                                        //视频解码
                                        count++;
                                        Log.e("ReadAACFileThread", "Length : " + (headSecondIndex - headFirstIndex));
                                        audioUtil.decode(frame, headFirstIndex, headSecondIndex - headFirstIndex);
                                        //截取headSecondIndex之后到frame的有效数据,并放到frame最前面
                                        byte[] temp = Arrays.copyOfRange(frame, headSecondIndex, frameLen);
                                        System.arraycopy(temp, 0, frame, 0, temp.length);
                                        //修改frameLen的值
                                        frameLen = temp.length;
                                        //线程休眠
                                        sleepThread(startTime, System.currentTimeMillis());
                                        //重置开始时间
                                        startTime = System.currentTimeMillis();
                                        //继续寻找数据帧
                                        headFirstIndex = findHead(frame, 0, frameLen);
                                    } else {
                                        //找不到第二个帧头
                                        headFirstIndex = -1;
                                    }
                                }
                            } else {
                                //如果长度超过最大值,frameLen置0
                                frameLen = 0;
                            }
                        } else {
                            //文件读取结束
                            isFinish = true;
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                Log.e("ReadAACFileThread", "AllCount:" + count + "Error Count : " + audioUtil.getCount());
            } else {
                Log.e("ReadH264FileThread", "File not found");
            }
            audioUtil.stop();
        }
    
        /**
         * 寻找指定buffer中AAC帧头的开始位置
         *
         * @param startIndex 开始的位置
         * @param data       数据
         * @param max        需要检测的最大值
         * @return
         */
        private int findHead(byte[] data, int startIndex, int max) {
            int i;
            for (i = startIndex; i <= max; i++) {
                //发现帧头
                if (isHead(data, i))
                    break;
            }
            //检测到最大值,未发现帧头
            if (i == max) {
                i = -1;
            }
            return i;
        }
    
        /**
         * 判断aac帧头
         */
        private boolean isHead(byte[] data, int offset) {
            boolean result = false;
            if (data[offset] == (byte) 0xFF && data[offset + 1] == (byte) 0xF1
                    && data[offset + 3] == (byte) 0x80) {
                result = true;
            }
            return result;
        }
    
        //修眠
        private void sleepThread(long startTime, long endTime) {
            //根据读文件和解码耗时,计算需要休眠的时间
            long time = PRE_FRAME_TIME - (endTime - startTime);
            if (time > 0) {
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    这里没有太多的东西,就是通过帧头来判断aac帧,并截取每帧数据送入解码器。我这里只是取巧做了简单的判断,对帧头的判断并不一定满足所有的aac帧头,大家可以根据实际的情况自行修改。

    结语

    其实,实现分离音频帧,利用MediaExtractor这个类就可以实现,但是因为我实际的数据源是来自网络,所以才会demo才会复杂一点。

    相关文章

      网友评论

      • 呼格金贵:网络读取部分可以分享出来吗?
        我也有个需求是从阿里云的oss读取.
        今天搞解码这边也还未做好, 一直报错.

      本文标题:Android MediaCodec硬解码AAC音频文件(实时A

      本文链接:https://www.haomeiwen.com/subject/voftxxtx.html