close

原文鏈接: https://juejin.cn/post/7094551541700362248

AAC硬件編碼文章有幾篇,但是都是同步實現,這裡採用異步實現,代碼Kotlin。

雖然代碼是kotlin寫的,但是思路上面的按照java能複製出來

最後我會把代碼上傳到github,可以查看完整過程,為了方便查看,所以代碼都寫在 Activity中

錄音和編碼都設置在子線程

採取的是邊錄製邊編碼邊寫入文件

6.0注意動態權限問題,錄音,讀寫文件

示例使用的5.0以上的API

MediaCodec官方原理圖上面是官方的一張原理圖,我就先說下基本原理 MediaCodec給我們提供了一組InputBuffer和一組outputBuffer,我們通過手段獲取到空的InputBuffer,裝填原始音頻數據,然後把裝填後的buffer還給Codec讓他去編碼,他編碼 後會把編碼後文件放在outputBuffer中,我們獲取outputBuffer讀取數據保存到文件就好, 然後循環這個過程,完成所有文件的編碼和保存。

這裡注意並不是一個inputBuffer對應一個outputBuffer,一個輸入buffer可以對應多個輸出 buffer,比如你往inputBuffer添加1000個字節數據,他可能編碼後放入了多個outputBuffer 中,一個outputBuffer可能就200或者300個字節。

下面就是實際操作過程

