Android錄音支持的格式有amr、aac,但這兩種音頻格式在跨平臺上表現并不好。
MP3顯然才是跨平臺的最佳選擇。
項目地址
實現思路概述
在分析代碼前,我們需要明確幾個問題
1. 如何最終生成MP3
實現MP3格式最好是借助Lame這個成熟的解決方案。
對于Android來說,需要借助JNI來調用Lame的C語言代碼,實現音頻格式的轉化。
2. 如何獲取最初的音頻數據
AudioRecord類可以直接幫助我們獲取音頻數據。
3. 如何進行轉換
網上有代碼是先錄制后轉為MP3,這種效率比較低。因為如果錄音時間過長,轉換時間就會相應變長,用戶在存儲錄音時需要等待的時間就會變長。
Samsung Developers先錄后轉示例代碼
顯然,這種方案是不可取的。
我們需要的是邊錄邊轉的實現方式,這樣在停止錄音進行存儲的時候,就不會花費太長時間。
實現代碼介紹
既然是錄音,我們上面也提到了需要使用AudioRecord類,我們就從這個類的構造器開始說起
構造器
public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
構造器參數很多,我們一點一點來看:
其實從上面的解釋可以看到,類的參數很多,但為了保證在所有設備上可以使用,我們真正需要填寫的只有一個參數:bufferSizeInBytes,其他都可以使用通用的參數而不用自己費心來選擇。
在深究bufferSizeInBytes該傳入什么之前,我們先略過這一段,先來說一下錄音的讀取與轉換。
錄音的讀取與轉換策略
錄音的讀取其實和UDP差不多,需要不斷的讀取數據。
既然是不斷,那么我們當然需要循環讀取,意味著我們需要一個線程來單獨讀取錄音,避免阻塞主線程。
還和UDP差不多的是,如果不及時讀取,數據超過緩沖區大小,會造成這段錄音數據的丟失。
上面提到過,我們想要實現的是邊錄邊轉。那么問題來了,如果我們讀取完數據后接著將數據傳給Lame進行MP3編碼,Lame的編碼時間是不確定的,是不是有可能造成數據的丟失呢?
答案當然是有可能,所以我們不能巧合編程。
我們需要另外一個線程,即數據編碼線程來專門進行MP3編碼,而當前的錄音讀取線程只負責讀取錄音PCM數據。
有了兩條線程,我們還需要確認一點,什么時候編碼線程開始處理數據?
編碼線程處理數據的時機
傳統的方法是當線程中有數據的時候開始處理,這就需要在這個線程里面不斷循環查看是否有數據需要處理,有數據就開始處理,沒有數據我們可以暫時休息幾毫秒(當然一直不sleep也可以,但造成的系統消耗太多)。
這種方式顯然也是低效的,因為無論我們讓線程休息多久都可以判定為不合理。因為我們并不知道準確的時間。
那么還有別的方法么?
顯然錄音這個類是知道什么時候該處理數據,什么時候可以休息。
Don't call me , I will call you.
是的,我們應該去看看有沒有監聽器,讓錄音來通知編碼線程開始工作。
AudioRecord為我們提供了這樣的方法:
public int setPositionNotificationPeriod (int periodInFrames)Added in API level 3Sets 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.
設置通知周期。 以幀為單位。
到這里,我們可以回來來解釋bufferSizeInBytes大小的傳入了。
緩沖區的大小
其實AudioRecord類提供了一個方便的方法getMinBufferSize來獲取緩沖區的大小。
public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)
這里的3個參數,其實我們都可以從構造器的參數里看到,因此傳入并沒有什么問題。
但關鍵在如上面我們設置了周期單位,如果獲得的緩沖區大小不是周期單位的整數倍呢?
不是整數倍當然會如我們猜想的一樣造成數據丟失,因此我們還需要一些數據的糾正來保證緩沖區大小是整數倍。
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;}講完了數據的獲取線程和編碼線程,我們來仔細看看幫助我們實現MP3編碼的功臣:Lame
Lame的獲取與編譯
步驟
解壓libmp3lame 到jni目錄.
拷貝 lame.h (include目錄下)
創建Android.mk
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := mp3lameLOCAL_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.cinclude $(BUILD_SHARED_LIBRARY)
刪除非.c/.h文件:GNU autotools, Makefile.am Makefile.in libmp3lame_vc8.vcproj logoe.ico depcomp, folders i386 等無用文件。
編輯 jni/utils.h。把extern ieee754_float32_t fast_log2(ieee754_float32_t x);替換為extern float fast_log2(float x);。如果忘了替換,編譯時會報出以下錯誤:
[armeabi] Compile thumb : mp3lame <= bitstream.cIn 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
編譯庫文件??赡軙蟪鼍?,忽略即可。
Lame需要對外提供的方法
推薦:
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相關,因為是mono單聲,所以是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
這里需要解釋一下:
Task task = mTasks.remove(0);short[] buffer = task.getData();int readSize = task.getReadSize();int encodedSize = LameUtil.encode(buffer, buffer, readSize, mMp3Buffer);
flush
將MP3結尾信息寫入buffer中。
傳入參數:mp3buf至少7200字節。這里還是用以前定義的mp3buf來傳入,避免創建過多的數組。
close
關閉釋放Lame
OK,到這里,核心的轉換代碼就完成了,我們再來點錦上添花的東西。
音量
一般我們在做錄音的時候,都會有一個需求,根據音量的大小顯示一個動畫,讓錄音顯得更生動一些。
當然,我在這個庫里也提供了。
那么怎么來計算音量呢?
我參考了三星的音量計算。
總結如下:
/*** 此計算方法來自samsung開發范例* * @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); }};關于最大音量
其實對于音量,我不是特別明白。
最大音量在三星的代碼中給出的是4000,但是我在實際的測試中發現,這個計算公式得出的音量大小一般都在1500以內。
因此在我提供的錄音庫里面,我把最大音量規定為了2000。
這塊兒歡迎大家來提寶貴意見。
MP3錄音實現參考
yhirano/Mp3VoiceRecorderSampleForAndroid
日本人寫的,感覺他的判斷不完善,有點巧合編程的意思,也或許是我沒看懂。
talzeus/AndroidMp3Recorder
比較嚴謹的代碼。主要依據這個庫進行的修改。
存在的問題:
AudioRecord傳入參數很多沒有按Android規定傳入。如采樣頻率使用了22050Hz。
使用了自己構造的RingBuffer,看這有點頭暈。 我在庫里使用List來存儲未編碼的音頻數據,更容易理解。
沒有提供音量大小。
新聞熱點
疑難解答