1. 程式人生 > >MediaCodec進行編解碼AAC(檔案格式轉換)

MediaCodec進行編解碼AAC(檔案格式轉換)

本文來自eric原創授權釋出,eric,音視訊開發愛好者,簡書地址:https://www.jianshu.com/u/1502591a1753。歡迎大家關注。

AAC,全稱Advanced Audio Coding,是一種專為聲音資料設計的檔案壓縮格式。與MP3不同,它採用了全新的演算法進行編碼,更加高效,具有更高的“價效比”。利用AAC格式,可使人感覺聲音質量沒有明顯降低的前提下,更加小巧。
在介紹AAC編解碼之前,首先要先學習幾個新知識
MediaExtractorADTS格式

MediaExtractor

前面在介紹視訊編碼的時候使用到了MediaCodec,其功能主要是進行音視訊的編解碼。下面要介紹另外一個類MediaExtractor:負責將指定型別的媒體檔案從檔案中找到軌道,可以用來分離容器中的視訊track和音訊track。將得到的原始資料解析成解碼器需要的資料。

640?wx_fmt=png&wxfrom=5&wx_lazy=1

物件建立和設定源
物件的建立直接new出來即可。然後最要要的是設定資料來源。呼叫setDataSource即可

Sets the data source (file-path or http URL) to use.

這個方法的註釋寫的比較清楚,可以設定本地檔案的位置或者一個http URL。

分離軌道資訊

  • getTrackCount()獲取軌道數量

  • MediaFormat format = mediaExtractor.getTrackFormat(i);獲取對應軌道的資訊。通過MediaFormat我們就可以知道每個track的詳細資訊,如音訊/視訊、格式等等。

  • selectTrack選擇軌道

讀取資料

制定軌道後就可以開始讀取資料了。

  • readSampleData將資料讀取到ByteBuffer 中。返回-1時代表沒有更多資料了

  • advance跳到下一個資料包,如果沒有下一個就返回false

釋放資源

  • 使用完後呼叫release進行資源釋放

ADTS

ADTS是AAC音訊檔案常見的傳輸格式。當你編碼AAC裸流的時候,會遇到寫出來的AAC檔案並不能在PC和手機上播放,很大的可能就是AAC檔案的每一幀裡缺少了ADTS頭資訊檔案的包裝拼接。只需要加入標頭檔案ADTS即可。一個AAC原始資料塊長度是可變的,對原始幀加上ADTS頭進行ADTS的封裝,就形成了ADTS幀。

0?wx_fmt=png

0?wx_fmt=png

檔案格式轉換

先來張流程圖

0?wx_fmt=png

第一步 初始化解碼器

讀取視訊檔案初始化解碼器

/**
* 初始化解碼器
*/


private void initMediaDecode() {
   try {
       mediaExtractor = new MediaExtractor();//此類可分離視訊檔案的音軌和視訊軌道
       mediaExtractor.setDataSource(srcPath);//媒體檔案的位置
       for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {//遍歷媒體軌道 此處我們傳入的是音訊檔案,所以也就只有一條軌道
           MediaFormat format = mediaExtractor.getTrackFormat(i);
           String mime = format.getString(MediaFormat.KEY_MIME);
           if (mime.startsWith("audio")) {//獲取音訊軌道
               mediaExtractor.selectTrack(i);//選擇此音訊軌道
               LogUtils.d("mime:" + mime);
               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);
               sampleRateType = ADTSUtils.getSampleRateType(key_sample_rate);
               mediaDecode = MediaCodec.createDecoderByType(mime);//建立Decode解碼器
               mediaDecode.configure(format, null, null, 0);
               break;
           }
       }
   } catch (IOException e) {
       e.printStackTrace();
   }
   if (mediaDecode == null) {
       LogUtils.e("create mediaDecode failed");
       return;
   }
   mediaDecode.start();//啟動MediaCodec ,等待傳入資料
   decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中獲取輸入資料
   decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec將解碼後的資料放到此ByteBuffer[]中 我們可以直接在這裡面得到PCM資料
   decodeBufferInfo = new MediaCodec.BufferInfo();//用於描述解碼得到的byte[]資料的相關資訊
   LogUtils.d("buffers:" + decodeInputBuffers.length);
}

