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