Android 音視頻開發 - 使用AudioRecord採集音頻

序言

AudioRecord 是 Android 系統提供的用於實現錄音功能的 API,官方文檔是這麼解釋的:

AndioRecord類的主要功能是讓各種Java應用能夠管理音頻資源,以便它們通過此類能夠錄製聲音相關的硬件所收集的聲音。此功能的實現就是通過“pulling”(讀取)AudioRecord對象的聲音數據來完成的。在錄音過程中,應用所需要做的就是通過後面三個類方法中的一個去及時地獲取AudioRecord對象的錄音數據。AudioRecord類提供的三個獲取聲音數據的方法分別是read(byte[], int, int),、read(short[], int, int)和read(ByteBuffer, int)。無論選擇使用那一個方法都必須事先設定方便用戶的聲音數據存儲格式。

開始錄音的時候,AudioRecord需要初始化一個相關聯的聲音buffer, 這個buffer主要是用來保存新的聲音數據。這個buffer的大小,我們可以在對象構造期間去指定。它表明一個AudioRecord對象還沒有被讀取(同步)聲音數據前能錄多長的音(即一次可以錄製的聲音容量)。聲音數據從音頻硬件中被讀出,數據大小不超過整個錄音數據的大小(可以分多次讀出),即每次讀取初始化buffer容量的數據。

實現Android錄音的流程爲:

  1. 構造一個AudioRecord對象,其中最小錄音緩存buffer大小可以通過getMinBufferSize方法得到。如果buffer容量過小,將導致對象構造的失敗。
  2. 初始化一個buffer,該buffer大於等於AudioRecord對象用於寫聲音數據的buffer大小。
  3. 開始錄音
  4. 創建一個數據流,一邊從AudioRecord中讀取聲音數據到初始化的buffer,一邊將buffer中數據導入數據流。
  5. 關閉數據流
  6. 停止錄音

其中,構造 AudioRecord 對象,需要這麼幾個參數:

  1. 音頻源:一般可以使用麥克風作爲採集音頻的數據源。
  2. 採樣率:一秒內對聲音數據的採樣次數,採樣率越高,音質越好。
  3. 音頻通道:單聲道,雙聲道等。
  4. 音頻格式:一般選用 PCM 格式,即原始的音頻樣本。
  5. 緩衝區大小:音頻數據寫入緩衝區的總數,可以通過 AudioRecord.getMinBufferSize 獲取最小的緩衝區。

注意,最後生成的音頻文件是 PCM 格式的,也就是最原始的音頻數據,它沒有頭信息,不能直接播放,必須轉換成可識別的格式才行。這裏我們把它轉成 WAV 格式,在文件的數據開頭加入 WAVE HEAD 即可。

用代碼實踐一下錄音的過程

  1. 創建錄音對象,指定具體的參數。
    public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
        // 獲得緩衝區字節大小
        mBufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
        mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferSizeInBytes);
        int state = mAudioRecord.getState();
        Log.i(TAG, "createAudio state:" + state + ", initialized:" + (state == AudioRecord.STATE_INITIALIZED));
        mFileName = fileName;
        mStatus = Status.STATUS_READY;
    }

  1. 開始錄音,不斷讀取 Buffer 中聲音數據,並保存到文件。
    public void startRecord() {
        if (mStatus == Status.STATUS_NO_READY || mAudioRecord == null) {
            throw new IllegalStateException("錄音尚未初始化");
        }
        if (mStatus == Status.STATUS_START) {
            throw new IllegalStateException("正在錄音...");
        }
        Log.d(TAG, "===startRecord===");
        mAudioRecord.startRecording();
        String currentFileName = FileUtils.getPcmFilePath(mContext, mFileName);
        final String finalFileName = currentFileName;
        //將錄音狀態設置成正在錄音狀態
        mStatus = Status.STATUS_START;

        //使用線程池管理線程
        mExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                writeDataToFile(finalFileName);
            }
        });
    }

    private void writeDataToFile(String currentFileName) {
        byte[] audioData = new byte[mBufferSizeInBytes];
        BufferedOutputStream bos = null;
        int readSize;
        try {
            File file = new File(currentFileName);
            if (file.exists()) {
                file.delete();
            }
            bos = new BufferedOutputStream(new FileOutputStream(file));

            while (mStatus == Status.STATUS_START) {
                readSize = mAudioRecord.read(audioData, 0, mBufferSizeInBytes);
                if (AudioRecord.ERROR_INVALID_OPERATION != readSize) {
                    try {
                        bos.write(audioData);
                        if (mRecordStreamListener != null) {
                            mRecordStreamListener.onRecording(audioData, 0, audioData.length);
                        }
                    } catch (IOException e) {
                        Log.e(TAG, e.getMessage());
                    }
                }
            }
            bos.flush();
            if (mRecordStreamListener != null) {
                mRecordStreamListener.finishRecord();
            }
        } catch (IOException e) {
            Log.e(TAG, e.getMessage());
        } finally {
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                Log.e(TAG, e.getMessage());
            }
        }
    }

    public interface RecordStreamListener {
        /**
         * 錄音過程中
         *
         * @param bytes
         * @param offset
         * @param length
         */
        void onRecording(byte[] bytes, int offset, int length);

        /**
         * 錄音完成
         */
        void finishRecord();
    }

  1. 停止錄音,釋放 AudioRecord
    public void stopRecord() {
        Log.d(TAG, "===stopRecord===");
        if (mStatus == Status.STATUS_NO_READY || mStatus == Status.STATUS_READY) {
            throw new IllegalStateException("錄音尚未開始");
        } else {
            mAudioRecord.stop();
            mStatus = Status.STATUS_STOP;
            release();
        }
    }

    public void release() {
        Log.d(TAG, "===release===");
        if (mAudioRecord != null) {
            mAudioRecord.release();
            mAudioRecord = null;
        }
        mStatus = Status.STATUS_NO_READY;
    }

PCM 轉 WAV 的代碼就不貼了,有需要的可以到 GitHub 上查看。
【附錄】

需要資料的朋友可以加入Android架構交流QQ羣聊:513088520

點擊鏈接加入羣聊【Android移動架構總羣】:加入羣聊

獲取免費學習視頻,學習大綱另外還有像高級UI、性能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章