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

Published on with 0 views and 0 comments

我本来是做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文件的转码很有代表性,代表了不同采样率之间的音频文件进行转码,还有一个问题也需要注意,就是声道数,这个也是需要注意的。当原始文件与转码之后文件的声道数不一致时,可以手动取某一个声道数,在此过程中注意字节序的问题。理解了整个过程之后,不同文件之间的互相转码也可以实现了。

说你懂得生之微末,我便做了这壮大与你看,你说再热闹也终需离散,我便做了这一辈子与你看,你说冷暖自知,我便做了这冬花夏雪与你看,你说恋恋旧日好时光,我便做了这描金绣凤的浮世绘与你看。你说应愁高处不胜寒,我便拱手河山,讨你欢。