热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MediaCodec进行AAC编解码(文件格式转换)

AAC,全称AdvancedAudioCoding,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,

AAC,全称Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。至于AAC的其他特点网上资料就很多,就不多做介绍了。
在介绍AAC编解码之前,首先要先学习几个新知识MediaExtractor和ADTS格式
仓库源码FFmpegSample,对应版本代码v1.6

MediaExtractor

前面在介绍视频编码的时候使用到了MediaCodec,其功能主要是进行音视频的编解码。下面要介绍另外一个类MediaExtractor:负责将指定类型的媒体文件从文件中找到轨道,可以用来分离容器中的视频track和音频track。将得到的原始数据解析成解码器需要的数据。

《MediaCodec进行AAC编解码(文件格式转换)》 1.png

对象创建和设置源

对象的创建直接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帧。

《MediaCodec进行AAC编解码(文件格式转换)》 2.png

长度说明
Syncword12总是0xFFF, 代表一个ADTS帧的开始, 用于同步
MPEG version10 for MPEG-4 、 1 for MPEG-2
Layer2always 0
Protection Absent1et to 1 if there is no CRC and 0 if there is CRC
Profile2表示使用哪个级别的AAC( Audio Object Type的值减1)
MPEG-4 Sampling Frequency Index4采样率的下标
Originality1set to 0 when encoding, ignore when decoding
Home1set to 0 when encoding, ignore when decoding
Copyrighted Stream1set to 0 when encoding, ignore when decoding
Copyrighted Start1set to 0 when encoding, ignore when decoding
Frame Length13一个ADTS帧的长度包括ADTS头和AAC原始流。aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame)
Buffer Fullness110x7FF 说明是码率可变的码流
Number of AAC Frames2表示ADTS帧中有number_of_raw_data_blocks_in_frame number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块。 (一个AAC原始帧包含一段时间内1024个采样及相关数据)

文件格式转换

先来张流程图

《MediaCodec进行AAC编解码(文件格式转换)》 5.png

第一步 初始化解码器

读取视频文件初始化解码器

/**
* 初始化解码器
*/
private void initMediaDecode() {
try {
mediaExtractor = new MediaExtractor();//此类可分离视频文件的音轨和视频轨道
mediaExtractor.setDataSource(srcPath);//媒体文件的位置
for (int i = 0; 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 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 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个字节。这里直接上代码

/**
* 添加ADTS头
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = sampleRateType; // 44.1KHz
int chanCfg = 2; // CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) <<6) + (freqIdx <<2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) <<6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) <<5) + 0x1F);
packet[6] = (byte) 0xFC;
}

第四步 释放资源

/**
* 释放资源
*/
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的释放。

到这里整个流程完成

提示:在使用项目代码时注意对应版本v1.6:

《MediaCodec进行AAC编解码(文件格式转换)》 6.png


推荐阅读
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 合并列值-合并为一列问题需求:createtabletab(Aint,Bint,Cint)inserttabselect1,2,3unionallsel ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
  • 本文详细介绍了使用C#实现Word模版打印的方案。包括添加COM引用、新建Word操作类、开启Word进程、加载模版文件等步骤。通过该方案可以实现C#对Word文档的打印功能。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有