Android錄音mp3格式實(shí)例詳解
Android錄音支持的格式有amr、aac,但這兩種音頻格式在跨平臺(tái)上表現(xiàn)并不好。
MP3顯然才是跨平臺(tái)的最佳選擇。
項(xiàng)目地址
實(shí)現(xiàn)思路概述
在分析代碼前,我們需要明確幾個(gè)問題
1. 如何最終生成MP3
實(shí)現(xiàn)MP3格式最好是借助Lame這個(gè)成熟的解決方案。
對(duì)于Android來(lái)說,需要借助JNI來(lái)調(diào)用Lame的C語(yǔ)言代碼,實(shí)現(xiàn)音頻格式的轉(zhuǎn)化。
2. 如何獲取最初的音頻數(shù)據(jù)
AudioRecord類可以直接幫助我們獲取音頻數(shù)據(jù)。
3. 如何進(jìn)行轉(zhuǎn)換
網(wǎng)上有代碼是先錄制后轉(zhuǎn)為MP3,這種效率比較低。因?yàn)槿绻浺魰r(shí)間過長(zhǎng),轉(zhuǎn)換時(shí)間就會(huì)相應(yīng)變長(zhǎng),用戶在存儲(chǔ)錄音時(shí)需要等待的時(shí)間就會(huì)變長(zhǎng)。
Samsung Developers先錄后轉(zhuǎn)示例代碼
顯然,這種方案是不可取的。
我們需要的是邊錄邊轉(zhuǎn)的實(shí)現(xiàn)方式,這樣在停止錄音進(jìn)行存儲(chǔ)的時(shí)候,就不會(huì)花費(fèi)太長(zhǎng)時(shí)間。
實(shí)現(xiàn)代碼介紹
既然是錄音,我們上面也提到了需要使用AudioRecord類,我們就從這個(gè)類的構(gòu)造器開始說起
構(gòu)造器
public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
構(gòu)造器參數(shù)很多,我們一點(diǎn)一點(diǎn)來(lái)看:
- audioSource : 聲源,一般使用MediaRecorder.AudioSource.MIC表示來(lái)自于麥克風(fēng)
- sampleRateInHz :官方明確說到只有44100Hz是所有設(shè)備都支持的。其他22050、16000和11025只能在某些設(shè)備上使用。
- channelConfig : 有立體聲(CHANNEL_IN_STEREO)和單聲道(CHANNEL_IN_MONO)兩種。但只有單聲道(CHANNEL_IN_MONO)是所有設(shè)備都支持的。
- audioFormat : 有ENCODING_PCM_16BIT和ENCODING_PCM_8BIT兩種音頻編碼格式。同樣的,官方聲明只有ENCODING_PCM_16BIT是所有設(shè)備都支持的。
- bufferSizeInBytes : 錄音期間聲音數(shù)據(jù)的寫入緩沖區(qū)大?。▎挝皇亲止?jié))。
其實(shí)從上面的解釋可以看到,類的參數(shù)很多,但為了保證在所有設(shè)備上可以使用,我們真正需要填寫的只有一個(gè)參數(shù):bufferSizeInBytes
,其他都可以使用通用的參數(shù)而不用自己費(fèi)心來(lái)選擇。
在深究bufferSizeInBytes
該傳入什么之前,我們先略過這一段,先來(lái)說一下錄音的讀取與轉(zhuǎn)換。
錄音的讀取與轉(zhuǎn)換策略
錄音的讀取其實(shí)和UDP差不多,需要不斷的讀取數(shù)據(jù)。
既然是不斷,那么我們當(dāng)然需要循環(huán)讀取,意味著我們需要一個(gè)線程來(lái)單獨(dú)讀取錄音,避免阻塞主線程。
還和UDP差不多的是,如果不及時(shí)讀取,數(shù)據(jù)超過緩沖區(qū)大小,會(huì)造成這段錄音數(shù)據(jù)的丟失。
上面提到過,我們想要實(shí)現(xiàn)的是邊錄邊轉(zhuǎn)。那么問題來(lái)了,如果我們讀取完數(shù)據(jù)后接著將數(shù)據(jù)傳給Lame進(jìn)行MP3編碼,Lame的編碼時(shí)間是不確定的,是不是有可能造成數(shù)據(jù)的丟失呢?
答案當(dāng)然是有可能,所以我們不能巧合編程。
我們需要另外一個(gè)線程,即數(shù)據(jù)編碼線程來(lái)專門進(jìn)行MP3編碼,而當(dāng)前的錄音讀取線程只負(fù)責(zé)讀取錄音PCM數(shù)據(jù)。
有了兩條線程,我們還需要確認(rèn)一點(diǎn),什么時(shí)候編碼線程開始處理數(shù)據(jù)?
編碼線程處理數(shù)據(jù)的時(shí)機(jī)
傳統(tǒng)的方法是當(dāng)線程中有數(shù)據(jù)的時(shí)候開始處理,這就需要在這個(gè)線程里面不斷循環(huán)查看是否有數(shù)據(jù)需要處理,有數(shù)據(jù)就開始處理,沒有數(shù)據(jù)我們可以暫時(shí)休息幾毫秒(當(dāng)然一直不sleep也可以,但造成的系統(tǒng)消耗太多)。
這種方式顯然也是低效的,因?yàn)闊o(wú)論我們讓線程休息多久都可以判定為不合理。因?yàn)槲覀儾⒉恢罍?zhǔn)確的時(shí)間。
那么還有別的方法么?
顯然錄音這個(gè)類是知道什么時(shí)候該處理數(shù)據(jù),什么時(shí)候可以休息。
Don't call me , I will call you.
是的,我們應(yīng)該去看看有沒有監(jiān)聽器,讓錄音來(lái)通知編碼線程開始工作。
AudioRecord為我們提供了這樣的方法:
public int setPositionNotificationPeriod (int periodInFrames) Added in API level 3 Sets the period at which the listener is called, if set with setRecordPositionUpdateListener(OnRecordPositionUpdateListener) or setRecordPositionUpdateListener(OnRecordPositionUpdateListener, Handler). It is possible for notifications to be lost if the period is too small.
設(shè)置通知周期。 以幀為單位。
到這里,我們可以回來(lái)來(lái)解釋bufferSizeInBytes
大小的傳入了。
緩沖區(qū)的大小
其實(shí)AudioRecord類提供了一個(gè)方便的方法getMinBufferSize來(lái)獲取緩沖區(qū)的大小。
public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)
這里的3個(gè)參數(shù),其實(shí)我們都可以從構(gòu)造器的參數(shù)里看到,因此傳入并沒有什么問題。
但關(guān)鍵在如上面我們?cè)O(shè)置了周期單位,如果獲得的緩沖區(qū)大小不是周期單位的整數(shù)倍呢?
不是整數(shù)倍當(dāng)然會(huì)如我們猜想的一樣造成數(shù)據(jù)丟失,因此我們還需要一些數(shù)據(jù)的糾正來(lái)保證緩沖區(qū)大小是整數(shù)倍。
mBufferSize = AudioRecord.getMinBufferSize(DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat()); int bytesPerFrame = DEFAULT_AUDIO_FORMAT.getBytesPerFrame(); /* Get number of samples. Calculate the buffer size * (round up to the factor of given frame size) * 使能被整除,方便下面的周期性通知 * */ int frameSize = mBufferSize / bytesPerFrame; if (frameSize % FRAME_COUNT != 0) { frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT); mBufferSize = frameSize * bytesPerFrame; }
講完了數(shù)據(jù)的獲取線程和編碼線程,我們來(lái)仔細(xì)看看幫助我們實(shí)現(xiàn)MP3編碼的功臣:Lame
Lame的獲取與編譯
步驟
解壓libmp3lame 到j(luò)ni目錄.
拷貝 lame.h (include目錄下)
創(chuàng)建Android.mk
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mp3lame LOCAL_SRC_FILES := bitstream.c fft.c id3tag.c mpglib_interface.c presets.c quantize.c reservoir.c tables.c util.c VbrTag.c encoder.c gain_analysis.c lame.c newmdct.c psymodel.c quantize_pvt.c set_get.c takehiro.c vbrquantize.c version.c include $(BUILD_SHARED_LIBRARY)
刪除非.c/.h文件:GNU autotools, Makefile.am Makefile.in libmp3lame_vc8.vcproj logoe.ico depcomp, folders i386 等無(wú)用文件。
編輯 jni/utils.h。把extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替換為extern float fast_log2(float x);
。如果忘了替換,編譯時(shí)會(huì)報(bào)出以下錯(cuò)誤:
[armeabi] Compile thumb : mp3lame <= bitstream.c In file included from jni/bitstream.c:36:0: jni/util.h:574:5: error: unknown type name 'ieee754_float32_t' jni/util.h:574:40: error: unknown type name 'ieee754_float32_t' make.exe: *** [obj/local/armeabi/objs/mp3lame/bitstream.o] Error 1
編譯庫(kù)文件。可能會(huì)報(bào)出警告,忽略即可。
Lame需要對(duì)外提供的方法
- init 初始化
- inSamplerate : 輸入采樣頻率 Hz
- inChannel : 輸入聲道數(shù)
- outSamplerate : 輸出采樣頻率 Hz
- outBitrate : Encoded bit rate. KHz
- quality : MP3音頻質(zhì)量。0~9。 其中0是最好,非常慢,9是最差。
推薦:
2 :near-best quality, not too slow
5 :good quality, fast
7 :ok quality, really fast
private static final int DEFAULT_LAME_MP3_QUALITY = 7; /** * 與DEFAULT_CHANNEL_CONFIG相關(guān),因?yàn)槭莔ono單聲,所以是1 */ private static final int DEFAULT_LAME_IN_CHANNEL = 1; /** * Encoded bit rate. MP3 file will be encoded with bit rate 32kbps */ private static final int DEFAULT_LAME_MP3_BIT_RATE = 32; /* * Initialize lame buffer * mp3 sampling rate is the same as the recorded pcm sampling rate * The bit rate is 32kbps * */ LameUtil.init(DEFAULT_SAMPLING_RATE, DEFAULT_LAME_IN_CHANNEL, DEFAULT_SAMPLING_RATE, DEFAULT_LAME_MP3_BIT_RATE, DEFAULT_LAME_MP3_QUALITY);
encode
- bufferLeft : 左聲道數(shù)據(jù)
- bufferRight:右聲道數(shù)據(jù)
- samples :每個(gè)聲道輸入數(shù)據(jù)大小
- mp3buf :用于接收轉(zhuǎn)換后的數(shù)據(jù)。7200 + (1.25 * buffer_l.length)
這里需要解釋一下:
Task task = mTasks.remove(0); short[] buffer = task.getData(); int readSize = task.getReadSize(); int encodedSize = LameUtil.encode(buffer, buffer, readSize, mMp3Buffer);
- 左右聲道 :當(dāng)前聲道選的是單聲道,因此兩邊傳入一樣的buffer。
- 輸入數(shù)據(jù)大小 :錄音線程讀取到buffer中的數(shù)據(jù)不一定是占滿的,所以read方法會(huì)返回當(dāng)前大小size,即前size個(gè)數(shù)據(jù)是有效的音頻數(shù)據(jù),后面的數(shù)據(jù)是以前留下的廢數(shù)據(jù)。 這個(gè)size同樣需要傳入到Lame編碼器中用于編碼。
- mp3的buffer:官方規(guī)定了計(jì)算公式:7200 + (1.25 * buffer_l.length)。(可以在lame.h文件中看到)
flush
將MP3結(jié)尾信息寫入buffer中。
傳入?yún)?shù):mp3buf至少7200字節(jié)。這里還是用以前定義的mp3buf來(lái)傳入,避免創(chuàng)建過多的數(shù)組。
close
關(guān)閉釋放Lame
OK,到這里,核心的轉(zhuǎn)換代碼就完成了,我們?cè)賮?lái)點(diǎn)錦上添花的東西。
音量
一般我們?cè)谧鲣浺舻臅r(shí)候,都會(huì)有一個(gè)需求,根據(jù)音量的大小顯示一個(gè)動(dòng)畫,讓錄音顯得更生動(dòng)一些。
當(dāng)然,我在這個(gè)庫(kù)里也提供了。
那么怎么來(lái)計(jì)算音量呢?
我參考了三星的音量計(jì)算。
總結(jié)如下:
/** * 此計(jì)算方法來(lái)自samsung開發(fā)范例 * * @param buffer * @param readSize */ private void calculateRealVolume(short[] buffer, int readSize) { int sum = 0; for (int i = 0; i < readSize; i++) { sum += buffer[i] * buffer[i]; } if (readSize > 0) { double amplitude = sum / readSize; mVolume = (int) Math.sqrt(amplitude); } };
關(guān)于最大音量
其實(shí)對(duì)于音量,我不是特別明白。
最大音量在三星的代碼中給出的是4000,但是我在實(shí)際的測(cè)試中發(fā)現(xiàn),這個(gè)計(jì)算公式得出的音量大小一般都在1500以內(nèi)。
因此在我提供的錄音庫(kù)里面,我把最大音量規(guī)定為了2000。
這塊兒歡迎大家來(lái)提寶貴意見。
MP3錄音實(shí)現(xiàn)參考
yhirano/Mp3VoiceRecorderSampleForAndroid
日本人寫的,感覺他的判斷不完善,有點(diǎn)巧合編程的意思,也或許是我沒看懂。
talzeus/AndroidMp3Recorder
比較嚴(yán)謹(jǐn)?shù)拇a。主要依據(jù)這個(gè)庫(kù)進(jìn)行的修改。
存在的問題:
AudioRecord傳入?yún)?shù)很多沒有按Android規(guī)定傳入。如采樣頻率使用了22050Hz。
使用了自己構(gòu)造的RingBuffer,看這有點(diǎn)頭暈。 我在庫(kù)里使用List來(lái)存儲(chǔ)未編碼的音頻數(shù)據(jù),更容易理解。
沒有提供音量大小。
- Android編程錄音工具類RecorderUtil定義與用法示例
- Android編程實(shí)現(xiàn)錄音及保存播放功能的方法【附demo源碼下載】
- Android編程檢測(cè)手機(jī)錄音權(quán)限是否打開的方法
- Android實(shí)現(xiàn)錄音功能實(shí)現(xiàn)實(shí)例(MediaRecorder)
- android語(yǔ)音即時(shí)通訊之錄音、播放功能實(shí)現(xiàn)代碼
- Android6.0編程實(shí)現(xiàn)雙向通話自動(dòng)錄音功能的方法詳解
- Android使用AudioRecord實(shí)現(xiàn)暫停錄音功能實(shí)例代碼
- Android 錄音與播放功能的簡(jiǎn)單實(shí)例
- Android編程實(shí)現(xiàn)通話錄音功能的方法
- 利用libmp3lame實(shí)現(xiàn)在Android上錄音MP3文件示例
- Android錄音--AudioRecord、MediaRecorder的使用
- android 通過MediaRecorder實(shí)現(xiàn)簡(jiǎn)單的錄音示例
- Android使用MediaRecorder實(shí)現(xiàn)錄音及播放
- Android錄音播放管理工具
- Android實(shí)現(xiàn)拍照、錄像、錄音代碼范例
- Android實(shí)現(xiàn)自制和播放錄音程序
- Android中簡(jiǎn)單調(diào)用圖片、視頻、音頻、錄音和拍照的方法
- Android編程開發(fā)錄音和播放錄音簡(jiǎn)單示例
相關(guān)文章
Android使用Recyclerview實(shí)現(xiàn)圖片水平自動(dòng)循環(huán)滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android使用Recyclerview實(shí)現(xiàn)圖片水平自動(dòng)循環(huán)滾動(dòng)效果,實(shí)現(xiàn)精彩的跑馬燈效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android Insets相關(guān)知識(shí)總結(jié)
這篇文章主要介紹了Android Insets相關(guān)知識(shí)總結(jié),幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-03-03Android中的Dalvik和ART詳解及區(qū)別分析
小編通過這篇文章給大家整理了什么是Dalvik和ART,并進(jìn)行了區(qū)別的分析,下面一起來(lái)看看。2016-07-07Android標(biāo)題欄上添加多個(gè)Menu按鈕的實(shí)例
這篇文章主要介紹了Android標(biāo)題欄上添加多個(gè)Menu按鈕的實(shí)例的相關(guān)資料,這里提供簡(jiǎn)單實(shí)例說明如何添加多個(gè)menu按鈕的方法,需要的朋友可以參考下2017-09-09Android實(shí)現(xiàn)手機(jī)震動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)手機(jī)震動(dòng)效果的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-02-02實(shí)例詳解用戶輸入 i. 檢測(cè)常用手勢(shì)
通過本段代碼給大家介紹當(dāng)用戶輸入i檢測(cè)常用手勢(shì)的相關(guān)內(nèi)容,代碼簡(jiǎn)單易懂,感興趣的朋友一起學(xué)習(xí)吧2016-01-01Android編程實(shí)現(xiàn)監(jiān)控apk安裝,卸載,替換的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)監(jiān)控apk安裝,卸載,替換的方法,涉及Android基于Intent監(jiān)控apk狀態(tài)的功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-01-01Android MQTT與WebSocket協(xié)議詳細(xì)講解
MQTT(消息隊(duì)列遙測(cè)傳輸)是ISO 標(biāo)準(zhǔn)(ISO/IEC PRF 20922)下基于發(fā)布/訂閱范式的消息協(xié)議。它工作在TCP/IP協(xié)議族上,是為硬件性能低下的遠(yuǎn)程設(shè)備以及網(wǎng)絡(luò)狀況糟糕的情況下而設(shè)計(jì)的發(fā)布/訂閱型消息協(xié)議2022-11-11Android 判斷某個(gè)服務(wù)(service)是否運(yùn)行
這篇文章主要介紹了 Android 判斷某個(gè)服務(wù)(service)是否運(yùn)行的相關(guān)資料,需要的朋友可以參考下2017-06-06