這篇文章主要介紹在Android平臺上使用AudioRecord采集聲音數(shù)據(jù)务嫡,采集到的數(shù)據(jù)是PCM格式的闸迷,由于需要上傳以及在其他平臺設(shè)備上播放冒签,所以使用Lame庫將PCM數(shù)據(jù)進行編碼轉(zhuǎn)成Mp3格式,有關(guān)于聲音采集的基礎(chǔ)知識可以參考這篇筆記聲音采集-筆記
聲音錄制
Android中使用AudioRecord錄制聲音笔诵,根據(jù)上面講述的聲音采集原理返吻,需要傳遞給AudioRecord采樣頻率、采樣位數(shù)和聲道數(shù)乎婿,除此之外還需要傳入兩個參數(shù)测僵,一個是聲音源,一個是緩沖區(qū)大小。
權(quán)限
在Android中錄制聲音需要相應(yīng)的權(quán)限捍靠,6.0需要動態(tài)申請權(quán)限沐旨。
<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;
/** 麥克風(fēng)聲音 */
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;
/** 根據(jù)攝像頭轉(zhuǎn)向選擇麥克風(fēng)*/
public static final int CAMCORDER = 5;
/** 對麥克風(fēng)聲音進行聲音識別磁携,然后進行錄制 */
public static final int VOICE_RECOGNITION = 6;
/** 對麥克風(fēng)中類似ip通話的交流聲音進行識別,默認會開啟回聲消除和自動增益 */
public static final int VOICE_COMMUNICATION = 7;
/** 錄制系統(tǒng)內(nèi)置聲音 */
public static final int REMOTE_SUBMIX = 8;
sampleRateInHz
第二個參數(shù)就是采樣頻率
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.
根據(jù)文檔可以看到良风,Android系統(tǒng)要求所有的設(shè)備都要支持44100HZ的采樣頻率谊迄,而其他的在一些設(shè)備上不一定支持。
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是立體聲这吻,想要在所有設(shè)備上都適用的話吊档,推薦使用單聲道。
audioFormat
即我們所說的采樣位數(shù)唾糯。
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能夠兼容大多數(shù)設(shè)備移怯。
想要進一步了解PCM格式的編碼的可以看雷神的這篇文章香璃。
視音頻數(shù)據(jù)處理入門:PCM音頻采樣數(shù)據(jù)處理
bufferSizeInBytes
緩沖區(qū)的大小,采集到的數(shù)據(jù)會先寫到緩沖區(qū)舟误,之后從緩沖區(qū)中讀取數(shù)據(jù)葡秒,從而獲取到麥克風(fēng)錄制的音頻數(shù)據(jù)。在Android中不同的聲道數(shù)嵌溢、采樣位數(shù)和采樣頻率會有不同的最小緩沖區(qū)大小眯牧,當AudioRecord傳入的緩沖區(qū)大小小于最小緩沖區(qū)大小的時候則會初始化失敗。大的緩沖區(qū)大小可以達到更為平滑的錄制效果赖草,相應(yīng)的也會帶來更大一點的延時学少。
mBufferSize=AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, audioFormat);
通過上面的代碼可以獲取到最小緩沖區(qū)的大小。
在我們自己使用lame對pcm數(shù)據(jù)進行編碼時秧骑,需要周期性的通知版确,所以需要將bufferSize像上取整到滿足周期的大小。
private static final int FRAME_COUNT = 160;
/**
*bytesPerFrame
*PCM_8BIT 1字節(jié)
*PCM_16BIT 2字節(jié)
**/
int frameSize = mBufferSize / bytesPerFrame;
if (frameSize % FRAME_COUNT != 0) {
frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT);
mBufferSize = frameSize * bytesPerFrame;
}
讀取數(shù)據(jù)
AudioRecord可以通過下面的方法進行數(shù)據(jù)讀取乎折。讀取失敗的話會返回失敗碼绒疗。
public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {
return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING);
}
監(jiān)聽AudioRecord進行轉(zhuǎn)碼
給AudioRecord設(shè)置刷新監(jiān)聽,待錄音幀數(shù)每次達到FRAME_COUNT骂澄,就通知轉(zhuǎn)換線程轉(zhuǎn)換一次數(shù)據(jù)吓蘑。
audioRecord.setRecordPositionUpdateListener(OnRecordPositionUpdateListener listener, Handler handler);
audioRecord.setPositionNotificationPeriod(FRAME_COUNT);
在OnRecordPositionUpdateListener的onPeriodicNotification(AudioRecord recorder)的回調(diào)方法中就可以使用Lame對讀取到的數(shù)據(jù)進行編碼,然后寫入文件酗洒。
導(dǎo)入lame庫
Android studio已經(jīng)支持使用CMake了士修,所以這里就使用CMake來集成lame枷遂。如何創(chuàng)建項目可以參考我之前的這篇文章《android opencv JNI開發(fā)環(huán)境搭建》。
下載Lame源碼
下載地址棋嘲。
修改Lame內(nèi)容
- 下載完之后解壓酒唉,然后找到libmp3lame文件夾,將里面的.c和.h文件全部復(fù)制到項目的cpp目錄中沸移。
注意:libmp3lame文件夾內(nèi)還包含其他文件夾痪伦,不用管它。
然后雹锣,再找到include文件夾网沾,將lame.h文件拷貝到cpp目錄中。(總共43個文件) - 接下來需要將源文件導(dǎo)入到項目中修改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“l(fā)ame.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是一樣的醋旦,網(wǎng)上有很多教程。
然后会放,需要修改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的版本信息說明導(dǎo)入Lame庫成功。
編寫JNI代碼
我們需要Lame提供如下幾個方法供Java層調(diào)用
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結(jié)尾信息寫入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 jsize mp3buf_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進行初始化摄狱,初始化需要的參數(shù)在前面已經(jīng)分析過脓诡。初始化完之后設(shè)置監(jiān)聽,周期性的對數(shù)據(jù)進行重新編碼媒役,編碼的操作需要放在一個新的線程中完成祝谚。
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中讀取數(shù)據(jù)酣衷,然后交給EncodeThread進行編碼交惯。
public void start() throws IOException {
if (mIsRecording) {
return;
}
mIsRecording = true; // 提早,防止init或startRecording被多次調(diào)用
initAudioRecorder();
mAudioRecord.startRecording();
new Thread() {
@Override
public void run() {
//設(shè)置線程權(quán)限
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中把數(shù)據(jù)轉(zhuǎn)碼然后寫入文件。
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;
}
結(jié)束錄制的時候需要把mp3的結(jié)尾信息寫入,然后釋放資源席爽。
if (msg.what == PROCESS_STOP) {
//處理緩沖區(qū)中的數(shù)據(jù)
while (encodeThread.processData() > 0);
// Cancel any event left in the queue
removeCallbacksAndMessages(null);
encodeThread.flushAndRelease();
getLooper().quit();
}
private void flushAndRelease() {
//將MP3結(jié)尾信息寫入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();
}
}
}