AAC轧飞,全稱Advanced Audio Coding,是一種專為聲音數(shù)據(jù)設(shè)計(jì)的文件壓縮格式毅厚。與MP3不同塞颁,它采用了全新的算法進(jìn)行編碼,更加高效吸耿,具有更高的“性價(jià)比”祠锣。利用AAC格式,可使人感覺(jué)聲音質(zhì)量沒(méi)有明顯降低的前提下咽安,更加小巧伴网。至于AAC的其他特點(diǎn)網(wǎng)上資料就很多,就不多做介紹了妆棒。
在介紹AAC編解碼之前澡腾,首先要先學(xué)習(xí)幾個(gè)新知識(shí)MediaExtractor和ADTS格式
倉(cāng)庫(kù)源碼FFmpegSample,對(duì)應(yīng)版本代碼v1.6
MediaExtractor
前面在介紹視頻編碼的時(shí)候使用到了MediaCodec糕珊,其功能主要是進(jìn)行音視頻的編解碼动分。下面要介紹另外一個(gè)類MediaExtractor:負(fù)責(zé)將指定類型的媒體文件從文件中找到軌道,可以用來(lái)分離容器中的視頻track和音頻track红选。將得到的原始數(shù)據(jù)解析成解碼器需要的數(shù)據(jù)澜公。
對(duì)象創(chuàng)建和設(shè)置源
對(duì)象的創(chuàng)建直接new出來(lái)即可。然后最要要的是設(shè)置數(shù)據(jù)源喇肋。調(diào)用setDataSource
即可坟乾,
Sets the data source (file-path or http URL) to use.
這個(gè)方法的注釋寫(xiě)的比較清楚,可以設(shè)置本地文件的位置或者一個(gè)http URL蝶防。
分離軌道信息
-
getTrackCount()
獲取軌道數(shù)量 -
MediaFormat format = mediaExtractor.getTrackFormat(i);
獲取對(duì)應(yīng)軌道的信息甚侣。通過(guò)MediaFormat我們就可以知道每個(gè)track的詳細(xì)信息,如音頻/視頻间学、格式等等殷费。 -
selectTrack
選擇軌道
讀取數(shù)據(jù)
制定軌道后就可以開(kāi)始讀取數(shù)據(jù)了印荔。
-
readSampleData
將數(shù)據(jù)讀取到ByteBuffer 中。返回-1時(shí)代表沒(méi)有更多數(shù)據(jù)了 -
advance
跳到下一個(gè)數(shù)據(jù)包宗兼,如果沒(méi)有下一個(gè)就返回false
釋放資源
使用完后調(diào)用release
進(jìn)行資源釋放
ADTS
ADTS是AAC音頻文件常見(jiàn)的傳輸格式。當(dāng)你編碼AAC裸流的時(shí)候氮采,會(huì)遇到寫(xiě)出來(lái)的AAC文件并不能在PC和手機(jī)上播放殷绍,很大的可能就是AAC文件的每一幀里缺少了ADTS頭信息文件的包裝拼接。只需要加入頭文件ADTS即可鹊漠。一個(gè)AAC原始數(shù)據(jù)塊長(zhǎng)度是可變的主到,對(duì)原始幀加上ADTS頭進(jìn)行ADTS的封裝,就形成了ADTS幀躯概。
域 | 長(zhǎng)度 | 說(shuō)明 |
---|---|---|
Syncword | 12 | 總是0xFFF , 代表一個(gè)ADTS幀的開(kāi)始, 用于同步 |
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 | 表示使用哪個(gè)級(jí)別的AAC( Audio Object Type的值減1) |
MPEG-4 Sampling Frequency Index | 4 | 采樣率的下標(biāo) |
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 | 一個(gè)ADTS幀的長(zhǎng)度包括ADTS頭和AAC原始流。aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame) |
Buffer Fullness | 11 | 0x7FF 說(shuō)明是碼率可變的碼流 |
Number of AAC Frames | 2 | 表示ADTS幀中有number_of_raw_data_blocks_in_frame number_of_raw_data_blocks_in_frame == 0 表示說(shuō)ADTS幀中有一個(gè)AAC數(shù)據(jù)塊娶靡。 (一個(gè)AAC原始幀包含一段時(shí)間內(nèi)1024個(gè)采樣及相關(guān)數(shù)據(jù)) |
文件格式轉(zhuǎn)換
先來(lái)張流程圖
第一步 初始化解碼器
讀取視頻文件初始化解碼器
/**
* 初始化解碼器
*/
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);//創(chuàng)建Decode解碼器
mediaDecode.configure(format, null, null, 0);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
if (mediaDecode == null) {
LogUtils.e("create mediaDecode failed");
return;
}
mediaDecode.start();//啟動(dòng)MediaCodec ,等待傳入數(shù)據(jù)
decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中獲取輸入數(shù)據(jù)
decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec將解碼后的數(shù)據(jù)放到此ByteBuffer[]中 我們可以直接在這里面得到PCM數(shù)據(jù)
decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解碼得到的byte[]數(shù)據(jù)的相關(guān)信息
LogUtils.d("buffers:" + decodeInputBuffers.length);
}
前面已經(jīng)介紹了MediaExtractor的用法姿锭,這里就是解析得到音頻軌道塔鳍,然后創(chuàng)建一個(gè)對(duì)應(yīng)解碼格式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);//參數(shù)對(duì)應(yīng)-> mime type、采樣率焚鲜、聲道數(shù)
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();
}
這里也是創(chuàng)建一個(gè)MediaCodec用于編碼掌唾,同時(shí)設(shè)置相關(guān)參數(shù),我們保持和源文件的參數(shù)一致忿磅,也就是MediaExtractor解析得到的碼率糯彬、聲道數(shù)、采樣率等等葱她。
第三步 分別開(kāi)啟線程編解碼
/**
* 開(kāi)始轉(zhuǎn)碼
* 音頻數(shù)據(jù){@link #srcPath}先解碼成PCM PCM數(shù)據(jù)在編碼成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數(shù)據(jù)塊
*
* @return 是否解碼完所有數(shù)據(jù)
*/
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內(nèi)的數(shù)據(jù)
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor讀取數(shù)據(jù)到inputBuffer中
if (sampleSize < 0) {//小于0 代表所有數(shù)據(jù)已讀取完成
codeOver = true;
} else {
mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解碼剛剛傳入的數(shù)據(jù)
mediaExtractor.advance();//MediaExtractor移動(dòng)到下一取樣處
decodeSize += sampleSize;
LogUtils.d("read:" + sampleSize);
if (onProgressListener != null) {
onProgressListener.progress(decodeSize, fileTotalSize);
}
}
}
//獲取解碼得到的byte[]數(shù)據(jù) 參數(shù)BufferInfo上面已介紹 10000同樣為等待時(shí)間 同上-1代表一直等待,0代表不等待览效。此處單位為微秒
//此處建議不要填-1 有些時(shí)候并沒(méi)有數(shù)據(jù)輸出却舀,那么他就會(huì)一直卡在這 等待
int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);
ByteBuffer outputBuffer;
byte[] chunkPCM;
while (outputIndex >= 0) {//每次解碼完成的數(shù)據(jù)不一定能一次吐出 所以用while循環(huán),保證解碼器吐出所有數(shù)據(jù)
outputBuffer = decodeOutputBuffers[outputIndex];//拿到用于存放PCM數(shù)據(jù)的Buffer
chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo內(nèi)定義了此數(shù)據(jù)塊的大小
outputBuffer.get(chunkPCM);//將Buffer內(nèi)的數(shù)據(jù)取出到字節(jié)數(shù)組中
outputBuffer.clear();//數(shù)據(jù)取出后一定記得清空此Buffer MediaCodec是循環(huán)使用這些Buffer的锤灿,不清空下次會(huì)得到同樣的數(shù)據(jù)
putPCMData(chunkPCM);//自己定義的方法挽拔,供編碼器所在的線程獲取數(shù)據(jù),下面會(huì)貼出代碼
mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 將不能向外輸出數(shù)據(jù)
outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取數(shù)據(jù)但校,如果沒(méi)有數(shù)據(jù)輸出則outputIndex=-1 循環(huán)結(jié)束
}
}
其實(shí)就是基本的MediaCodec操作螃诅。使用MediaExtractor.readSampleData讀取文件音頻數(shù)據(jù),然后交給MediaCodec進(jìn)行解碼,最后將得到的PCM數(shù)據(jù)加入隊(duì)列中
這里隊(duì)列我們使用ArrayBlockingQueue术裸,在多線程操作時(shí)候倘是,這個(gè)容器還是比較好用的
接下來(lái)看到編碼流程
/**
* 編碼線程
*/
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));
}
}
這里判斷如果解碼未結(jié)束或者隊(duì)列不為空就進(jìn)入編碼流程
/**
* 編碼PCM數(shù)據(jù) 得到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();//獲取解碼器所在線程輸出的數(shù)據(jù) 代碼后邊會(huì)貼上
if (chunkPCM == null) {
break;
}
inputIndex = mediaEncode.dequeueInputBuffer(-1);//同解碼器
inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
inputBuffer.clear();//同解碼器
inputBuffer.limit(chunkPCM.length);
inputBuffer.put(chunkPCM);//PCM數(shù)據(jù)填充給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 代碼后面會(huì)貼上
outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC數(shù)據(jù) 取出到byte[]中 偏移量offset=7 你懂得
outputBuffer.position(encodeBufferInfo.offset);
try {
bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內(nèi)存卡中 *.aac
LogUtils.d("write " + chunkAudio.length);
} catch (IOException e) {
e.printStackTrace();
}
mediaEncode.releaseOutputBuffer(outputIndex, false);
outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
}
}
這里也是常規(guī)的MediaCodec操作袭艺,只是多了一個(gè)ADTS封裝操作搀崭。ADTS前面有介紹,就是多了7個(gè)字節(jié)猾编。這里直接上代碼
/**
* 添加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的釋放答倡。
到這里整個(gè)流程完成
提示:在使用項(xiàng)目代碼時(shí)注意對(duì)應(yīng)版本v1.6: