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