前面已經介紹了MediaExtractor的用法,這裡就是解析得到音訊軌道,然後建立一個對應解碼格式MediaCodec用於解碼。MediaCodec的用法在前面視訊編碼文章中有介紹,這裡就不累述。

第二步 初始化編碼器

/**
* 初始化AAC編碼器
*/

private void initAACMediaEncode() {
   try {
       LogUtils.d(key_bit_rate + " " + key_channel_count + " " + key_sample_rate + " " + sampleRateType);
       MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
               key_sample_rate, key_channel_count);//引數對應-> mime type、取樣率、聲道數
       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);
       mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
       mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
   } catch (IOException e) {
       e.printStackTrace();
   }
   if (mediaEncode == null) {
       LogUtils.e("create mediaEncode failed");
       return;
   }
   mediaEncode.start();
   encodeInputBuffers = mediaEncode.getInputBuffers();
   encodeOutputBuffers = mediaEncode.getOutputBuffers();
   encodeBufferInfo = new MediaCodec.BufferInfo();
}

這裡也是建立一個MediaCodec用於編碼,同時設定相關引數,我們保持和原始檔的引數一致,也就是MediaExtractor解析得到的位元速率、聲道數、取樣率等等。

第三步 分別開啟執行緒編解碼

/**
* 開始轉碼
* 音訊資料{@link #srcPath}先解碼成PCM  
* PCM資料在編碼成MediaFormat.MIMETYPE_AUDIO_AAC音訊格式
* mp3->PCM->aac
*/

public void startAsync() {
   LogUtils.w("start");
   new Thread(new DecodeRunnable()).start();
   new Thread(new EncodeRunnable()).start();
}

先看到解碼邏輯

/**
* 解碼{@link #srcPath}音訊檔案 得到PCM資料塊
*
* @return 是否解碼完所有資料
*/

private void srcAudioFormatToPCM() {
   for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
       int inputIndex = mediaDecode.dequeueInputBuffer(-1);//獲取可用的inputBuffer -1代表一直等待,0表示不等待 建議-1,避免丟幀
       if (inputIndex < 0) {
           codeOver = true;
           return;
       }
       ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];//拿到inputBuffer
       inputBuffer.clear();//清空之前傳入inputBuffer內的資料
       int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor讀取資料到inputBuffer中
       if (sampleSize < 0) {//小於0 代表所有資料已讀取完成
           codeOver = true;
       } else {
           mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解碼剛剛傳入的資料
           mediaExtractor.advance();//MediaExtractor移動到下一取樣處
           decodeSize += sampleSize;
           LogUtils.d("read:" + sampleSize);
           if (onProgressListener != null) {
               onProgressListener.progress(decodeSize, fileTotalSize);
           }
       }
   }
   //獲取解碼得到的byte[]資料 引數BufferInfo上面已介紹 10000同樣為等待時間 同上-1代表一直等待,0代表不等待。此處單位為微秒
   //此處建議不要填-1 有些時候並沒有資料輸出,那麼他就會一直卡在這 等待
   int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);
   ByteBuffer outputBuffer;
   byte[] chunkPCM;
   while (outputIndex >= 0) {//每次解碼完成的資料不一定能一次吐出 所以用while迴圈,保證解碼器吐出所有資料
       outputBuffer = decodeOutputBuffers[outputIndex];//拿到用於存放PCM資料的Buffer
       chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo內定義了此資料塊的大小
       outputBuffer.get(chunkPCM);//將Buffer內的資料取出到位元組陣列中
       outputBuffer.clear();//資料取出後一定記得清空此Buffer MediaCodec是迴圈使用這些Buffer的,不清空下次會得到同樣的資料
       putPCMData(chunkPCM);//自己定義的方法,供編碼器所在的執行緒獲取資料,下面會貼出程式碼
       mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer後 將不能向外輸出資料
       outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取資料,如果沒有資料輸出則outputIndex=-1 迴圈結束
   }
}

