Android MediaCodec 音频转码——硬编硬解

我本来是做Android的,但是来公司之后主要负责Android端的多媒体相关,很多有关音视频编解码的都没有接触过。刚开始有一个项目使用硬编硬解完成音频的转码,刚开始我连怎么用硬编硬解都不知道,所幸在百度上找到一篇文章android MediaCodec 音频编解码的实现——转码。这篇文章介绍的很好,介绍了硬编硬解的整个流程,也接触了MediaCodec这个用来硬编硬解的类,后来还找到一个很好的学习该类的使用方法的一个网站http://bigflake.com/mediacodec/。

我的需求是将原始的视频文件中的音频转码为amr格式的音频,原始音频主要是aac格式。android MediaCodec 音频编解码的实现——转码这篇文章中是MP3到aac的转换。
原理在上述博客中讲的很清楚了,这里不再重复。

一、初始化解码器

    private void initDecoder(String srcPath) {
        long time = System.currentTimeMillis();
        //private MediaExtractor mediaExtractor;
        mediaExtractor = new MediaExtractor();
        try {
            mediaExtractor.setDataSource(srcPath);
            //遍历媒体轨道,然后选取音频轨道
            for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
                MediaFormat format = mediaExtractor.getTrackFormat(i);
                //获取音频轨道
                String mime = format.getString(MediaFormat.KEY_MIME);
                //public static final String AUDIO = "audio/";
                if (mime.startsWith(AUDIO)) {
                    LogUtils.d(TAG, format.toString());
                    //选择此音频轨道
                    mediaExtractor.selectTrack(i);
                    mediaDecode = MediaCodec.createDecoderByType(mime);
                    //第二个参数是surface,解码视频的时候需要,第三个是MediaCrypto, 是关于加密的,最后一个flag填0即可
                    //configure会使MediaCodec进入Configured state
                    mediaDecode.configure(format, null, null, 0);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (mediaDecode == null) {
            LogUtils.e(TAG, "create mediaDecode failed");
            return;
        }
        //启动MediaCodec,等待传入数据
        //调用此方法之后mediaCodec进入Executing state
        mediaDecode.start();

        //MediaCodec在此ByteBuffer[]中获取输入数据
        decodeInputBuffers = mediaDecode.getInputBuffers();
        decodeOutputBuffers = mediaDecode.getOutputBuffers();
        //用于描述解码得到的byte[]数据的相关信息
        decodeBufferInfo = new MediaCodec.BufferInfo();

        LogUtils.d(TAG, " initial time:" + (System.currentTimeMillis() - time) + " ms");
    }

二、初始化编码器

    private void initEncoder(String outPath) {
        long time = System.currentTimeMillis();
        try {
            //参数对应-> mime type、采样率、声道数
            //public static final String AUDIO_AMR = "audio/3gpp";
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(AUDIO_AMR, 8000, 1);
            //设置比特率,AMR一共有8中比特率
            //public static final int MR795 = 7950;  /* 7.95 kbps */
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, BitRate.MR795);
            //设置nputBuffer的大小
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
            mediaEncode = MediaCodec.createEncoderByType(AUDIO_AMR);
            //最后一个参数当使用编码器时设置
            mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (mediaEncode == null) {
            Log.e(TAG, "create mediaEncode failed");
            return;
        }
        mediaEncode.start();
        encodeInputBuffers = mediaEncode.getInputBuffers();
        encodeOutputBuffers = mediaEncode.getOutputBuffers();

        //用于描述解码得到的byte[]数据的相关信息
        encodeBufferInfo = new MediaCodec.BufferInfo();
        LogUtils.d(TAG, "format:" + mediaEncode.getOutputFormat());
        try {
            fos = new FileOutputStream(new File(outPath));
            bos = new BufferedOutputStream(fos, 10 * 1024);
            //AMR对应的文件头
            byte[] header = new byte[]{0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A};
            bos.write(header);
            LogUtils.d(TAG, "Write head success");
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        LogUtils.d(TAG, " initial time:" + (System.currentTimeMillis() - time) + " ms");
    }

其中,关于AMR文件头的格式以及AMR不同频率时的帧头可以参见这篇博客AMR文件格式分析

三、编解码的流程

//解码的实现
private void srcAudioFormatToPCM() {
        long kTimeOutUs = 1000;
        long time = System.currentTimeMillis();
        while (true) {
            //decodeInputBuffers.length一般为4,可以全部使用为了加速写入数据
            for (int i = 0; i < decodeInputBuffers.length; i++) {
                //获取可用的inputBuffer -1代表一直等待,0表示不等待。以μs为单位
                int inputIndex = mediaDecode.dequeueInputBuffer(kTimeOutUs);
                if (inputIndex < 0) {
                    continue;
                }
                ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];
                // 清空之前传入的数据
                inputBuffer.clear();
                int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
                if (sampleSize < 0) {
                    codeOver = true;
                    mediaDecode.queueInputBuffer(inputIndex, 0, 0, 0, BUFFER_FLAG_END_OF_STREAM);
                } else {
                    // 通知mediaDecode解码刚刚传入的数据
                    //经测试presentationTimeUs不设置没有问题,但是我好像在stackoverflow上看见说如果不设置,会在部分手机上出现问题
                    presentationTimeUs = mediaExtractor.getSampleTime();
                    mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);
                    // MediaExtractor移动到下一个Sample
                    mediaExtractor.advance();
                    decodeSize += sampleSize;
                }
            }
            //获取解码得到的byte[]数据 参数BufferInfo上面已介绍 1000同样为等待时间 同上-1代表一直等待,0代表不等待。
            //此处单位为微秒,此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这等待
            //decodeBufferInfo = new MediaCodec.BufferInfo();
            int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 0);
            LogUtils.d(TAG, "firstOutputIndex: " + outputIndex);
            ByteBuffer outputBuffer;
            byte[] chunkPCM;
            //每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
            if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // Subsequent data will conform to new format.
                decodeOutputBuffers = mediaDecode.getOutputBuffers();
            } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            }
            while (outputIndex >= 0) {
                //拿到用于存放PCM数据的Buffer
                outputBuffer = decodeOutputBuffers[outputIndex];
                //BufferInfo内定义了此数据块的大小
                //LogUtils.d(TAG, "数据块大小: " + decodeBufferInfo.size);
                chunkPCM = new byte[decodeBufferInfo.size];
                //将Buffer内的数据取出到字节数组中
                outputBuffer.get(chunkPCM);
                //数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
                outputBuffer.clear();
                putPCMData(chunkPCM);
//              
                //此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
                mediaDecode.releaseOutputBuffer(outputIndex, false);
                //再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
                outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 0);
            }
            if((decodeBufferInfo.flags & BUFFER_FLAG_END_OF_STREAM) != 0){
                break;
            }
        }
        try {
            rawBos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        LogUtils.d(TAG, " decode time:" + (System.currentTimeMillis() - time) + " ms");
    }

    /**
     * 编码的实现
     */
    private void encodeAudioFromPCM() {
        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;
        byte[] chunkAudio;
        int outBitSize;
        byte[] chunkPCM;
        long kTimeOutUs = 10000;

        int numBytesSubmitted = 0;
        boolean doneSubmittingInput = false;
        int numBytesDequeued = 0;
        boolean encodeDone = false;
        for (; ; ) {
            for (int i = 0; i < encodeInputBuffers.length; i++) {
                inputIndex = mediaEncode.dequeueInputBuffer(kTimeOutUs);
                if(inputIndex < 0){
                    continue;
                }
                chunkPCM = getPCMData();
                //将PCM的数据填充给inputBuffer
                if(chunkPCM != null) {
                        inputBuffer = encodeInputBuffers[inputIndex];
                        inputBuffer.clear();
                        if (chunkPCMDataContainer.size() == 0) {
                            //如果输入结束,设置BUFFER_FLAG_END_OF_STREAM
                            mediaEncode.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                            break;
                        }
                        //将PCM的数据填充给inputBuffer
                        inputBuffer.put(chunkPCM);
                        //通知mediaEncode编码刚刚传入的数据
                        mediaEncode.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);
                        numBytesSubmitted += chunkPCM.length;
                    }
            }
            outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 1000);
            if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                encodeOutputBuffers = mediaEncode.getOutputBuffers();
            } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // Subsequent data will conform to new format.
                MediaFormat format = mediaEncode.getOutputFormat();
            }
            while (outputIndex >= 0) {
                outBitSize = encodeBufferInfo.size;
                outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
                outputBuffer.position(encodeBufferInfo.offset);
                outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
                chunkAudio = new byte[outBitSize];
                outputBuffer.get(chunkAudio, 0, chunkAudio.length);
                try {
                    bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.amr
                    numBytesDequeued += chunkAudio.length;
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mediaEncode.releaseOutputBuffer(outputIndex, false);
                //encodeBufferInfo = new MediaCodec.BufferInfo();
                outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 1000);
            }
            if (codeOver && (encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                Log.d(TAG, "encode finish");
                break;
            }
        }
        Log.d(TAG, "queued a total of " + numBytesSubmitted + "bytes, "
                + "dequeued " + numBytesDequeued + " bytes.");
        try {
            bos.flush();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

四、遇到的问题
在编写完代码之后,满怀兴喜的运行,但是在将aac文件转为amr文件之后,播放的时候却不对,是杂音。我刚开始以为是我的流程不对,但是如果将aac文件转为MP3文件,却可以转码成功。然后我上网查如何将aac转为amr文件,找到这篇文章,http://blog.csdn.net/honeybaby201314/article/details/50379040,发现使用上述文章的AmrInputStream和开源库opencore转出来的结果都不对。然后纳闷了好长时间,也找了很多资料,都没有找到。后来终于在stackoverflow上找到一个提问https://stackoverflow.com/questions/14929478/downsampling-pcm-wav-audio-from-22khz-to-8khz。原来使用的aac的采样率一般是44100Hz,但是amr的采样率一般设置为8000Hz,所以将aac转为amr时需要downSample,将采样率从44100 变为8000,这个不是线性的,自己实现起来比较麻烦。通过这篇文章找了一个库,http://blog.csdn.net/vertx/article/details/19078391?utm_source=tuicool
这个库的地址为
JSSRC

五、downSample
调用JSSRC的代码如下

    private void downSample(){
        File file = new File("aacdata.pcm");
        FileInputStream fis = null;
        FileOutputStream fileOutputStream = null;
        try {
            fis = new FileInputStream(file);
            fileOutputStream = new FileOutputStream("aac8000.pcm");
            //参数从左到右分别是原始采样率,输出采样率,每一帧所占字节,都是2个字节
            //然后是声道数,长度,attenuation衰减,dither抖动相关吧(这个我也不知道),quite是否打印相关信息
            new SSRC(fis, fileOutputStream, 44100, 8000, 2, 2,
                    1, (int) file.length(), 0, 0, false);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            CloseUtil.close(fis);
            CloseUtil.close(fileOutputStream);
        }
    }

六、后话
通过降低采样率之后终于得到了正常的AMR文件,整个过程中遇到了很多问题,但是最后总算是解决了。利用上述的方法进行AAC到AMR文件的转码很有代表性,代表了不同采样率之间的音频文件进行转码,还有一个问题也需要注意,就是声道数,这个也是需要注意的。当原始文件与转码之后文件的声道数不一致时,可以手动取某一个声道数,在此过程中注意字节序的问题。理解了整个过程之后,不同文件之间的互相转码也可以实现了。

你可能感兴趣的:(安卓)