天天看點

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檔案的轉碼很有代表性,代表了不同采樣率之間的音頻檔案進行轉碼,還有一個問題也需要注意,就是聲道數,這個也是需要注意的。當原始檔案與轉碼之後檔案的聲道數不一緻時,可以手動取某一個聲道數,在此過程中注意位元組序的問題。了解了整個過程之後,不同檔案之間的互相轉碼也可以實作了。