Android錄製音訊並使用Lame轉成mp3
這篇文章主要介紹在Android平臺上使用AudioRecord採集聲音資料,採集到的資料是PCM格式的,由於需要上傳以及在其他平臺裝置上播放,所以使用Lame庫將PCM資料進行編碼轉成Mp3格式,有關於聲音採集的基礎知識可以參考這篇筆記聲音採集-筆記
聲音錄製
Android中使用AudioRecord錄製聲音,根據上面講述的聲音採集原理,需要傳遞給AudioRecord取樣頻率、取樣位數和聲道數,除此之外還需要傳入兩個引數,一個是聲音源,一個是緩衝區大小。
許可權
在Android中錄製聲音需要相應的許可權,6.0需要動態申請許可權。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
初始化AudioRecord
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
audioSource
聲音源(在MediaRecorder.AudioSource中進行定義),支援的音訊源有如下幾種,這裡我們使用的是MIC。
/** 預設聲音 **/ public static final int DEFAULT = 0; /** 麥克風聲音 */ public static final int MIC = 1; /** 通話上行聲音 */ public static final int VOICE_UPLINK = 2; /** 通話下行聲音 */ public static final int VOICE_DOWNLINK = 3; /** 通話上下行聲音 */ public static final int VOICE_CALL = 4; /** 根據攝像頭轉向選擇麥克風*/ public static final int CAMCORDER = 5; /** 對麥克風聲音進行聲音識別,然後進行錄製 */ public static final int VOICE_RECOGNITION = 6; /** 對麥克風中類似ip通話的交流聲音進行識別,預設會開啟回聲消除和自動增益 */ public static final int VOICE_COMMUNICATION = 7; /** 錄製系統內建聲音 */ public static final int REMOTE_SUBMIX = 8;
sampleRateInHz
第二個引數就是取樣頻率
44100Hz is currently the only *rate that is guaranteed to work on all devices, but other rates such as 22050, *16000, and 11025 may work on some devices.
根據文件可以看到,Android系統要求所有的裝置都要支援44100HZ的取樣頻率,而其他的在一些裝置上不一定支援。
8000, 11025, 16000, 22050, 44100, 48000
上面是一些常用的取樣頻率,可以通過如下程式碼獲取手機支援的音訊取樣率:
public void getValidSampleRates() { for (int rate : new int[] {8000, 11025, 16000, 22050, 44100}) {// add the rates you wish to check against int bufferSize = AudioRecord.getMinBufferSize(rate, AudioFormat.CHANNEL_CONFIGURATION_DEFAULT, AudioFormat.ENCODING_PCM_16BIT); if (bufferSize > 0) { // buffer size is valid, Sample rate supported } } }
channelConfig
See {@link AudioFormat#CHANNEL_IN_MONO} and *{@link AudioFormat#CHANNEL_IN_STEREO}.{@link AudioFormat#CHANNEL_IN_MONO} is guaranteed *to work on all devices.
MONO是單聲道,而STEREO是立體聲,想要在所有裝置上都適用的話,推薦使用單聲道。
audioFormat
即我們所說的取樣位數。
See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT}, *and {@link AudioFormat#ENCODING_PCM_FLOAT}.
常用的是ENCODING_PCM_8BIT,和ENCODING_PCM_16BIT,ENCODING_PCM_16BIT能夠相容大多數裝置。
想要進一步瞭解PCM格式的編碼的可以看雷神的這篇文章。
ofollow,noindex">視音訊資料處理入門:PCM音訊取樣資料處理
bufferSizeInBytes
緩衝區的大小,採集到的資料會先寫到緩衝區,之後從緩衝區中讀取資料,從而獲取到麥克風錄製的音訊資料。在Android中不同的聲道數、取樣位數和取樣頻率會有不同的最小緩衝區大小,當AudioRecord傳入的緩衝區大小小於最小緩衝區大小的時候則會初始化失敗。大的緩衝區大小可以達到更為平滑的錄製效果,相應的也會帶來更大一點的延時。
mBufferSize=AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
通過上面的程式碼可以獲取到最小緩衝區的大小。
在我們自己使用lame對pcm資料進行編碼時,需要週期性的通知,所以需要將bufferSize像上取整到滿足週期的大小。
private static final int FRAME_COUNT = 160; /** *bytesPerFrame *PCM_8BIT 1位元組 *PCM_16BIT 2位元組 **/ int frameSize = mBufferSize / bytesPerFrame; if (frameSize % FRAME_COUNT != 0) { frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT); mBufferSize = frameSize * bytesPerFrame; }
讀取資料
AudioRecord可以通過下面的方法進行資料讀取。讀取失敗的話會返回失敗碼。
public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) { return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING); }
監聽AudioRecord進行轉碼
給AudioRecord設定重新整理監聽,待錄音幀數每次達到FRAME_COUNT,就通知轉換執行緒轉換一次資料。
audioRecord.setRecordPositionUpdateListener(OnRecordPositionUpdateListener listener, Handler handler); audioRecord.setPositionNotificationPeriod(FRAME_COUNT);
在OnRecordPositionUpdateListener的onPeriodicNotification(AudioRecord recorder)的回撥方法中就可以使用Lame對讀取到的資料進行編碼,然後寫入檔案。
匯入lame庫
Android studio已經支援使用CMake了,所以這裡就使用CMake來整合lame。如何建立專案可以參考我之前的這篇文章《android opencv JNI開發環境搭建》 。
下載Lame原始碼
下載地址 。
修改Lame內容
-
下載完之後解壓,然後找到libmp3lame資料夾,將裡面的.c和.h檔案全部複製到專案的cpp目錄中。
注意:libmp3lame資料夾內還包含其他資料夾,不用管它。
然後,再找到include資料夾,將lame.h檔案拷貝到cpp目錄中。(總共43個檔案) - 接下來需要將原始檔匯入到專案中修改CMakeLists將Lame的原始碼加入。
aux_source_directory(src/main/cpp/libmp3lame SRC_LIST) add_library(lamemp3 SHARED src/main/cpp/native-lib.cpp ${SRC_LIST})
3.移植修改
首先,需要對lame中的三個檔案進行一些小改動。
- fft.c中47行將vector/lame_intrin.h這個標頭檔案註釋了或者去掉
#ifdef HAVE_CONFIG_H # include <config.h> #endif #include "lame.h" #include "machine.h" #include "encoder.h" #include "util.h" #include "fft.h" //#include "vector/lame_intrin.h"
- 修改set_get.h檔案的24行的#include“lame.h”
#ifndef __SET_GET_H__ #define __SET_GET_H__ #include "lame.h"
-
將util.h檔案的574行的”extern ieee754_float32_t fast_log2(ieee754_float32_t x);”
替換為 “extern float fast_log2(float x);”因為android下不支援該型別。
這些跟ndk-builde是一樣的,網上有很多教程。
然後,需要修改app -> build.gradle檔案
android { ... defaultConfig { ... externalNativeBuild{ cmake{ cFlags "-DSTDC_HEADERS" } } } }
新增-D標誌的意思就是給編譯器新增巨集定義。那麼-DSTDC_HEADERS就相當於給專案增加一句"#define STDC_HEADERS"。
我們開啟machine.h檔案看一下第34行:
#ifdef STDC_HEADERS # include <stdlib.h> # include <string.h> #else # ifndef HAVE_STRCHR #define strchr index #define strrchr rindex # endif char*strchr(), *strrchr(); # ifndef HAVE_MEMCPY #define memcpy(d, s, n) bcopy ((s), (d), (n)) #define memmove(d, s, n) bcopy ((s), (d), (n)) # endif #endif
意思很明白,如果沒有定義STDC_HEADERS這個巨集則會用到bcopy方法,而這個方法我們根本沒有,於是就報錯了。
測試
開啟native-lib.cpp檔案,進行修改
extern "C" JNIEXPORT jstring JNICALL Java_zeller_com_mp3recorder_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(get_lame_version()); }
app中顯示Lame的版本資訊說明匯入Lame庫成功。
編寫JNI程式碼
我們需要Lame提供如下幾個方法供Java層呼叫
public native static void close(); public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf); public native static int flush(byte[] mp3buf); public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);
init方法,初始化Lame
static lame_global_flags *glf = NULL; extern "C" JNIEXPORT void JNICALL Java_zeller_com_mp3recorder_Utils_LameUtils_init(JNIEnv *env, jclass type, jint inSampleRate, jint outChannel, jint outSampleRate, jint outBitrate, jint quality) { if (glf != NULL) { lame_close(glf); glf = NULL; } glf = lame_init(); lame_set_in_samplerate(glf, inSampleRate); lame_set_num_channels(glf, outChannel); lame_set_out_samplerate(glf, outSampleRate); lame_set_brate(glf, outBitrate); lame_set_quality(glf, quality); lame_init_params(glf); }
encode方法,將PCM編碼成MP3格式
extern "C" JNIEXPORT jint JNICALL Java_zeller_com_mp3recorder_Utils_LameUtils_encode(JNIEnv *env, jclass type, jshortArray buffer_l_, jshortArray buffer_r_, jint samples, jbyteArray mp3buf_) { jshort *buffer_l = env->GetShortArrayElements(buffer_l_, NULL); jshort *buffer_r = env->GetShortArrayElements(buffer_r_, NULL); jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL); const jsize mp3buf_size = env->GetArrayLength(mp3buf_); int result =lame_encode_buffer(glf, buffer_l, buffer_r, samples, (u_char*)mp3buf, mp3buf_size); env->ReleaseShortArrayElements(buffer_l_, buffer_l, 0); env->ReleaseShortArrayElements(buffer_r_, buffer_r, 0); env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0); return result; }
flush方法
將MP3結尾資訊寫入buffer中
extern "C" JNIEXPORT jint JNICALL Java_zeller_com_mp3recorder_Utils_LameUtils_flush(JNIEnv *env, jclass type, jbyteArray mp3buf_) { jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL); const jsizemp3buf_size = env->GetArrayLength(mp3buf_); int result = lame_encode_flush(glf, (u_char*)mp3buf, mp3buf_size); env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0); return result; }
close方法
extern "C" JNIEXPORT void JNICALL Java_zeller_com_mp3recorder_Utils_LameUtils_close(JNIEnv *env, jclass type) { lame_close(glf); glf = NULL; }
Java層程式碼
Jni層的事情到這裡就做完了,接下來就交給Java層去做了。
初始化
首先需要對AudioRecord以及Lame進行初始化,初始化需要的引數在前面已經分析過。初始化完之後設定監聽,週期性的對資料進行重新編碼,編碼的操作需要放在一個新的執行緒中完成。
private void initAudioRecorder() throws IOException { 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; } /* Setup audio recorder */ mAudioRecord = new AudioRecord(DEFAULT_AUDIO_SOURCE, DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat(), mBufferSize); mPCMBuffer = new short[mBufferSize]; /* * 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); // Create and run thread used to encode data // The thread will mEncodeThread = new DataEncodeThread(mRecordFile, mBufferSize); mEncodeThread.start(); mAudioRecord.setRecordPositionUpdateListener(mEncodeThread, mEncodeThread.getHandler()); mAudioRecord.setPositionNotificationPeriod(FRAME_COUNT); }
不斷的從audioRecord中讀取資料,然後交給EncodeThread進行編碼。
public void start() throws IOException { if (mIsRecording) { return; } mIsRecording = true; // 提早,防止init或startRecording被多次呼叫 initAudioRecorder(); mAudioRecord.startRecording(); new Thread() { @Override public void run() { //設定執行緒許可權 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); while (mIsRecording) { int readSize = mAudioRecord.read(mPCMBuffer, 0, mBufferSize); if (readSize > 0) { mEncodeThread.addTask(mPCMBuffer, readSize); } } // release and finalize audioRecord mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; // stop the encoding thread and try to wait // until the thread finishes its job mEncodeThread.sendStopMessage(); } }.start(); }
在DataEncodeThread中把資料轉碼然後寫入檔案。
private int processData() { if (mTasks.size() > 0) { Task task = mTasks.remove(0); short[] buffer = task.getData(); int readSize = task.getReadSize(); int encodedSize = LameUtil.encode(buffer, buffer, readSize, mMp3Buffer); if (encodedSize > 0){ try { mFileOutputStream.write(mMp3Buffer, 0, encodedSize); } catch (IOException e) { e.printStackTrace(); } } return readSize; } return 0; }
結束錄製的時候需要把mp3的結尾資訊寫入,然後釋放資源。
if (msg.what == PROCESS_STOP) { //處理緩衝區中的資料 while (encodeThread.processData() > 0); // Cancel any event left in the queue removeCallbacksAndMessages(null); encodeThread.flushAndRelease(); getLooper().quit(); } private void flushAndRelease() { //將MP3結尾資訊寫入buffer中 final int flushResult = LameUtil.flush(mMp3Buffer); if (flushResult > 0) { try { mFileOutputStream.write(mMp3Buffer, 0, flushResult); } catch (IOException e) { e.printStackTrace(); }finally{ if (mFileOutputStream != null) { try { mFileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } LameUtil.close(); } } }