AudioRecord音頻錄製

 

一、前言

  Android SDK 提供了兩套音頻採集的API,分別是:MediaRecorder 和 AudioRecord,前者是一個更加上層一點的API,它可以直接把手機麥克風錄入的音頻數據進行編碼壓縮(如AMR、MP3等)並存成文件,而後者則更接近底層,能夠更加自由靈活地控制,可以得到原始的一幀幀PCM音頻數據。                                                                                                                                         如果想簡單地做一個錄音機,錄製成音頻文件,則推薦使用 MediaRecorder,而如果需要對音頻做進一步的算法處理、或者採用第三方的編碼庫進行壓縮、以及網絡傳輸等應用,則建議使用 AudioRecord,其實 MediaRecorder 底層也是調用了 AudioRecord 與 Android Framework 層的 AudioFlinger 進行交互的。直播中實時採集音頻自然是要用AudioRecord了。

二、實現流程

1.獲取權限
2.初始化獲取每一幀流的Size
3.初始化音頻錄製AudioRecord
4.開始錄製與保存錄制音頻文件
4.停止錄製
5.給音頻文件添加頭部信息,並且轉換格式成wav

1.獲取權限

<!--音頻錄製權限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--讀取和寫入存儲權限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

備註:如果是Android5.0以上,以上3個權限需要動態授權。

2.初始化獲取每一幀流的Size

// 聲明recordBufffer的大小字段
private Integer mRecordBufferSize;
private void initMinBufferSize(){
    // 獲取每一幀的字節流大小
    mRecordBufferSize = AudioRecord.getMinBufferSize(44100 
        , AudioFormat.CHANNEL_IN_MONO
        , AudioFormat.ENCODING_PCM_16BIT);
}

第一個參數sampleRateInHz:採樣率(赫茲)

採樣率就是採樣頻率,每秒鐘記錄多少個樣本。現在能夠保證在所有設備上使用的採樣率是44100Hz, 但是其他的採樣率(22050, 16000, 11025)在一些設備上也可以使用。

第二個參數channelConfig:聲道配置,描述音頻聲道的配置,例如左聲道/右聲道/前聲道/後聲道。

public static final int CHANNEL_IN_LEFT = 0x4;//左聲道
public static final int CHANNEL_IN_RIGHT = 0x8;//右聲道
public static final int CHANNEL_IN_FRONT = 0x10;//前聲道
public static final int CHANNEL_IN_BACK = 0x20;//後聲道
public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;
public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;
public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;
public static final int CHANNEL_IN_BACK_PROCESSED = 0x200;
public static final int CHANNEL_IN_PRESSURE = 0x400;
public static final int CHANNEL_IN_X_AXIS = 0x800;
public static final int CHANNEL_IN_Y_AXIS = 0x1000;
public static final int CHANNEL_IN_Z_AXIS = 0x2000;
public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;
public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;//單聲道
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);//立體聲道(左右聲道)

第三個參數audioFormat:音頻格式,表示音頻數據的格式。

public static final int ENCODING_PCM_16BIT = 2; //16位PCM編碼
public static final int ENCODING_PCM_8BIT = 3; //8位PCM編碼
public static final int ENCODING_PCM_FLOAT = 4; //4位PCM編碼
public static final int ENCODING_AC3 = 5;
public static final int ENCODING_E_AC3 = 6;
public static final int ENCODING_DTS = 7;
public static final int ENCODING_DTS_HD = 8;
public static final int ENCODING_MP3 = 9; //MP3編碼 此格式可能會因爲不設備不支持報錯
public static final int ENCODING_AAC_LC = 10;
public static final int ENCODING_AAC_HE_V1 = 11;
public static final int ENCODING_AAC_HE_V2 = 12;

注意:一般的手機設備可能只支持 16位PCM編碼,如果其他的都會報錯爲壞值。

3.初始化音頻錄製AudioRecord

// 聲明AudioRecord對象
private AudioRecord mAudioRecord;
private void initAudioRecord(){
    mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC
        , 44100
        , AudioFormat.CHANNEL_IN_MONO
        , AudioFormat.ENCODING_PCM_16BIT
        , mRecordBufferSize);
}

第一個參數audioSource 音頻源——這裏選擇使用麥克風:MediaRecorder.AudioSource.MIC
第二個參數sampleRateInHz 採樣率(赫茲)——與前面初始化獲取每一幀流的Size方法getMinBufferSize的參數保持一致
第三個參數channelConfig 聲道配置 描述音頻聲道的配置,例如左聲道/右聲道/前聲道/後聲道。——與前面初始化獲取每一幀流的Size方法getMinBufferSize的參數保持一致
第四個參數audioFormat 音頻格式  表示音頻數據的格式。——與前面初始化獲取每一幀流的Size方法getMinBufferSize的參數保持一致
第五個參數緩存區大小,就是上面我們配置的AudioRecord.getMinBufferSize的值

4.開始錄製與保存錄制音頻文件

