Android音视频开发入门(八)

任务目标

学习 MediaCodec API,完成音频 AAC 硬编、硬解

MediaCodec API

MediaCodec API的学习在之前一篇文章已经记录,请参考这儿,虽然翻译的不太好,但是一定要结合英文去认真看一下API(英文好的就不要听我说了)。

原理介绍

本文是以mp3为例,过程:MP3->PCM->AAC。先把MP3文件解码成PCM格式数据,然后编码成AAC格式的音频文件。
注意:此处编码成AAC格式的文件时,要记得加入ADTS文件头,否则无法播放。
关于PCM音频编码的介绍请参考这篇文章
调用逻辑引用Android MediaCodec小结的一个图:
在这里插入图片描述
此图已经很清晰了,下面看主要代码实现。

代码实现
1.初始化解码器
	/**
     * 初始化解码器
     */
    private void initDecoder() {
        try {
            mExtractor = new MediaExtractor();
            mExtractor.setDataSource(mSrcPath);
            int trackCount = mExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                MediaFormat format = mExtractor.getTrackFormat(i);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("audio")) { //获取音频轨道
                    mExtractor.selectTrack(i);
                    key_bit_rate = format.getInteger(MediaFormat.KEY_BIT_RATE);
                    key_channel_count = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                    key_sample_rate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                    //创建解码器
                    mDecodeCodec = MediaCodec.createDecoderByType(mime);
                    //第二个参数是surface,解码视频的时候需要,第三个是MediaCrypto, 是关于加密的,最后一个flag填0即可
                    //configure会使MediaCodec进入Configured state
                    mDecodeCodec.configure(format, null, null, 0);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (mDecodeCodec == null) {
            Log.e("codec", "create mDecodeCodec failed");
            return;
        }
        //启动MediaCodec,等待传入数据
        //调用此方法之后mediaCodec进入Executing state
        mDecodeCodec.start();
        //MediaCodec在此ByteBuffer[]中获取输入数据
        mDecoderInputBuffers = mDecodeCodec.getInputBuffers();
        //MediaCodec将解码后的数据放到此ByteBuffer[]中 我们可以直接在这里面得到PCM数据
        mDecoderOutputBuffers = mDecodeCodec.getOutputBuffers();
        //用于描述解码得到的byte[]数据的相关信息
        mDecoderInfo = new MediaCodec.BufferInfo();
    }
2.初始化编码器
	/**
     * 初始化编码器
     */
    private void initEncoder() {
        try {
            //参数对应-> mime type、采样率、声道数
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(mEncodeType, key_sample_rate, key_channel_count);
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, key_bit_rate);
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
            mEncodeCodec = MediaCodec.createEncoderByType(mEncodeType);
            //最后一个参数当使用编码器时设置
            mEncodeCodec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (mEncodeCodec == null) {
            Log.e("codec", "create mEncodeCodec failed");
            return;
        }
        mEncodeCodec.start();

        mEncoderInputBuffers = mEncodeCodec.getInputBuffers();
        mEncoderOutputBuffers = mEncodeCodec.getOutputBuffers();
        mEncoderInfo = new MediaCodec.BufferInfo();
    }
3.音频解码PCM数据
	/**
     * 解码音频文件,得到PCM数据
     */
    private void audioToPCM() {
        for (int i = 0; i < mDecoderInputBuffers.length - 1; i++) {
            //获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
            int inputIndex = mDecodeCodec.dequeueInputBuffer(-1);
            if (inputIndex < 0) {
                mCodecOver = true;
                return;
            }
            ByteBuffer inputBuffer = mDecoderInputBuffers[inputIndex];//拿到inputBuffer
            inputBuffer.clear();//清空之前传入inputBuffer内的数据
            //MediaExtractor读取数据到inputBuffer中
            int sampleSize = mExtractor.readSampleData(inputBuffer, 0);
            if (sampleSize < 0) {//小于0 代表所有数据已读取完成
                mCodecOver = true;
            } else {
                //通知MediaDecode解码刚刚传入的数据
                mDecodeCodec.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);
                mExtractor.advance();//移动到下一帧
                mDecodeSize += sampleSize;
            }
        }
        //获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
        //此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
        int outputIndex = mDecodeCodec.dequeueOutputBuffer(mDecoderInfo, 10000);

        ByteBuffer outputBuffer;
        byte[] chunkPCM;
        while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
            outputBuffer = mDecoderOutputBuffers[outputIndex];
            //BufferInfo内定义了此数据块的大小
            chunkPCM = new byte[mDecoderInfo.size];
            //将Buffer内的数据取出到字节数组中
            outputBuffer.get(chunkPCM);
            //数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
            outputBuffer.clear();
            //自己定义的方法,供编码器所在的线程获取数据
            putPCMData(chunkPCM);
            //此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
            mDecodeCodec.releaseOutputBuffer(outputIndex, false);
            //再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
            outputIndex = mDecodeCodec.dequeueOutputBuffer(mDecoderInfo, 10000);
        }
    }
4.编码PCM数据成AAC
	/**
     * 编码PCM数据,得到{@link #mEncodeType}格式的音频文件,并保存到{@link #mDstPath}
     */
    private void pcmToTargetAudio() {
        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;
        byte[] chunkAudio;
        int outBitSize;
        int outPacketSize;
        byte[] chunkPCM;

        for (int i = 0; i < mEncoderInputBuffers.length - 1; i++) {
            chunkPCM = getPCMData();//获取解码器所在线程输出的数据
            if (chunkPCM == null) {
                break;
            }
            //以下操作同解码器
            inputIndex = mEncodeCodec.dequeueInputBuffer(-1);
            inputBuffer = mEncoderInputBuffers[inputIndex];
            inputBuffer.clear();
            inputBuffer.limit(chunkPCM.length);
            //PCM数据填充给inputBuffer
            inputBuffer.put(chunkPCM);
            //通知编码器 编码
            mEncodeCodec.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);
        }

        outputIndex = mEncodeCodec.dequeueOutputBuffer(mEncoderInfo, 10000);
        while (outputIndex > 0) {
            outBitSize = mEncoderInfo.size;
            outPacketSize = outBitSize + 7; //7为ADTS头部的大小
            //拿到输出Buffer
            outputBuffer = mEncoderOutputBuffers[outputIndex];
            outputBuffer.position(mEncoderInfo.offset);
            outputBuffer.limit(mEncoderInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            //添加ADTS
            addADTStoPacket(chunkAudio, outPacketSize);
            //将编码得到的AAC数据 取出到byte[]中 偏移量offset=7
            outputBuffer.get(chunkAudio, 7, outBitSize);
            outputBuffer.position(mEncoderInfo.offset);
            try {
                //BufferOutputStream 将文件保存到内存卡中 *.aac
                mOutputStream.write(chunkAudio, 0, chunkAudio.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mEncodeCodec.releaseOutputBuffer(outputIndex, false);
            outputIndex = mEncodeCodec.dequeueOutputBuffer(mEncoderInfo, 10000);
        }
    }

注意: 在Android 21之后getInputBuffers()、getOutputBuffers()方法已标记过时,推荐使用getInputBuffer(int)、getOutputBuffers(int)。但是我并没有找到什么好的方式这样使用,可能异步方式是一种可能的方式。

关于异步调用可以参考这个示例

关于ADTS格式

请参考【多媒体封装格式详解】— AAC ADTS格式分析

参考

android MediaCodec 音频编解码的实现——转码

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值