使用AudioRecord錄制音頻厢呵,MediaCodec編碼為AAC

在實(shí)現(xiàn)錄制音頻需求的過程中的一些筆記,參考了很多有用的文章纳鼎,希望能幫到他人

Android 系統(tǒng) Java 層提供兩個(gè) Recorder Api, MediaRecorder 與 AudioRecorder笙瑟,前者能夠生成編碼后的錄音文件楼镐,而后者則是 PCM Audio RAW Data,我們希望通過對(duì) PCM 的操作往枷,根據(jù)需求完成任何操作框产。

AudioRecorder 基本使用方法

  1. 在 Recorder Thread 中創(chuàng)建 Recorder 與相關(guān)的 Encoder
  2. loop 讀取 Recorder 里面的 PCM data凄杯,不斷地將 PCM 喂入 Encoder中
  3. 外部停止錄音后,將 run 這個(gè) flag 置為 false 跳出循環(huán)秉宿,并且 close 相關(guān) Encoder 并且保存他們的結(jié)果戒突。
  4. 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睹耐。

  1. 在調(diào)用start()方法后MediaCodec立即進(jìn)入Flushed子狀態(tài),此時(shí)MediaCodec會(huì)擁有所有的緩存部翘。
  2. 一旦第一個(gè)輸入緩存(input buffer)被移出隊(duì)列硝训,MediaCodec就轉(zhuǎn)入Running子狀態(tài),這種狀態(tài)占據(jù)了MediaCodec的大部分生命周期新思。
  3. 當(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秆剪,一起剝皮案震驚了整個(gè)濱河市赊淑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仅讽,老刑警劉巖陶缺,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異洁灵,居然都是意外死亡饱岸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門徽千,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苫费,“玉大人,你說我怎么就攤上這事双抽“倏颍” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵牍汹,是天一觀的道長(zhǎng)铐维。 經(jīng)常有香客問我柬泽,道長(zhǎng),這世上最難降的妖魔是什么方椎? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任聂抢,我火速辦了婚禮,結(jié)果婚禮上棠众,老公的妹妹穿的比我還像新娘琳疏。我一直安慰自己,他們只是感情好闸拿,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布空盼。 她就那樣靜靜地躺著,像睡著了一般新荤。 火紅的嫁衣襯著肌膚如雪揽趾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天苛骨,我揣著相機(jī)與錄音篱瞎,去河邊找鬼。 笑死痒芝,一個(gè)胖子當(dāng)著我的面吹牛俐筋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播严衬,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼澄者,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了请琳?” 一聲冷哼從身側(cè)響起粱挡,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俄精,沒想到半個(gè)月后询筏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竖慧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年屈留,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片测蘑。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖康二,靈堂內(nèi)的尸體忽然破棺而出碳胳,到底是詐尸還是另有隱情,我是刑警寧澤沫勿,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布挨约,位于F島的核電站味混,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏诫惭。R本人自食惡果不足惜翁锡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夕土。 院中可真熱鬧馆衔,春花似錦、人聲如沸怨绣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)篮撑。三九已至减细,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赢笨,已是汗流浹背未蝌。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茧妒,地道東北人萧吠。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嘶伟,于是被迫代替她去往敵國(guó)和親怎憋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容