MediaCodec進行編解碼AAC(檔案格式轉換)
本文來自eric原創授權釋出,eric,音視訊開發愛好者,簡書地址:https://www.jianshu.com/u/1502591a1753。歡迎大家關注。
AAC,全稱Advanced Audio Coding
,是一種專為聲音資料設計的檔案壓縮格式。與MP3不同,它採用了全新的演算法進行編碼,更加高效,具有更高的“價效比”。利用AAC格式,可使人感覺聲音質量沒有明顯降低的前提下,更加小巧。
在介紹AAC編解碼之前,首先要先學習幾個新知識MediaExtractor
和ADTS
格式
MediaExtractor
前面在介紹視訊編碼的時候使用到了MediaCodec,其功能主要是進行音視訊的編解碼。下面要介紹另外一個類MediaExtractor:負責將指定型別的媒體檔案從檔案中找到軌道,可以用來分離容器中的視訊track和音訊track。將得到的原始資料解析成解碼器需要的資料。
物件建立和設定源
物件的建立直接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幀。
檔案格式轉換
先來張流程圖
第一步 初始化解碼器
讀取視訊檔案初始化解碼器
/**
* 初始化解碼器
*/
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 {
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的釋放。
到這裡整個流程完成。