Android錄制音頻并使用Lame轉(zhuǎn)成mp3

這篇文章主要介紹在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)容
  1. 下載完之后解壓酒唉,然后找到libmp3lame文件夾,將里面的.c和.h文件全部復(fù)制到項目的cpp目錄中沸移。
    注意:libmp3lame文件夾內(nèi)還包含其他文件夾痪伦,不用管它。
    然后雹锣,再找到include文件夾网沾,將lame.h文件拷貝到cpp目錄中。(總共43個文件)
  2. 接下來需要將源文件導(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();
            }
        }
    }           

參考文章

Android手機直播(三)聲音采集

利用Cmake在AndroidStudio來使用lame庫

Android NDK 開發(fā)之 CMake 必知必會

Android移植lame庫(采用CMake)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末意荤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子只锻,更是在濱河造成了極大的恐慌玖像,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齐饮,死亡現(xiàn)場離奇詭異捐寥,居然都是意外死亡,警方通過查閱死者的電腦和手機祖驱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門握恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捺僻,你說我怎么就攤上這事乡洼。” “怎么了陵像?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵就珠,是天一觀的道長寇壳。 經(jīng)常有香客問我醒颖,道長,這世上最難降的妖魔是什么壳炎? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任泞歉,我火速辦了婚禮,結(jié)果婚禮上匿辩,老公的妹妹穿的比我還像新娘腰耙。我一直安慰自己,他們只是感情好铲球,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布挺庞。 她就那樣靜靜地躺著,像睡著了一般稼病。 火紅的嫁衣襯著肌膚如雪选侨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天然走,我揣著相機與錄音援制,去河邊找鬼。 笑死芍瑞,一個胖子當著我的面吹牛晨仑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洪己,長吁一口氣:“原來是場噩夢啊……” “哼妥凳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起答捕,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤猾封,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后噪珊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晌缘,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年痢站,在試婚紗的時候發(fā)現(xiàn)自己被綠了磷箕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡阵难,死狀恐怖岳枷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呜叫,我是刑警寧澤空繁,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站朱庆,受9級特大地震影響盛泡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娱颊,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一傲诵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箱硕,春花似錦拴竹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惠昔,卻和暖如春幕与,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舰罚。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工纽门, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人营罢。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓赏陵,卻偏偏與公主長得像饼齿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蝙搔,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容