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

這篇文章主要介紹在Android平臺(tái)上使用AudioRecord采集聲音數(shù)據(jù),采集到的數(shù)據(jù)是PCM格式的,由于需要上傳以及在其他平臺(tái)設(shè)備上播放,所以使用Lame庫(kù)將PCM數(shù)據(jù)進(jìn)行編碼轉(zhuǎn)成Mp3格式,有關(guān)于聲音采集的基礎(chǔ)知識(shí)可以參考這篇筆記聲音采集-筆記

聲音錄制

Android中使用AudioRecord錄制聲音,根據(jù)上面講述的聲音采集原理,需要傳遞給AudioRecord采樣頻率、采樣位數(shù)和聲道數(shù),除此之外還需要傳入兩個(gè)參數(shù),一個(gè)是聲音源,一個(gè)是緩沖區(qū)大小。

權(quán)限

在Android中錄制聲音需要相應(yīng)的權(quán)限,6.0需要?jiǎng)討B(tài)申請(qǐng)權(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中進(jìn)行定義),支持的音頻源有如下幾種,這里我們使用的是MIC。

/** 默認(rèn)聲音 **/
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;
/** 對(duì)麥克風(fēng)聲音進(jìn)行聲音識(shí)別,然后進(jìn)行錄制 */
public static final int VOICE_RECOGNITION = 6;
/** 對(duì)麥克風(fēng)中類似ip通話的交流聲音進(jìn)行識(shí)別,默認(rèn)會(huì)開啟回聲消除和自動(dòng)增益 */
public static final int VOICE_COMMUNICATION = 7;
/** 錄制系統(tǒng)內(nèi)置聲音 */
public static final int REMOTE_SUBMIX = 8;
sampleRateInHz

第二個(gè)參數(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

上面是一些常用的采樣頻率,可以通過(guò)如下代碼獲取手機(jī)支持的音頻采樣率:

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

即我們所說(shuō)的采樣位數(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è)備。
想要進(jìn)一步了解PCM格式的編碼的可以看雷神的這篇文章。

視音頻數(shù)據(jù)處理入門:PCM音頻采樣數(shù)據(jù)處理

bufferSizeInBytes

緩沖區(qū)的大小,采集到的數(shù)據(jù)會(huì)先寫到緩沖區(qū),之后從緩沖區(qū)中讀取數(shù)據(jù),從而獲取到麥克風(fēng)錄制的音頻數(shù)據(jù)。在Android中不同的聲道數(shù)、采樣位數(shù)和采樣頻率會(huì)有不同的最小緩沖區(qū)大小,當(dāng)AudioRecord傳入的緩沖區(qū)大小小于最小緩沖區(qū)大小的時(shí)候則會(huì)初始化失敗。大的緩沖區(qū)大小可以達(dá)到更為平滑的錄制效果,相應(yīng)的也會(huì)帶來(lái)更大一點(diǎn)的延時(shí)。

mBufferSize=AudioRecord.getMinBufferSize(sampleRateInHz,
                channelConfig, audioFormat);

通過(guò)上面的代碼可以獲取到最小緩沖區(qū)的大小。
在我們自己使用lame對(duì)pcm數(shù)據(jù)進(jìn)行編碼時(shí),需要周期性的通知,所以需要將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可以通過(guò)下面的方法進(jìn)行數(shù)據(jù)讀取。讀取失敗的話會(huì)返回失敗碼。

public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {    
        return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING);
}

監(jiān)聽AudioRecord進(jìn)行轉(zhuǎn)碼

給AudioRecord設(shè)置刷新監(jiān)聽,待錄音幀數(shù)每次達(dá)到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對(duì)讀取到的數(shù)據(jù)進(jìn)行編碼,然后寫入文件。

導(dǎo)入lame庫(kù)

Android studio已經(jīng)支持使用CMake了,所以這里就使用CMake來(lái)集成lame。如何創(chuàng)建項(xiàng)目可以參考我之前的這篇文章《android opencv JNI開發(fā)環(huán)境搭建》。

下載Lame源碼

下載地址。

修改Lame內(nèi)容
  1. 下載完之后解壓,然后找到libmp3lame文件夾,將里面的.c和.h文件全部復(fù)制到項(xiàng)目的cpp目錄中。
    注意:libmp3lame文件夾內(nèi)還包含其他文件夾,不用管它。
    然后,再找到include文件夾,將lame.h文件拷貝到cpp目錄中。(總共43個(gè)文件)
  2. 接下來(lái)需要將源文件導(dǎo)入到項(xiàng)目中修改CMakeLists將Lame的源碼加入。
aux_source_directory(src/main/cpp/libmp3lame SRC_LIST)

add_library(lamemp3
             SHARED
             src/main/cpp/native-lib.cpp
              ${SRC_LIST})

3.移植修改
首先,需要對(duì)lame中的三個(gè)文件進(jìn)行一些小改動(dòng)。

  • fft.c中47行將vector/lame_intrin.h這個(gè)頭文件注釋了或者去掉
#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);”因?yàn)閍ndroid下不支持該類型。

這些跟ndk-builde是一樣的,網(wǎng)上有很多教程。
然后,需要修改app -> build.gradle文件

android {
...
    defaultConfig {
    ...
        externalNativeBuild{
            cmake{
                cFlags "-DSTDC_HEADERS"
            }
        }
    }
}

添加-D標(biāo)志的意思就是給編譯器添加宏定義。那么-DSTDC_HEADERS就相當(dāng)于給項(xiàng)目增加一句"#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這個(gè)宏則會(huì)用到bcopy方法,而這個(gè)方法我們根本沒有,于是就報(bào)錯(cuò)了。

測(cè)試

打開native-lib.cpp文件,進(jìn)行修改

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的版本信息說(shuō)明導(dǎo)入Lame庫(kù)成功。

編寫JNI代碼

我們需要Lame提供如下幾個(gè)方法供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層的事情到這里就做完了,接下來(lái)就交給Java層去做了。

初始化

首先需要對(duì)AudioRecord以及Lame進(jìn)行初始化,初始化需要的參數(shù)在前面已經(jīng)分析過(guò)。初始化完之后設(shè)置監(jiān)聽,周期性的對(duì)數(shù)據(jù)進(jìn)行重新編碼,編碼的操作需要放在一個(gè)新的線程中完成。

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進(jìn)行編碼。

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é)束錄制的時(shí)候需要把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手機(jī)直播(三)聲音采集

利用Cmake在AndroidStudio來(lái)使用lame庫(kù)

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

Android移植lame庫(kù)(采用CMake)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容