其實就是基本的MediaCodec操作。使用MediaExtractor.readSampleData讀取檔案音訊資料,然後交給MediaCodec進行解碼,最後將得到的PCM資料加入佇列中

這裡佇列我們使用ArrayBlockingQueue,在多執行緒操作時候,這個容器還是比較好用的

接下來看到編碼流程

/**
* 編碼執行緒
*/

private class EncodeRunnable implements Runnable {
   @Override
   public void run() {
       long t = System.currentTimeMillis();
       while (!codeOver || !queue.isEmpty()) {
           dstAudioFormatFromPCM();
       }
       if (onCompleteListener != null) {
           onCompleteListener.completed();
       }
       LogUtils.w("size:" + fileTotalSize + " decodeSize:" + decodeSize + "time:" + (System.currentTimeMillis() - t));
   }
}

這裡判斷如果解碼未結束或者佇列不為空就進入編碼流程

/**
* 編碼PCM資料 得到MediaFormat.MIMETYPE_AUDIO_AAC格式的音訊檔案,並儲存到{@link #dstPath}
*/

private void dstAudioFormatFromPCM() {
   int inputIndex;
   ByteBuffer inputBuffer;
   int outputIndex;
   ByteBuffer outputBuffer;
   byte[] chunkAudio;
   int outBitSize;
   int outPacketSize;
   byte[] chunkPCM;
   for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
       chunkPCM = getPCMData();//獲取解碼器所線上程輸出的資料 程式碼後邊會貼上
       if (chunkPCM == null) {
           break;
       }
       inputIndex = mediaEncode.dequeueInputBuffer(-1);//同解碼器
       inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
       inputBuffer.clear();//同解碼器
       inputBuffer.limit(chunkPCM.length);
       inputBuffer.put(chunkPCM);//PCM資料填充給inputBuffer
       mediaEncode.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知編碼器 編碼
   }
   outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解碼器
   while (outputIndex >= 0) {//同解碼器
       outBitSize = encodeBufferInfo.size;
       outPacketSize = outBitSize + 7;//7為ADTS頭部的大小
       outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
       outputBuffer.position(encodeBufferInfo.offset);
       outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
       chunkAudio = new byte[outPacketSize];
       addADTStoPacket(chunkAudio, outPacketSize);//新增ADTS 程式碼後面會貼上
       outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC資料 取出到byte[]中 偏移量offset=7 你懂得
       outputBuffer.position(encodeBufferInfo.offset);
       try {
           bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將檔案儲存到記憶體卡中 *.aac
           LogUtils.d("write " + chunkAudio.length);
       } catch (IOException e) {
           e.printStackTrace();
       }
       mediaEncode.releaseOutputBuffer(outputIndex, false);
       outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
   }
}

這裡也是常規的MediaCodec操作,只是多了一個ADTS封裝操作。ADTS前面有介紹,就是多了7個位元組。這裡直接上程式碼

第四步 釋放資源

/**
* 釋放資源
*/

public void release() {
   try {
       if (bos != null) {
           bos.flush();
       }
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       if (bos != null) {
           try {
               bos.close();
           } catch (IOException e) {
               e.printStackTrace();
           } finally {
               bos = null;
           }
       }
   }
   try {
       if (fos != null) {
           fos.close();
       }
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       fos = null;
   }
   if (mediaEncode != null) {
       mediaEncode.stop();
       mediaEncode.release();
       mediaEncode = null;
   }
   if (mediaDecode != null) {
       mediaDecode.stop();
       mediaDecode.release();
       mediaDecode = null;
   }
   if (mediaExtractor != null) {
       mediaExtractor.release();
       mediaExtractor = null;
   }
   if (onCompleteListener != null) {
       onCompleteListener = null;
   }
   if (onProgressListener != null) {
       onProgressListener = null;
   }
   LogUtils.w("release");
}

主要就是I/O流、MediaCodec、MediaExtractor的釋放。

到這裡整個流程完成。

0?wx_fmt=jpeg