AAC,全称Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。至于AAC的其他特点网上资料就很多,就不多做介绍了。 在介绍AAC编解码之前,首先要先学习几个新知识MediaExtractor和ADTS格式 仓库源码FFmpegSample,对应版本代码v1.6
前面在介绍视频编码的时候使用到了MediaCodec,其功能主要是进行音视频的编解码。下面要介绍另外一个类MediaExtractor:负责将指定类型的媒体文件从文件中找到轨道,可以用来分离容器中的视频track和音频track。将得到的原始数据解析成解码器需要的数据。
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是AAC音频文件常见的传输格式。当你编码AAC裸流的时候,会遇到写出来的AAC文件并不能在PC和手机上播放,很大的可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。只需要加入头文件ADTS即可。一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧。
2.png
域 | 长度 | 说明 |
---|---|---|
Syncword | 12 | 总是0xFFF, 代表一个ADTS帧的开始, 用于同步 |
MPEG version | 1 | 0 for MPEG-4 、 1 for MPEG-2 |
Layer | 2 | always 0 |
Protection Absent | 1 | et to 1 if there is no CRC and 0 if there is CRC |
Profile | 2 | 表示使用哪个级别的AAC( Audio Object Type的值减1) |
MPEG-4 Sampling Frequency Index | 4 | 采样率的下标 |
Originality | 1 | set to 0 when encoding, ignore when decoding |
Home | 1 | set to 0 when encoding, ignore when decoding |
Copyrighted Stream | 1 | set to 0 when encoding, ignore when decoding |
Copyrighted Start | 1 | set to 0 when encoding, ignore when decoding |
Frame Length | 13 | 一个ADTS帧的长度包括ADTS头和AAC原始流。aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame) |
Buffer Fullness | 11 | 0x7FF 说明是码率可变的码流 |
Number of AAC Frames | 2 | 表示ADTS帧中有number_of_raw_data_blocks_in_frame number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块。 (一个AAC原始帧包含一段时间内1024个采样及相关数据) |
先来张流程图
5.png
读取视频文件初始化解码器
/** * 初始化解码器 */ 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 { @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 < 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个字节。这里直接上代码
/** * 添加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:
6.png
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句