1.完成AudioRecord配置/***初始化音頻採集*/privatefuninitAudioRecorder(){//設置最小緩衝區大小根據系統提供的方法計算minBufferSize=AudioRecord.getMinBufferSize(AudioConfig.SAMPLE_RATE,AudioConfig.CHANNEL_CONFIG,AudioConfig.AUDIO_FORMAT)//創建音頻記錄器對象audioRecorder=AudioRecord(MediaRecorder.AudioSource.MIC,AudioConfig.SAMPLE_RATE,AudioConfig.CHANNEL_CONFIG,AudioConfig.AUDIO_FORMAT,minBufferSize)}

上面的AudioConfig配置文件如下

constvalSAMPLE_RATE=44100//採樣率/***CHANNEL_IN_MONO單聲道能夠保證所有設備都支持*CHANNEL_IN_STEREO立體聲*/constvalCHANNEL_CONFIG=AudioFormat.CHANNEL_IN_MONO/***返回的音頻數據格式*/constvalAUDIO_FORMAT=AudioFormat.ENCODING_PCM_16BIT/***輸出的音頻聲道*/constvalCHANNEL_OUT_CONFIG=AudioFormat.CHANNEL_OUT_MONO2.創建LinkedBlockingQeque準備保存錄音數據

因為錄音和編碼都放在子線程去做,所以數據難以傳遞,通過同步隊列保存來保證 正確讀取數據,保存的數據類型是字節數組

privatevaraudioList:LinkedBlockingDeque<ByteArray>?=LinkedBlockingDeque()3.開始錄音並保存//開啟線程啟動錄音thread(priority=android.os.Process.THREAD_PRIORITY_URGENT_AUDIO){try{//判斷AudioRecord是否初始化成功if(AudioRecord.STATE_INITIALIZED==audioRecorder.state){isRecording=true//標記是否在錄製中audioRecorder.startRecording()valoutputArray=ByteArray(minBufferSize)while(isRecording){varreadCode=audioRecorder.read(outputArray,0,minBufferSize)//這個readCode還有很多小於0的數字,表示某種錯誤,//這裡為了方便就沒處理if(readCode>0){valrealArray=ByteArray(readCode)System.arraycopy(outputArray,0,realArray,0,readCode)//將讀取的數據保存到LinkedBlockingDequeaudioList?.offer(realArray)}}//錄製結束,添加結束標記,方便編碼時候知道錄製結束了//這個結束標記只是一個例子,不一定必須是這個數組,只要能//區別正常數據,能被作為特殊的數據被識別就行valstopArray=byteArrayOf((-777).toByte(),(-888).toByte())audioList?.offer(stopArray)}}catch(e:IOException){}finally{if(audioRecorder!=null)//釋放資源,別忘記!!!audioRecorder.release()}}

上面採用添加結束數據的方式標識錄取音頻結束,編碼的時候可以根據這個來判斷 是否已到達編碼流的結尾

4.配置和啟動編碼(子線程中開啟任務)編碼和解碼必填數據

上圖為編碼和解碼必要的數據,如果解碼的MediaFormat是MediaExtractor取出,一般 就直接配置好了,就不需要自己填寫了

funmediaCodecEncodeToAAC(){valcurrentTime=Date().time*1000try{valisSupprot=isSupprotAAC()//創建音頻MediaFormat,也可以new的方式創建,不過那樣需要//自己再setXXX設置數據valencodeFormat=MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,AudioConfig.SAMPLE_RATE,1)/***下面配置幾個比較關鍵的參數*///配置比特率encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE,96000)//配置AAC描述,AAC有很多規格LC是其中一個encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,MediaCodecInfo.CodecProfileLevel.AACObjectLC)//配置最大輸入大小,這裡配置的是前面起算的大小的2倍encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,minBufferSize*2)//初始化編碼器mediaEncode=MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)//設置異步回調,後面會貼出callback實現mediaEncode.setCallback(callback)//調用configure,進入configured狀態mediaEncode.configure(encodeFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)//調用start,進入starting狀態mediaEncode.start()}catch(e:IOException){}finally{}}

下面是callback的實現代碼

object:MediaCodec.Callback(){overridefunonOutputFormatChanged(codec:MediaCodec,format:MediaFormat){}overridefunonError(codec:MediaCodec,e:MediaCodec.CodecException){Log.i("error",e.message)}//系統獲取到有可用的輸出buffer時候自動回調overridefunonOutputBufferAvailable(codec:MediaCodec,index:Int,info:MediaCodec.BufferInfo){//通過bufferinfo獲取Buffer的數據,這些數據就是編碼後的數據valoutBitsSize=info.size//為AAC文件添加頭部,頭部占7字節//AAC有ADIF和ADTS兩種ADIF只有一個頭部剩下都是音頻文件//ADTS是每一段編碼都有一個頭部//outpacketSize是最後頭部加上返回數據後的總大小valoutPacketSize=outBitsSize+7//7isADTSsize//根據index獲取buffervaloutputBuffer=codec.getOutputBuffer(index)//防止buffer有offset導致自己從0開始獲取,//取出數據(但是我實驗的offset都為0,可能有些不為0的情況)outputBuffer.position(info.offset)//設置buffer的操作上限位置,不清楚的可以查下ByteBuffer(NIO知識),//了解limit,position,clear(),filp()都是啥作用outputBuffer.limit(info.offset+outBitsSize)//創建byte數組保存組合數據valoutData=ByteArray(outPacketSize)//為數據添加頭部,後面會貼出,就是在頭部寫入7個數據addADTStoPacket(AudioConfig.SAMPLE_RATE,outData,outPacketSize)//將buffer的數據存入數組中outputBuffer.get(outData,7,outBitsSize)outputBuffer.position(info.offset)//bufferedOutputStream是我創建的包裝流,//包裝的FileOutputStream//將數據寫到文件bufferedOutputStream.write(outData)bufferedOutputStream.flush()outputBuffer.clear()//釋放輸出buffer!!!!!一定要釋放codec.releaseOutputBuffer(index,false)}/***當系統有可用的輸入buffer就會自動回調*/overridefunonInputBufferAvailable(codec:MediaCodec,index:Int){//根據index獲取buffervalinputBuffer=codec.getInputBuffer(index)//從LinkBlockingDeque中獲取還未編碼的原音頻數據valpop=audioList?.poll()//判斷是否到達音頻數據的結尾,這個條件根據自己設定的結束標誌而定,//這裡我是這樣判斷if(pop!=null&&pop.size>=2&&(pop[0]==(-777).toByte()&&pop[1]==(-888).toByte())){//結束標誌isEndTip=true}//如果數據不為空,而且不是結束標誌,寫入buffer,讓MediaCodec去編碼//currentTime是之前創建的變量Date().getTime(),下面用當前時間減去他,//是為了最終傳入的數據小點if(pop!=null&&!isEndTip){//填入數據inputBuffer?.clear()inputBuffer?.limit(pop.size)inputBuffer?.put(pop,0,pop.size)//將buffer還給MediaCodec,這個一定要還//第四個參數為時間戳,也就是,必須是遞增的,系統根據這個計算//音頻總時長和時間間隔codec.queueInputBuffer(index,0,pop.size,Date().time*1000-currentTime,0)}//由於2個線程誰先執行不確定,所以可能編碼線程先啟動,獲取到隊列的數據為null//而且也不是結尾數據,這個時候也要調用queueInputBuffer,將buffer換回去,寫入//數據大小就寫0//如果為null就不調用queueInputBuffer回調幾次後就會導致無可用InputBuffer,//從而導致MediaCodec任務結束只能寫個配置文件if(pop==null&&!isEndTip){codec.queueInputBuffer(index,0,0,Date().time*1000-currentTime,0)}//發現結束標誌,寫入結束標誌,//flag為MediaCodec.BUFFER_FLAG_END_OF_STREAM//通知編碼結束if(isEndTip){codec.queueInputBuffer(index,0,0,Date().time*1000-currentTime,MediaCodec.BUFFER_FLAG_END_OF_STREAM)}}})

注意事項

onInputBufferAvaliable回調,發現獲取到的ByteArray為null,也就是錄音還沒錄入數據切記也要將獲取到的inputBuffer還回去,調用queueInputBuffer,不然一會就發現inputBuffer都沒還導致無可用buffer,這個方法就不會在回調,就再也無法寫入數據了!!!!!!!!!!!!!!!血的教訓,之前忘了還導致每次文件大小都是9B(2字節系統默認寫入,7字節添加的頭部)

感知到錄音結束,沒有數據了,要寫入流結束標誌,讓onInputBufferAvaliable停止回調,不再接收編碼數據,標誌為 MediaCodec.BUFFER_FLAG_END_OF_STREAM

5.編寫頭文件

頭文件我直接在網上找的

/***添加ADTS頭,如果要與視頻流合併就不用添加,單獨AAC文件就需要添加,否則無法正常播放*@paramsampleRateType就是之前配置的採樣率*@parampacket之前創建的字節數組,保存頭和編碼後音頻數據*@parampacketLen字節數組總長度*/funaddADTStoPacket(sampleRateType:Int,packet:ByteArray,packetLen:Int){valprofile=2//AACLCvalchanCfg=1//聲道數這個就是你之前配置的聲道數量packet[0]=0xFF.toByte()packet[1]=0xF9.toByte()packet[2]=((profile-1shl6)+(sampleRateTypeshl2)+(chanCfgshr2)).toByte()packet[3]=((chanCfgand3shl6)+(packetLenshr11)).toByte()packet[4]=(packetLenand0x7FFshr3).toByte()packet[5]=((packetLenand7shl5)+0x1F).toByte()packet[6]=0xFC.toByte()}6.創建輸出文件

這一步可能不是必須的,因為我們在寫入文件時候,系統發現沒有文件,就會先創建 ,但是如果路徑設置問題,有可能寫入時候老是寫不進去,我就遇到,數據不為0,但是 最終寫入後文件大小為0,所以保險起見我都是先創建文件,如果你們遇到一樣的情況,可以 試下這一步

//filesDir就是context.getFilesDirfile=File(filesDir,"record.aac")if(!file.exists()){//不存在就創建file.createNewFile()}if(file.isDirectory){}else{outputStream=FileOutputStream(file,true)bufferedOutputStream=BufferedOutputStream(outputStream,4096)}

完整代碼地址:https://github.com/wu781521512/MediaStudyProject

-- END --

進技術交流群,掃碼添加我的微信:Byte-Flow

獲取視頻教程和源碼


推薦:

Android FFmpeg 實現帶濾鏡的微信小視頻錄製功能

全網最全的 Android 音視頻和 OpenGL ES 乾貨,都在這了

一文掌握 YUV 圖像的基本處理

抖音傳送帶特效是怎麼實現的?

所有你想要的圖片轉場效果,都在這了

面試官:如何利用 Shader 實現 RGBA 到 NV21 圖像格式轉換?

我用 OpenGL ES 給小姐姐做了幾個抖音濾鏡

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()