学习 MediaCodec API,完成音频 AAC 硬编、硬解
MediaCodec API的学习在之前一篇文章已经记录,请参考这儿,虽然翻译的不太好,但是一定要结合英文去认真看一下API(英文好的就不要听我说了)。
本文是以mp3为例,过程:MP3->PCM->AAC。先把MP3文件解码成PCM格式数据,然后编码成AAC格式的音频文件。
注意:此处编码成AAC格式的文件时,要记得加入ADTS文件头,否则无法播放。
关于PCM音频编码的介绍请参考这篇文章。
调用逻辑引用Android MediaCodec小结的一个图:
此图已经很清晰了,下面看主要代码实现。
/**
* 初始化解码器
*/
private void initDecoder() {
try {
mExtractor = new MediaExtractor();
mExtractor.setDataSource(mSrcPath);
int trackCount = mExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio")) { //获取音频轨道
mExtractor.selectTrack(i);
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);
//创建解码器
mDecodeCodec = MediaCodec.createDecoderByType(mime);
//第二个参数是surface,解码视频的时候需要,第三个是MediaCrypto, 是关于加密的,最后一个flag填0即可
//configure会使MediaCodec进入Configured state
mDecodeCodec.configure(format, null, null, 0);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
if (mDecodeCodec == null) {
Log.e("codec", "create mDecodeCodec failed");
return;
}
//启动MediaCodec,等待传入数据
//调用此方法之后mediaCodec进入Executing state
mDecodeCodec.start();
//MediaCodec在此ByteBuffer[]中获取输入数据
mDecoderInputBuffers = mDecodeCodec.getInputBuffers();
//MediaCodec将解码后的数据放到此ByteBuffer[]中 我们可以直接在这里面得到PCM数据
mDecoderOutputBuffers = mDecodeCodec.getOutputBuffers();
//用于描述解码得到的byte[]数据的相关信息
mDecoderInfo = new MediaCodec.BufferInfo();
}
/**
* 初始化编码器
*/
private void initEncoder() {
try {
//参数对应-> mime type、采样率、声道数
MediaFormat encodeFormat = MediaFormat.createAudioFormat(mEncodeType, key_sample_rate, key_channel_count);
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);
mEncodeCodec = MediaCodec.createEncoderByType(mEncodeType);
//最后一个参数当使用编码器时设置
mEncodeCodec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
}
if (mEncodeCodec == null) {
Log.e("codec", "create mEncodeCodec failed");
return;
}
mEncodeCodec.start();
mEncoderInputBuffers = mEncodeCodec.getInputBuffers();
mEncoderOutputBuffers = mEncodeCodec.getOutputBuffers();
mEncoderInfo = new MediaCodec.BufferInfo();
}
/**
* 解码音频文件,得到PCM数据
*/
private void audioToPCM() {
for (int i = 0; i < mDecoderInputBuffers.length - 1; i++) {
//获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
int inputIndex = mDecodeCodec.dequeueInputBuffer(-1);
if (inputIndex < 0) {
mCodecOver = true;
return;
}
ByteBuffer inputBuffer = mDecoderInputBuffers[inputIndex];//拿到inputBuffer
inputBuffer.clear();//清空之前传入inputBuffer内的数据
//MediaExtractor读取数据到inputBuffer中
int sampleSize = mExtractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {//小于0 代表所有数据已读取完成
mCodecOver = true;
} else {
//通知MediaDecode解码刚刚传入的数据
mDecodeCodec.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);
mExtractor.advance();//移动到下一帧
mDecodeSize += sampleSize;
}
}
//获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
//此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
int outputIndex = mDecodeCodec.dequeueOutputBuffer(mDecoderInfo, 10000);
ByteBuffer outputBuffer;
byte[] chunkPCM;
while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
outputBuffer = mDecoderOutputBuffers[outputIndex];
//BufferInfo内定义了此数据块的大小
chunkPCM = new byte[mDecoderInfo.size];
//将Buffer内的数据取出到字节数组中
outputBuffer.get(chunkPCM);
//数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
outputBuffer.clear();
//自己定义的方法,供编码器所在的线程获取数据
putPCMData(chunkPCM);
//此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
mDecodeCodec.releaseOutputBuffer(outputIndex, false);
//再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
outputIndex = mDecodeCodec.dequeueOutputBuffer(mDecoderInfo, 10000);
}
}
/**
* 编码PCM数据,得到{@link #mEncodeType}格式的音频文件,并保存到{@link #mDstPath}
*/
private void pcmToTargetAudio() {
int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
byte[] chunkAudio;
int outBitSize;
int outPacketSize;
byte[] chunkPCM;
for (int i = 0; i < mEncoderInputBuffers.length - 1; i++) {
chunkPCM = getPCMData();//获取解码器所在线程输出的数据
if (chunkPCM == null) {
break;
}
//以下操作同解码器
inputIndex = mEncodeCodec.dequeueInputBuffer(-1);
inputBuffer = mEncoderInputBuffers[inputIndex];
inputBuffer.clear();
inputBuffer.limit(chunkPCM.length);
//PCM数据填充给inputBuffer
inputBuffer.put(chunkPCM);
//通知编码器 编码
mEncodeCodec.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);
}
outputIndex = mEncodeCodec.dequeueOutputBuffer(mEncoderInfo, 10000);
while (outputIndex > 0) {
outBitSize = mEncoderInfo.size;
outPacketSize = outBitSize + 7; //7为ADTS头部的大小
//拿到输出Buffer
outputBuffer = mEncoderOutputBuffers[outputIndex];
outputBuffer.position(mEncoderInfo.offset);
outputBuffer.limit(mEncoderInfo.offset + outBitSize);
chunkAudio = new byte[outPacketSize];
//添加ADTS
addADTStoPacket(chunkAudio, outPacketSize);
//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7
outputBuffer.get(chunkAudio, 7, outBitSize);
outputBuffer.position(mEncoderInfo.offset);
try {
//BufferOutputStream 将文件保存到内存卡中 *.aac
mOutputStream.write(chunkAudio, 0, chunkAudio.length);
} catch (IOException e) {
e.printStackTrace();
}
mEncodeCodec.releaseOutputBuffer(outputIndex, false);
outputIndex = mEncodeCodec.dequeueOutputBuffer(mEncoderInfo, 10000);
}
}
注意: 在Android 21之后getInputBuffers()、getOutputBuffers()方法已标记过时,推荐使用getInputBuffer(int)、getOutputBuffers(int)。但是我并没有找到什么好的方式这样使用,可能异步方式是一种可能的方式。
关于异步调用可以参考这个示例。
请参考【多媒体封装格式详解】— AAC ADTS格式分析
android MediaCodec 音频编解码的实现——转码