public void startRecord() {
    // mRecordBufferSize爲getMinBufferSize值
    final byte data[] = new byte[mRecordBufferSize];
    final File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
    if (!file.mkdirs()) {
        Log.e(TAG, "Directory not created");
    }
    if (file.exists()) {
        file.delete();
    }
    // 開始錄音
    mAudioRecord.startRecording();
    // 全局布爾變量,用來開始/停止獲取錄音數據
    isRecording = true;
    // ps: pcm數據無法直接播放,需要轉換爲WAV格式。
    new Thread(new Runnable() {
        @Override
        public void run() {
            FileOutputStream os = null;
            try {
                os = new FileOutputStream(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            if (os != null) {
                while (isRecording) {
                    int read = audioRecord.read(data, 0, minBufferSize);
                    // 如果讀取音頻數據沒有出現錯誤,就將數據寫入到文件
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        try {
                            os.write(data);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
                try {
                    Log.i(TAG, "run: close file output stream !");
                    os.flush();
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                addHeadData(); // 添加音頻頭部信息並且轉成wav格式
            }
        }
    }).start();
}

這裏說明一下爲什麼用布爾值來關閉錄製。有些小夥伴會發現AudioRecord是可以獲取到錄製狀態的,那麼肯定有人會用狀態來判斷while是否還需要處理流,這種是錯誤的做法。因爲MIC屬於硬件層任何硬件的東西都是異步的而且會有很大的延時,所以回調的狀態也是有延時的,有時候流沒了,但是狀態還是顯示爲正在錄製。簡單來說就是爲了能實時對應狀態,減少延時帶來的影響。

5.停止錄製

就是調用mAudioRecord.stop();方法來停止錄製,但是因爲我在上面的保存流後做了調用停止視頻錄製,所以我這裏只需要切換布爾值就可以關閉音頻錄製。

public void stopRecord() {
    isRecording = false;
    // 釋放資源
    if (mAudioRecord != null) {
        mAudioRecord.stop();
        mAudioRecord.release();
        mAudioRecord = null;
    }
}

6.給音頻文件添加頭部信息,並且轉換格式成wav

音頻錄製完成後,這個時候去存儲目錄找到音頻文件部分,會提示無法播放文件。其實是因爲沒有加入音頻頭部信息。一般通過麥克風採集的錄音數據都是PCM格式的,即不包含頭部信息,播放器無法知道音頻採樣率、位寬等參數,導致無法播放,顯然是非常不方便的。pcm轉換成wav,我們只需要在pcm的文件起始位置加上至少44個字節的WAV頭信息即可。

偏移地址 命名 內容
00-03 ChunkId "RIFF"
04-07 ChunkSize 下一個地址開始到文件尾的總字節數(此Chunk的數據大小)
08-11 fccType "WAVE"
12-15 SubChunkId1 "fmt",最後一位空格
16-19 SubChunkSize1 一般爲16,表示fmt Chunk的數據塊大小爲16字節
20-21 FormatTag 1:表示PCM編碼
22-23 Channels 聲道數,單聲道爲1,雙聲道爲2
24-27 SamplesPerSec 採樣率
28-31 BytesPerSec 碼率 :採樣率 * 採樣位數 * 聲道個數,bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
32-33 BlockAlign 每次採樣的大小:位寬*聲道數/8
34-35 BitsPerSample 位寬
36-39 SubChunkId2 "data"
40-43 SubChunkSize2 音頻數據的長度
44-... data 音頻數據
// 增加頭部信息
private void addHeadData(){
    PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(44100, AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    File pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test" +
                        ".pcm");
    File wavFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test" +
                        ".wav");
    if (!wavFile.mkdirs()) {
        Log.e(TAG, "wavFile Directory not created");
    }
    if (wavFile.exists()) {
        wavFile.delete();
    }
    pcmToWavUtil.pcmToWav(pcmFile.getAbsolutePath(), wavFile.getAbsolutePath());
}

三、寫入頭部信息的工具類

注意輸入File和輸出File不能同一個,因爲沒有做緩存。

/**
 * 將pcm音頻文件轉換爲wav音頻文件
 */
public class PcmToWavUtil {

    /**
     * 緩存的音頻大小
     */
    private int mBufferSize;
    /**
     * 採樣率
     */
    private int mSampleRate;
    /**
     * 聲道數
     */
    private int mChannel;

    /**
     * @param sampleRate sample rate、採樣率
     * @param channel channel、聲道
     * @param encoding Audio data format、音頻格式
     */
    public PcmToWavUtil(int sampleRate, int channel, int encoding) {
        this.mSampleRate = sampleRate;
        this.mChannel = channel;
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
    }

    /**
     * pcm文件轉wav文件
     *
     * @param inFilename 源文件路徑
     * @param outFilename 目標文件路徑
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen; // 總錄音長度
        long totalDataLen; // 總數據長度
        long longSampleRate = mSampleRate;
        int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加入wav文件頭
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        // format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // block align
        header[32] = (byte) (2 * 16 / 8);
        header[33] = 0;
        // bits per sample
        header[34] = 16;
        header[35] = 0;
        //data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}

四、最後介紹下其他API

1.獲取AudioRecord初始化狀態

/**
 * 注意:這裏是初始化狀態,不是錄製狀態,它只會返回2個狀態
 * AudioRecord#STATE_INITIALIZED // 已經初始化
 * AudioRecord#STATE_UNINITIALIZED // 沒有初始化
 */
public int getState() {
    return mState;
}

2.獲取AudioRecord錄製狀態

/**
 * 返回錄製狀態,它只返回2個狀態
 * AudioRecord#RECORDSTATE_STOPPED //停止錄製
 * AudioRecord#RECORDSTATE_RECORDING //正在錄製
 */
public int getRecordingState() {
    synchronized (mRecordingStateLock) {
        return mRecordingState;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章