在實(shí)現(xiàn)錄制音頻需求的過程中的一些筆記,參考了很多有用的文章纳鼎,希望能幫到他人
Android 系統(tǒng) Java 層提供兩個(gè) Recorder Api, MediaRecorder 與 AudioRecorder笙瑟,前者能夠生成編碼后的錄音文件楼镐,而后者則是 PCM Audio RAW Data,我們希望通過對(duì) PCM 的操作往枷,根據(jù)需求完成任何操作框产。
AudioRecorder 基本使用方法
- 在 Recorder Thread 中創(chuàng)建 Recorder 與相關(guān)的 Encoder
- loop 讀取 Recorder 里面的 PCM data凄杯,不斷地將 PCM 喂入 Encoder中
- 外部停止錄音后,將 run 這個(gè) flag 置為 false 跳出循環(huán)秉宿,并且 close 相關(guān) Encoder 并且保存他們的結(jié)果戒突。
- release 相關(guān)資源。
遇到的問題:
在 loop 中 Encoder的process block 時(shí)間太長(zhǎng)描睦,會(huì)導(dǎo)致來不及讀取 AudioRecorder buffer膊存,最終導(dǎo)致錄制的音頻丟失數(shù)據(jù)
為了支持更多的音頻格式,Recorder 就需要掛載不同的 Encoder
所有encode的流程是相仿的忱叭,如初始化階段隔崎,編碼階段,結(jié)束階段韵丑,釋放資源階段爵卒。只是其中某些的具體實(shí)現(xiàn)步驟不同,因此通過AudioProcessor接口按照流程抽離出具體實(shí)現(xiàn)類encoder撵彻,將其注入到Recorder中钓株,我們只需要掛載新的 Encoder,即可編碼成我們所需要的文件格式
解決方法:
所以我們創(chuàng)建一個(gè)ProcessThread陌僵, 讓 Encoder 在 ProcessThread 中執(zhí)行轴合,這樣 RecoderThread 不會(huì)因?yàn)?Encoder 而 block 導(dǎo)致數(shù)據(jù)丟失。
由于項(xiàng)目的需要碗短,要把pcm轉(zhuǎn)碼為aac音頻受葛,目前大致兩種方案,F(xiàn)Fmpeg和MediaCodec豪椿,我們這里使用MediaCodec
接下來就是具體的encode階段奔坟,先初始化編碼器 ,和解碼器的MediaFormat直接在音頻文件內(nèi)獲取不同搭盾,編碼器的MediaFormat需要自己來創(chuàng)建,對(duì)MediaCodec還不是很了解的同學(xué)婉支,可以參考這篇文章http://www.reibang.com/p/30e596112015
下文引用了很多部分
用編碼器把PCM轉(zhuǎn)為AAC
我們?cè)贏udioProcessor的初始化方法中鸯隅,完成創(chuàng)建輸出文件和初始化編碼器的步驟.創(chuàng)建了一個(gè)MediaCodec對(duì)象,此時(shí)MediaCodec處于Uninitialized狀態(tài)屈糊。首先名段,需要使用configure(…)方法對(duì)MediaCodec進(jìn)行配置柱彻,這時(shí)MediaCodec轉(zhuǎn)為Configured狀態(tài)。然后調(diào)用start()方法使其轉(zhuǎn)入Executing狀態(tài)跟畅。
@Override
public void start() {
try {
fos = new FileOutputStream(filePath);
bos = new BufferedOutputStream(fos, 200 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
try {
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 16000, 1);//參數(shù)對(duì)應(yīng)-> mime type、采樣率溶推、聲道數(shù)
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128 * 100);//比特率
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16*1024);
codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
codec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
e.printStackTrace();
}
if (codec == null) {
Log.e(TAG, "create mediaEncode failed");
return;
}
//調(diào)用MediaCodec的start()方法徊件,此時(shí)MediaCodec處于Executing狀態(tài)
codec.start();
}
初始化結(jié)束我們就可以loop喂數(shù)據(jù)給我們的AudioProcessor奸攻,AudioProcessor會(huì)調(diào)用我們的這個(gè)方法,進(jìn)行具體的編碼工作.
Executing狀態(tài)包含三個(gè)子狀態(tài): Flushed虱痕、 Running 以及End-of-Stream睹耐。
- 在調(diào)用start()方法后MediaCodec立即進(jìn)入Flushed子狀態(tài),此時(shí)MediaCodec會(huì)擁有所有的緩存部翘。
- 一旦第一個(gè)輸入緩存(input buffer)被移出隊(duì)列硝训,MediaCodec就轉(zhuǎn)入Running子狀態(tài),這種狀態(tài)占據(jù)了MediaCodec的大部分生命周期新思。
- 當(dāng)你將一個(gè)帶有end-of-stream marker標(biāo)記的輸入緩存入隊(duì)列時(shí)窖梁,MediaCodec將轉(zhuǎn)入End-of-Stream子狀態(tài)。在這種狀態(tài)下夹囚,MediaCodec不再接收之后的輸入緩存纵刘,但它仍然產(chǎn)生輸出緩存直到end-of- stream標(biāo)記輸出。
@Override
public void flow(byte[] bytes, int size) {
int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
byte[] chunkAudio;
int outBitSize;
int outPacketSize;
//通過getInputBuffers()方法和getOutputBuffers()方法獲取緩存隊(duì)列
encodeInputBuffers = codec.getInputBuffers();
encodeOutputBuffers = codec.getOutputBuffers();
//用于存儲(chǔ)ByteBuffer的信息
encodeBufferInfo = new MediaCodec.BufferInfo();
//首先通過dequeueInputBuffer(long timeoutUs)請(qǐng)求一個(gè)輸入緩存崔兴,timeoutUs代表等待時(shí)間彰导,設(shè)置為-1代表無限等待
int inputBufferIndex = codec.dequeueInputBuffer(-1);
//返回的整型變量為請(qǐng)求到的輸入緩存的index,通過getInputBuffers()得到的輸入緩存數(shù)組,再用index和輸入緩存數(shù)組即可得到當(dāng)前請(qǐng)求的輸入緩存
if (inputBufferIndex >= 0) {
inputBuffer = encodeInputBuffers[inputBufferIndex];
//使用之前要clear一下敲茄,避免之前的緩存數(shù)據(jù)影響當(dāng)前數(shù)據(jù)
inputBuffer.clear();
//把數(shù)據(jù)添加到輸入緩存中位谋,
inputBuffer.put(bytes);
//并調(diào)用queueInputBuffer()把緩存數(shù)據(jù)入隊(duì)
codec.queueInputBuffer(inputBufferIndex, 0, size, 0, 0);
}
//通過dequeueOutputBuffer(BufferInfo info, long timeoutUs)來請(qǐng)求一個(gè)輸出緩存,傳入一個(gè)上面的BufferInfo對(duì)象
outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);
//然后通過返回的index得到輸出緩存,并通過BufferInfo獲取ByteBuffer的信息
while (outputIndex >= 0) {
outBitSize = encodeBufferInfo.size;
//添加ADTS頭,ADTS頭包含了AAC文件的采樣率堰燎、通道數(shù)掏父、幀數(shù)據(jù)長(zhǎng)度等信息。
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);
//showLog("outPacketSize:" + outPacketSize + " encodeOutBufferRemain:" + outputBuffer.remaining());
try {
bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內(nèi)存卡中 *.aac
} catch (IOException e) {
e.printStackTrace();
}
//releaseOutputBuffer方法必須調(diào)用
codec.releaseOutputBuffer(outputIndex, false);
outputIndex = codec.dequeueOutputBuffer(encodeBufferInfo, 10000);
}
}
/**
* 添加ADTS頭
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 8; // 44.1KHz
int chanCfg = 1; // 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;
}
結(jié)束MediaCodec,并釋放掉占用資源
@Override
public void end() {
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 (codec != null) {
codec.stop();
codec.release();
codec=null;
}
}
參考:
https://juejin.im/entry/58fd31b75c497d005802c5e3
http://www.reibang.com/p/30e596112015